Skip to content

Commit

Permalink
feat: add no-important rule
Browse files Browse the repository at this point in the history
  • Loading branch information
anubra266 committed Feb 9, 2024
1 parent a43bf98 commit 9e43308
Show file tree
Hide file tree
Showing 7 changed files with 249 additions and 4 deletions.
5 changes: 5 additions & 0 deletions .changeset/tidy-rocks-tap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@pandacss/eslint-plugin": patch
---

Add `no-important` rule
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,11 +83,12 @@ Where rules are included in the configs `recommended`, or `all` it is indicated
- [`@pandacss/no-dynamic-styling`](docs/rules/no-dynamic-styling.md) `all`, `recommended`
- [`@pandacss/no-escape-hatch`](docs/rules/no-escape-hatch.md) `all`
- [`@pandacss/no-hardcoded-color`](docs/rules/no-hardcoded-color.md) `all`
- [`@pandacss/no-important`](docs/rules/no-important.md) `all`
- [`@pandacss/no-invalid-token-paths`](docs/rules/no-invalid-token-paths.md) `all`, `recommended`
- [`@pandacss/no-property-renaming`](docs/rules/no-property-renaming.md) `all`, `recommended`
- [`@pandacss/no-unsafe-token-fn-usage`](docs/rules/no-unsafe-token-fn-usage.md) `all`
- [`@pandacss/prefer-longhand-properties`](docs/rules/prefer-longhand-properties.md) `all`
- [`@pandacss/prefer-shorthand-properties`](docs/rules/prefer-shorthand-properties.md) `all`
- [`@pandacss/no-unsafe-token-fn-usage`](docs/rules/no-unsafe-token-fn-usage.md) `all`
- [`@pandacss/prefer-atomic-properties`](docs/rules/prefer-atomic-properties.md) `all`
- [`@pandacss/prefer-composite-properties`](docs/rules/prefer-composite-properties.md) `all`
- [`@pandacss/prefer-unified-property-style`](docs/rules/prefer-unified-property-style.md) `all`, `recommended`
Expand Down
60 changes: 60 additions & 0 deletions docs/rules/no-important.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
[//]: # (This file is generated by eslint-docgen. Do not edit it directly.)

# no-important

Disallow usage of important keyword. Prioroitize specificity for a maintainable and predictable styling structure.

📋 This rule is enabled in `plugin:@pandacss/all`.

## Rule details

❌ Examples of **incorrect** code:
```js
import { css } from './panda/css';

const styles = css({ marginLeft: '4px!' });
```
```js

import { css } from './panda/css';

function App(){
return <div className={css({ background: '#111 !important' })} />;
};
```
```js

import { Circle } from './panda/jsx';

function App(){
return <Circle _hover={{ position: 'absolute !' }} />;
}
```

✔️ Examples of **correct** code:
```js
import { css } from './panda/css';

const styles = css({ marginLeft: '4' });
```
```js

import { css } from './panda/css';

function App(){
return <div className={css({ background: 'red.100' })} />;
};
```
```js

import { Circle } from './panda/jsx';

function App(){
return <Circle _hover={{ position: 'absolute' }} />;
}
```

## Resources

* [Rule source](/plugin/src/rules/no-important.ts)
* [Test source](/tests/no-important.test.ts)
6 changes: 4 additions & 2 deletions plugin/src/rules/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ import noDebug, { RULE_NAME as NoDebug } from './no-debug'
import noDynamicStyling, { RULE_NAME as NoDynamicStyling } from './no-dynamic-styling'
import noEscapeHatch, { RULE_NAME as NoEscapeHatch } from './no-escape-hatch'
import noHardCodedColor, { RULE_NAME as NoHardCodedColor } from './no-hardcoded-color'
import noImportant, { RULE_NAME as NoImportant } from './no-important'
import noInvalidTokenPaths, { RULE_NAME as NoInvalidTokenPaths } from './no-invalid-token-paths'
import noPropertyRenaming, { RULE_NAME as NoPropertyRenaming } from './no-property-renaming'
import noUnsafeTokenUsage, { RULE_NAME as NoUnsafeTokenUsage } from './no-unsafe-token-fn-usage'
import preferLonghandProperties, { RULE_NAME as PreferLonghandProperties } from './prefer-longhand-properties'
import preferShorthandProperties, { RULE_NAME as PreferShorthandProperties } from './prefer-shorthand-properties'
import noUnsafeTokenUsage, { RULE_NAME as NoUnsafeTokenUsage } from './no-unsafe-token-fn-usage'
import preferAtomicProperties, { RULE_NAME as PreferAtomicProperties } from './prefer-atomic-properties'
import preferCompositeProperties, { RULE_NAME as PreferCompositeProperties } from './prefer-composite-properties'
import preferUnifiedPropertyStyle, { RULE_NAME as PreferUnifiedPropertyStyle } from './prefer-unified-property-style'
Expand All @@ -20,11 +21,12 @@ export const rules = {
[NoDynamicStyling]: noDynamicStyling,
[NoEscapeHatch]: noEscapeHatch,
[NoHardCodedColor]: noHardCodedColor,
[NoImportant]: noImportant,
[NoInvalidTokenPaths]: noInvalidTokenPaths,
[NoPropertyRenaming]: noPropertyRenaming,
[NoUnsafeTokenUsage]: noUnsafeTokenUsage,
[PreferLonghandProperties]: preferLonghandProperties,
[PreferShorthandProperties]: preferShorthandProperties,
[NoUnsafeTokenUsage]: noUnsafeTokenUsage,
[PreferAtomicProperties]: preferAtomicProperties,
[PreferCompositeProperties]: preferCompositeProperties,
[PreferUnifiedPropertyStyle]: preferUnifiedPropertyStyle,
Expand Down
109 changes: 109 additions & 0 deletions plugin/src/rules/no-important.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { isPandaAttribute, isPandaProp } from '../utils/helpers'
import { type Rule, createRule } from '../utils'
import { isIdentifier, isJSXExpressionContainer, isLiteral, isTemplateLiteral, type Node } from '../utils/nodes'

// Check if the string ends with '!' with optional whitespace before it
const exclamationRegex = /\s*!$/
// Check if the string ends with '!important' with optional whitespace before it and after, but not within '!important'
const importantRegex = /\s*!important\s*$/

export const RULE_NAME = 'no-important'

const rule: Rule = createRule({
name: RULE_NAME,
meta: {
docs: {
description:
'Disallow usage of important keyword. Prioroitize specificity for a maintainable and predictable styling structure.',
},
messages: {
important:
'Avoid using the !important keyword. Refactor your code to prioritize specificity for predictable styling.',
remove: 'Remove the `{{keyword}}` keyword.',
},
type: 'suggestion',
hasSuggestions: true,
schema: [],
},
defaultOptions: [],
create(context) {
const removeQuotes = ([start, end]: readonly [number, number]) => [start + 1, end - 1] as const

const hasImportantKeyword = (value?: string) => {
if (!value) return false
return exclamationRegex.test(value) || importantRegex.test(value)
}

const removeImportantKeyword = (input: string) => {
if (exclamationRegex.test(input)) {
// Remove trailing '!'
return { fixed: input.replace(exclamationRegex, ''), keyword: '!' }
} else if (importantRegex.test(input)) {
// Remove '!important' with optional whitespace
return { fixed: input.replace(importantRegex, ''), keyword: '!important' }
} else {
// No match, return the original string
return { fixed: input, keyword: null }
}
}

const handleLiteral = (node: Node) => {
if (!isLiteral(node)) return
if (!hasImportantKeyword(node.value?.toString())) return

sendReport(node)
}

const handleTemplateLiteral = (node: Node) => {
if (!isTemplateLiteral(node)) return
if (node.expressions.length > 0) return
if (!hasImportantKeyword(node.quasis[0].value.raw)) return

sendReport(node.quasis[0], node.quasis[0].value.raw)
}

const sendReport = (node: any, _value?: string) => {
const value = _value ?? node.value?.toString()
const { keyword, fixed } = removeImportantKeyword(value)

return context.report({
node,
messageId: 'important',
suggest: [
{
messageId: 'remove',
data: { keyword },
fix: (fixer) => {
return fixer.replaceTextRange(removeQuotes(node.range), fixed)
},
},
],
})
}

return {
JSXAttribute(node) {
if (!node.value) return
if (!isPandaProp(node, context)) return

handleLiteral(node.value)

if (!isJSXExpressionContainer(node.value)) return

handleLiteral(node.value.expression)
handleTemplateLiteral(node.value.expression)
},

Property(node) {
if (!isIdentifier(node.key)) return
if (!isLiteral(node.value) && !isTemplateLiteral(node.value)) return
if (!isPandaAttribute(node, context)) return

handleLiteral(node.value)
handleTemplateLiteral(node.value)
},
}
},
})

export default rule
68 changes: 68 additions & 0 deletions plugin/tests/no-important.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { tester } from '../test-utils'
import rule, { RULE_NAME } from '../src/rules/no-important'

const javascript = String.raw

const valids = [
{
code: javascript`
import { css } from './panda/css';
const styles = css({ marginLeft: '4' })`,
},

{
code: javascript`
import { css } from './panda/css';
function App(){
return <div className={css({ background: 'red.100' })} />;
}`,
},

{
code: javascript`
import { Circle } from './panda/jsx';
function App(){
return <Circle _hover={{ position: 'absolute' }} />;
}`,
},
]

const invalids = [
{
code: javascript`
import { css } from './panda/css';
const styles = css({ marginLeft: '4px!' })`,
},

{
code: javascript`
import { css } from './panda/css';
function App(){
return <div className={css({ background: '#111 !important' })} />;
}`,
},

{
code: javascript`
import { Circle } from './panda/jsx';
function App(){
return <Circle _hover={{ position: 'absolute !' }} />;
}`,
},
]

tester.run(RULE_NAME, rule, {
valid: valids.map(({ code }) => ({
code,
})),
invalid: invalids.map(({ code }) => ({
code,
errors: 1,
})),
})
2 changes: 1 addition & 1 deletion sandbox/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ function App() {
className={stack({
debug: true,
padding: '40px',
align: 'stretch',
align: 'stretch !',
color: '#111',
background: 'red',
backgroundColor: color,
Expand Down

0 comments on commit 9e43308

Please sign in to comment.