Skip to content

Commit af043db

Browse files
authored
feat: add no-important rule (eslint#124)
* feat: add no-important rule * review suggestions * remove periods from list items
1 parent e020854 commit af043db

File tree

6 files changed

+432
-0
lines changed

6 files changed

+432
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ export default defineConfig([
6262
| :----------------------------------------------------------------------- | :------------------------------------ | :-------------: |
6363
| [`no-duplicate-imports`](./docs/rules/no-duplicate-imports.md) | Disallow duplicate @import rules | yes |
6464
| [`no-empty-blocks`](./docs/rules/no-empty-blocks.md) | Disallow empty blocks | yes |
65+
| [`no-important`](./docs/rules/no-important.md) | Disallow !important flags | yes |
6566
| [`no-invalid-at-rules`](./docs/rules/no-invalid-at-rules.md) | Disallow invalid at-rules | yes |
6667
| [`no-invalid-properties`](./docs/rules/no-invalid-properties.md) | Disallow invalid properties | yes |
6768
| [`prefer-logical-properties`](./docs/rules/prefer-logical-properties.md) | Enforce the use of logical properties | no |

docs/rules/no-important.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# no-important
2+
3+
Disallow `!important` flags.
4+
5+
## Background
6+
7+
The `!important` flag is a CSS declaration modifier that overrides normal cascade behavior, forcing a declaration to take precedence over competing rules—regardless of specificity or source order. While it can be necessary in rare cases (e.g., overriding third-party styles or enforcing accessibility fixes), it’s widely considered an anti-pattern because:
8+
9+
- It breaks the natural cascade of CSS
10+
- It makes debugging more difficult
11+
- It can lead to specificity wars where developers keep adding more `!important` declarations to override each other
12+
- It makes the code harder to maintain
13+
14+
## Rule Details
15+
16+
This rule warns when it detects the `!important` flag in declarations.
17+
18+
Examples of incorrect code:
19+
20+
```css
21+
.foo {
22+
color: red !important;
23+
}
24+
```
25+
26+
Examples of correct code:
27+
28+
```css
29+
.foo {
30+
color: red;
31+
}
32+
```
33+
34+
## When Not to Use It
35+
36+
You may disable this rule if you are using `!important` in these specific cases:
37+
38+
- Overriding third-party styles where you lack control over the source CSS
39+
- Fixing accessibility issues (e.g., enforcing focus states or color contrast)
40+
- Working with legacy code where refactoring isn’t feasible
41+
42+
## Prior Art
43+
44+
- [`important`](https://github.com/CSSLint/csslint/wiki/Disallow-%21important)
45+
- [`declaration-no-important`](https://stylelint.io/user-guide/rules/declaration-no-important/)

src/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ 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 noImportant from "./rules/no-important.js";
1415
import noInvalidProperties from "./rules/no-invalid-properties.js";
1516
import noInvalidAtRules from "./rules/no-invalid-at-rules.js";
1617
import preferLogicalProperties from "./rules/prefer-logical-properties.js";
@@ -32,6 +33,7 @@ const plugin = {
3233
rules: {
3334
"no-empty-blocks": noEmptyBlocks,
3435
"no-duplicate-imports": noDuplicateImports,
36+
"no-important": noImportant,
3537
"no-invalid-at-rules": noInvalidAtRules,
3638
"no-invalid-properties": noInvalidProperties,
3739
"prefer-logical-properties": preferLogicalProperties,
@@ -44,6 +46,7 @@ const plugin = {
4446
rules: /** @type {const} */ ({
4547
"css/no-empty-blocks": "error",
4648
"css/no-duplicate-imports": "error",
49+
"css/no-important": "error",
4750
"css/no-invalid-at-rules": "error",
4851
"css/no-invalid-properties": "error",
4952
"css/use-baseline": "warn",

src/rules/no-important.js

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/**
2+
* @fileoverview Rule to disallow `!important` flags.
3+
* @author thecalamiity
4+
* @author Yann Bertrand
5+
*/
6+
7+
//-----------------------------------------------------------------------------
8+
// Imports
9+
//-----------------------------------------------------------------------------
10+
11+
import { findOffsets } from "../util.js";
12+
13+
//-----------------------------------------------------------------------------
14+
// Type Definitions
15+
//-----------------------------------------------------------------------------
16+
17+
/**
18+
* @import { CSSRuleDefinition } from "../types.js"
19+
* @typedef {"unexpectedImportant"} NoImportantMessageIds
20+
* @typedef {CSSRuleDefinition<{ RuleOptions: [], MessageIds: NoImportantMessageIds }>} NoImportantRuleDefinition
21+
*/
22+
23+
//-----------------------------------------------------------------------------
24+
// Rule Definition
25+
//-----------------------------------------------------------------------------
26+
27+
/** @type {NoImportantRuleDefinition} */
28+
export default {
29+
meta: {
30+
type: "problem",
31+
32+
docs: {
33+
description: "Disallow !important flags",
34+
recommended: true,
35+
url: "https://github.com/eslint/css/blob/main/docs/rules/no-important.md",
36+
},
37+
38+
messages: {
39+
unexpectedImportant: "Unexpected !important flag found.",
40+
},
41+
},
42+
43+
create(context) {
44+
const importantPattern = /!(\s|\/\*.*?\*\/)*important/iu;
45+
46+
return {
47+
Declaration(node) {
48+
if (node.important) {
49+
const declarationText = context.sourceCode.getText(node);
50+
const importantMatch =
51+
importantPattern.exec(declarationText);
52+
53+
const {
54+
lineOffset: startLineOffset,
55+
columnOffset: startColumnOffset,
56+
} = findOffsets(declarationText, importantMatch.index);
57+
58+
const {
59+
lineOffset: endLineOffset,
60+
columnOffset: endColumnOffset,
61+
} = findOffsets(
62+
declarationText,
63+
importantMatch.index + importantMatch[0].length,
64+
);
65+
66+
const nodeStartLine = node.loc.start.line;
67+
const nodeStartColumn = node.loc.start.column;
68+
const startLine = nodeStartLine + startLineOffset;
69+
const endLine = nodeStartLine + endLineOffset;
70+
const startColumn =
71+
(startLine === nodeStartLine ? nodeStartColumn : 1) +
72+
startColumnOffset;
73+
const endColumn =
74+
(endLine === nodeStartLine ? nodeStartColumn : 1) +
75+
endColumnOffset;
76+
77+
context.report({
78+
loc: {
79+
start: {
80+
line: startLine,
81+
column: startColumn,
82+
},
83+
end: {
84+
line: endLine,
85+
column: endColumn,
86+
},
87+
},
88+
messageId: "unexpectedImportant",
89+
});
90+
}
91+
},
92+
};
93+
},
94+
};

src/util.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,28 @@
2323
export function isSyntaxMatchError(error) {
2424
return typeof error.syntax === "string";
2525
}
26+
27+
/**
28+
* Finds the line and column offsets for a given offset in a string.
29+
* @param {string} text The text to search.
30+
* @param {number} offset The offset to find.
31+
* @returns {{lineOffset:number,columnOffset:number}} The location of the offset.
32+
*/
33+
export function findOffsets(text, offset) {
34+
let lineOffset = 0;
35+
let columnOffset = 0;
36+
37+
for (let i = 0; i < offset; i++) {
38+
if (text[i] === "\n") {
39+
lineOffset++;
40+
columnOffset = 0;
41+
} else {
42+
columnOffset++;
43+
}
44+
}
45+
46+
return {
47+
lineOffset,
48+
columnOffset,
49+
};
50+
}

0 commit comments

Comments
 (0)