Skip to content

Commit 422c96c

Browse files
committed
feat: no-deprecated-tokens
1 parent d90f2c4 commit 422c96c

File tree

11 files changed

+282
-11
lines changed

11 files changed

+282
-11
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ Where rules are included in the configs `recommended`, or `all` it is indicated
117117
| [`@pandacss/file-not-included`](docs/rules/file-not-included.md) | ✔️ |
118118
| [`@pandacss/no-config-function-in-source`](docs/rules/no-config-function-in-source.md) | ✔️ |
119119
| [`@pandacss/no-debug`](docs/rules/no-debug.md) | ✔️ |
120+
| [`@pandacss/no-deprecated-tokens`](docs/rules/no-deprecated-tokens.md) | ✔️ |
120121
| [`@pandacss/no-dynamic-styling`](docs/rules/no-dynamic-styling.md) | ✔️ |
121122
| [`@pandacss/no-escape-hatch`](docs/rules/no-escape-hatch.md) | |
122123
| [`@pandacss/no-hardcoded-color`](docs/rules/no-hardcoded-color.md) ⚙️ | ✔️ |

docs/rules/no-deprecated-tokens.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
[//]: # (This file is generated by eslint-docgen. Do not edit it directly.)
2+
3+
# no-deprecated-tokens
4+
5+
Disallow the use of deprecated tokens within token function syntax.
6+
7+
📋 This rule is enabled in `plugin:@pandacss/all`.
8+
9+
📋 This rule is enabled in `plugin:@pandacss/recommended`.
10+
11+
## Rule details
12+
13+
❌ Examples of **incorrect** code:
14+
```js
15+
import { css } from './panda/css';
16+
17+
// Assumes that the token is deprecated
18+
const styles = css({ color: 'red.400' })
19+
```
20+
21+
✔️ Examples of **correct** code:
22+
```js
23+
import { css } from './panda/css';
24+
25+
const styles = css({ color: 'red.100' })
26+
```
27+
28+
## Resources
29+
30+
* [Rule source](/plugin/src/rules/no-deprecated-tokens.ts)
31+
* [Test source](/plugin/tests/no-deprecated-tokens.test.ts)

fixture/src/create-context.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { PandaContext } from '@pandacss/node'
44
import { stringifyJson, parseJson } from '@pandacss/shared'
55
import type { Config, LoadConfigResult, UserConfig } from '@pandacss/types'
66
import { fixturePreset } from './config'
7+
import v9Config from '../../sandbox/v9/panda.config'
78

89
const config: UserConfig = {
910
...fixturePreset,
@@ -34,9 +35,7 @@ export const createGeneratorContext = (userConfig?: Config) => {
3435
}
3536

3637
export const createContext = (userConfig?: Config) => {
37-
const resolvedConfig = (
38-
userConfig ? mergeConfigs([fixtureDefaults.config, userConfig]) : fixtureDefaults.config
39-
) as UserConfig
38+
const resolvedConfig = mergeConfigs([fixtureDefaults.config, v9Config, { importMap: './panda' }]) as UserConfig
4039

4140
return new PandaContext({
4241
...fixtureDefaults,

plugin/src/configs/recommended.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,6 @@ export default {
1212
'@pandacss/no-invalid-token-paths': 'error',
1313
'@pandacss/no-property-renaming': 'warn',
1414
'@pandacss/no-unsafe-token-fn-usage': 'warn',
15+
'@pandacss/no-deprecated-tokens': 'warn',
1516
},
1617
}

plugin/src/rules/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import fileNotIncluded, { RULE_NAME as FileNotIncluded } from './file-not-included'
22
import noConfigunctionInSource, { RULE_NAME as NoConfigunctionInSource } from './no-config-function-in-source'
33
import noDebug, { RULE_NAME as NoDebug } from './no-debug'
4+
import noDeprecatedTokens, { RULE_NAME as NoDeprecatedTokens } from './no-deprecated-tokens'
45
import noDynamicStyling, { RULE_NAME as NoDynamicStyling } from './no-dynamic-styling'
56
import noEscapeHatch, { RULE_NAME as NoEscapeHatch } from './no-escape-hatch'
67
import noHardCodedColor, { RULE_NAME as NoHardCodedColor } from './no-hardcoded-color'
@@ -21,6 +22,7 @@ export const rules = {
2122
[FileNotIncluded]: fileNotIncluded,
2223
[NoConfigunctionInSource]: noConfigunctionInSource,
2324
[NoDebug]: noDebug,
25+
[NoDeprecatedTokens]: noDeprecatedTokens,
2426
[NoDynamicStyling]: noDynamicStyling,
2527
[NoEscapeHatch]: noEscapeHatch,
2628
[NoHardCodedColor]: noHardCodedColor,
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import {
2+
getDeprecatedTokens,
3+
getTaggedTemplateCaller,
4+
isPandaAttribute,
5+
isPandaIsh,
6+
isPandaProp,
7+
isRecipeVariant,
8+
} from '../utils/helpers'
9+
import { type Rule, createRule } from '../utils'
10+
import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/utils'
11+
import { isNodeOfTypes } from '@typescript-eslint/utils/ast-utils'
12+
import { isIdentifier, isJSXExpressionContainer, isLiteral, isTemplateLiteral } from '../utils/nodes'
13+
import type { DeprecatedToken } from '../utils/worker'
14+
15+
export const RULE_NAME = 'no-deprecated-tokens'
16+
17+
const rule: Rule = createRule({
18+
name: RULE_NAME,
19+
meta: {
20+
docs: {
21+
description: 'Disallow the use of deprecated tokens within token function syntax.',
22+
},
23+
messages: {
24+
noDeprecatedTokenPaths: '`{{token}}` is a deprecated token.',
25+
noDeprecatedTokens: '`{{token}}` is a deprecated {{category}} token.',
26+
},
27+
type: 'problem',
28+
schema: [],
29+
},
30+
defaultOptions: [],
31+
create(context) {
32+
// Cache for deprecated tokens to avoid redundant computations
33+
const deprecatedTokensCache = new Map<string, DeprecatedToken[]>()
34+
35+
const sendReport = (prop: string, node: TSESTree.Node, value: string | undefined) => {
36+
if (!value) return
37+
38+
let tokens: DeprecatedToken[] | undefined = deprecatedTokensCache.get(value)
39+
if (!tokens) {
40+
tokens = getDeprecatedTokens(prop, value, context)
41+
deprecatedTokensCache.set(value, tokens)
42+
}
43+
44+
if (tokens.length === 0) return
45+
46+
tokens.forEach((token) => {
47+
context.report({
48+
node,
49+
messageId: typeof token === 'string' ? 'noDeprecatedTokenPaths' : 'noDeprecatedTokens',
50+
data: {
51+
token: typeof token === 'string' ? token : token.value,
52+
category: typeof token === 'string' ? undefined : token.category,
53+
},
54+
})
55+
})
56+
}
57+
58+
const handleLiteralOrTemplate = (prop: string, node: TSESTree.Node | undefined) => {
59+
if (!node) return
60+
61+
if (isLiteral(node)) {
62+
const value = node.value?.toString()
63+
sendReport(prop, node, value)
64+
} else if (isTemplateLiteral(node) && node.expressions.length === 0) {
65+
const value = node.quasis[0].value.raw
66+
sendReport(prop, node.quasis[0], value)
67+
}
68+
}
69+
70+
return {
71+
JSXAttribute(node: TSESTree.JSXAttribute) {
72+
if (!node.value || !isPandaProp(node, context)) return
73+
74+
const prop = node.name.name as string
75+
76+
if (isLiteral(node.value)) {
77+
handleLiteralOrTemplate(prop, node.value)
78+
} else if (isJSXExpressionContainer(node.value)) {
79+
handleLiteralOrTemplate(prop, node.value.expression)
80+
}
81+
},
82+
83+
Property(node: TSESTree.Property) {
84+
if (
85+
!isIdentifier(node.key) ||
86+
!isNodeOfTypes([AST_NODE_TYPES.Literal, AST_NODE_TYPES.TemplateLiteral])(node.value) ||
87+
!isPandaAttribute(node, context) ||
88+
isRecipeVariant(node, context)
89+
) {
90+
return
91+
}
92+
93+
const prop = node.key.name as string
94+
95+
handleLiteralOrTemplate(prop, node.value)
96+
},
97+
98+
TaggedTemplateExpression(node: TSESTree.TaggedTemplateExpression) {
99+
const caller = getTaggedTemplateCaller(node)
100+
if (!caller || !isPandaIsh(caller, context)) return
101+
102+
const quasis = node.quasi.quasis
103+
quasis.forEach((quasi) => {
104+
const styles = quasi.value.raw
105+
if (!styles) return
106+
107+
// Use the same pattern as sendReport function
108+
let tokens: DeprecatedToken[] | undefined = deprecatedTokensCache.get(styles)
109+
if (!tokens) {
110+
// For template literals, we don't have a specific prop, so we use an empty string
111+
tokens = getDeprecatedTokens('', styles, context)
112+
deprecatedTokensCache.set(styles, tokens)
113+
}
114+
115+
if (tokens.length === 0) return
116+
117+
tokens.forEach((token) => {
118+
const tokenValue = typeof token === 'string' ? token : token.value
119+
let index = styles.indexOf(tokenValue)
120+
121+
while (index !== -1) {
122+
const start = quasi.range[0] + index + 1 // +1 for the backtick
123+
const end = start + tokenValue.length
124+
125+
context.report({
126+
loc: {
127+
start: context.sourceCode.getLocFromIndex(start),
128+
end: context.sourceCode.getLocFromIndex(end),
129+
},
130+
messageId: typeof token === 'string' ? 'noDeprecatedTokenPaths' : 'noDeprecatedTokens',
131+
data: {
132+
token: tokenValue,
133+
category: typeof token === 'string' ? undefined : token.category,
134+
},
135+
})
136+
137+
// Check for other occurrences of the deprecated token
138+
index = styles.indexOf(tokenValue, index + tokenValue.length)
139+
}
140+
})
141+
})
142+
},
143+
}
144+
},
145+
})
146+
147+
export default rule

plugin/src/utils/helpers.ts

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
isVariableDeclarator,
1919
type Node,
2020
} from './nodes'
21+
import type { DeprecatedToken } from './worker'
2122

2223
export const getAncestor = <N extends Node>(ofType: (node: Node) => node is N, for_: Node): N | undefined => {
2324
let current: Node | undefined = for_.parent
@@ -68,7 +69,7 @@ const _getImports = (context: RuleContext<any, any>) => {
6869
const specifiers = getImportSpecifiers(context)
6970

7071
const imports: ImportResult[] = specifiers.map(({ specifier, mod }) => ({
71-
name: specifier.imported.name,
72+
name: (specifier.imported as any).name,
7273
alias: specifier.local.name,
7374
mod,
7475
}))
@@ -244,9 +245,6 @@ export const isColorToken = (value: string | undefined, context: RuleContext<any
244245
return syncAction('isColorToken', getSyncOpts(context), value)
245246
}
246247

247-
// Caching invalid tokens to avoid redundant computations
248-
const invalidTokensCache = new Map<string, string[]>()
249-
250248
export const extractTokens = (value: string) => {
251249
const regex = /token\(([^"'(),]+)(?:,\s*([^"'(),]+))?\)|\{([^{\r\n}]+)\}/g
252250
const matches = []
@@ -268,6 +266,9 @@ export const extractTokens = (value: string) => {
268266
return matches.filter(Boolean)
269267
}
270268

269+
// Caching invalid tokens to avoid redundant computations
270+
const invalidTokensCache = new Map<string, string[]>()
271+
271272
export const getInvalidTokens = (value: string, context: RuleContext<any, any>) => {
272273
if (invalidTokensCache.has(value)) {
273274
return invalidTokensCache.get(value)!
@@ -281,6 +282,28 @@ export const getInvalidTokens = (value: string, context: RuleContext<any, any>)
281282
return invalidTokens
282283
}
283284

285+
// Caching deprecated tokens to avoid redundant computations
286+
const deprecatedTokensCache = new Map<string, DeprecatedToken[]>()
287+
288+
export const getDeprecatedTokens = (prop: string, value: string, context: RuleContext<any, any>) => {
289+
const propCategory = syncAction('getPropCategory', getSyncOpts(context), prop)
290+
291+
const tokens = extractTokens(value)
292+
293+
if (!propCategory && !tokens.length) return []
294+
295+
const values = tokens.length ? tokens : [{ category: propCategory, value: value.split('/')[0] }]
296+
297+
if (deprecatedTokensCache.has(value)) {
298+
return deprecatedTokensCache.get(value)!
299+
}
300+
301+
const deprecatedTokens = syncAction('filterDeprecatedTokens', getSyncOpts(context), values)
302+
deprecatedTokensCache.set(value, deprecatedTokens)
303+
304+
return deprecatedTokens
305+
}
306+
284307
export const getTokenImport = (context: RuleContext<any, any>) => {
285308
const imports = _getImports(context)
286309
return imports.find((imp) => imp.name === 'token')

plugin/src/utils/worker.ts

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ async function _getContext(configPath: string | undefined) {
2626
export async function getContext(opts: Opts) {
2727
if (process.env.NODE_ENV === 'test') {
2828
configPath = opts.configPath
29-
const ctx = createContext({ importMap: './panda' }) as unknown as PandaContext
29+
const ctx = createContext() as unknown as PandaContext
3030
ctx.getFiles = () => ['App.tsx']
3131
return ctx
3232
} else {
@@ -45,15 +45,34 @@ async function filterInvalidTokens(ctx: PandaContext, paths: string[]): Promise<
4545
return paths.filter((path) => !ctx.utility.tokens.view.get(path))
4646
}
4747

48+
export type DeprecatedToken =
49+
| string
50+
| {
51+
category: string
52+
value: string
53+
}
54+
55+
async function filterDeprecatedTokens(ctx: PandaContext, tokens: DeprecatedToken[]): Promise<DeprecatedToken[]> {
56+
return tokens.filter((token) => {
57+
const value = typeof token === 'string' ? token : token.category + '.' + token.value
58+
return ctx.utility.tokens.isDeprecated(value)
59+
})
60+
}
61+
4862
async function isColorToken(ctx: PandaContext, value: string): Promise<boolean> {
4963
return !!ctx.utility.tokens.view.categoryMap.get('colors')?.get(value)
5064
}
5165

52-
async function isColorAttribute(ctx: PandaContext, _attr: string): Promise<boolean> {
66+
async function getPropCategory(ctx: PandaContext, _attr: string) {
5367
const longhand = await resolveLongHand(ctx, _attr)
5468
const attr = longhand || _attr
5569
const attrConfig = ctx.utility.config[attr]
56-
return attrConfig?.values === 'colors'
70+
return typeof attrConfig?.values === 'string' ? attrConfig.values : undefined
71+
}
72+
73+
async function isColorAttribute(ctx: PandaContext, _attr: string): Promise<boolean> {
74+
const category = await getPropCategory(ctx, _attr)
75+
return category === 'colors'
5776
}
5877

5978
const arePathsEqual = (path1: string, path2: string) => {
@@ -121,6 +140,12 @@ export function runAsync(action: 'resolveLongHand', opts: Opts, name: string): P
121140
export function runAsync(action: 'isValidProperty', opts: Opts, name: string, patternName?: string): Promise<boolean>
122141
export function runAsync(action: 'matchFile', opts: Opts, name: string, imports: ImportResult[]): Promise<boolean>
123142
export function runAsync(action: 'matchImports', opts: Opts, result: MatchImportResult): Promise<boolean>
143+
export function runAsync(action: 'getPropCategory', opts: Opts, prop: string): Promise<string>
144+
export function runAsync(
145+
action: 'filterDeprecatedTokens',
146+
opts: Opts,
147+
tokens: DeprecatedToken[],
148+
): Promise<DeprecatedToken[]>
124149
export async function runAsync(action: string, opts: Opts, ...args: any): Promise<any> {
125150
const ctx = await getContext(opts)
126151

@@ -151,6 +176,12 @@ export async function runAsync(action: string, opts: Opts, ...args: any): Promis
151176
case 'filterInvalidTokens':
152177
// @ts-expect-error cast
153178
return filterInvalidTokens(ctx, ...args)
179+
case 'getPropCategory':
180+
// @ts-expect-error cast
181+
return getPropCategory(ctx, ...args)
182+
case 'filterDeprecatedTokens':
183+
// @ts-expect-error cast
184+
return filterDeprecatedTokens(ctx, ...args)
154185
}
155186
}
156187

@@ -163,6 +194,8 @@ export function run(action: 'resolveLongHand', opts: Opts, name: string): string
163194
export function run(action: 'isValidProperty', opts: Opts, name: string, patternName?: string): boolean
164195
export function run(action: 'matchFile', opts: Opts, name: string, imports: ImportResult[]): boolean
165196
export function run(action: 'matchImports', opts: Opts, result: MatchImportResult): boolean
197+
export function run(action: 'getPropCategory', opts: Opts, prop: string): string
198+
export function run(action: 'filterDeprecatedTokens', opts: Opts, tokens: DeprecatedToken[]): DeprecatedToken[]
166199
export function run(action: string, opts: Opts, ...args: any[]): any {
167200
// @ts-expect-error cast
168201
return runAsync(action, opts, ...args)

plugin/test-utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,4 @@ const baseTesterConfig = {
1212
}
1313

1414
export const tester = new RuleTester(baseTesterConfig)
15-
export const eslintTester = new ERuleTester(baseTesterConfig)
15+
export const eslintTester = new ERuleTester(baseTesterConfig as any)

0 commit comments

Comments
 (0)