From 6fa8861efd3830aaeda14fc3100a56c5b45f2e1b Mon Sep 17 00:00:00 2001 From: Brad Cornes Date: Tue, 18 Apr 2023 08:54:14 +0100 Subject: [PATCH] Fix CSS conflict diagnostic false negatives (#761) --- .../diagnostics/getCssConflictDiagnostics.ts | 59 +++++++++++++++---- .../src/util/jit.ts | 20 +++++-- 2 files changed, 60 insertions(+), 19 deletions(-) diff --git a/packages/tailwindcss-language-service/src/diagnostics/getCssConflictDiagnostics.ts b/packages/tailwindcss-language-service/src/diagnostics/getCssConflictDiagnostics.ts index a762871..7ee3f84 100644 --- a/packages/tailwindcss-language-service/src/diagnostics/getCssConflictDiagnostics.ts +++ b/packages/tailwindcss-language-service/src/diagnostics/getCssConflictDiagnostics.ts @@ -1,12 +1,43 @@ import { joinWithAnd } from '../util/joinWithAnd' 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 { findClassListsInDocument, getClassNamesInClassList } from '../util/find' import { getClassNameDecls } from '../util/getClassNameDecls' import { getClassNameMeta } from '../util/getClassNameMeta' import { equal } from '../util/array' 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( state: State, @@ -24,16 +55,17 @@ export async function getCssConflictDiagnostics( classNames.forEach((className, index) => { if (state.jit) { - let { rules } = jit.generateRules(state, [className.className]) + let { rules } = jit.generateRules( + state, + [className.className], + (rule) => !isKeyframes(rule) + ) if (rules.length === 0) { return } let info: Array<{ context: string[]; properties: string[] }> = rules.map((rule) => { - let properties: string[] = [] - rule.walkDecls(({ prop }) => { - properties.push(prop) - }) + let properties = getRuleProperties(rule) let context = jit.getRuleContext(state, rule, className.className) return { context, properties } }) @@ -41,21 +73,22 @@ export async function getCssConflictDiagnostics( let otherClassNames = classNames.filter((_className, i) => i !== index) 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) { return false } for (let i = 0; i < otherRules.length; i++) { - let rule = otherRules[i] - let properties: string[] = [] - rule.walkDecls(({ prop }) => { - properties.push(prop) - }) + let otherRule = otherRules[i] + let properties = getRuleProperties(otherRule) if (!equal(info[i].properties, properties)) { return false } - let context = jit.getRuleContext(state, rule, otherClassName.className) + let context = jit.getRuleContext(state, otherRule, otherClassName.className) if (!equal(info[i].context, context)) { return false } diff --git a/packages/tailwindcss-language-service/src/util/jit.ts b/packages/tailwindcss-language-service/src/util/jit.ts index d308960..40250d1 100644 --- a/packages/tailwindcss-language-service/src/util/jit.ts +++ b/packages/tailwindcss-language-service/src/util/jit.ts @@ -1,6 +1,5 @@ import { State } from './state' -import type { Container, Document, Root, Rule } from 'postcss' -import dlv from 'dlv' +import type { Container, Document, Root, Rule, Node, AtRule } from 'postcss' import { remToPx } from './remToPx' export function bigSign(bigIntValue) { @@ -8,7 +7,11 @@ export function bigSign(bigIntValue) { 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 .module(new Set(classNames), state.jitContext) .sort(([a], [z]) => bigSign(a - z)) @@ -18,7 +21,9 @@ export function generateRules(state: State, classNames: string[]): { root: Root; let actualRules: Rule[] = [] root.walkRules((subRule) => { - actualRules.push(subRule) + if (filter(subRule)) { + actualRules.push(subRule) + } }) return { @@ -84,14 +89,17 @@ function replaceClassName(state: State, selector: string, find: string, replace: 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[] { let context: string[] = [replaceClassName(state, rule.selector, className, '__placeholder__')] let p: Container | Document = rule while (p.parent && p.parent.type !== 'root') { p = p.parent - if (p.type === 'atrule') { - // @ts-ignore + if (isAtRule(p)) { context.unshift(`@${p.name} ${p.params}`) } }