Skip to content

Commit 021e691

Browse files
authored
Apply memoization in rules (#161)
2 parents 15aee80 + 121ad0f commit 021e691

24 files changed

+1217
-560
lines changed

.changeset/wise-seas-beg.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@pandacss/eslint-plugin': minor
3+
---
4+
5+
Use memoization in rules

plugin/src/rules/file-not-included.ts

+21-5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { type Rule, createRule } from '../utils'
22
import { isPandaImport, isValidFile } from '../utils/helpers'
3+
import { TSESTree } from '@typescript-eslint/utils'
34

45
export const RULE_NAME = 'file-not-included'
56

@@ -8,25 +9,40 @@ const rule: Rule = createRule({
89
meta: {
910
docs: {
1011
description:
11-
'Disallow the use of panda css in files that are not included in the specified panda `include` config.',
12+
'Disallow the use of Panda CSS in files that are not included in the specified Panda CSS `include` config.',
1213
},
1314
messages: {
14-
include: 'The use of Panda CSS is not allowed in this file. Please check the specified `include` config.',
15+
include:
16+
'The use of Panda CSS is not allowed in this file. Please ensure the file is included in the Panda CSS `include` configuration.',
1517
},
16-
type: 'suggestion',
18+
type: 'problem',
1719
schema: [],
1820
},
1921
defaultOptions: [],
2022
create(context) {
23+
// Determine if the current file is included in the Panda CSS configuration
24+
const isFileIncluded = isValidFile(context)
25+
26+
// If the file is included, no need to proceed
27+
if (isFileIncluded) {
28+
return {}
29+
}
30+
31+
let hasReported = false
32+
2133
return {
22-
ImportDeclaration(node) {
34+
ImportDeclaration(node: TSESTree.ImportDeclaration) {
35+
if (hasReported) return
36+
2337
if (!isPandaImport(node, context)) return
24-
if (isValidFile(context)) return
2538

39+
// Report only on the first import declaration
2640
context.report({
2741
node,
2842
messageId: 'include',
2943
})
44+
45+
hasReported = true
3046
},
3147
}
3248
},

plugin/src/rules/no-config-function-in-source.ts

+65-34
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,103 @@
11
import { isIdentifier, isVariableDeclaration } from '../utils/nodes'
22
import { type Rule, createRule } from '../utils'
33
import { getAncestor, getImportSpecifiers, hasPkgImport, isPandaConfigFunction, isValidFile } from '../utils/helpers'
4+
import { TSESTree } from '@typescript-eslint/utils'
45

56
export const RULE_NAME = 'no-config-function-in-source'
67

8+
const CONFIG_FUNCTIONS = new Set([
9+
'defineConfig',
10+
'defineRecipe',
11+
'defineSlotRecipe',
12+
'defineParts',
13+
'definePattern',
14+
'definePreset',
15+
'defineKeyframes',
16+
'defineGlobalStyles',
17+
'defineUtility',
18+
'defineTextStyles',
19+
'defineLayerStyles',
20+
'defineStyles',
21+
'defineTokens',
22+
'defineSemanticTokens',
23+
])
24+
725
const rule: Rule = createRule({
826
name: RULE_NAME,
927
meta: {
1028
docs: {
11-
description: 'Prohibit the use of config functions outside the Panda config.',
29+
description: 'Prohibit the use of config functions outside the Panda config file.',
1230
},
1331
messages: {
14-
configFunction: 'Unnecessary`{{name}}` call. \nConfig functions should only be used in panda config.',
32+
configFunction: 'Unnecessary `{{name}}` call. Config functions should only be used in the Panda config file.',
1533
delete: 'Delete `{{name}}` call.',
1634
},
17-
type: 'suggestion',
35+
type: 'problem',
1836
hasSuggestions: true,
1937
schema: [],
2038
},
2139
defaultOptions: [],
2240
create(context) {
23-
if (!hasPkgImport(context)) return {}
41+
// Check if the package is imported; if not, exit early
42+
if (!hasPkgImport(context)) {
43+
return {}
44+
}
45+
46+
// Determine if the current file is the Panda config file
47+
const isPandaFile = isValidFile(context)
48+
49+
// If we are in the config file, no need to proceed
50+
if (!isPandaFile) {
51+
return {}
52+
}
2453

2554
return {
26-
CallExpression(node) {
27-
if (!isValidFile(context)) return
55+
CallExpression(node: TSESTree.CallExpression) {
56+
// Ensure the callee is an identifier
2857
if (!isIdentifier(node.callee)) return
29-
if (!CONFIG_FUNCTIONS.includes(node.callee.name)) return
30-
if (!isPandaConfigFunction(context, node.callee.name)) return
58+
59+
const functionName = node.callee.name
60+
61+
// Check if the function is a config function
62+
if (!CONFIG_FUNCTIONS.has(functionName)) return
63+
64+
// Verify that it's a Panda config function
65+
if (!isPandaConfigFunction(context, functionName)) return
3166

3267
context.report({
3368
node,
3469
messageId: 'configFunction',
3570
data: {
36-
name: node.callee.name,
71+
name: functionName,
3772
},
3873
suggest: [
3974
{
4075
messageId: 'delete',
4176
data: {
42-
name: node.callee.name,
77+
name: functionName,
4378
},
4479
fix(fixer) {
4580
const declaration = getAncestor(isVariableDeclaration, node)
46-
const importSpec = getImportSpecifiers(context).find(
47-
(s) => isIdentifier(node.callee) && s.specifier.local.name === node.callee.name,
48-
)
49-
return [
50-
fixer.remove(declaration ?? node),
51-
importSpec?.specifier ? fixer.remove(importSpec?.specifier) : ({} as any),
52-
]
81+
const importSpecifiers = getImportSpecifiers(context)
82+
83+
// Find the import specifier for the function
84+
const importSpec = importSpecifiers.find((s) => s.specifier.local.name === functionName)
85+
86+
const fixes = []
87+
88+
// Remove the variable declaration if it exists; otherwise, remove the call expression
89+
if (declaration) {
90+
fixes.push(fixer.remove(declaration))
91+
} else {
92+
fixes.push(fixer.remove(node))
93+
}
94+
95+
// Remove the import specifier if it exists
96+
if (importSpec?.specifier) {
97+
fixes.push(fixer.remove(importSpec.specifier))
98+
}
99+
100+
return fixes
53101
},
54102
},
55103
],
@@ -60,20 +108,3 @@ const rule: Rule = createRule({
60108
})
61109

62110
export default rule
63-
64-
const CONFIG_FUNCTIONS = [
65-
'defineConfig',
66-
'defineRecipe',
67-
'defineSlotRecipe',
68-
'defineParts',
69-
'definePattern',
70-
'definePreset',
71-
'defineKeyframes',
72-
'defineGlobalStyles',
73-
'defineUtility',
74-
'defineTextStyles',
75-
'defineLayerStyles',
76-
'defineStyles',
77-
'defineTokens',
78-
'defineSemanticTokens',
79-
]

plugin/src/rules/no-debug.ts

+8-7
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { isIdentifier, isJSXIdentifier } from '../utils/nodes'
21
import { type Rule, createRule } from '../utils'
3-
import { isPandaAttribute, isPandaProp, isRecipeVariant } from '../utils/helpers'
2+
import { isPandaProp, isPandaAttribute, isRecipeVariant } from '../utils/helpers'
3+
import { TSESTree } from '@typescript-eslint/utils'
44

55
export const RULE_NAME = 'no-debug'
66

@@ -15,15 +15,15 @@ const rule: Rule = createRule({
1515
prop: 'Remove the debug prop.',
1616
property: 'Remove the debug property.',
1717
},
18-
type: 'suggestion',
18+
type: 'problem',
1919
hasSuggestions: true,
2020
schema: [],
2121
},
2222
defaultOptions: [],
2323
create(context) {
2424
return {
25-
JSXAttribute(node) {
26-
if (!isJSXIdentifier(node.name) || node.name.name !== 'debug') return
25+
'JSXAttribute[name.name="debug"]'(node: TSESTree.JSXAttribute) {
26+
// Ensure the attribute is a Panda prop
2727
if (!isPandaProp(node, context)) return
2828

2929
context.report({
@@ -38,9 +38,10 @@ const rule: Rule = createRule({
3838
})
3939
},
4040

41-
Property(node) {
42-
if (!isIdentifier(node.key) || node.key.name !== 'debug') return
41+
'Property[key.name="debug"]'(node: TSESTree.Property) {
42+
// Ensure the property is a Panda attribute
4343
if (!isPandaAttribute(node, context)) return
44+
// Exclude recipe variants
4445
if (isRecipeVariant(node, context)) return
4546

4647
context.report({
+53-45
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { TSESTree } from '@typescript-eslint/utils'
1+
import { type TSESTree } from '@typescript-eslint/utils'
22
import { type Rule, createRule } from '../utils'
33
import { isInPandaFunction, isPandaAttribute, isPandaProp, isRecipeVariant } from '../utils/helpers'
44
import {
@@ -17,48 +17,69 @@ const rule: Rule = createRule({
1717
meta: {
1818
docs: {
1919
description:
20-
"Ensure user doesn't use dynamic styling at any point. \nPrefer to use static styles, leverage css variables or recipes for known dynamic styles.",
20+
"Ensure users don't use dynamic styling. Prefer static styles, leverage CSS variables, or recipes for known dynamic styles.",
2121
},
2222
messages: {
23-
dynamic: 'Remove dynamic value. Prefer static styles',
24-
dynamicProperty: 'Remove dynamic property. Prefer static style property',
25-
dynamicRecipeVariant: 'Remove dynamic variant. Prefer static variant definition',
23+
dynamic: 'Remove dynamic value. Prefer static styles.',
24+
dynamicProperty: 'Remove dynamic property. Prefer static style property.',
25+
dynamicRecipeVariant: 'Remove dynamic variant. Prefer static variant definition.',
2626
},
27-
type: 'suggestion',
27+
type: 'problem',
2828
schema: [],
2929
},
3030
defaultOptions: [],
3131
create(context) {
32+
// Helper function to determine if a node represents a static value
33+
function isStaticValue(node: TSESTree.Node | null | undefined): boolean {
34+
if (!node) return false
35+
if (isLiteral(node)) return true
36+
if (isTemplateLiteral(node) && node.expressions.length === 0) return true
37+
if (isObjectExpression(node)) return true // Conditions are acceptable
38+
return false
39+
}
40+
41+
// Function to check array elements for dynamic values
42+
function checkArrayElements(array: TSESTree.ArrayExpression) {
43+
array.elements.forEach((element) => {
44+
if (!element) return
45+
if (isStaticValue(element)) return
46+
47+
context.report({
48+
node: element,
49+
messageId: 'dynamic',
50+
})
51+
})
52+
}
53+
3254
return {
33-
JSXAttribute(node) {
55+
// JSX Attributes
56+
JSXAttribute(node: TSESTree.JSXAttribute) {
3457
if (!node.value) return
35-
if (isLiteral(node.value)) return
36-
if (isJSXExpressionContainer(node.value) && isLiteral(node.value.expression)) return
37-
38-
// For syntax like: <Circle property={`value that could be multiline`} />
39-
if (
40-
isJSXExpressionContainer(node.value) &&
41-
isTemplateLiteral(node.value.expression) &&
42-
node.value.expression.expressions.length === 0
43-
)
44-
return
4558

46-
// Don't warn for objects. Those are conditions
47-
if (isObjectExpression(node.value.expression)) return
59+
if (isLiteral(node.value)) return
60+
// Check if it's a Panda prop early to avoid unnecessary processing
4861
if (!isPandaProp(node, context)) return
4962

50-
if (isArrayExpression(node.value.expression)) {
51-
return checkArrayElements(node.value.expression, context)
63+
if (isJSXExpressionContainer(node.value)) {
64+
const expr = node.value.expression
65+
66+
if (isStaticValue(expr)) return
67+
68+
if (isArrayExpression(expr)) {
69+
checkArrayElements(expr)
70+
return
71+
}
5272
}
5373

74+
// Report dynamic value usage
5475
context.report({
5576
node: node.value,
5677
messageId: 'dynamic',
5778
})
5879
},
5980

60-
// Dynamic properties
61-
'Property[computed=true]'(node: TSESTree.Property) {
81+
// Dynamic properties with computed keys
82+
'Property[computed=true]': (node: TSESTree.Property) => {
6283
if (!isInPandaFunction(node, context)) return
6384

6485
context.report({
@@ -67,22 +88,22 @@ const rule: Rule = createRule({
6788
})
6889
},
6990

70-
Property(node) {
91+
// Object Properties
92+
Property(node: TSESTree.Property) {
7193
if (!isIdentifier(node.key)) return
72-
if (isLiteral(node.value)) return
73-
74-
// For syntax like: { property: `value that could be multiline` }
75-
if (isTemplateLiteral(node.value) && node.value.expressions.length === 0) return
76-
77-
// Don't warn for objects. Those are conditions
78-
if (isObjectExpression(node.value)) return
7994

95+
// Check if it's a Panda attribute early to avoid unnecessary processing
8096
if (!isPandaAttribute(node, context)) return
97+
if (isRecipeVariant(node, context)) return
98+
99+
if (isStaticValue(node.value)) return
81100

82101
if (isArrayExpression(node.value)) {
83-
return checkArrayElements(node.value, context)
102+
checkArrayElements(node.value)
103+
return
84104
}
85105

106+
// Report dynamic value usage
86107
context.report({
87108
node: node.value,
88109
messageId: 'dynamic',
@@ -92,17 +113,4 @@ const rule: Rule = createRule({
92113
},
93114
})
94115

95-
function checkArrayElements(array: TSESTree.ArrayExpression, context: Parameters<(typeof rule)['create']>[0]) {
96-
array.elements.forEach((node) => {
97-
if (!node) return
98-
if (isLiteral(node)) return
99-
if (isTemplateLiteral(node) && node.expressions.length === 0) return
100-
101-
context.report({
102-
node: node,
103-
messageId: 'dynamic',
104-
})
105-
})
106-
}
107-
108116
export default rule

0 commit comments

Comments
 (0)