Skip to content

Commit bb90e3a

Browse files
authored
feat: add no-invalid-at-rule-placement rule (#171)
1 parent 9bdb155 commit bb90e3a

File tree

5 files changed

+472
-13
lines changed

5 files changed

+472
-13
lines changed

README.md

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -65,18 +65,19 @@ export default defineConfig([
6565

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

68-
| **Rule Name** | **Description** | **Recommended** |
69-
| :--------------------------------------------------------------------------- | :------------------------------------- | :-------------: |
70-
| [`no-duplicate-imports`](./docs/rules/no-duplicate-imports.md) | Disallow duplicate @import rules | yes |
71-
| [`no-empty-blocks`](./docs/rules/no-empty-blocks.md) | Disallow empty blocks | yes |
72-
| [`no-important`](./docs/rules/no-important.md) | Disallow !important flags | yes |
73-
| [`no-invalid-at-rules`](./docs/rules/no-invalid-at-rules.md) | Disallow invalid at-rules | yes |
74-
| [`no-invalid-named-grid-areas`](./docs/rules/no-invalid-named-grid-areas.md) | Disallow invalid named grid areas | yes |
75-
| [`no-invalid-properties`](./docs/rules/no-invalid-properties.md) | Disallow invalid properties | yes |
76-
| [`prefer-logical-properties`](./docs/rules/prefer-logical-properties.md) | Enforce the use of logical properties | no |
77-
| [`relative-font-units`](./docs/rules/relative-font-units.md) | Enforce the use of relative font units | no |
78-
| [`use-baseline`](./docs/rules/use-baseline.md) | Enforce the use of baseline features | yes |
79-
| [`use-layers`](./docs/rules/use-layers.md) | Require use of layers | no |
68+
| **Rule Name** | **Description** | **Recommended** |
69+
| :----------------------------------------------------------------------------- | :------------------------------------- | :-------------: |
70+
| [`no-duplicate-imports`](./docs/rules/no-duplicate-imports.md) | Disallow duplicate @import rules | yes |
71+
| [`no-empty-blocks`](./docs/rules/no-empty-blocks.md) | Disallow empty blocks | yes |
72+
| [`no-important`](./docs/rules/no-important.md) | Disallow !important flags | yes |
73+
| [`no-invalid-at-rule-placement`](./docs/rules/no-invalid-at-rule-placement.md) | Disallow invalid placement of at-rules | yes |
74+
| [`no-invalid-at-rules`](./docs/rules/no-invalid-at-rules.md) | Disallow invalid at-rules | yes |
75+
| [`no-invalid-named-grid-areas`](./docs/rules/no-invalid-named-grid-areas.md) | Disallow invalid named grid areas | yes |
76+
| [`no-invalid-properties`](./docs/rules/no-invalid-properties.md) | Disallow invalid properties | yes |
77+
| [`prefer-logical-properties`](./docs/rules/prefer-logical-properties.md) | Enforce the use of logical properties | no |
78+
| [`relative-font-units`](./docs/rules/relative-font-units.md) | Enforce the use of relative font units | no |
79+
| [`use-baseline`](./docs/rules/use-baseline.md) | Enforce the use of baseline features | yes |
80+
| [`use-layers`](./docs/rules/use-layers.md) | Require use of layers | no |
8081

8182
<!-- Rule Table End -->
8283

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
# no-invalid-at-rule-placement
2+
3+
Disallow invalid placement of at-rules.
4+
5+
## Background
6+
7+
At-rules are CSS statements that instruct CSS how to behave. Some at-rules have strict placement requirements that must be followed for the stylesheet to work correctly. For example:
8+
9+
- The `@charset` rule must be placed at the very beginning of a stylesheet, before any other rules, comments, or whitespace.
10+
- The `@import` rule must be placed at the beginning of a stylesheet, before any other at-rules (except `@charset` and `@layer` statements) and style rules.
11+
12+
If these rules are placed incorrectly, browsers will ignore them, resulting in potential encoding issues or missing imported styles.
13+
14+
## Rule Details
15+
16+
This rule warns when it finds:
17+
18+
1. A `@charset` rule that is not the first rule in the stylesheet
19+
2. An `@import` rule that appears after any other at-rules or style rules (except `@charset` and `@layer` statements)
20+
21+
Examples of **incorrect** code:
22+
23+
```css
24+
/* eslint css/no-invalid-at-rule-placement: "error" */
25+
26+
/* @charset not at the beginning */
27+
@import "foo.css";
28+
@charset "utf-8";
29+
```
30+
31+
```css
32+
/* eslint css/no-invalid-at-rule-placement: "error" */
33+
34+
/* @import after style rules */
35+
a {
36+
color: red;
37+
}
38+
@import "foo.css";
39+
```
40+
41+
```css
42+
/* eslint css/no-invalid-at-rule-placement: "error" */
43+
44+
/* @import after @layer block */
45+
@layer base {
46+
}
47+
@import "bar.css";
48+
```
49+
50+
Examples of **correct** code:
51+
52+
```css
53+
/* eslint css/no-invalid-at-rule-placement: "error" */
54+
55+
@charset "utf-8"; /* @charset at the beginning */
56+
@import "foo.css";
57+
```
58+
59+
```css
60+
/* eslint css/no-invalid-at-rule-placement: "error" */
61+
62+
/* @import before style rules */
63+
@import "foo.css";
64+
a {
65+
color: red;
66+
}
67+
```
68+
69+
```css
70+
/* eslint css/no-invalid-at-rule-placement: "error" */
71+
72+
/* @import after @layer statement */
73+
@layer base;
74+
@import "baz.css";
75+
```
76+
77+
```css
78+
/* eslint css/no-invalid-at-rule-placement: "error" */
79+
80+
/* Multiple @import rules together */
81+
@import "foo.css";
82+
@import "bar.css";
83+
a {
84+
color: red;
85+
}
86+
```
87+
88+
## When Not to Use It
89+
90+
You can disable this rule if your stylesheets don't use `@charset` or `@import` rules, or if you're not concerned about the impact of incorrect placement on encoding and style loading.
91+
92+
## Prior Art
93+
94+
- [`no-invalid-position-at-import-rule`](https://stylelint.io/user-guide/rules/no-invalid-position-at-import-rule/)
95+
96+
## Further Reading
97+
98+
- [@charset - CSS | MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/@charset)
99+
- [@import - CSS | MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/@import)

src/index.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,10 @@ 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";
1414
import noImportant from "./rules/no-important.js";
15-
import noInvalidProperties from "./rules/no-invalid-properties.js";
15+
import noInvalidAtRulePlacement from "./rules/no-invalid-at-rule-placement.js";
1616
import noInvalidAtRules from "./rules/no-invalid-at-rules.js";
1717
import noInvalidNamedGridAreas from "./rules/no-invalid-named-grid-areas.js";
18+
import noInvalidProperties from "./rules/no-invalid-properties.js";
1819
import preferLogicalProperties from "./rules/prefer-logical-properties.js";
1920
import relativeFontUnits from "./rules/relative-font-units.js";
2021
import useLayers from "./rules/use-layers.js";
@@ -36,6 +37,7 @@ const plugin = {
3637
"no-empty-blocks": noEmptyBlocks,
3738
"no-duplicate-imports": noDuplicateImports,
3839
"no-important": noImportant,
40+
"no-invalid-at-rule-placement": noInvalidAtRulePlacement,
3941
"no-invalid-at-rules": noInvalidAtRules,
4042
"no-invalid-named-grid-areas": noInvalidNamedGridAreas,
4143
"no-invalid-properties": noInvalidProperties,
@@ -51,6 +53,7 @@ const plugin = {
5153
"css/no-empty-blocks": "error",
5254
"css/no-duplicate-imports": "error",
5355
"css/no-important": "error",
56+
"css/no-invalid-at-rule-placement": "error",
5457
"css/no-invalid-at-rules": "error",
5558
"css/no-invalid-named-grid-areas": "error",
5659
"css/no-invalid-properties": "error",
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/**
2+
* @fileoverview Rule to enforce correct placement of at-rules.
3+
* @author thecalamiity
4+
*/
5+
6+
//-----------------------------------------------------------------------------
7+
// Type Definitions
8+
//-----------------------------------------------------------------------------
9+
10+
/**
11+
* @import { CSSRuleDefinition } from "../types.js"
12+
* @typedef {"invalidCharsetPlacement" | "invalidImportPlacement"} NoInvalidAtRulePlacementMessageIds
13+
* @typedef {CSSRuleDefinition<{ RuleOptions: [], MessageIds: NoInvalidAtRulePlacementMessageIds }>} NoInvalidAtRulePlacementRuleDefinition
14+
*/
15+
16+
//-----------------------------------------------------------------------------
17+
// Rule Definition
18+
//-----------------------------------------------------------------------------
19+
20+
/** @type {NoInvalidAtRulePlacementRuleDefinition} */
21+
export default {
22+
meta: {
23+
type: "problem",
24+
25+
docs: {
26+
description: "Disallow invalid placement of at-rules",
27+
recommended: true,
28+
url: "https://github.com/eslint/css/blob/main/docs/rules/no-invalid-at-rule-placement.md",
29+
},
30+
31+
messages: {
32+
invalidCharsetPlacement:
33+
"@charset must be placed at the very beginning of the stylesheet, before any rules, comments, or whitespace.",
34+
invalidImportPlacement:
35+
"@import must be placed before all other rules, except @charset and @layer statements.",
36+
},
37+
},
38+
39+
create(context) {
40+
let hasSeenNonImportRule = false;
41+
let hasSeenLayerBlock = false;
42+
43+
return {
44+
Atrule(node) {
45+
const name = node.name.toLowerCase();
46+
47+
if (name === "charset") {
48+
if (
49+
node.loc.start.line !== 1 ||
50+
node.loc.start.column !== 1
51+
) {
52+
context.report({
53+
node,
54+
messageId: "invalidCharsetPlacement",
55+
});
56+
}
57+
return;
58+
}
59+
60+
if (name === "layer") {
61+
if (node.block) {
62+
hasSeenLayerBlock = true;
63+
}
64+
return;
65+
}
66+
67+
if (name === "import") {
68+
if (hasSeenNonImportRule || hasSeenLayerBlock) {
69+
context.report({
70+
node,
71+
messageId: "invalidImportPlacement",
72+
});
73+
}
74+
return;
75+
}
76+
77+
hasSeenNonImportRule = true;
78+
},
79+
80+
Rule() {
81+
hasSeenNonImportRule = true;
82+
},
83+
};
84+
},
85+
};

0 commit comments

Comments
 (0)