Fix CSS conflict diagnostic false negatives (#761)

master
Brad Cornes 2023-04-18 08:54:14 +01:00 committed by GitHub
parent 105b8b86ec
commit 6fa8861efd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 60 additions and 19 deletions

View File

@ -1,12 +1,43 @@
import { joinWithAnd } from '../util/joinWithAnd' import { joinWithAnd } from '../util/joinWithAnd'
import { State, Settings } from '../util/state' import { State, Settings } from '../util/state'
import type { TextDocument, DiagnosticSeverity } from 'vscode-languageserver' import type { TextDocument } from 'vscode-languageserver'
import { CssConflictDiagnostic, DiagnosticKind } from './types' import { CssConflictDiagnostic, DiagnosticKind } from './types'
import { findClassListsInDocument, getClassNamesInClassList } from '../util/find' import { findClassListsInDocument, getClassNamesInClassList } from '../util/find'
import { getClassNameDecls } from '../util/getClassNameDecls' import { getClassNameDecls } from '../util/getClassNameDecls'
import { getClassNameMeta } from '../util/getClassNameMeta' import { getClassNameMeta } from '../util/getClassNameMeta'
import { equal } from '../util/array' import { equal } from '../util/array'
import * as jit from '../util/jit' import * as jit from '../util/jit'
import type { AtRule, Node, Rule } from 'postcss'
function isCustomProperty(property: string): boolean {
return property.startsWith('--')
}
function isAtRule(node: Node): node is AtRule {
return node.type === 'atrule'
}
function isKeyframes(rule: Rule): boolean {
let parent = rule.parent
if (!parent) {
return false
}
if (isAtRule(parent) && parent.name === 'keyframes') {
return true
}
return false
}
function getRuleProperties(rule: Rule): string[] {
let properties: string[] = []
rule.walkDecls(({ prop }) => {
properties.push(prop)
})
if (properties.findIndex((p) => !isCustomProperty(p)) > -1) {
properties = properties.filter((p) => !isCustomProperty(p))
}
return properties
}
export async function getCssConflictDiagnostics( export async function getCssConflictDiagnostics(
state: State, state: State,
@ -24,16 +55,17 @@ export async function getCssConflictDiagnostics(
classNames.forEach((className, index) => { classNames.forEach((className, index) => {
if (state.jit) { if (state.jit) {
let { rules } = jit.generateRules(state, [className.className]) let { rules } = jit.generateRules(
state,
[className.className],
(rule) => !isKeyframes(rule)
)
if (rules.length === 0) { if (rules.length === 0) {
return return
} }
let info: Array<{ context: string[]; properties: string[] }> = rules.map((rule) => { let info: Array<{ context: string[]; properties: string[] }> = rules.map((rule) => {
let properties: string[] = [] let properties = getRuleProperties(rule)
rule.walkDecls(({ prop }) => {
properties.push(prop)
})
let context = jit.getRuleContext(state, rule, className.className) let context = jit.getRuleContext(state, rule, className.className)
return { context, properties } return { context, properties }
}) })
@ -41,21 +73,22 @@ export async function getCssConflictDiagnostics(
let otherClassNames = classNames.filter((_className, i) => i !== index) let otherClassNames = classNames.filter((_className, i) => i !== index)
let conflictingClassNames = otherClassNames.filter((otherClassName) => { let conflictingClassNames = otherClassNames.filter((otherClassName) => {
let { rules: otherRules } = jit.generateRules(state, [otherClassName.className]) let { rules: otherRules } = jit.generateRules(
state,
[otherClassName.className],
(rule) => !isKeyframes(rule)
)
if (otherRules.length !== rules.length) { if (otherRules.length !== rules.length) {
return false return false
} }
for (let i = 0; i < otherRules.length; i++) { for (let i = 0; i < otherRules.length; i++) {
let rule = otherRules[i] let otherRule = otherRules[i]
let properties: string[] = [] let properties = getRuleProperties(otherRule)
rule.walkDecls(({ prop }) => {
properties.push(prop)
})
if (!equal(info[i].properties, properties)) { if (!equal(info[i].properties, properties)) {
return false return false
} }
let context = jit.getRuleContext(state, rule, otherClassName.className) let context = jit.getRuleContext(state, otherRule, otherClassName.className)
if (!equal(info[i].context, context)) { if (!equal(info[i].context, context)) {
return false return false
} }

View File

@ -1,6 +1,5 @@
import { State } from './state' import { State } from './state'
import type { Container, Document, Root, Rule } from 'postcss' import type { Container, Document, Root, Rule, Node, AtRule } from 'postcss'
import dlv from 'dlv'
import { remToPx } from './remToPx' import { remToPx } from './remToPx'
export function bigSign(bigIntValue) { export function bigSign(bigIntValue) {
@ -8,7 +7,11 @@ export function bigSign(bigIntValue) {
return (bigIntValue > 0n) - (bigIntValue < 0n) return (bigIntValue > 0n) - (bigIntValue < 0n)
} }
export function generateRules(state: State, classNames: string[]): { root: Root; rules: Rule[] } { export function generateRules(
state: State,
classNames: string[],
filter: (rule: Rule) => boolean = () => true
): { root: Root; rules: Rule[] } {
let rules: [bigint, Rule][] = state.modules.jit.generateRules let rules: [bigint, Rule][] = state.modules.jit.generateRules
.module(new Set(classNames), state.jitContext) .module(new Set(classNames), state.jitContext)
.sort(([a], [z]) => bigSign(a - z)) .sort(([a], [z]) => bigSign(a - z))
@ -18,7 +21,9 @@ export function generateRules(state: State, classNames: string[]): { root: Root;
let actualRules: Rule[] = [] let actualRules: Rule[] = []
root.walkRules((subRule) => { root.walkRules((subRule) => {
actualRules.push(subRule) if (filter(subRule)) {
actualRules.push(subRule)
}
}) })
return { return {
@ -84,14 +89,17 @@ function replaceClassName(state: State, selector: string, find: string, replace:
return state.modules.postcssSelectorParser.module(transform).processSync(selector) return state.modules.postcssSelectorParser.module(transform).processSync(selector)
} }
function isAtRule(node: Node): node is AtRule {
return node.type === 'atrule'
}
export function getRuleContext(state: State, rule: Rule, className: string): string[] { export function getRuleContext(state: State, rule: Rule, className: string): string[] {
let context: string[] = [replaceClassName(state, rule.selector, className, '__placeholder__')] let context: string[] = [replaceClassName(state, rule.selector, className, '__placeholder__')]
let p: Container | Document = rule let p: Container | Document = rule
while (p.parent && p.parent.type !== 'root') { while (p.parent && p.parent.type !== 'root') {
p = p.parent p = p.parent
if (p.type === 'atrule') { if (isAtRule(p)) {
// @ts-ignore
context.unshift(`@${p.name} ${p.params}`) context.unshift(`@${p.name} ${p.params}`)
} }
} }