Skip to content

Commit 9ac7fbc

Browse files
authored
Merge branch 'main' into no-dup-keyframe-selectors
2 parents 53d4a68 + 4ccc3cc commit 9ac7fbc

File tree

6 files changed

+346
-110
lines changed

6 files changed

+346
-110
lines changed

.gitignore

Lines changed: 10 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -7,120 +7,37 @@ yarn-error.log*
77
lerna-debug.log*
88
.pnpm-debug.log*
99

10-
# Diagnostic reports (https://nodejs.org/api/report.html)
11-
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
12-
1310
# Runtime data
1411
pids
1512
*.pid
1613
*.seed
1714
*.pid.lock
1815

19-
# Directory for instrumented libs generated by jscoverage/JSCover
20-
lib-cov
21-
2216
# Coverage directory used by tools like istanbul
2317
coverage
2418
*.lcov
2519

2620
# nyc test coverage
2721
.nyc_output
2822

29-
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
30-
.grunt
31-
32-
# Bower dependency directory (https://bower.io/)
33-
bower_components
34-
35-
# node-waf configuration
36-
.lock-wscript
37-
38-
# Compiled binary addons (https://nodejs.org/api/addons.html)
39-
build/Release
40-
4123
# Dependency directories
4224
node_modules/
43-
jspm_packages/
44-
45-
# Snowpack dependency directory (https://snowpack.dev/)
46-
web_modules/
4725

4826
# TypeScript cache
4927
*.tsbuildinfo
5028

5129
# Optional npm cache directory
5230
.npm
5331

54-
# Optional eslint cache
55-
.eslintcache
56-
57-
# Optional stylelint cache
58-
.stylelintcache
59-
60-
# Microbundle cache
61-
.rpt2_cache/
62-
.rts2_cache_cjs/
63-
.rts2_cache_es/
64-
.rts2_cache_umd/
65-
66-
# Optional REPL history
67-
.node_repl_history
68-
6932
# Output of 'npm pack'
7033
*.tgz
7134

7235
# Yarn Integrity file
7336
.yarn-integrity
7437

75-
# dotenv environment variable files
76-
.env
77-
.env.development.local
78-
.env.test.local
79-
.env.production.local
80-
.env.local
81-
82-
# parcel-bundler cache (https://parceljs.org/)
83-
.cache
84-
.parcel-cache
85-
86-
# Next.js build output
87-
.next
88-
out
89-
90-
# Nuxt.js build / generate output
91-
.nuxt
92-
dist
93-
94-
# Gatsby files
95-
.cache/
96-
# Comment in the public line in if your project uses Gatsby and not Next.js
97-
# https://nextjs.org/blog/next-9-1#public-directory-support
98-
# public
99-
100-
# vuepress build output
101-
.vuepress/dist
102-
103-
# vuepress v2.x temp and cache directory
104-
.temp
105-
.cache
106-
107-
# Docusaurus cache and generated files
108-
.docusaurus
109-
110-
# Serverless directories
111-
.serverless/
112-
113-
# FuseBox cache
114-
.fusebox/
115-
116-
# DynamoDB Local files
117-
.dynamodb/
118-
119-
# TernJS port file
120-
.tern-port
121-
122-
# Stores VSCode versions used for testing VSCode extensions
123-
.vscode-test
38+
# VSCode
39+
.vscode
40+
*.code-workspace
12441

12542
# yarn v2
12643
.yarn/cache
@@ -132,7 +49,13 @@ dist
13249
# eslint
13350
.eslintcache
13451

52+
# Lockfiles
13553
package-lock.json
13654
yarn.lock
13755
bun.lockb
138-
test.css
56+
pnpm-lock.yaml
57+
58+
# Build
59+
dist/
60+
61+
test.css

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ yarn add @eslint/css -D
2424
# or
2525
pnpm install @eslint/css -D
2626
# or
27-
bun install @eslint/css -D
27+
bun add @eslint/css -D
2828
```
2929

3030
For Deno (experimental):
@@ -285,7 +285,7 @@ to get your logo on our READMEs and [website](https://eslint.org/sponsors).
285285
<p><a href="https://automattic.com"><img src="https://images.opencollective.com/automattic/d0ef3e1/logo.png" alt="Automattic" height="128"></a> <a href="https://www.airbnb.com/"><img src="https://images.opencollective.com/airbnb/d327d66/logo.png" alt="Airbnb" height="128"></a></p><h3>Gold Sponsors</h3>
286286
<p><a href="https://qlty.sh/"><img src="https://images.opencollective.com/qltysh/33d157d/logo.png" alt="Qlty Software" height="96"></a> <a href="https://trunk.io/"><img src="https://images.opencollective.com/trunkio/fb92d60/avatar.png" alt="trunk.io" height="96"></a> <a href="https://shopify.engineering/"><img src="https://avatars.githubusercontent.com/u/8085" alt="Shopify" height="96"></a></p><h3>Silver Sponsors</h3>
287287
<p><a href="https://vite.dev/"><img src="https://images.opencollective.com/vite/e6d15e1/logo.png" alt="Vite" height="64"></a> <a href="https://liftoff.io/"><img src="https://images.opencollective.com/liftoff/5c4fa84/logo.png" alt="Liftoff" height="64"></a> <a href="https://americanexpress.io"><img src="https://avatars.githubusercontent.com/u/3853301" alt="American Express" height="64"></a> <a href="https://stackblitz.com"><img src="https://avatars.githubusercontent.com/u/28635252" alt="StackBlitz" height="64"></a></p><h3>Bronze Sponsors</h3>
288-
<p><a href="https://cybozu.co.jp/"><img src="https://images.opencollective.com/cybozu/933e46d/logo.png" alt="Cybozu" height="32"></a> <a href="https://www.crosswordsolver.org/anagram-solver/"><img src="https://images.opencollective.com/anagram-solver/2666271/logo.png" alt="Anagram Solver" height="32"></a> <a href="https://icons8.com/"><img src="https://images.opencollective.com/icons8/7fa1641/logo.png" alt="Icons8" height="32"></a> <a href="https://discord.com"><img src="https://images.opencollective.com/discordapp/f9645d9/logo.png" alt="Discord" height="32"></a> <a href="https://www.gitbook.com"><img src="https://avatars.githubusercontent.com/u/7111340" alt="GitBook" height="32"></a> <a href="https://nolebase.ayaka.io"><img src="https://avatars.githubusercontent.com/u/11081491" alt="Neko" height="32"></a> <a href="https://nx.dev"><img src="https://avatars.githubusercontent.com/u/23692104" alt="Nx" height="32"></a> <a href="https://opensource.mercedes-benz.com/"><img src="https://avatars.githubusercontent.com/u/34240465" alt="Mercedes-Benz Group" height="32"></a> <a href="https://herocoders.com"><img src="https://avatars.githubusercontent.com/u/37549774" alt="HeroCoders" height="32"></a> <a href="https://www.lambdatest.com"><img src="https://avatars.githubusercontent.com/u/171592363" alt="LambdaTest" height="32"></a></p>
288+
<p><a href="https://sentry.io"><img src="https://github.com/getsentry.png" alt="Sentry" height="32"></a> <a href="https://syntax.fm"><img src="https://github.com/syntaxfm.png" alt="Syntax" height="32"></a> <a href="https://cybozu.co.jp/"><img src="https://images.opencollective.com/cybozu/933e46d/logo.png" alt="Cybozu" height="32"></a> <a href="https://www.crosswordsolver.org/anagram-solver/"><img src="https://images.opencollective.com/anagram-solver/2666271/logo.png" alt="Anagram Solver" height="32"></a> <a href="https://icons8.com/"><img src="https://images.opencollective.com/icons8/7fa1641/logo.png" alt="Icons8" height="32"></a> <a href="https://discord.com"><img src="https://images.opencollective.com/discordapp/f9645d9/logo.png" alt="Discord" height="32"></a> <a href="https://www.gitbook.com"><img src="https://avatars.githubusercontent.com/u/7111340" alt="GitBook" height="32"></a> <a href="https://nolebase.ayaka.io"><img src="https://avatars.githubusercontent.com/u/11081491" alt="Neko" height="32"></a> <a href="https://nx.dev"><img src="https://avatars.githubusercontent.com/u/23692104" alt="Nx" height="32"></a> <a href="https://opensource.mercedes-benz.com/"><img src="https://avatars.githubusercontent.com/u/34240465" alt="Mercedes-Benz Group" height="32"></a> <a href="https://herocoders.com"><img src="https://avatars.githubusercontent.com/u/37549774" alt="HeroCoders" height="32"></a> <a href="https://www.lambdatest.com"><img src="https://avatars.githubusercontent.com/u/171592363" alt="LambdaTest" height="32"></a></p>
289289
<h3>Technology Sponsors</h3>
290290
Technology sponsors allow us to use their products and services for free as part of a contribution to the open source ecosystem and our work.
291291
<p><a href="https://netlify.com"><img src="https://raw.githubusercontent.com/eslint/eslint.org/main/src/assets/images/techsponsors/netlify-icon.svg" alt="Netlify" height="32"></a> <a href="https://algolia.com"><img src="https://raw.githubusercontent.com/eslint/eslint.org/main/src/assets/images/techsponsors/algolia-icon.svg" alt="Algolia" height="32"></a> <a href="https://1password.com"><img src="https://raw.githubusercontent.com/eslint/eslint.org/main/src/assets/images/techsponsors/1password-icon.svg" alt="1Password" height="32"></a></p>

docs/rules/no-invalid-properties.md

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,21 @@ body {
4646

4747
### Limitations
4848

49-
This rule uses the lexer from [CSSTree](https://github.com/csstree/csstree), which does not support validation of property values that contain variable references (i.e., `var(--bg-color)`). The lexer throws an error when it comes across a variable reference, and rather than displaying that error, this rule ignores it. This unfortunately means that this rule cannot properly validate properties values that contain variable references. We'll continue to work towards a solution for this.
49+
When a variable is used in a property value, such as `var(--my-color)`, the rule can only properly be validated if the parser has already encountered the `--my-color` custom property. For example, this will validate correctly:
50+
51+
```css
52+
:root {
53+
--my-color: red;
54+
}
55+
56+
a {
57+
color: var(--my-color);
58+
}
59+
```
60+
61+
This code defines `--my-color` before it is used and therefore the rule can validate the `color` property. If `--my-color` was not defined before `var(--my-color)` was used, it results in a lint error because the validation cannot be completed.
62+
63+
If the custom property is defined in another file, it's recommended to create a dummy rule just to ensure proper validation.
5064

5165
## When Not to Use It
5266

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@
5555
],
5656
"!(*.js)": "prettier --write --ignore-unknown",
5757
"{src/rules/*.js,tools/update-rules-docs.js}": [
58-
"node tools/update-rules-docs.js",
58+
"npm run build:update-rules-docs",
5959
"git add README.md"
6060
]
6161
},

src/rules/no-invalid-properties.js

Lines changed: 144 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,48 @@ import { isSyntaxMatchError } from "../util.js";
1515

1616
/**
1717
* @import { CSSRuleDefinition } from "../types.js"
18-
* @typedef {"invalidPropertyValue" | "unknownProperty"} NoInvalidPropertiesMessageIds
18+
* @import { ValuePlain, FunctionNodePlain, CssLocationRange } from "@eslint/css-tree";
19+
* @typedef {"invalidPropertyValue" | "unknownProperty" | "unknownVar"} NoInvalidPropertiesMessageIds
1920
* @typedef {CSSRuleDefinition<{ RuleOptions: [], MessageIds: NoInvalidPropertiesMessageIds }>} NoInvalidPropertiesRuleDefinition
2021
*/
2122

23+
//-----------------------------------------------------------------------------
24+
// Helpers
25+
//-----------------------------------------------------------------------------
26+
27+
/**
28+
* Replaces all instances of a regex pattern with a replacement and tracks the offsets
29+
* @param {string} text The text to perform replacements on
30+
* @param {string} varName The regex pattern string to search for
31+
* @param {string} replaceValue The string to replace with
32+
* @returns {{text: string, offsets: Array<number>}} The updated text and array of offsets
33+
* where replacements occurred
34+
*/
35+
function replaceWithOffsets(text, varName, replaceValue) {
36+
const offsets = [];
37+
let result = "";
38+
let lastIndex = 0;
39+
40+
const regex = new RegExp(`var\\(\\s*${varName}\\s*\\)`, "gu");
41+
let match;
42+
43+
while ((match = regex.exec(text)) !== null) {
44+
result += text.slice(lastIndex, match.index);
45+
46+
/*
47+
* We need the offset of the replacement after other replacements have
48+
* been made, so we push the current length of the result before appending
49+
* the replacement value.
50+
*/
51+
offsets.push(result.length);
52+
result += replaceValue;
53+
lastIndex = match.index + match[0].length;
54+
}
55+
56+
result += text.slice(lastIndex);
57+
return { text: result, offsets };
58+
}
59+
2260
//-----------------------------------------------------------------------------
2361
// Rule Definition
2462
//-----------------------------------------------------------------------------
@@ -38,48 +76,135 @@ export default {
3876
invalidPropertyValue:
3977
"Invalid value '{{value}}' for property '{{property}}'. Expected {{expected}}.",
4078
unknownProperty: "Unknown property '{{property}}' found.",
79+
unknownVar: "Can't validate with unknown variable '{{var}}'.",
4180
},
4281
},
4382

4483
create(context) {
45-
const lexer = context.sourceCode.lexer;
84+
const sourceCode = context.sourceCode;
85+
const lexer = sourceCode.lexer;
86+
87+
/** @type {Map<string,ValuePlain>} */
88+
const vars = new Map();
89+
90+
/**
91+
* We need to track this as a stack because we can have nested
92+
* rules that use the `var()` function, and we need to
93+
* ensure that we validate the innermost rule first.
94+
* @type {Array<Map<string,FunctionNodePlain>>}
95+
*/
96+
const replacements = [];
4697

4798
return {
48-
"Rule > Block > Declaration"(node) {
49-
// don't validate custom properties
99+
"Rule > Block > Declaration"() {
100+
replacements.push(new Map());
101+
},
102+
103+
"Function[name=var]"(node) {
104+
const map = replacements.at(-1);
105+
if (!map) {
106+
return;
107+
}
108+
109+
/*
110+
* Store the custom property name and the function node
111+
* so can use these to validate the value later.
112+
*/
113+
const name = node.children[0].name;
114+
map.set(name, node);
115+
},
116+
117+
"Rule > Block > Declaration:exit"(node) {
50118
if (node.property.startsWith("--")) {
119+
// store the custom property name and value to validate later
120+
vars.set(node.property, node.value);
121+
122+
// don't validate custom properties
51123
return;
52124
}
53125

54-
const { error } = lexer.matchDeclaration(node);
126+
const varsFound = replacements.pop();
127+
128+
/** @type {Map<number,CssLocationRange>} */
129+
const varsFoundLocs = new Map();
130+
const usingVars = varsFound?.size > 0;
131+
let value = node.value;
132+
133+
if (usingVars) {
134+
// need to use a text version of the value here
135+
value = sourceCode.getText(node.value);
136+
let offsets;
137+
138+
// replace any custom properties with their values
139+
for (const [name, func] of varsFound) {
140+
const varValue = vars.get(name);
141+
142+
if (varValue) {
143+
({ text: value, offsets } = replaceWithOffsets(
144+
value,
145+
name,
146+
sourceCode.getText(varValue).trim(),
147+
));
148+
149+
/*
150+
* Store the offsets of the replacements so we can
151+
* report the correct location of any validation error.
152+
*/
153+
offsets.forEach(offset => {
154+
varsFoundLocs.set(offset, func.loc);
155+
});
156+
} else {
157+
context.report({
158+
loc: func.children[0].loc,
159+
messageId: "unknownVar",
160+
data: {
161+
var: name,
162+
},
163+
});
164+
165+
return;
166+
}
167+
}
168+
}
169+
170+
const { error } = lexer.matchProperty(node.property, value);
55171

56172
if (error) {
57173
// validation failure
58174
if (isSyntaxMatchError(error)) {
59175
context.report({
60-
loc: error.loc,
176+
/*
177+
* When using variables, check to see if the error
178+
* occurred at a location where a variable was replaced.
179+
* If so, use that location; otherwise, use the error's
180+
* reported location.
181+
*/
182+
loc: usingVars
183+
? (varsFoundLocs.get(error.mismatchOffset) ??
184+
node.value.loc)
185+
: error.loc,
61186
messageId: "invalidPropertyValue",
62187
data: {
63188
property: node.property,
64-
value: error.css,
189+
190+
/*
191+
* When using variables, slice the value to
192+
* only include the part that caused the error.
193+
* Otherwise, use the full value from the error.
194+
*/
195+
value: usingVars
196+
? value.slice(
197+
error.mismatchOffset,
198+
error.mismatchOffset +
199+
error.mismatchLength,
200+
)
201+
: error.css,
65202
expected: error.syntax,
66203
},
67204
});
68205
return;
69206
}
70207

71-
/*
72-
* There's no current way to get lexing to work when a
73-
* `var()` is present in a value. Rather than blowing up,
74-
* we'll just ignore it.
75-
*
76-
* https://github.com/csstree/csstree/issues/317
77-
*/
78-
79-
if (error.message.endsWith("var() is not supported")) {
80-
return;
81-
}
82-
83208
// unknown property
84209
context.report({
85210
loc: {

0 commit comments

Comments
 (0)