Skip to content

Commit 9e43308

Browse files
committed
feat: add no-important rule
1 parent a43bf98 commit 9e43308

File tree

7 files changed

+249
-4
lines changed

7 files changed

+249
-4
lines changed

.changeset/tidy-rocks-tap.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@pandacss/eslint-plugin": patch
3+
---
4+
5+
Add `no-important` rule

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,11 +83,12 @@ Where rules are included in the configs `recommended`, or `all` it is indicated
8383
- [`@pandacss/no-dynamic-styling`](docs/rules/no-dynamic-styling.md) `all`, `recommended`
8484
- [`@pandacss/no-escape-hatch`](docs/rules/no-escape-hatch.md) `all`
8585
- [`@pandacss/no-hardcoded-color`](docs/rules/no-hardcoded-color.md) `all`
86+
- [`@pandacss/no-important`](docs/rules/no-important.md) `all`
8687
- [`@pandacss/no-invalid-token-paths`](docs/rules/no-invalid-token-paths.md) `all`, `recommended`
8788
- [`@pandacss/no-property-renaming`](docs/rules/no-property-renaming.md) `all`, `recommended`
89+
- [`@pandacss/no-unsafe-token-fn-usage`](docs/rules/no-unsafe-token-fn-usage.md) `all`
8890
- [`@pandacss/prefer-longhand-properties`](docs/rules/prefer-longhand-properties.md) `all`
8991
- [`@pandacss/prefer-shorthand-properties`](docs/rules/prefer-shorthand-properties.md) `all`
90-
- [`@pandacss/no-unsafe-token-fn-usage`](docs/rules/no-unsafe-token-fn-usage.md) `all`
9192
- [`@pandacss/prefer-atomic-properties`](docs/rules/prefer-atomic-properties.md) `all`
9293
- [`@pandacss/prefer-composite-properties`](docs/rules/prefer-composite-properties.md) `all`
9394
- [`@pandacss/prefer-unified-property-style`](docs/rules/prefer-unified-property-style.md) `all`, `recommended`

docs/rules/no-important.md

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
[//]: # (This file is generated by eslint-docgen. Do not edit it directly.)
2+
3+
# no-important
4+
5+
Disallow usage of important keyword. Prioroitize specificity for a maintainable and predictable styling structure.
6+
7+
📋 This rule is enabled in `plugin:@pandacss/all`.
8+
9+
## Rule details
10+
11+
❌ Examples of **incorrect** code:
12+
```js
13+
import { css } from './panda/css';
14+
15+
const styles = css({ marginLeft: '4px!' });
16+
```
17+
```js
18+
19+
import { css } from './panda/css';
20+
21+
function App(){
22+
return <div className={css({ background: '#111 !important' })} />;
23+
};
24+
```
25+
```js
26+
27+
import { Circle } from './panda/jsx';
28+
29+
function App(){
30+
return <Circle _hover={{ position: 'absolute !' }} />;
31+
}
32+
```
33+
34+
✔️ Examples of **correct** code:
35+
```js
36+
import { css } from './panda/css';
37+
38+
const styles = css({ marginLeft: '4' });
39+
```
40+
```js
41+
42+
import { css } from './panda/css';
43+
44+
function App(){
45+
return <div className={css({ background: 'red.100' })} />;
46+
};
47+
```
48+
```js
49+
50+
import { Circle } from './panda/jsx';
51+
52+
function App(){
53+
return <Circle _hover={{ position: 'absolute' }} />;
54+
}
55+
```
56+
57+
## Resources
58+
59+
* [Rule source](/plugin/src/rules/no-important.ts)
60+
* [Test source](/tests/no-important.test.ts)

plugin/src/rules/index.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@ import noDebug, { RULE_NAME as NoDebug } from './no-debug'
44
import noDynamicStyling, { RULE_NAME as NoDynamicStyling } from './no-dynamic-styling'
55
import noEscapeHatch, { RULE_NAME as NoEscapeHatch } from './no-escape-hatch'
66
import noHardCodedColor, { RULE_NAME as NoHardCodedColor } from './no-hardcoded-color'
7+
import noImportant, { RULE_NAME as NoImportant } from './no-important'
78
import noInvalidTokenPaths, { RULE_NAME as NoInvalidTokenPaths } from './no-invalid-token-paths'
89
import noPropertyRenaming, { RULE_NAME as NoPropertyRenaming } from './no-property-renaming'
10+
import noUnsafeTokenUsage, { RULE_NAME as NoUnsafeTokenUsage } from './no-unsafe-token-fn-usage'
911
import preferLonghandProperties, { RULE_NAME as PreferLonghandProperties } from './prefer-longhand-properties'
1012
import preferShorthandProperties, { RULE_NAME as PreferShorthandProperties } from './prefer-shorthand-properties'
11-
import noUnsafeTokenUsage, { RULE_NAME as NoUnsafeTokenUsage } from './no-unsafe-token-fn-usage'
1213
import preferAtomicProperties, { RULE_NAME as PreferAtomicProperties } from './prefer-atomic-properties'
1314
import preferCompositeProperties, { RULE_NAME as PreferCompositeProperties } from './prefer-composite-properties'
1415
import preferUnifiedPropertyStyle, { RULE_NAME as PreferUnifiedPropertyStyle } from './prefer-unified-property-style'
@@ -20,11 +21,12 @@ export const rules = {
2021
[NoDynamicStyling]: noDynamicStyling,
2122
[NoEscapeHatch]: noEscapeHatch,
2223
[NoHardCodedColor]: noHardCodedColor,
24+
[NoImportant]: noImportant,
2325
[NoInvalidTokenPaths]: noInvalidTokenPaths,
2426
[NoPropertyRenaming]: noPropertyRenaming,
27+
[NoUnsafeTokenUsage]: noUnsafeTokenUsage,
2528
[PreferLonghandProperties]: preferLonghandProperties,
2629
[PreferShorthandProperties]: preferShorthandProperties,
27-
[NoUnsafeTokenUsage]: noUnsafeTokenUsage,
2830
[PreferAtomicProperties]: preferAtomicProperties,
2931
[PreferCompositeProperties]: preferCompositeProperties,
3032
[PreferUnifiedPropertyStyle]: preferUnifiedPropertyStyle,

plugin/src/rules/no-important.ts

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import { isPandaAttribute, isPandaProp } from '../utils/helpers'
2+
import { type Rule, createRule } from '../utils'
3+
import { isIdentifier, isJSXExpressionContainer, isLiteral, isTemplateLiteral, type Node } from '../utils/nodes'
4+
5+
// Check if the string ends with '!' with optional whitespace before it
6+
const exclamationRegex = /\s*!$/
7+
// Check if the string ends with '!important' with optional whitespace before it and after, but not within '!important'
8+
const importantRegex = /\s*!important\s*$/
9+
10+
export const RULE_NAME = 'no-important'
11+
12+
const rule: Rule = createRule({
13+
name: RULE_NAME,
14+
meta: {
15+
docs: {
16+
description:
17+
'Disallow usage of important keyword. Prioroitize specificity for a maintainable and predictable styling structure.',
18+
},
19+
messages: {
20+
important:
21+
'Avoid using the !important keyword. Refactor your code to prioritize specificity for predictable styling.',
22+
remove: 'Remove the `{{keyword}}` keyword.',
23+
},
24+
type: 'suggestion',
25+
hasSuggestions: true,
26+
schema: [],
27+
},
28+
defaultOptions: [],
29+
create(context) {
30+
const removeQuotes = ([start, end]: readonly [number, number]) => [start + 1, end - 1] as const
31+
32+
const hasImportantKeyword = (value?: string) => {
33+
if (!value) return false
34+
return exclamationRegex.test(value) || importantRegex.test(value)
35+
}
36+
37+
const removeImportantKeyword = (input: string) => {
38+
if (exclamationRegex.test(input)) {
39+
// Remove trailing '!'
40+
return { fixed: input.replace(exclamationRegex, ''), keyword: '!' }
41+
} else if (importantRegex.test(input)) {
42+
// Remove '!important' with optional whitespace
43+
return { fixed: input.replace(importantRegex, ''), keyword: '!important' }
44+
} else {
45+
// No match, return the original string
46+
return { fixed: input, keyword: null }
47+
}
48+
}
49+
50+
const handleLiteral = (node: Node) => {
51+
if (!isLiteral(node)) return
52+
if (!hasImportantKeyword(node.value?.toString())) return
53+
54+
sendReport(node)
55+
}
56+
57+
const handleTemplateLiteral = (node: Node) => {
58+
if (!isTemplateLiteral(node)) return
59+
if (node.expressions.length > 0) return
60+
if (!hasImportantKeyword(node.quasis[0].value.raw)) return
61+
62+
sendReport(node.quasis[0], node.quasis[0].value.raw)
63+
}
64+
65+
const sendReport = (node: any, _value?: string) => {
66+
const value = _value ?? node.value?.toString()
67+
const { keyword, fixed } = removeImportantKeyword(value)
68+
69+
return context.report({
70+
node,
71+
messageId: 'important',
72+
suggest: [
73+
{
74+
messageId: 'remove',
75+
data: { keyword },
76+
fix: (fixer) => {
77+
return fixer.replaceTextRange(removeQuotes(node.range), fixed)
78+
},
79+
},
80+
],
81+
})
82+
}
83+
84+
return {
85+
JSXAttribute(node) {
86+
if (!node.value) return
87+
if (!isPandaProp(node, context)) return
88+
89+
handleLiteral(node.value)
90+
91+
if (!isJSXExpressionContainer(node.value)) return
92+
93+
handleLiteral(node.value.expression)
94+
handleTemplateLiteral(node.value.expression)
95+
},
96+
97+
Property(node) {
98+
if (!isIdentifier(node.key)) return
99+
if (!isLiteral(node.value) && !isTemplateLiteral(node.value)) return
100+
if (!isPandaAttribute(node, context)) return
101+
102+
handleLiteral(node.value)
103+
handleTemplateLiteral(node.value)
104+
},
105+
}
106+
},
107+
})
108+
109+
export default rule

plugin/tests/no-important.test.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { tester } from '../test-utils'
2+
import rule, { RULE_NAME } from '../src/rules/no-important'
3+
4+
const javascript = String.raw
5+
6+
const valids = [
7+
{
8+
code: javascript`
9+
import { css } from './panda/css';
10+
11+
const styles = css({ marginLeft: '4' })`,
12+
},
13+
14+
{
15+
code: javascript`
16+
import { css } from './panda/css';
17+
18+
function App(){
19+
return <div className={css({ background: 'red.100' })} />;
20+
}`,
21+
},
22+
23+
{
24+
code: javascript`
25+
import { Circle } from './panda/jsx';
26+
27+
function App(){
28+
return <Circle _hover={{ position: 'absolute' }} />;
29+
}`,
30+
},
31+
]
32+
33+
const invalids = [
34+
{
35+
code: javascript`
36+
import { css } from './panda/css';
37+
38+
const styles = css({ marginLeft: '4px!' })`,
39+
},
40+
41+
{
42+
code: javascript`
43+
import { css } from './panda/css';
44+
45+
function App(){
46+
return <div className={css({ background: '#111 !important' })} />;
47+
}`,
48+
},
49+
50+
{
51+
code: javascript`
52+
import { Circle } from './panda/jsx';
53+
54+
function App(){
55+
return <Circle _hover={{ position: 'absolute !' }} />;
56+
}`,
57+
},
58+
]
59+
60+
tester.run(RULE_NAME, rule, {
61+
valid: valids.map(({ code }) => ({
62+
code,
63+
})),
64+
invalid: invalids.map(({ code }) => ({
65+
code,
66+
errors: 1,
67+
})),
68+
})

sandbox/src/App.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ function App() {
4545
className={stack({
4646
debug: true,
4747
padding: '40px',
48-
align: 'stretch',
48+
align: 'stretch !',
4949
color: '#111',
5050
background: 'red',
5151
backgroundColor: color,

0 commit comments

Comments
 (0)