-
-
Notifications
You must be signed in to change notification settings - Fork 20
feat: add no-invalid-import-placement rule #156
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
# no-invalid-import-placement | ||
|
||
Disallow invalid placement of `@import` rules. | ||
|
||
## Background | ||
|
||
The `@import` rule must be placed at the beginning of a stylesheet, before any other at-rules (except `@charset` and `@layer`) and style rules. If placed elsewhere, browsers will ignore the `@import` rule, resulting in the imported styles being missing from the page. | ||
|
||
## Rule Details | ||
|
||
This rule warns when it finds an `@import` rule that appears after any other at-rules or style rules in a stylesheet (ignoring `@charset` and `@layer` rules). | ||
|
||
Examples of **incorrect** code: | ||
|
||
```css | ||
/* eslint css/no-invalid-import-placement: "error" */ | ||
|
||
/* @import after style rules */ | ||
a { | ||
color: red; | ||
} | ||
@import "foo.css"; | ||
``` | ||
|
||
```css | ||
/* eslint css/no-invalid-import-placement: "error" */ | ||
|
||
/* @import after at-rules */ | ||
@media screen { | ||
} | ||
@import "bar.css"; | ||
``` | ||
|
||
Examples of **correct** code: | ||
|
||
```css | ||
/* eslint css/no-invalid-import-placement: "error" */ | ||
|
||
/* @import at the beginning */ | ||
@import "foo.css"; | ||
a { | ||
color: red; | ||
} | ||
``` | ||
|
||
```css | ||
/* eslint css/no-invalid-import-placement: "error" */ | ||
|
||
/* @import after @charset */ | ||
@charset "utf-8"; | ||
@import "bar.css"; | ||
a { | ||
color: red; | ||
} | ||
``` | ||
|
||
```css | ||
/* eslint css/no-invalid-import-placement: "error" */ | ||
|
||
/* @import after @layer */ | ||
@layer base; | ||
@import "baz.css"; | ||
a { | ||
color: red; | ||
} | ||
``` | ||
|
||
```css | ||
/* eslint css/no-invalid-import-placement: "error" */ | ||
|
||
/* Multiple @import rules together */ | ||
@import "foo.css"; | ||
@import "bar.css"; | ||
a { | ||
color: red; | ||
} | ||
``` | ||
|
||
## When Not to Use It | ||
|
||
You can disable this rule if your stylesheets don't use `@import` or if you're not concerned about the impact of incorrect placement on style loading. | ||
|
||
## Prior Art | ||
|
||
- [`no-invalid-position-at-import-rule`](https://stylelint.io/user-guide/rules/no-invalid-position-at-import-rule/) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
/** | ||
* @fileoverview Rule to enforce correct placement of `@import` rules in CSS. | ||
* @author thecalamiity | ||
*/ | ||
|
||
//----------------------------------------------------------------------------- | ||
// Type Definitions | ||
//----------------------------------------------------------------------------- | ||
|
||
/** | ||
* @import { CSSRuleDefinition } from "../types.js" | ||
* @typedef {"invalidImportPlacement"} NoInvalidImportPlacementMessageIds | ||
* @typedef {CSSRuleDefinition<{ RuleOptions: [], MessageIds: NoInvalidImportPlacementMessageIds }>} NoInvalidImportPlacementRuleDefinition | ||
*/ | ||
|
||
//----------------------------------------------------------------------------- | ||
// Helpers | ||
//----------------------------------------------------------------------------- | ||
|
||
const IGNORED_AT_RULES = new Set(["charset", "layer"]); | ||
|
||
//----------------------------------------------------------------------------- | ||
// Rule Definition | ||
//----------------------------------------------------------------------------- | ||
|
||
/** @type {NoInvalidImportPlacementRuleDefinition} */ | ||
export default { | ||
meta: { | ||
type: "problem", | ||
|
||
docs: { | ||
description: "Disallow invalid placement of @import rules", | ||
recommended: true, | ||
url: "https://github.com/eslint/css/blob/main/docs/rules/no-invalid-import-placement.md", | ||
}, | ||
|
||
messages: { | ||
invalidImportPlacement: | ||
"@import must be placed before all other rules, except @charset and @layer.", | ||
}, | ||
}, | ||
|
||
create(context) { | ||
let hasSeenNonImportRule = false; | ||
|
||
return { | ||
Atrule(node) { | ||
const name = node.name.toLowerCase(); | ||
|
||
if (IGNORED_AT_RULES.has(name)) { | ||
return; | ||
} | ||
Comment on lines
+50
to
+52
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is not an invalid placement though? |
||
|
||
if (name === "import") { | ||
if (hasSeenNonImportRule) { | ||
context.report({ | ||
node, | ||
messageId: "invalidImportPlacement", | ||
}); | ||
} | ||
} else { | ||
hasSeenNonImportRule = true; | ||
} | ||
}, | ||
|
||
Rule() { | ||
hasSeenNonImportRule = true; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we set it to false again once the traversing is complete? |
||
}, | ||
}; | ||
}, | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,163 @@ | ||
/** | ||
* @fileoverview Tests for no-invalid-import-placement rule. | ||
* @author thecalamiity | ||
*/ | ||
|
||
//------------------------------------------------------------------------------ | ||
// Imports | ||
//------------------------------------------------------------------------------ | ||
|
||
import rule from "../../src/rules/no-invalid-import-placement.js"; | ||
import css from "../../src/index.js"; | ||
import { RuleTester } from "eslint"; | ||
import dedent from "dedent"; | ||
|
||
//------------------------------------------------------------------------------ | ||
// Tests | ||
//------------------------------------------------------------------------------ | ||
|
||
const ruleTester = new RuleTester({ | ||
plugins: { | ||
css, | ||
}, | ||
language: "css/css", | ||
}); | ||
|
||
ruleTester.run("no-invalid-import-placement", rule, { | ||
valid: [ | ||
"@import 'foo.css';", | ||
"@import url('foo.css');", | ||
"@import 'foo.css' screen;", | ||
"@import url('foo.css') supports(display: grid) screen and (max-width: 400px);", | ||
"@import 'foo.css' layer;", | ||
"@import 'foo.css' layer(base);", | ||
dedent` | ||
/* comment */ | ||
@import 'foo.css';`, | ||
dedent` | ||
@charset 'utf-8'; | ||
@import 'foo.css';`, | ||
dedent` | ||
@layer base; | ||
@import 'foo.css';`, | ||
dedent` | ||
@import 'foo.css'; | ||
@import 'bar.css';`, | ||
dedent` | ||
@CHARSET 'utf-8'; | ||
@LAYER base; | ||
@imPORT 'foo.css'; | ||
@import 'bar.css';`, | ||
dedent` | ||
@import 'foo.css'; | ||
a { color: red; }`, | ||
dedent` | ||
@charset 'UTF-8'; | ||
@layer base; | ||
@import 'foo.css'; | ||
a { color: red; }`, | ||
], | ||
invalid: [ | ||
{ | ||
code: dedent` | ||
a { color: red; } | ||
@import 'foo.css';`, | ||
errors: [ | ||
{ | ||
messageId: "invalidImportPlacement", | ||
line: 2, | ||
column: 1, | ||
endLine: 2, | ||
endColumn: 19, | ||
}, | ||
], | ||
}, | ||
{ | ||
code: dedent` | ||
@media screen { } | ||
@import url('foo.css');`, | ||
errors: [ | ||
{ | ||
messageId: "invalidImportPlacement", | ||
line: 2, | ||
column: 1, | ||
endLine: 2, | ||
endColumn: 24, | ||
}, | ||
], | ||
}, | ||
{ | ||
code: dedent` | ||
@media print { } | ||
@imPort URl('foo.css');`, | ||
errors: [ | ||
{ | ||
messageId: "invalidImportPlacement", | ||
line: 2, | ||
column: 1, | ||
endLine: 2, | ||
endColumn: 24, | ||
}, | ||
], | ||
}, | ||
{ | ||
code: dedent` | ||
@import 'foo.css'; | ||
a {} | ||
@import 'bar.css';`, | ||
errors: [ | ||
{ | ||
messageId: "invalidImportPlacement", | ||
line: 3, | ||
column: 1, | ||
endLine: 3, | ||
endColumn: 19, | ||
}, | ||
], | ||
}, | ||
{ | ||
code: dedent` | ||
a {} | ||
@import 'foo.css'; | ||
@import 'bar.css';`, | ||
errors: [ | ||
{ | ||
messageId: "invalidImportPlacement", | ||
line: 2, | ||
column: 1, | ||
endLine: 2, | ||
endColumn: 19, | ||
}, | ||
{ | ||
messageId: "invalidImportPlacement", | ||
line: 3, | ||
column: 1, | ||
endLine: 3, | ||
endColumn: 19, | ||
}, | ||
], | ||
}, | ||
{ | ||
code: dedent` | ||
@custom-rule {} | ||
@import | ||
'foo.css';`, | ||
languageOptions: { | ||
customSyntax: { | ||
atrules: { | ||
"custom-rule": {}, | ||
}, | ||
}, | ||
}, | ||
errors: [ | ||
{ | ||
messageId: "invalidImportPlacement", | ||
line: 2, | ||
column: 1, | ||
endLine: 3, | ||
endColumn: 11, | ||
}, | ||
], | ||
}, | ||
], | ||
}); |
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's also add the
ignoreAtRules
option similar to https://stylelint.io/user-guide/rules/no-invalid-position-at-import-rule/#ignoreatrules?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I considered it but didn’t find a need. Want it added anyway?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes, we should add it for easy migration.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We've decided to expand no-invalid-import-placement to no-invalid-at-rule-placement. Given this change, should we rename the ignoreAtRules option to something more specific, like ignoreAtRulesForImport?