Skip to content

Commit b90ee0e

Browse files
authored
feat: no-unknown-at-rules -> no-invalid-at-rules (#12)
* feat!: no-unknown-at-rule -> no-invalid-at-rule * Update README * Catch more errors in at-rules * Adjust error reporting * Remove unnecessary if statement * Update README * Fix type error * Fix validation issues * Fix README * Remove unused function
1 parent 005565e commit b90ee0e

9 files changed

+341
-139
lines changed

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,8 @@ export default [
6060
| :--------------------------------------------------------------- | :------------------------------- | :-------------: |
6161
| [`no-duplicate-imports`](./docs/rules/no-duplicate-imports.md) | Disallow duplicate @import rules | yes |
6262
| [`no-empty-blocks`](./docs/rules/no-empty-blocks.md) | Disallow empty blocks | yes |
63+
| [`no-invalid-at-rules`](./docs/rules/no-invalid-at-rules.md) | Disallow invalid at-rules | yes |
6364
| [`no-invalid-properties`](./docs/rules/no-invalid-properties.md) | Disallow invalid properties | yes |
64-
| [`no-unknown-at-rules`](./docs/rules/no-unknown-at-rules.md) | Disallow unknown at-rules | yes |
6565

6666
<!-- Rule Table End -->
6767

docs/rules/no-invalid-at-rules.md

+80
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# no-invalid-at-rules
2+
3+
Disallow invalid at-rules.
4+
5+
## Background
6+
7+
CSS contains a number of at-rules, each beginning with a `@`, that perform various operations. Some common at-rules include:
8+
9+
- `@import`
10+
- `@media`
11+
- `@font-face`
12+
- `@keyframes`
13+
- `@supports`
14+
- `@namespace`
15+
- `@page`
16+
- `@charset`
17+
18+
It's important to use a known at-rule because unknown at-rules cause the browser to ignore the entire block, including any rules contained within. For example:
19+
20+
```css
21+
/* typo */
22+
@charse "UTF-8";
23+
```
24+
25+
Here, the `@charset` at-rule is incorrectly spelled as `@charse`, which means that it will be ignored.
26+
27+
Each at-rule also has a defined prelude (which may be empty) and potentially one or more descriptors. For example:
28+
29+
```css
30+
@property --main-bg-color {
31+
syntax: "<color>";
32+
inherits: false;
33+
initial-value: #000000;
34+
}
35+
```
36+
37+
Here, `--main-bg-color` is the prelude for `@property` while `syntax`, `inherits`, and `initial-value` are descriptors. The `@property` at-rule requires a specific format for its prelude and only specific descriptors to be present. If any of these are incorrect, the browser ignores the at-rule.
38+
39+
## Rule Details
40+
41+
This rule warns when it finds a CSS at-rule that is unknown or invalid according to the CSS specification. As such, the rule warns for the following problems:
42+
43+
- An unknown at-rule
44+
- An invalid prelude for a known at-rule
45+
- An unknown descriptor for a known at-rule
46+
- An invalid descriptor value for a known at-rule
47+
48+
The at-rule data is provided via the [CSSTree](https://github.com/csstree/csstree) project.
49+
50+
Examples of incorrect code:
51+
52+
```css
53+
@charse "UTF-8";
54+
55+
@importx url(foo.css);
56+
57+
@foobar {
58+
.my-style {
59+
color: red;
60+
}
61+
}
62+
63+
@property main-bg-color {
64+
syntax: "<color>";
65+
inherits: false;
66+
initial-value: #000000;
67+
}
68+
69+
@property --main-bg-color {
70+
syntax: red;
71+
}
72+
```
73+
74+
## When Not to Use It
75+
76+
If you are purposely using at-rules that aren't part of the CSS specification, then you can safely disable this rule.
77+
78+
## Prior Art
79+
80+
- [`at-rule-no-unknown`](https://stylelint.io/user-guide/rules/at-rule-no-unknown)

docs/rules/no-unknown-at-rules.md

-51
This file was deleted.

src/index.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ import { CSSLanguage } from "./languages/css-language.js";
1111
import { CSSSourceCode } from "./languages/css-source-code.js";
1212
import noEmptyBlocks from "./rules/no-empty-blocks.js";
1313
import noDuplicateImports from "./rules/no-duplicate-imports.js";
14-
import noUnknownAtRules from "./rules/no-unknown-at-rules.js";
1514
import noInvalidProperties from "./rules/no-invalid-properties.js";
15+
import noInvalidAtRules from "./rules/no-invalid-at-rules.js";
1616

1717
//-----------------------------------------------------------------------------
1818
// Plugin
@@ -29,7 +29,7 @@ const plugin = {
2929
rules: {
3030
"no-empty-blocks": noEmptyBlocks,
3131
"no-duplicate-imports": noDuplicateImports,
32-
"no-unknown-at-rules": noUnknownAtRules,
32+
"no-invalid-at-rules": noInvalidAtRules,
3333
"no-invalid-properties": noInvalidProperties,
3434
},
3535
configs: {},
@@ -41,7 +41,7 @@ Object.assign(plugin.configs, {
4141
rules: {
4242
"css/no-empty-blocks": "error",
4343
"css/no-duplicate-imports": "error",
44-
"css/no-unknown-at-rules": "error",
44+
"css/no-invalid-at-rules": "error",
4545
"css/no-invalid-properties": "error",
4646
},
4747
},

src/rules/no-invalid-at-rules.js

+155
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
/**
2+
* @fileoverview Rule to prevent the use of unknown at-rules in CSS.
3+
* @author Nicholas C. Zakas
4+
*/
5+
6+
//-----------------------------------------------------------------------------
7+
// Imports
8+
//-----------------------------------------------------------------------------
9+
10+
import { lexer } from "css-tree";
11+
import { isSyntaxMatchError } from "../util.js";
12+
13+
//-----------------------------------------------------------------------------
14+
// Helpers
15+
//-----------------------------------------------------------------------------
16+
17+
/**
18+
* Extracts metadata from an error object.
19+
* @param {SyntaxError} error The error object to extract metadata from.
20+
* @returns {Object} The metadata extracted from the error.
21+
*/
22+
function extractMetaDataFromError(error) {
23+
const message = error.message;
24+
const atRuleName = /`@(.*)`/u.exec(message)[1];
25+
let messageId = "unknownAtRule";
26+
27+
if (message.endsWith("prelude")) {
28+
messageId = message.includes("should not")
29+
? "invalidExtraPrelude"
30+
: "missingPrelude";
31+
}
32+
33+
return {
34+
messageId,
35+
data: {
36+
name: atRuleName,
37+
},
38+
};
39+
}
40+
41+
//-----------------------------------------------------------------------------
42+
// Rule Definition
43+
//-----------------------------------------------------------------------------
44+
45+
export default {
46+
meta: {
47+
type: "problem",
48+
49+
docs: {
50+
description: "Disallow invalid at-rules",
51+
recommended: true,
52+
},
53+
54+
messages: {
55+
unknownAtRule: "Unknown at-rule '@{{name}}' found.",
56+
invalidPrelude:
57+
"Invalid prelude '{{prelude}}' found for at-rule '@{{name}}'. Expected '{{expected}}'.",
58+
unknownDescriptor:
59+
"Unknown descriptor '{{descriptor}}' found for at-rule '@{{name}}'.",
60+
invalidDescriptor:
61+
"Invalid value '{{value}}' for descriptor '{{descriptor}}' found for at-rule '@{{name}}'. Expected {{expected}}.",
62+
invalidExtraPrelude:
63+
"At-rule '@{{name}}' should not contain a prelude.",
64+
missingPrelude: "At-rule '@{{name}}' should contain a prelude.",
65+
},
66+
},
67+
68+
create(context) {
69+
const { sourceCode } = context;
70+
71+
return {
72+
Atrule(node) {
73+
// checks both name and prelude
74+
const { error } = lexer.matchAtrulePrelude(
75+
node.name,
76+
node.prelude,
77+
);
78+
79+
if (error) {
80+
if (isSyntaxMatchError(error)) {
81+
context.report({
82+
loc: error.loc,
83+
messageId: "invalidPrelude",
84+
data: {
85+
name: node.name,
86+
prelude: error.css,
87+
expected: error.syntax,
88+
},
89+
});
90+
return;
91+
}
92+
93+
const loc = node.loc;
94+
95+
context.report({
96+
loc: {
97+
start: loc.start,
98+
end: {
99+
line: loc.start.line,
100+
101+
// add 1 to account for the @ symbol
102+
column: loc.start.column + node.name.length + 1,
103+
},
104+
},
105+
...extractMetaDataFromError(error),
106+
});
107+
}
108+
},
109+
110+
"AtRule > Block > Declaration"(node) {
111+
// get at rule node
112+
const atRule = sourceCode.getParent(sourceCode.getParent(node));
113+
114+
const { error } = lexer.matchAtruleDescriptor(
115+
atRule.name,
116+
node.property,
117+
node.value,
118+
);
119+
120+
if (error) {
121+
if (isSyntaxMatchError(error)) {
122+
context.report({
123+
loc: error.loc,
124+
messageId: "invalidDescriptor",
125+
data: {
126+
name: atRule.name,
127+
descriptor: node.property,
128+
value: error.css,
129+
expected: error.syntax,
130+
},
131+
});
132+
return;
133+
}
134+
135+
const loc = node.loc;
136+
137+
context.report({
138+
loc: {
139+
start: loc.start,
140+
end: {
141+
line: loc.start.line,
142+
column: loc.start.column + node.property.length,
143+
},
144+
},
145+
messageId: "unknownDescriptor",
146+
data: {
147+
name: atRule.name,
148+
descriptor: node.property,
149+
},
150+
});
151+
}
152+
},
153+
};
154+
},
155+
};

src/rules/no-invalid-properties.js

+1-19
Original file line numberDiff line numberDiff line change
@@ -8,25 +8,7 @@
88
//-----------------------------------------------------------------------------
99

1010
import { lexer } from "css-tree";
11-
12-
//-----------------------------------------------------------------------------
13-
// Type Definitions
14-
//-----------------------------------------------------------------------------
15-
16-
/** @typedef {import("css-tree").SyntaxMatchError} SyntaxMatchError */
17-
18-
//-----------------------------------------------------------------------------
19-
// Helpers
20-
//-----------------------------------------------------------------------------
21-
22-
/**
23-
* Determines if an error is a syntax match error.
24-
* @param {Object} error The error object from the CSS parser.
25-
* @returns {error is SyntaxMatchError} True if the error is a syntax match error, false if not.
26-
*/
27-
function isSyntaxMatchError(error) {
28-
return typeof error.css === "string";
29-
}
11+
import { isSyntaxMatchError } from "../util.js";
3012

3113
//-----------------------------------------------------------------------------
3214
// Rule Definition

0 commit comments

Comments
 (0)