diff --git a/.changeset/wise-seas-beg.md b/.changeset/wise-seas-beg.md
new file mode 100644
index 0000000..6e2b42b
--- /dev/null
+++ b/.changeset/wise-seas-beg.md
@@ -0,0 +1,5 @@
+---
+'@pandacss/eslint-plugin': minor
+---
+
+Use memoization in rules
diff --git a/plugin/src/rules/file-not-included.ts b/plugin/src/rules/file-not-included.ts
index 698c1c4..0522c9e 100644
--- a/plugin/src/rules/file-not-included.ts
+++ b/plugin/src/rules/file-not-included.ts
@@ -1,5 +1,6 @@
import { type Rule, createRule } from '../utils'
import { isPandaImport, isValidFile } from '../utils/helpers'
+import { TSESTree } from '@typescript-eslint/utils'
export const RULE_NAME = 'file-not-included'
@@ -8,25 +9,40 @@ const rule: Rule = createRule({
meta: {
docs: {
description:
- 'Disallow the use of panda css in files that are not included in the specified panda `include` config.',
+ 'Disallow the use of Panda CSS in files that are not included in the specified Panda CSS `include` config.',
},
messages: {
- include: 'The use of Panda CSS is not allowed in this file. Please check the specified `include` config.',
+ include:
+ 'The use of Panda CSS is not allowed in this file. Please ensure the file is included in the Panda CSS `include` configuration.',
},
- type: 'suggestion',
+ type: 'problem',
schema: [],
},
defaultOptions: [],
create(context) {
+ // Determine if the current file is included in the Panda CSS configuration
+ const isFileIncluded = isValidFile(context)
+
+ // If the file is included, no need to proceed
+ if (isFileIncluded) {
+ return {}
+ }
+
+ let hasReported = false
+
return {
- ImportDeclaration(node) {
+ ImportDeclaration(node: TSESTree.ImportDeclaration) {
+ if (hasReported) return
+
if (!isPandaImport(node, context)) return
- if (isValidFile(context)) return
+ // Report only on the first import declaration
context.report({
node,
messageId: 'include',
})
+
+ hasReported = true
},
}
},
diff --git a/plugin/src/rules/no-config-function-in-source.ts b/plugin/src/rules/no-config-function-in-source.ts
index a6cb91a..a8be3c4 100644
--- a/plugin/src/rules/no-config-function-in-source.ts
+++ b/plugin/src/rules/no-config-function-in-source.ts
@@ -1,55 +1,103 @@
import { isIdentifier, isVariableDeclaration } from '../utils/nodes'
import { type Rule, createRule } from '../utils'
import { getAncestor, getImportSpecifiers, hasPkgImport, isPandaConfigFunction, isValidFile } from '../utils/helpers'
+import { TSESTree } from '@typescript-eslint/utils'
export const RULE_NAME = 'no-config-function-in-source'
+const CONFIG_FUNCTIONS = new Set([
+ 'defineConfig',
+ 'defineRecipe',
+ 'defineSlotRecipe',
+ 'defineParts',
+ 'definePattern',
+ 'definePreset',
+ 'defineKeyframes',
+ 'defineGlobalStyles',
+ 'defineUtility',
+ 'defineTextStyles',
+ 'defineLayerStyles',
+ 'defineStyles',
+ 'defineTokens',
+ 'defineSemanticTokens',
+])
+
const rule: Rule = createRule({
name: RULE_NAME,
meta: {
docs: {
- description: 'Prohibit the use of config functions outside the Panda config.',
+ description: 'Prohibit the use of config functions outside the Panda config file.',
},
messages: {
- configFunction: 'Unnecessary`{{name}}` call. \nConfig functions should only be used in panda config.',
+ configFunction: 'Unnecessary `{{name}}` call. Config functions should only be used in the Panda config file.',
delete: 'Delete `{{name}}` call.',
},
- type: 'suggestion',
+ type: 'problem',
hasSuggestions: true,
schema: [],
},
defaultOptions: [],
create(context) {
- if (!hasPkgImport(context)) return {}
+ // Check if the package is imported; if not, exit early
+ if (!hasPkgImport(context)) {
+ return {}
+ }
+
+ // Determine if the current file is the Panda config file
+ const isPandaFile = isValidFile(context)
+
+ // If we are in the config file, no need to proceed
+ if (!isPandaFile) {
+ return {}
+ }
return {
- CallExpression(node) {
- if (!isValidFile(context)) return
+ CallExpression(node: TSESTree.CallExpression) {
+ // Ensure the callee is an identifier
if (!isIdentifier(node.callee)) return
- if (!CONFIG_FUNCTIONS.includes(node.callee.name)) return
- if (!isPandaConfigFunction(context, node.callee.name)) return
+
+ const functionName = node.callee.name
+
+ // Check if the function is a config function
+ if (!CONFIG_FUNCTIONS.has(functionName)) return
+
+ // Verify that it's a Panda config function
+ if (!isPandaConfigFunction(context, functionName)) return
context.report({
node,
messageId: 'configFunction',
data: {
- name: node.callee.name,
+ name: functionName,
},
suggest: [
{
messageId: 'delete',
data: {
- name: node.callee.name,
+ name: functionName,
},
fix(fixer) {
const declaration = getAncestor(isVariableDeclaration, node)
- const importSpec = getImportSpecifiers(context).find(
- (s) => isIdentifier(node.callee) && s.specifier.local.name === node.callee.name,
- )
- return [
- fixer.remove(declaration ?? node),
- importSpec?.specifier ? fixer.remove(importSpec?.specifier) : ({} as any),
- ]
+ const importSpecifiers = getImportSpecifiers(context)
+
+ // Find the import specifier for the function
+ const importSpec = importSpecifiers.find((s) => s.specifier.local.name === functionName)
+
+ const fixes = []
+
+ // Remove the variable declaration if it exists; otherwise, remove the call expression
+ if (declaration) {
+ fixes.push(fixer.remove(declaration))
+ } else {
+ fixes.push(fixer.remove(node))
+ }
+
+ // Remove the import specifier if it exists
+ if (importSpec?.specifier) {
+ fixes.push(fixer.remove(importSpec.specifier))
+ }
+
+ return fixes
},
},
],
@@ -60,20 +108,3 @@ const rule: Rule = createRule({
})
export default rule
-
-const CONFIG_FUNCTIONS = [
- 'defineConfig',
- 'defineRecipe',
- 'defineSlotRecipe',
- 'defineParts',
- 'definePattern',
- 'definePreset',
- 'defineKeyframes',
- 'defineGlobalStyles',
- 'defineUtility',
- 'defineTextStyles',
- 'defineLayerStyles',
- 'defineStyles',
- 'defineTokens',
- 'defineSemanticTokens',
-]
diff --git a/plugin/src/rules/no-debug.ts b/plugin/src/rules/no-debug.ts
index 3291f95..bbae102 100644
--- a/plugin/src/rules/no-debug.ts
+++ b/plugin/src/rules/no-debug.ts
@@ -1,6 +1,6 @@
-import { isIdentifier, isJSXIdentifier } from '../utils/nodes'
import { type Rule, createRule } from '../utils'
-import { isPandaAttribute, isPandaProp, isRecipeVariant } from '../utils/helpers'
+import { isPandaProp, isPandaAttribute, isRecipeVariant } from '../utils/helpers'
+import { TSESTree } from '@typescript-eslint/utils'
export const RULE_NAME = 'no-debug'
@@ -15,15 +15,15 @@ const rule: Rule = createRule({
prop: 'Remove the debug prop.',
property: 'Remove the debug property.',
},
- type: 'suggestion',
+ type: 'problem',
hasSuggestions: true,
schema: [],
},
defaultOptions: [],
create(context) {
return {
- JSXAttribute(node) {
- if (!isJSXIdentifier(node.name) || node.name.name !== 'debug') return
+ 'JSXAttribute[name.name="debug"]'(node: TSESTree.JSXAttribute) {
+ // Ensure the attribute is a Panda prop
if (!isPandaProp(node, context)) return
context.report({
@@ -38,9 +38,10 @@ const rule: Rule = createRule({
})
},
- Property(node) {
- if (!isIdentifier(node.key) || node.key.name !== 'debug') return
+ 'Property[key.name="debug"]'(node: TSESTree.Property) {
+ // Ensure the property is a Panda attribute
if (!isPandaAttribute(node, context)) return
+ // Exclude recipe variants
if (isRecipeVariant(node, context)) return
context.report({
diff --git a/plugin/src/rules/no-dynamic-styling.ts b/plugin/src/rules/no-dynamic-styling.ts
index 6bff5f3..e80a208 100644
--- a/plugin/src/rules/no-dynamic-styling.ts
+++ b/plugin/src/rules/no-dynamic-styling.ts
@@ -1,4 +1,4 @@
-import type { TSESTree } from '@typescript-eslint/utils'
+import { type TSESTree } from '@typescript-eslint/utils'
import { type Rule, createRule } from '../utils'
import { isInPandaFunction, isPandaAttribute, isPandaProp, isRecipeVariant } from '../utils/helpers'
import {
@@ -17,48 +17,69 @@ const rule: Rule = createRule({
meta: {
docs: {
description:
- "Ensure user doesn't use dynamic styling at any point. \nPrefer to use static styles, leverage css variables or recipes for known dynamic styles.",
+ "Ensure users don't use dynamic styling. Prefer static styles, leverage CSS variables, or recipes for known dynamic styles.",
},
messages: {
- dynamic: 'Remove dynamic value. Prefer static styles',
- dynamicProperty: 'Remove dynamic property. Prefer static style property',
- dynamicRecipeVariant: 'Remove dynamic variant. Prefer static variant definition',
+ dynamic: 'Remove dynamic value. Prefer static styles.',
+ dynamicProperty: 'Remove dynamic property. Prefer static style property.',
+ dynamicRecipeVariant: 'Remove dynamic variant. Prefer static variant definition.',
},
- type: 'suggestion',
+ type: 'problem',
schema: [],
},
defaultOptions: [],
create(context) {
+ // Helper function to determine if a node represents a static value
+ function isStaticValue(node: TSESTree.Node | null | undefined): boolean {
+ if (!node) return false
+ if (isLiteral(node)) return true
+ if (isTemplateLiteral(node) && node.expressions.length === 0) return true
+ if (isObjectExpression(node)) return true // Conditions are acceptable
+ return false
+ }
+
+ // Function to check array elements for dynamic values
+ function checkArrayElements(array: TSESTree.ArrayExpression) {
+ array.elements.forEach((element) => {
+ if (!element) return
+ if (isStaticValue(element)) return
+
+ context.report({
+ node: element,
+ messageId: 'dynamic',
+ })
+ })
+ }
+
return {
- JSXAttribute(node) {
+ // JSX Attributes
+ JSXAttribute(node: TSESTree.JSXAttribute) {
if (!node.value) return
- if (isLiteral(node.value)) return
- if (isJSXExpressionContainer(node.value) && isLiteral(node.value.expression)) return
-
- // For syntax like:
- if (
- isJSXExpressionContainer(node.value) &&
- isTemplateLiteral(node.value.expression) &&
- node.value.expression.expressions.length === 0
- )
- return
- // Don't warn for objects. Those are conditions
- if (isObjectExpression(node.value.expression)) return
+ if (isLiteral(node.value)) return
+ // Check if it's a Panda prop early to avoid unnecessary processing
if (!isPandaProp(node, context)) return
- if (isArrayExpression(node.value.expression)) {
- return checkArrayElements(node.value.expression, context)
+ if (isJSXExpressionContainer(node.value)) {
+ const expr = node.value.expression
+
+ if (isStaticValue(expr)) return
+
+ if (isArrayExpression(expr)) {
+ checkArrayElements(expr)
+ return
+ }
}
+ // Report dynamic value usage
context.report({
node: node.value,
messageId: 'dynamic',
})
},
- // Dynamic properties
- 'Property[computed=true]'(node: TSESTree.Property) {
+ // Dynamic properties with computed keys
+ 'Property[computed=true]': (node: TSESTree.Property) => {
if (!isInPandaFunction(node, context)) return
context.report({
@@ -67,22 +88,22 @@ const rule: Rule = createRule({
})
},
- Property(node) {
+ // Object Properties
+ Property(node: TSESTree.Property) {
if (!isIdentifier(node.key)) return
- if (isLiteral(node.value)) return
-
- // For syntax like: { property: `value that could be multiline` }
- if (isTemplateLiteral(node.value) && node.value.expressions.length === 0) return
-
- // Don't warn for objects. Those are conditions
- if (isObjectExpression(node.value)) return
+ // Check if it's a Panda attribute early to avoid unnecessary processing
if (!isPandaAttribute(node, context)) return
+ if (isRecipeVariant(node, context)) return
+
+ if (isStaticValue(node.value)) return
if (isArrayExpression(node.value)) {
- return checkArrayElements(node.value, context)
+ checkArrayElements(node.value)
+ return
}
+ // Report dynamic value usage
context.report({
node: node.value,
messageId: 'dynamic',
@@ -92,17 +113,4 @@ const rule: Rule = createRule({
},
})
-function checkArrayElements(array: TSESTree.ArrayExpression, context: Parameters<(typeof rule)['create']>[0]) {
- array.elements.forEach((node) => {
- if (!node) return
- if (isLiteral(node)) return
- if (isTemplateLiteral(node) && node.expressions.length === 0) return
-
- context.report({
- node: node,
- messageId: 'dynamic',
- })
- })
-}
-
export default rule
diff --git a/plugin/src/rules/no-escape-hatch.ts b/plugin/src/rules/no-escape-hatch.ts
index cf0998f..2bd52ff 100644
--- a/plugin/src/rules/no-escape-hatch.ts
+++ b/plugin/src/rules/no-escape-hatch.ts
@@ -1,7 +1,8 @@
import { isPandaAttribute, isPandaProp, isRecipeVariant } from '../utils/helpers'
import { type Rule, createRule } from '../utils'
import { getArbitraryValue } from '@pandacss/shared'
-import { isIdentifier, isJSXExpressionContainer, isLiteral, isTemplateLiteral, type Node } from '../utils/nodes'
+import { isIdentifier, isJSXExpressionContainer, isLiteral, isTemplateLiteral } from '../utils/nodes'
+import { TSESTree } from '@typescript-eslint/utils'
export const RULE_NAME = 'no-escape-hatch'
@@ -13,48 +14,41 @@ const rule: Rule = createRule({
},
messages: {
escapeHatch:
- 'Avoid using the escape hatch [value] for undefined tokens. \nDefine a corresponding token in your design system for better consistency and maintainability.',
+ 'Avoid using the escape hatch [value] for undefined tokens. Define a corresponding token in your design system for better consistency and maintainability.',
remove: 'Remove the square brackets (`[]`).',
},
- type: 'suggestion',
+ type: 'problem',
hasSuggestions: true,
schema: [],
},
defaultOptions: [],
create(context) {
- const removeQuotes = ([start, end]: readonly [number, number]) => [start + 1, end - 1] as const
+ // Helper function to adjust the range for fixing (removing brackets)
+ const removeBrackets = (range: readonly [number, number]) => {
+ const [start, end] = range
+ return [start + 1, end - 1] as const
+ }
- const hasEscapeHatch = (value?: string) => {
+ // Function to check if a value contains escape hatch syntax
+ const hasEscapeHatch = (value: string | undefined): boolean => {
if (!value) return false
+ // Early return if the value doesn't contain brackets
+ if (!value.includes('[')) return false
return getArbitraryValue(value) !== value.trim()
}
- const handleLiteral = (node: Node) => {
- if (!isLiteral(node)) return
- if (!hasEscapeHatch(node.value?.toString())) return
-
- sendReport(node)
- }
+ // Unified function to handle reporting
+ const handleNodeValue = (node: TSESTree.Node, value: string) => {
+ if (!hasEscapeHatch(value)) return
- const handleTemplateLiteral = (node: Node) => {
- if (!isTemplateLiteral(node)) return
- if (node.expressions.length > 0) return
- if (!hasEscapeHatch(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()
-
- return context.report({
+ context.report({
node,
messageId: 'escapeHatch',
suggest: [
{
messageId: 'remove',
fix: (fixer) => {
- return fixer.replaceTextRange(removeQuotes(node.range), getArbitraryValue(value))
+ return fixer.replaceTextRange(removeBrackets(node.range as [number, number]), getArbitraryValue(value))
},
},
],
@@ -62,26 +56,43 @@ const rule: Rule = createRule({
}
return {
- JSXAttribute(node) {
+ JSXAttribute(node: TSESTree.JSXAttribute) {
if (!node.value) return
+ // Ensure the attribute is a Panda prop
if (!isPandaProp(node, context)) return
- handleLiteral(node.value)
-
- if (!isJSXExpressionContainer(node.value)) return
-
- handleLiteral(node.value.expression)
- handleTemplateLiteral(node.value.expression)
+ const { value } = node
+
+ if (isLiteral(value)) {
+ const val = value.value?.toString() ?? ''
+ handleNodeValue(value, val)
+ } else if (isJSXExpressionContainer(value)) {
+ const expr = value.expression
+ if (isLiteral(expr)) {
+ const val = expr.value?.toString() ?? ''
+ handleNodeValue(expr, val)
+ } else if (isTemplateLiteral(expr) && expr.expressions.length === 0) {
+ const val = expr.quasis[0].value.raw
+ handleNodeValue(expr.quasis[0], val)
+ }
+ }
},
- Property(node) {
+ Property(node: TSESTree.Property) {
if (!isIdentifier(node.key)) return
- if (!isLiteral(node.value) && !isTemplateLiteral(node.value)) return
+ // Ensure the property is a Panda attribute
if (!isPandaAttribute(node, context)) return
+ // Exclude recipe variants
if (isRecipeVariant(node, context)) return
- handleLiteral(node.value)
- handleTemplateLiteral(node.value)
+ const value = node.value
+ if (isLiteral(value)) {
+ const val = value.value?.toString() ?? ''
+ handleNodeValue(value, val)
+ } else if (isTemplateLiteral(value) && value.expressions.length === 0) {
+ const val = value.quasis[0].value.raw
+ handleNodeValue(value.quasis[0], val)
+ }
},
}
},
diff --git a/plugin/src/rules/no-hardcoded-color.ts b/plugin/src/rules/no-hardcoded-color.ts
index c0d4e9f..6135de7 100644
--- a/plugin/src/rules/no-hardcoded-color.ts
+++ b/plugin/src/rules/no-hardcoded-color.ts
@@ -1,13 +1,14 @@
import {
extractTokens,
- isColorAttribute,
- isColorToken,
+ isColorAttribute as originalIsColorAttribute,
+ isColorToken as originalIsColorToken,
isPandaAttribute,
isPandaProp,
isRecipeVariant,
} from '../utils/helpers'
import { type Rule, createRule } from '../utils'
-import { isIdentifier, isJSXExpressionContainer, isJSXIdentifier, isLiteral } from '../utils/nodes'
+import { isIdentifier, isJSXExpressionContainer, isJSXIdentifier, isLiteral, isTemplateLiteral } from '../utils/nodes'
+import { TSESTree } from '@typescript-eslint/utils'
export const RULE_NAME = 'no-hardcoded-color'
@@ -20,7 +21,7 @@ const rule: Rule = createRule({
messages: {
invalidColor: '`{{color}}` is not a valid color token.',
},
- type: 'suggestion',
+ type: 'problem',
schema: [
{
type: 'object',
@@ -29,6 +30,7 @@ const rule: Rule = createRule({
type: 'boolean',
},
},
+ additionalProperties: false,
},
],
},
@@ -40,75 +42,101 @@ const rule: Rule = createRule({
create(context) {
const noOpacity = context.options[0]?.noOpacity
- const isTokenFn = (value?: string) => {
+ // Caches for isColorToken and isColorAttribute results
+ const colorTokenCache = new Map()
+ const colorAttributeCache = new Map()
+
+ // Cached version of isColorToken
+ const isColorToken = (token: string): boolean => {
+ if (colorTokenCache.has(token)) {
+ return colorTokenCache.get(token)!
+ }
+ const result = originalIsColorToken(token, context)
+ colorTokenCache.set(token, result)
+ return !!result
+ }
+
+ // Cached version of isColorAttribute
+ const isColorAttribute = (attribute: string): boolean => {
+ if (colorAttributeCache.has(attribute)) {
+ return colorAttributeCache.get(attribute)!
+ }
+ const result = originalIsColorAttribute(attribute, context)
+ colorAttributeCache.set(attribute, result)
+ return result
+ }
+
+ const isTokenFunctionUsed = (value: string): boolean => {
if (!value) return false
const tokens = extractTokens(value)
return tokens.length > 0
}
- const testColorToken = (value?: string) => {
+ const isValidColorToken = (value: string): boolean => {
if (!value) return false
- const color = value?.split('/')
- const isOpacityToken = !!color[1]?.length
- const isValidToken = isColorToken(color[0], context)
- return noOpacity ? isValidToken && !isOpacityToken : isValidToken
+ const [colorToken, opacity] = value.split('/')
+ const hasOpacity = opacity !== undefined && opacity.length > 0
+ const isValidToken = isColorToken(colorToken)
+
+ return noOpacity ? isValidToken && !hasOpacity : isValidToken
+ }
+
+ const reportInvalidColor = (node: TSESTree.Node, color: string) => {
+ context.report({
+ node,
+ messageId: 'invalidColor',
+ data: {
+ color,
+ },
+ })
+ }
+
+ const checkColorValue = (node: TSESTree.Node, value: string, attributeName: string) => {
+ if (!isColorAttribute(attributeName)) return
+ if (isTokenFunctionUsed(value)) return
+ if (isValidColorToken(value)) return
+
+ reportInvalidColor(node, value)
}
return {
- JSXAttribute(node) {
+ JSXAttribute(node: TSESTree.JSXAttribute) {
if (!isJSXIdentifier(node.name)) return
if (!isPandaProp(node, context) || !node.value) return
- if (
- isLiteral(node.value) &&
- isColorAttribute(node.name.name, context) &&
- !isTokenFn(node.value.value?.toString()) &&
- !testColorToken(node.value.value?.toString())
- ) {
- context.report({
- node: node.value,
- messageId: 'invalidColor',
- data: {
- color: node.value.value?.toString(),
- },
- })
- }
+ const attributeName = node.name.name
+ const valueNode = node.value
- if (!isJSXExpressionContainer(node.value)) return
-
- if (
- isLiteral(node.value.expression) &&
- isColorAttribute(node.name.name, context) &&
- !isTokenFn(node.value.expression.value?.toString()) &&
- !testColorToken(node.value.expression.value?.toString())
- ) {
- context.report({
- node: node.value.expression,
- messageId: 'invalidColor',
- data: {
- color: node.value.expression.value?.toString(),
- },
- })
+ if (isLiteral(valueNode)) {
+ const value = valueNode.value?.toString() || ''
+ checkColorValue(valueNode, value, attributeName)
+ } else if (isJSXExpressionContainer(valueNode)) {
+ const expression = valueNode.expression
+ if (isLiteral(expression)) {
+ const value = expression.value?.toString() || ''
+ checkColorValue(expression, value, attributeName)
+ } else if (isTemplateLiteral(expression) && expression.expressions.length === 0) {
+ const value = expression.quasis[0].value.raw
+ checkColorValue(expression.quasis[0], value, attributeName)
+ }
}
},
- Property(node) {
+ Property(node: TSESTree.Property) {
if (!isIdentifier(node.key)) return
- if (!isLiteral(node.value)) return
-
if (!isPandaAttribute(node, context)) return
if (isRecipeVariant(node, context)) return
- if (!isColorAttribute(node.key.name, context)) return
- if (isTokenFn(node.value.value?.toString())) return
- if (testColorToken(node.value.value?.toString())) return
-
- context.report({
- node: node.value,
- messageId: 'invalidColor',
- data: {
- color: node.value.value?.toString(),
- },
- })
+
+ const attributeName = node.key.name
+ const valueNode = node.value
+
+ if (isLiteral(valueNode)) {
+ const value = valueNode.value?.toString() || ''
+ checkColorValue(valueNode, value, attributeName)
+ } else if (isTemplateLiteral(valueNode) && valueNode.expressions.length === 0) {
+ const value = valueNode.quasis[0].value.raw
+ checkColorValue(valueNode.quasis[0], value, attributeName)
+ }
},
}
},
diff --git a/plugin/src/rules/no-important.ts b/plugin/src/rules/no-important.ts
index 1a29d18..8ba1f43 100644
--- a/plugin/src/rules/no-important.ts
+++ b/plugin/src/rules/no-important.ts
@@ -1,11 +1,11 @@
import { isPandaAttribute, isPandaProp, isRecipeVariant } from '../utils/helpers'
import { type Rule, createRule } from '../utils'
-import { isIdentifier, isJSXExpressionContainer, isLiteral, isTemplateLiteral, type Node } from '../utils/nodes'
+import { isIdentifier, isJSXExpressionContainer, isLiteral, isTemplateLiteral } from '../utils/nodes'
import { getArbitraryValue } from '@pandacss/shared'
+import { TSESTree } from '@typescript-eslint/utils'
-// Check if the string ends with '!' with optional whitespace before it
+// Regular expressions to detect '!important' and '!' at the end of a value
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'
@@ -15,68 +15,97 @@ const rule: Rule = createRule({
meta: {
docs: {
description:
- 'Disallow usage of important keyword. Prioroitize specificity for a maintainable and predictable styling structure.',
+ 'Disallow usage of !important keyword. Prioritize specificity for a maintainable and predictable styling structure.',
},
messages: {
important:
- 'Avoid using the !important keyword. Refactor your code to prioritize specificity for predictable styling.',
+ 'Avoid using the {{keyword}} keyword. Refactor your code to prioritize specificity for predictable styling.',
remove: 'Remove the `{{keyword}}` keyword.',
},
- type: 'suggestion',
+ type: 'problem',
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
- const value = getArbitraryValue(_value)
- return exclamationRegex.test(value) || importantRegex.test(value)
+ // Helper function to adjust the range for fixing (removing quotes)
+ const removeQuotes = (range: readonly [number, number]) => {
+ const [start, end] = range
+ return [start + 1, end - 1] as const
}
- 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 }
+ // Caches for helper functions
+ const pandaPropCache = new WeakMap()
+ const pandaAttributeCache = new WeakMap()
+ const recipeVariantCache = new WeakMap()
+
+ // Cached version of isPandaProp
+ const isCachedPandaProp = (node: TSESTree.JSXAttribute): boolean => {
+ if (pandaPropCache.has(node)) {
+ return pandaPropCache.get(node)!
}
+ const result = isPandaProp(node, context)
+ pandaPropCache.set(node, result)
+ return !!result
}
- const handleLiteral = (node: Node) => {
- if (!isLiteral(node)) return
- if (!hasImportantKeyword(node.value?.toString())) return
+ // Cached version of isPandaAttribute
+ const isCachedPandaAttribute = (node: TSESTree.Property): boolean => {
+ if (pandaAttributeCache.has(node)) {
+ return pandaAttributeCache.get(node)!
+ }
+ const result = isPandaAttribute(node, context)
+ pandaAttributeCache.set(node, result)
+ return !!result
+ }
- sendReport(node)
+ // Cached version of isRecipeVariant
+ const isCachedRecipeVariant = (node: TSESTree.Property): boolean => {
+ if (recipeVariantCache.has(node)) {
+ return recipeVariantCache.get(node)!
+ }
+ const result = isRecipeVariant(node, context)
+ recipeVariantCache.set(node, result)
+ return !!result
}
- const handleTemplateLiteral = (node: Node) => {
- if (!isTemplateLiteral(node)) return
- if (node.expressions.length > 0) return
- if (!hasImportantKeyword(node.quasis[0].value.raw)) return
+ // Function to check if a value contains '!important' or '!'
+ const hasImportantKeyword = (value: string | undefined): boolean => {
+ if (!value) return false
+ const arbitraryValue = getArbitraryValue(value)
+ return exclamationRegex.test(arbitraryValue) || importantRegex.test(arbitraryValue)
+ }
- sendReport(node.quasis[0], node.quasis[0].value.raw)
+ // Function to remove '!important' or '!' from a string
+ const removeImportantKeyword = (input: string): { fixed: string; keyword: string | null } => {
+ if (importantRegex.test(input)) {
+ // Remove '!important' with optional whitespace
+ return { fixed: input.replace(importantRegex, '').trimEnd(), keyword: '!important' }
+ } else if (exclamationRegex.test(input)) {
+ // Remove trailing '!'
+ return { fixed: input.replace(exclamationRegex, '').trimEnd(), keyword: '!' }
+ } else {
+ // No match, return the original string
+ return { fixed: input, keyword: null }
+ }
}
- const sendReport = (node: any, _value?: string) => {
- const value = _value ?? node.value?.toString()
- const { keyword, fixed } = removeImportantKeyword(value)
+ // Unified function to handle reporting
+ const handleNodeValue = (node: TSESTree.Node, value: string) => {
+ if (!hasImportantKeyword(value)) return
- return context.report({
+ const { fixed, keyword } = removeImportantKeyword(value)
+
+ context.report({
node,
messageId: 'important',
+ data: { keyword },
suggest: [
{
messageId: 'remove',
data: { keyword },
fix: (fixer) => {
- return fixer.replaceTextRange(removeQuotes(node.range), fixed)
+ return fixer.replaceTextRange(removeQuotes(node.range as [number, number]), fixed)
},
},
],
@@ -84,26 +113,44 @@ const rule: Rule = createRule({
}
return {
- JSXAttribute(node) {
+ // JSX Attributes
+ JSXAttribute(node: TSESTree.JSXAttribute) {
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)
+ if (!isCachedPandaProp(node)) return
+
+ const valueNode = node.value
+
+ if (isLiteral(valueNode)) {
+ const val = valueNode.value?.toString() ?? ''
+ handleNodeValue(valueNode, val)
+ } else if (isJSXExpressionContainer(valueNode)) {
+ const expr = valueNode.expression
+
+ if (isLiteral(expr)) {
+ const val = expr.value?.toString() ?? ''
+ handleNodeValue(expr, val)
+ } else if (isTemplateLiteral(expr) && expr.expressions.length === 0) {
+ const val = expr.quasis[0].value.raw
+ handleNodeValue(expr.quasis[0], val)
+ }
+ }
},
- Property(node) {
+ // Object Properties
+ Property(node: TSESTree.Property) {
if (!isIdentifier(node.key)) return
- if (!isLiteral(node.value) && !isTemplateLiteral(node.value)) return
- if (!isPandaAttribute(node, context)) return
- if (isRecipeVariant(node, context)) return
-
- handleLiteral(node.value)
- handleTemplateLiteral(node.value)
+ if (!isCachedPandaAttribute(node)) return
+ if (isCachedRecipeVariant(node)) return
+
+ const valueNode = node.value
+
+ if (isLiteral(valueNode)) {
+ const val = valueNode.value?.toString() ?? ''
+ handleNodeValue(valueNode, val)
+ } else if (isTemplateLiteral(valueNode) && valueNode.expressions.length === 0) {
+ const val = valueNode.quasis[0].value.raw
+ handleNodeValue(valueNode.quasis[0], val)
+ }
},
}
},
diff --git a/plugin/src/rules/no-invalid-nesting.ts b/plugin/src/rules/no-invalid-nesting.ts
index e3f7384..9accf6f 100644
--- a/plugin/src/rules/no-invalid-nesting.ts
+++ b/plugin/src/rules/no-invalid-nesting.ts
@@ -1,6 +1,7 @@
-import { isIdentifier, isLiteral, isObjectExpression, isTemplateLiteral } from '../utils/nodes'
+import { isLiteral, isTemplateLiteral } from '../utils/nodes'
import { type Rule, createRule } from '../utils'
import { isInJSXProp, isInPandaFunction, isRecipeVariant, isStyledProperty } from '../utils/helpers'
+import { TSESTree } from '@typescript-eslint/utils'
export const RULE_NAME = 'no-invalid-nesting'
@@ -8,34 +9,88 @@ const rule: Rule = createRule({
name: RULE_NAME,
meta: {
docs: {
- description: "Warn against invalid nesting. i.e. nested styles that don't contain the `&` character.",
+ description: 'Warn against invalid nesting. Nested styles must contain the `&` character.',
},
messages: {
nesting: 'Invalid style nesting. Nested styles must contain the `&` character.',
},
- type: 'suggestion',
+ type: 'problem',
schema: [],
},
defaultOptions: [],
create(context) {
+ // Caches for helper functions
+ const pandaFunctionCache = new WeakMap()
+ const jsxPropCache = new WeakMap()
+ const recipeVariantCache = new WeakMap()
+ const styledPropertyCache = new WeakMap()
+
+ // Cached helper functions
+ const isCachedInPandaFunction = (node: TSESTree.Property): boolean => {
+ if (pandaFunctionCache.has(node)) {
+ return pandaFunctionCache.get(node)!
+ }
+ const result = !!isInPandaFunction(node, context)
+ pandaFunctionCache.set(node, result)
+ return !!result
+ }
+
+ const isCachedInJSXProp = (node: TSESTree.Property): boolean => {
+ if (jsxPropCache.has(node)) {
+ return jsxPropCache.get(node)!
+ }
+ const result = isInJSXProp(node, context)
+ jsxPropCache.set(node, result)
+ return !!result
+ }
+
+ const isCachedRecipeVariant = (node: TSESTree.Property): boolean => {
+ if (recipeVariantCache.has(node)) {
+ return recipeVariantCache.get(node)!
+ }
+ const result = isRecipeVariant(node, context)
+ recipeVariantCache.set(node, result)
+ return !!result
+ }
+
+ const isCachedStyledProperty = (node: TSESTree.Property): boolean => {
+ if (styledPropertyCache.has(node)) {
+ return styledPropertyCache.get(node)!
+ }
+ const result = isStyledProperty(node, context)
+ styledPropertyCache.set(node, result)
+ return !!result
+ }
+
+ // Function to check if a key is an invalid nesting selector
+ const isInvalidNestingSelector = (node: TSESTree.Expression): boolean => {
+ if (isLiteral(node) && typeof node.value === 'string') {
+ return !node.value.includes('&')
+ } else if (isTemplateLiteral(node) && node.expressions.length === 0) {
+ return !node.quasis[0].value.raw.includes('&')
+ }
+ return false
+ }
+
return {
- Property(node) {
- if (!isObjectExpression(node.value) || isIdentifier(node.key)) return
- const caller = isInPandaFunction(node, context)
- if (!caller && !isInJSXProp(node, context)) return
- if (isRecipeVariant(node, context)) return
- if (isStyledProperty(node, context)) return
-
- const invalidLiteral =
- isLiteral(node.key) && typeof node.key.value === 'string' && !node.key.value.includes('&')
- const invalidTemplateLiteral = isTemplateLiteral(node.key) && !node.key.quasis[0].value.raw.includes('&')
-
- if (!(invalidLiteral || invalidTemplateLiteral)) return
-
- context.report({
- node: node.key,
- messageId: 'nesting',
- })
+ // Use AST selector to target Property nodes with non-Identifier keys whose value is an ObjectExpression
+ 'Property[key.type!=/Identifier/][value.type="ObjectExpression"]'(node: TSESTree.Property) {
+ // Check if the node is within a Panda function or JSX prop
+ const inPandaFunction = isCachedInPandaFunction(node)
+ const inJSXProp = isCachedInJSXProp(node)
+
+ if (!inPandaFunction && !inJSXProp) return
+ if (isCachedRecipeVariant(node)) return
+ if (isCachedStyledProperty(node)) return
+
+ const keyNode = node.key
+
+ if (isInvalidNestingSelector(keyNode)) {
+ context.report({
+ node: keyNode,
+ messageId: 'nesting',
+ })
+ }
},
}
},
diff --git a/plugin/src/rules/no-invalid-token-paths.ts b/plugin/src/rules/no-invalid-token-paths.ts
index 9628953..13cc1e3 100644
--- a/plugin/src/rules/no-invalid-token-paths.ts
+++ b/plugin/src/rules/no-invalid-token-paths.ts
@@ -7,9 +7,9 @@ import {
isRecipeVariant,
} from '../utils/helpers'
import { type Rule, createRule } from '../utils'
-import { AST_NODE_TYPES } from '@typescript-eslint/utils'
+import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/utils'
import { isNodeOfTypes } from '@typescript-eslint/utils/ast-utils'
-import { isIdentifier, isJSXExpressionContainer, isLiteral, isTemplateLiteral, type Node } from '../utils/nodes'
+import { isIdentifier, isJSXExpressionContainer, isLiteral, isTemplateLiteral } from '../utils/nodes'
export const RULE_NAME = 'no-invalid-token-paths'
@@ -22,92 +22,107 @@ const rule: Rule = createRule({
messages: {
noInvalidTokenPaths: '`{{token}}` is an invalid token path.',
},
- type: 'suggestion',
+ type: 'problem',
schema: [],
},
defaultOptions: [],
create(context) {
- const handleLiteral = (node: Node) => {
- if (!isLiteral(node)) return
+ // Cache for invalid tokens to avoid redundant computations
+ const invalidTokensCache = new Map()
- sendReport(node)
- }
+ const sendReport = (node: TSESTree.Node, value: string | undefined) => {
+ if (!value) return
- const handleTemplateLiteral = (node: Node) => {
- if (!isTemplateLiteral(node)) return
- if (node.expressions.length > 0) return
- sendReport(node.quasis[0], node.quasis[0].value.raw)
- }
+ let tokens: string[] | undefined = invalidTokensCache.get(value)
+ if (!tokens) {
+ tokens = getInvalidTokens(value, context)
+ invalidTokensCache.set(value, tokens)
+ }
- const sendReport = (node: any, _value?: string) => {
- const value = _value ?? node.value?.toString()
- const tokens = getInvalidTokens(value, context)
- if (!tokens) return
-
- if (tokens.length > 0) {
- tokens.forEach((token) => {
- context.report({
- node,
- messageId: 'noInvalidTokenPaths',
- data: { token },
- })
+ if (tokens.length === 0) return
+
+ tokens.forEach((token) => {
+ context.report({
+ node,
+ messageId: 'noInvalidTokenPaths',
+ data: { token },
})
- }
+ })
}
- return {
- JSXAttribute(node) {
- if (!node.value) return
- if (!isPandaProp(node, context)) return
-
- handleLiteral(node.value)
+ const handleLiteralOrTemplate = (node: TSESTree.Node | undefined) => {
+ if (!node) return
- if (!isJSXExpressionContainer(node.value)) return
+ if (isLiteral(node)) {
+ const value = node.value?.toString()
+ sendReport(node, value)
+ } else if (isTemplateLiteral(node) && node.expressions.length === 0) {
+ const value = node.quasis[0].value.raw
+ sendReport(node.quasis[0], value)
+ }
+ }
- handleLiteral(node.value.expression)
- handleTemplateLiteral(node.value.expression)
+ return {
+ JSXAttribute(node: TSESTree.JSXAttribute) {
+ if (!node.value || !isPandaProp(node, context)) return
+
+ if (isLiteral(node.value)) {
+ handleLiteralOrTemplate(node.value)
+ } else if (isJSXExpressionContainer(node.value)) {
+ handleLiteralOrTemplate(node.value.expression)
+ }
},
- Property(node) {
- if (!isIdentifier(node.key)) return
- if (!isNodeOfTypes([AST_NODE_TYPES.Literal, AST_NODE_TYPES.TemplateLiteral])(node.value)) return
- if (!isPandaAttribute(node, context)) return
- if (isRecipeVariant(node, context)) return
-
- handleLiteral(node.value)
- handleTemplateLiteral(node.value)
+ Property(node: TSESTree.Property) {
+ if (
+ !isIdentifier(node.key) ||
+ !isNodeOfTypes([AST_NODE_TYPES.Literal, AST_NODE_TYPES.TemplateLiteral])(node.value) ||
+ !isPandaAttribute(node, context) ||
+ isRecipeVariant(node, context)
+ ) {
+ return
+ }
+
+ handleLiteralOrTemplate(node.value)
},
- TaggedTemplateExpression(node) {
+ TaggedTemplateExpression(node: TSESTree.TaggedTemplateExpression) {
const caller = getTaggedTemplateCaller(node)
- if (!caller) return
+ if (!caller || !isPandaIsh(caller, context)) return
- if (!isPandaIsh(caller, context)) return
+ const quasis = node.quasi.quasis
+ quasis.forEach((quasi) => {
+ const styles = quasi.value.raw
+ if (!styles) return
- const quasis = node.quasi.quasis[0]
- const styles = quasis.value.raw
- const tokens = getInvalidTokens(styles, context)
- if (!tokens) return
+ let tokens: string[] | undefined = invalidTokensCache.get(styles)
+ if (!tokens) {
+ tokens = getInvalidTokens(styles, context)
+ invalidTokensCache.set(styles, tokens)
+ }
- tokens.forEach((token, i, arr) => {
- // Avoid duplicate reports on the same token
- if (arr.indexOf(token) < i) return
+ if (tokens.length === 0) return
- let index = styles.indexOf(token)
+ tokens.forEach((token) => {
+ let index = styles.indexOf(token)
- while (index !== -1) {
- const start = quasis.range[0] + 1 + index
- const end = start + token.length
+ while (index !== -1) {
+ const start = quasi.range[0] + index + 1 // +1 for the backtick
+ const end = start + token.length
- context.report({
- loc: { start: context.sourceCode.getLocFromIndex(start), end: context.sourceCode.getLocFromIndex(end) },
- messageId: 'noInvalidTokenPaths',
- data: { token },
- })
+ context.report({
+ loc: {
+ start: context.sourceCode.getLocFromIndex(start),
+ end: context.sourceCode.getLocFromIndex(end),
+ },
+ messageId: 'noInvalidTokenPaths',
+ data: { token },
+ })
- // Check for other occurences of the invalid token
- index = styles.indexOf(token, index + 1)
- }
+ // Check for other occurences of the invalid token
+ index = styles.indexOf(token, index + token.length)
+ }
+ })
})
},
}
diff --git a/plugin/src/rules/no-margin-properties.ts b/plugin/src/rules/no-margin-properties.ts
index 36bab44..361d67f 100644
--- a/plugin/src/rules/no-margin-properties.ts
+++ b/plugin/src/rules/no-margin-properties.ts
@@ -21,29 +21,77 @@ const rule: Rule = createRule({
},
defaultOptions: [],
create(context) {
- const getLonghand = (name: string) => resolveLonghand(name, context) ?? name
+ // Cache for resolved longhand properties
+ const longhandCache = new Map()
+
+ const getLonghand = (name: string): string => {
+ if (longhandCache.has(name)) {
+ return longhandCache.get(name)!
+ }
+ const longhand = resolveLonghand(name, context) ?? name
+ longhandCache.set(name, longhand)
+ return longhand
+ }
+
+ const marginRegex = /margin/i
+
+ const isMarginProperty = (name: string): boolean => {
+ const longhand = getLonghand(name).toLowerCase()
+ return marginRegex.test(longhand)
+ }
const sendReport = (node: TSESTree.Identifier | TSESTree.JSXIdentifier) => {
- if (!getLonghand(node.name).toLowerCase().includes('margin')) return
+ if (!isMarginProperty(node.name)) return
- return context.report({
+ context.report({
node,
messageId: 'noMargin',
})
}
+ // Cache for helper functions
+ const pandaPropCache = new WeakMap()
+ const isCachedPandaProp = (node: TSESTree.JSXAttribute): boolean => {
+ if (pandaPropCache.has(node)) {
+ return pandaPropCache.get(node)!
+ }
+ const result = isPandaProp(node, context)
+ pandaPropCache.set(node, result)
+ return !!result
+ }
+
+ const pandaAttributeCache = new WeakMap()
+ const isCachedPandaAttribute = (node: TSESTree.Property): boolean => {
+ if (pandaAttributeCache.has(node)) {
+ return pandaAttributeCache.get(node)!
+ }
+ const result = isPandaAttribute(node, context)
+ pandaAttributeCache.set(node, result)
+ return !!result
+ }
+
+ const recipeVariantCache = new WeakMap()
+ const isCachedRecipeVariant = (node: TSESTree.Property): boolean => {
+ if (recipeVariantCache.has(node)) {
+ return recipeVariantCache.get(node)!
+ }
+ const result = isRecipeVariant(node, context)
+ recipeVariantCache.set(node, result)
+ return !!result
+ }
+
return {
- JSXAttribute(node) {
+ JSXAttribute(node: TSESTree.JSXAttribute) {
if (!isJSXIdentifier(node.name)) return
- if (!isPandaProp(node, context)) return
+ if (!isCachedPandaProp(node)) return
sendReport(node.name)
},
- Property(node) {
+ Property(node: TSESTree.Property) {
if (!isIdentifier(node.key)) return
- if (!isPandaAttribute(node, context)) return
- if (isRecipeVariant(node, context)) return
+ if (!isCachedPandaAttribute(node)) return
+ if (isCachedRecipeVariant(node)) return
sendReport(node.key)
},
diff --git a/plugin/src/rules/no-physical-properties.ts b/plugin/src/rules/no-physical-properties.ts
index 7d71dba..1fa9f75 100644
--- a/plugin/src/rules/no-physical-properties.ts
+++ b/plugin/src/rules/no-physical-properties.ts
@@ -11,11 +11,11 @@ const rule: Rule = createRule({
meta: {
docs: {
description:
- 'Encourage the use of [logical properties](https://mdn.io/logical-properties-basic-concepts) over physical proeprties, to foster a responsive and adaptable user interface.',
+ 'Encourage the use of logical properties over physical properties to foster a responsive and adaptable user interface.',
},
messages: {
- physical: 'Use logical property of {{physical}} instead. Prefer `{{logical}}`',
- replace: 'Replace `{{physical}}` with `{{logical}}`',
+ physical: 'Use logical property instead of {{physical}}. Prefer `{{logical}}`.',
+ replace: 'Replace `{{physical}}` with `{{logical}}`.',
},
type: 'suggestion',
hasSuggestions: true,
@@ -23,19 +23,62 @@ const rule: Rule = createRule({
},
defaultOptions: [],
create(context) {
- const getLonghand = (name: string) => resolveLonghand(name, context) ?? name
+ // Cache for resolved longhand properties
+ const longhandCache = new Map()
+
+ // Cache for helper functions
+ const pandaPropCache = new WeakMap()
+ const pandaAttributeCache = new WeakMap()
+ const recipeVariantCache = new WeakMap()
+
+ const getLonghand = (name: string): string => {
+ if (longhandCache.has(name)) {
+ return longhandCache.get(name)!
+ }
+ const longhand = resolveLonghand(name, context) ?? name
+ longhandCache.set(name, longhand)
+ return longhand
+ }
+
+ const isCachedPandaProp = (node: TSESTree.JSXAttribute): boolean => {
+ if (pandaPropCache.has(node)) {
+ return pandaPropCache.get(node)!
+ }
+ const result = isPandaProp(node, context)
+ pandaPropCache.set(node, result)
+ return !!result
+ }
+
+ const isCachedPandaAttribute = (node: TSESTree.Property): boolean => {
+ if (pandaAttributeCache.has(node)) {
+ return pandaAttributeCache.get(node)!
+ }
+ const result = isPandaAttribute(node, context)
+ pandaAttributeCache.set(node, result)
+ return !!result
+ }
+
+ const isCachedRecipeVariant = (node: TSESTree.Property): boolean => {
+ if (recipeVariantCache.has(node)) {
+ return recipeVariantCache.get(node)!
+ }
+ const result = isRecipeVariant(node, context)
+ recipeVariantCache.set(node, result)
+ return !!result
+ }
const sendReport = (node: TSESTree.Identifier | TSESTree.JSXIdentifier) => {
- if (!(getLonghand(node.name) in physicalProperties)) return
+ const longhandName = getLonghand(node.name)
+ if (!(longhandName in physicalProperties)) return
- const logical = physicalProperties[getLonghand(node.name)]
- const longhand = resolveLonghand(node.name, context)
+ const logical = physicalProperties[longhandName]
+ const physicalName = `\`${node.name}\`${longhandName !== node.name ? ` (resolved to \`${longhandName}\`)` : ''}`
- return context.report({
+ context.report({
node,
messageId: 'physical',
data: {
- physical: `\`${node.name}\`${longhand ? ` - \`${longhand}\`` : ''}`,
+ physical: physicalName,
logical,
},
suggest: [
@@ -46,7 +89,7 @@ const rule: Rule = createRule({
logical,
},
fix: (fixer) => {
- return fixer.replaceTextRange(node.range, logical)
+ return fixer.replaceText(node, logical)
},
},
],
@@ -54,17 +97,17 @@ const rule: Rule = createRule({
}
return {
- JSXAttribute(node) {
+ JSXAttribute(node: TSESTree.JSXAttribute) {
if (!isJSXIdentifier(node.name)) return
- if (!isPandaProp(node, context)) return
+ if (!isCachedPandaProp(node)) return
sendReport(node.name)
},
- Property(node) {
+ Property(node: TSESTree.Property) {
if (!isIdentifier(node.key)) return
- if (!isPandaAttribute(node, context)) return
- if (isRecipeVariant(node, context)) return
+ if (!isCachedPandaAttribute(node)) return
+ if (isCachedRecipeVariant(node)) return
sendReport(node.key)
},
diff --git a/plugin/src/rules/no-property-renaming.ts b/plugin/src/rules/no-property-renaming.ts
index 9e46a86..056b44a 100644
--- a/plugin/src/rules/no-property-renaming.ts
+++ b/plugin/src/rules/no-property-renaming.ts
@@ -9,20 +9,53 @@ const rule: Rule = createRule({
name: RULE_NAME,
meta: {
docs: {
- description: "Ensure user does not rename a property for a pattern or style prop. \nIt doesn't get tracked.",
+ description:
+ 'Ensure that properties for patterns or style props are not renamed, as it prevents proper tracking.',
},
messages: {
noRenaming:
- 'Incoming `{{prop}}` prop is different from the expected `{{expected}}` attribute. Panada will not track this prop.',
+ 'Incoming `{{prop}}` prop is different from the expected `{{expected}}` attribute. Panda will not track this prop.',
},
- type: 'suggestion',
+ type: 'problem',
schema: [],
},
defaultOptions: [],
create(context) {
- const sendReport = (node: any, expected: string, prop: string) => {
- return context.report({
- node: node.value,
+ // Caches for helper functions
+ const pandaPropCache = new WeakMap()
+ const pandaAttributeCache = new WeakMap()
+ const recipeVariantCache = new WeakMap()
+
+ const isCachedPandaProp = (node: TSESTree.JSXAttribute): boolean => {
+ if (pandaPropCache.has(node)) {
+ return pandaPropCache.get(node)!
+ }
+ const result = isPandaProp(node, context)
+ pandaPropCache.set(node, result)
+ return !!result
+ }
+
+ const isCachedPandaAttribute = (node: TSESTree.Property): boolean => {
+ if (pandaAttributeCache.has(node)) {
+ return pandaAttributeCache.get(node)!
+ }
+ const result = isPandaAttribute(node, context)
+ pandaAttributeCache.set(node, result)
+ return !!result
+ }
+
+ const isCachedRecipeVariant = (node: TSESTree.Property): boolean => {
+ if (recipeVariantCache.has(node)) {
+ return recipeVariantCache.get(node)!
+ }
+ const result = isRecipeVariant(node, context)
+ recipeVariantCache.set(node, result)
+ return !!result
+ }
+
+ const sendReport = (node: TSESTree.Node, expected: string, prop: string) => {
+ context.report({
+ node,
messageId: 'noRenaming',
data: {
expected,
@@ -31,38 +64,36 @@ const rule: Rule = createRule({
})
}
- const handleReport = (node: TSESTree.Node, value: any, attr: string) => {
+ const handleReport = (node: TSESTree.Node, value: TSESTree.Node, attr: string) => {
if (isIdentifier(value) && attr !== value.name) {
- return sendReport(node, attr, value.name)
- }
-
- if (isMemberExpression(value) && isIdentifier(value.property) && attr !== value.property.name) {
- return sendReport(node, attr, value.property.name)
+ sendReport(node, attr, value.name)
+ } else if (isMemberExpression(value) && isIdentifier(value.property) && attr !== value.property.name) {
+ sendReport(node, attr, value.property.name)
}
}
return {
- JSXAttribute(node) {
+ JSXAttribute(node: TSESTree.JSXAttribute) {
if (!node.value) return
if (!isJSXExpressionContainer(node.value)) return
- if (!isPandaProp(node, context)) return
+ if (!isCachedPandaProp(node)) return
const attr = node.name.name.toString()
const expression = node.value.expression
- handleReport(node, expression, attr)
+ handleReport(node.value, expression, attr)
},
- Property(node) {
+ Property(node: TSESTree.Property) {
if (!isIdentifier(node.key)) return
if (!isIdentifier(node.value) && !isMemberExpression(node.value)) return
- if (!isPandaAttribute(node, context)) return
- if (isRecipeVariant(node, context)) return
+ if (!isCachedPandaAttribute(node)) return
+ if (isCachedRecipeVariant(node)) return
- const attr = node.key.name.toString()
+ const attr = node.key.name
const value = node.value
- handleReport(node, value, attr)
+ handleReport(node.value, value, attr)
},
}
},
diff --git a/plugin/src/rules/no-unsafe-token-fn-usage.ts b/plugin/src/rules/no-unsafe-token-fn-usage.ts
index 1d628b4..4e3ca09 100644
--- a/plugin/src/rules/no-unsafe-token-fn-usage.ts
+++ b/plugin/src/rules/no-unsafe-token-fn-usage.ts
@@ -1,14 +1,7 @@
import { extractTokens, getTokenImport, isPandaAttribute, isPandaProp, isRecipeVariant } from '../utils/helpers'
import { type Rule, createRule } from '../utils'
-import { TSESTree } from '@typescript-eslint/utils'
-import {
- isCallExpression,
- isIdentifier,
- isJSXExpressionContainer,
- isLiteral,
- isTemplateLiteral,
- type Node,
-} from '../utils/nodes'
+import { isCallExpression, isIdentifier, isJSXExpressionContainer, isLiteral, isTemplateLiteral } from '../utils/nodes'
+import { type TSESTree } from '@typescript-eslint/utils'
import { getArbitraryValue } from '@pandacss/shared'
export const RULE_NAME = 'no-unsafe-token-fn-usage'
@@ -21,8 +14,8 @@ const rule: Rule = createRule({
'Prevent users from using the token function in situations where they could simply use the raw design token.',
},
messages: {
- noUnsafeTokenFnUsage: 'Unneccessary token function usage. Prefer design token',
- replace: 'Replace token function with `{{safe}}`',
+ noUnsafeTokenFnUsage: 'Unnecessary token function usage. Prefer design token.',
+ replace: 'Replace token function with `{{safe}}`.',
},
type: 'suggestion',
hasSuggestions: true,
@@ -30,35 +23,65 @@ const rule: Rule = createRule({
},
defaultOptions: [],
create(context) {
- const isUnsafeCallExpression = (node: TSESTree.CallExpression) => {
- const tkImport = getTokenImport(context)
+ // Cache for getTokenImport result
+ let tokenImportCache: { alias: string } | null | undefined
+
+ const getCachedTokenImport = (): { alias: string } | null | undefined => {
+ if (tokenImportCache !== undefined) {
+ return tokenImportCache
+ }
+ tokenImportCache = getTokenImport(context)
+ return tokenImportCache
+ }
+
+ const isUnsafeCallExpression = (node: TSESTree.CallExpression): boolean => {
+ const tkImport = getCachedTokenImport()
return isIdentifier(node.callee) && node.callee.name === tkImport?.alias
}
- const tokenWrap = (value?: string) => (value ? `token(${value})` : '')
+ const tokenWrap = (value?: string): string => (value ? `token(${value})` : '')
- const handleRuntimeFm = (node: Node) => {
+ const isCompositeValue = (input?: string): boolean => {
+ if (!input) return false
+ // Regular expression to match token-only values, e.g., token('space.2') or {space.2}
+ const tokenRegex = /^(?:token\([^)]*\)|\{[^}]*\})$/
+ return !tokenRegex.test(input)
+ }
+
+ const sendReport = (node: TSESTree.Node, value: string) => {
+ const tkImports = extractTokens(value)
+ if (!tkImports.length) return
+ const token = tkImports[0].replace(/^[^.]*\./, '')
+
+ context.report({
+ node,
+ messageId: 'noUnsafeTokenFnUsage',
+ suggest: [
+ {
+ messageId: 'replace',
+ data: { safe: token },
+ fix: (fixer) => fixer.replaceText(node, `'${token}'`),
+ },
+ ],
+ })
+ }
+
+ const handleRuntimeFm = (node: TSESTree.Node) => {
if (!isCallExpression(node)) return
if (!isUnsafeCallExpression(node)) return
const value = node.arguments[0]
if (isLiteral(value)) {
- sendReport(node, tokenWrap(getArbitraryValue(value.value?.toString() ?? '')))
- }
- if (isTemplateLiteral(value)) {
- sendReport(node, tokenWrap(getArbitraryValue(value.quasis[0].value.raw)))
+ const val = getArbitraryValue(value.value?.toString() ?? '')
+ sendReport(node, tokenWrap(val))
+ } else if (isTemplateLiteral(value) && value.expressions.length === 0) {
+ const val = getArbitraryValue(value.quasis[0].value.raw)
+ sendReport(node, tokenWrap(val))
}
}
- const isCompositeValue = (input?: string) => {
- if (!input) return
- // Regular expression to match token only values. i.e. token('space.2') or {space.2}
- const tokenRegex = /^(?:token\([^)]*\)|\{[^}]*\})$/
- return !tokenRegex.test(input)
- }
-
- const handleLiteral = (node: Node) => {
+ const handleLiteral = (node: TSESTree.Node) => {
if (!isLiteral(node)) return
const value = getArbitraryValue(node.value?.toString() ?? '')
if (isCompositeValue(value)) return
@@ -66,55 +89,69 @@ const rule: Rule = createRule({
sendReport(node, value)
}
- const handleTemplateLiteral = (node: Node) => {
- if (!isTemplateLiteral(node)) return
- if (node.expressions.length > 0) return
+ const handleTemplateLiteral = (node: TSESTree.Node) => {
+ if (!isTemplateLiteral(node) || node.expressions.length > 0) return
- sendReport(node, getArbitraryValue(node.quasis[0].value.raw))
+ const value = getArbitraryValue(node.quasis[0].value.raw)
+ sendReport(node, value)
}
- const sendReport = (node: any, value: string) => {
- const tkImports = extractTokens(value)
- if (!tkImports.length) return
- const token = tkImports[0].replace(/^[^.]*\./, '')
+ // Cached versions of helper functions
+ const pandaPropCache = new WeakMap()
+ const isCachedPandaProp = (node: TSESTree.JSXAttribute): boolean => {
+ if (pandaPropCache.has(node)) {
+ return pandaPropCache.get(node)!
+ }
+ const result = isPandaProp(node, context)
+ pandaPropCache.set(node, result)
+ return !!result
+ }
- return context.report({
- node,
- messageId: 'noUnsafeTokenFnUsage',
- suggest: [
- {
- messageId: 'replace',
- data: { safe: token },
- fix: (fixer) => {
- return fixer.replaceTextRange(node.range, `'${token}'`)
- },
- },
- ],
- })
+ const pandaAttributeCache = new WeakMap()
+ const isCachedPandaAttribute = (node: TSESTree.Property): boolean => {
+ if (pandaAttributeCache.has(node)) {
+ return pandaAttributeCache.get(node)!
+ }
+ const result = isPandaAttribute(node, context)
+ pandaAttributeCache.set(node, result)
+ return !!result
+ }
+
+ const recipeVariantCache = new WeakMap()
+ const isCachedRecipeVariant = (node: TSESTree.Property): boolean => {
+ if (recipeVariantCache.has(node)) {
+ return recipeVariantCache.get(node)!
+ }
+ const result = isRecipeVariant(node, context)
+ recipeVariantCache.set(node, result)
+ return !!result
}
return {
- JSXAttribute(node) {
+ JSXAttribute(node: TSESTree.JSXAttribute) {
if (!node.value) return
- if (!isPandaProp(node, context)) return
+ if (!isCachedPandaProp(node)) return
handleLiteral(node.value)
- if (!isJSXExpressionContainer(node.value)) return
-
- handleLiteral(node.value.expression)
- handleTemplateLiteral(node.value.expression)
- handleRuntimeFm(node.value.expression)
+ if (isJSXExpressionContainer(node.value)) {
+ const expression = node.value.expression
+ handleLiteral(expression)
+ handleTemplateLiteral(expression)
+ handleRuntimeFm(expression)
+ }
},
- Property(node) {
- if (!isCallExpression(node.value) && !isLiteral(node.value) && !isTemplateLiteral(node.value)) return
- if (!isPandaAttribute(node, context)) return
- if (isRecipeVariant(node, context)) return
+ Property(node: TSESTree.Property) {
+ if (!isCachedPandaAttribute(node)) return
+ if (isCachedRecipeVariant(node)) return
- handleRuntimeFm(node.value)
- handleLiteral(node.value)
- handleTemplateLiteral(node.value)
+ const valueNode = node.value
+ if (isCallExpression(valueNode) || isLiteral(valueNode) || isTemplateLiteral(valueNode)) {
+ handleRuntimeFm(valueNode)
+ handleLiteral(valueNode)
+ handleTemplateLiteral(valueNode)
+ }
},
}
},
diff --git a/plugin/src/rules/prefer-atomic-properties.ts b/plugin/src/rules/prefer-atomic-properties.ts
index 3066f38..99aeb9c 100644
--- a/plugin/src/rules/prefer-atomic-properties.ts
+++ b/plugin/src/rules/prefer-atomic-properties.ts
@@ -1,8 +1,8 @@
-import { isPandaAttribute, isPandaProp, isRecipeVariant, isValidProperty, resolveLonghand } from '../utils/helpers'
+import type { TSESTree } from '@typescript-eslint/utils'
import { type Rule, createRule } from '../utils'
+import { isPandaAttribute, isPandaProp, isRecipeVariant, isValidProperty, resolveLonghand } from '../utils/helpers'
import { compositeProperties } from '../utils/composite-properties'
import { isIdentifier, isJSXIdentifier } from '../utils/nodes'
-import type { TSESTree } from '@typescript-eslint/utils'
export const RULE_NAME = 'prefer-atomic-properties'
@@ -13,27 +13,83 @@ const rule: Rule = createRule({
description: 'Encourage the use of atomic properties instead of composite properties in the codebase.',
},
messages: {
- atomic: 'Use atomic properties of `{{composite}}` instead. Prefer: \n{{atomics}}',
+ atomic: 'Use atomic properties instead of `{{composite}}`. Prefer: \n{{atomics}}',
},
type: 'suggestion',
schema: [],
},
defaultOptions: [],
create(context) {
- const resolveCompositeProperty = (name: string) => {
- if (Object.hasOwn(compositeProperties, name)) return name
+ // Cache for resolved longhand properties
+ const longhandCache = new Map()
+ const getLonghand = (name: string): string => {
+ if (longhandCache.has(name)) {
+ return longhandCache.get(name)!
+ }
const longhand = resolveLonghand(name, context) ?? name
- if (isValidProperty(longhand, context) && Object.hasOwn(compositeProperties, longhand)) return longhand
+ longhandCache.set(name, longhand)
+ return longhand
}
- const sendReport = (node: TSESTree.Identifier | TSESTree.JSXIdentifier) => {
- const cmp = resolveCompositeProperty(node.name)
- if (!cmp) return
+ // Cache for composite property resolution
+ const compositePropertyCache = new Map()
+
+ const resolveCompositeProperty = (name: string): string | undefined => {
+ if (compositePropertyCache.has(name)) {
+ return compositePropertyCache.get(name)
+ }
- const atomics = compositeProperties[cmp].map((name) => `\`${name}\``).join(',\n')
+ if (Object.hasOwn(compositeProperties, name)) {
+ compositePropertyCache.set(name, name)
+ return name
+ }
- return context.report({
+ const longhand = getLonghand(name)
+ if (isValidProperty(longhand, context) && Object.hasOwn(compositeProperties, longhand)) {
+ compositePropertyCache.set(name, longhand)
+ return longhand
+ }
+
+ compositePropertyCache.set(name, undefined)
+ return undefined
+ }
+
+ // Caches for helper functions
+ const pandaPropCache = new WeakMap()
+ const isCachedPandaProp = (node: TSESTree.JSXAttribute): boolean => {
+ if (pandaPropCache.has(node)) {
+ return pandaPropCache.get(node)!
+ }
+ const result = isPandaProp(node, context)
+ pandaPropCache.set(node, result)
+ return !!result
+ }
+
+ const pandaAttributeCache = new WeakMap()
+ const isCachedPandaAttribute = (node: TSESTree.Property): boolean => {
+ if (pandaAttributeCache.has(node)) {
+ return pandaAttributeCache.get(node)!
+ }
+ const result = isPandaAttribute(node, context)
+ pandaAttributeCache.set(node, result)
+ return !!result
+ }
+
+ const recipeVariantCache = new WeakMap()
+ const isCachedRecipeVariant = (node: TSESTree.Property): boolean => {
+ if (recipeVariantCache.has(node)) {
+ return recipeVariantCache.get(node)!
+ }
+ const result = isRecipeVariant(node, context)
+ recipeVariantCache.set(node, result)
+ return !!result
+ }
+
+ const sendReport = (node: TSESTree.Identifier | TSESTree.JSXIdentifier, composite: string) => {
+ const atomics = compositeProperties[composite].map((name) => `\`${name}\``).join(',\n')
+
+ context.report({
node,
messageId: 'atomic',
data: {
@@ -44,19 +100,25 @@ const rule: Rule = createRule({
}
return {
- JSXAttribute(node) {
+ JSXAttribute(node: TSESTree.JSXAttribute) {
if (!isJSXIdentifier(node.name)) return
- if (!isPandaProp(node, context)) return
+ if (!isCachedPandaProp(node)) return
- sendReport(node.name)
+ const composite = resolveCompositeProperty(node.name.name)
+ if (!composite) return
+
+ sendReport(node.name, composite)
},
- Property(node) {
+ Property(node: TSESTree.Property) {
if (!isIdentifier(node.key)) return
- if (!isPandaAttribute(node, context)) return
- if (isRecipeVariant(node, context)) return
+ if (!isCachedPandaAttribute(node)) return
+ if (isCachedRecipeVariant(node)) return
+
+ const composite = resolveCompositeProperty(node.key.name)
+ if (!composite) return
- sendReport(node.key)
+ sendReport(node.key, composite)
},
}
},
diff --git a/plugin/src/rules/prefer-composite-properties.ts b/plugin/src/rules/prefer-composite-properties.ts
index a848be1..3393e78 100644
--- a/plugin/src/rules/prefer-composite-properties.ts
+++ b/plugin/src/rules/prefer-composite-properties.ts
@@ -13,48 +13,108 @@ const rule: Rule = createRule({
description: 'Encourage the use of composite properties instead of atomic properties in the codebase.',
},
messages: {
- composite: 'Use composite property of `{{atomic}}` instead. \nPrefer: {{composite}}',
+ composite: 'Use composite property instead of `{{atomic}}`. Prefer: `{{composite}}`.',
},
type: 'suggestion',
schema: [],
},
defaultOptions: [],
create(context) {
- const resolveCompositeProperty = (name: string) => {
+ // Cache for resolved longhand properties
+ const longhandCache = new Map()
+
+ const getLonghand = (name: string): string => {
+ if (longhandCache.has(name)) {
+ return longhandCache.get(name)!
+ }
const longhand = resolveLonghand(name, context) ?? name
+ longhandCache.set(name, longhand)
+ return longhand
+ }
+
+ // Cache for composite property resolution
+ const compositePropertyCache = new Map()
+
+ const resolveCompositeProperty = (name: string): string | undefined => {
+ if (compositePropertyCache.has(name)) {
+ return compositePropertyCache.get(name)
+ }
+
+ const longhand = getLonghand(name)
+
+ if (!isValidProperty(longhand, context)) {
+ compositePropertyCache.set(name, undefined)
+ return undefined
+ }
+
+ const composite = Object.keys(compositeProperties).find((cpd) => compositeProperties[cpd].includes(longhand))
- if (!isValidProperty(longhand, context)) return
- return Object.keys(compositeProperties).find((cpd) => compositeProperties[cpd].includes(longhand))
+ compositePropertyCache.set(name, composite)
+ return composite
}
- const sendReport = (node: TSESTree.Identifier | TSESTree.JSXIdentifier) => {
- const cmp = resolveCompositeProperty(node.name)
- if (!cmp) return
+ // Caches for helper functions
+ const pandaPropCache = new WeakMap()
+ const isCachedPandaProp = (node: TSESTree.JSXAttribute): boolean => {
+ if (pandaPropCache.has(node)) {
+ return pandaPropCache.get(node)!
+ }
+ const result = isPandaProp(node, context)
+ pandaPropCache.set(node, result)
+ return !!result
+ }
+
+ const pandaAttributeCache = new WeakMap()
+ const isCachedPandaAttribute = (node: TSESTree.Property): boolean => {
+ if (pandaAttributeCache.has(node)) {
+ return pandaAttributeCache.get(node)!
+ }
+ const result = isPandaAttribute(node, context)
+ pandaAttributeCache.set(node, result)
+ return !!result
+ }
- return context.report({
+ const recipeVariantCache = new WeakMap()
+ const isCachedRecipeVariant = (node: TSESTree.Property): boolean => {
+ if (recipeVariantCache.has(node)) {
+ return recipeVariantCache.get(node)!
+ }
+ const result = isRecipeVariant(node, context)
+ recipeVariantCache.set(node, result)
+ return !!result
+ }
+
+ const sendReport = (node: TSESTree.Identifier | TSESTree.JSXIdentifier, composite: string) => {
+ context.report({
node,
messageId: 'composite',
data: {
- composite: cmp,
+ composite,
atomic: node.name,
},
})
}
return {
- JSXAttribute(node) {
+ JSXAttribute(node: TSESTree.JSXAttribute) {
if (!isJSXIdentifier(node.name)) return
- if (!isPandaProp(node, context)) return
+ if (!isCachedPandaProp(node)) return
+
+ const composite = resolveCompositeProperty(node.name.name)
+ if (!composite) return
- sendReport(node.name)
+ sendReport(node.name, composite)
},
- Property(node) {
+ Property(node: TSESTree.Property) {
if (!isIdentifier(node.key)) return
- if (!isPandaAttribute(node, context)) return
- if (isRecipeVariant(node, context)) return
+ if (!isCachedPandaAttribute(node)) return
+ if (isCachedRecipeVariant(node)) return
+
+ const composite = resolveCompositeProperty(node.key.name)
+ if (!composite) return
- sendReport(node.key)
+ sendReport(node.key, composite)
},
}
},
diff --git a/plugin/src/rules/prefer-longhand-properties.ts b/plugin/src/rules/prefer-longhand-properties.ts
index ed8ea32..d204a82 100644
--- a/plugin/src/rules/prefer-longhand-properties.ts
+++ b/plugin/src/rules/prefer-longhand-properties.ts
@@ -13,8 +13,8 @@ const rule: Rule = createRule({
'Discourage the use of shorthand properties and promote the preference for longhand properties in the codebase.',
},
messages: {
- longhand: 'Use longhand property of `{{shorthand}}` instead. Prefer `{{longhand}}`',
- replace: 'Replace `{{shorthand}}` with `{{longhand}}`',
+ longhand: 'Use longhand property instead of `{{shorthand}}`. Prefer `{{longhand}}`.',
+ replace: 'Replace `{{shorthand}}` with `{{longhand}}`.',
},
type: 'suggestion',
hasSuggestions: true,
@@ -22,16 +22,59 @@ const rule: Rule = createRule({
},
defaultOptions: [],
create(context) {
+ // Cache for resolved longhand properties
+ const longhandCache = new Map()
+
+ const getLonghand = (name: string): string | undefined => {
+ if (longhandCache.has(name)) {
+ return longhandCache.get(name)!
+ }
+ const longhand = resolveLonghand(name, context)
+ longhandCache.set(name, longhand)
+ return longhand
+ }
+
+ // Caches for helper functions
+ const pandaPropCache = new WeakMap()
+ const isCachedPandaProp = (node: TSESTree.JSXAttribute): boolean => {
+ if (pandaPropCache.has(node)) {
+ return pandaPropCache.get(node)!
+ }
+ const result = isPandaProp(node, context)
+ pandaPropCache.set(node, result)
+ return !!result
+ }
+
+ const pandaAttributeCache = new WeakMap()
+ const isCachedPandaAttribute = (node: TSESTree.Property): boolean => {
+ if (pandaAttributeCache.has(node)) {
+ return pandaAttributeCache.get(node)!
+ }
+ const result = isPandaAttribute(node, context)
+ pandaAttributeCache.set(node, result)
+ return !!result
+ }
+
+ const recipeVariantCache = new WeakMap()
+ const isCachedRecipeVariant = (node: TSESTree.Property): boolean => {
+ if (recipeVariantCache.has(node)) {
+ return recipeVariantCache.get(node)!
+ }
+ const result = isRecipeVariant(node, context)
+ recipeVariantCache.set(node, result)
+ return !!result
+ }
+
const sendReport = (node: TSESTree.Identifier | TSESTree.JSXIdentifier) => {
- const longhand = resolveLonghand(node.name, context)!
- if (!longhand) return
+ const longhand = getLonghand(node.name)
+ if (!longhand || longhand === node.name) return
const data = {
shorthand: node.name,
longhand,
}
- return context.report({
+ context.report({
node,
messageId: 'longhand',
data,
@@ -39,26 +82,24 @@ const rule: Rule = createRule({
{
messageId: 'replace',
data,
- fix: (fixer) => {
- return fixer.replaceTextRange(node.range, longhand)
- },
+ fix: (fixer) => fixer.replaceText(node, longhand),
},
],
})
}
return {
- JSXAttribute(node) {
+ JSXAttribute(node: TSESTree.JSXAttribute) {
if (!isJSXIdentifier(node.name)) return
- if (!isPandaProp(node, context)) return
+ if (!isCachedPandaProp(node)) return
sendReport(node.name)
},
- Property(node) {
+ Property(node: TSESTree.Property) {
if (!isIdentifier(node.key)) return
- if (!isPandaAttribute(node, context)) return
- if (isRecipeVariant(node, context)) return
+ if (!isCachedPandaAttribute(node)) return
+ if (isCachedRecipeVariant(node)) return
sendReport(node.key)
},
diff --git a/plugin/src/rules/prefer-shorthand-properties.ts b/plugin/src/rules/prefer-shorthand-properties.ts
index 86f8ec7..a63d655 100644
--- a/plugin/src/rules/prefer-shorthand-properties.ts
+++ b/plugin/src/rules/prefer-shorthand-properties.ts
@@ -13,8 +13,8 @@ const rule: Rule = createRule({
'Discourage the use of longhand properties and promote the preference for shorthand properties in the codebase.',
},
messages: {
- shorthand: 'Use shorthand property of `{{longhand}}` instead. Prefer {{shorthand}}',
- replace: 'Replace `{{longhand}}` with `{{shorthand}}`',
+ shorthand: 'Use shorthand property instead of `{{longhand}}`. Prefer `{{shorthand}}`.',
+ replace: 'Replace `{{longhand}}` with `{{shorthand}}`.',
},
type: 'suggestion',
hasSuggestions: true,
@@ -22,21 +22,76 @@ const rule: Rule = createRule({
},
defaultOptions: [],
create(context) {
+ // Cache for resolved longhand properties
+ const longhandCache = new Map()
+
+ const getLonghand = (name: string): string | undefined => {
+ if (longhandCache.has(name)) {
+ return longhandCache.get(name)!
+ }
+ const longhand = resolveLonghand(name, context)
+ longhandCache.set(name, longhand)
+ return longhand
+ }
+
+ // Cache for resolved shorthands
+ const shorthandsCache = new Map()
+
+ const getShorthands = (name: string): string[] | undefined => {
+ if (shorthandsCache.has(name)) {
+ return shorthandsCache.get(name)!
+ }
+ const shorthands = resolveShorthands(name, context)
+ shorthandsCache.set(name, shorthands)
+ return shorthands
+ }
+
+ // Caches for helper functions
+ const pandaPropCache = new WeakMap()
+ const isCachedPandaProp = (node: TSESTree.JSXAttribute): boolean => {
+ if (pandaPropCache.has(node)) {
+ return pandaPropCache.get(node)!
+ }
+ const result = isPandaProp(node, context)
+ pandaPropCache.set(node, result)
+ return !!result
+ }
+
+ const pandaAttributeCache = new WeakMap()
+ const isCachedPandaAttribute = (node: TSESTree.Property): boolean => {
+ if (pandaAttributeCache.has(node)) {
+ return pandaAttributeCache.get(node)!
+ }
+ const result = isPandaAttribute(node, context)
+ pandaAttributeCache.set(node, result)
+ return !!result
+ }
+
+ const recipeVariantCache = new WeakMap()
+ const isCachedRecipeVariant = (node: TSESTree.Property): boolean => {
+ if (recipeVariantCache.has(node)) {
+ return recipeVariantCache.get(node)!
+ }
+ const result = isRecipeVariant(node, context)
+ recipeVariantCache.set(node, result)
+ return !!result
+ }
+
const sendReport = (node: TSESTree.Identifier | TSESTree.JSXIdentifier) => {
- const longhand = resolveLonghand(node.name, context)
- if (longhand) return
+ const longhand = getLonghand(node.name)
+ if (longhand) return // If it's already shorthand, no need to report
- const shorthands = resolveShorthands(node.name, context)
- if (!shorthands) return
+ const shorthands = getShorthands(node.name)
+ if (!shorthands || shorthands.length === 0) return
- const shorthand = shorthands.map((s) => `\`${s}\``)?.join(', ')
+ const shorthandList = shorthands.map((s) => `\`${s}\``).join(', ')
const data = {
longhand: node.name,
- shorthand,
+ shorthand: shorthandList,
}
- return context.report({
+ context.report({
node,
messageId: 'shorthand',
data,
@@ -44,26 +99,24 @@ const rule: Rule = createRule({
{
messageId: 'replace',
data,
- fix: (fixer) => {
- return fixer.replaceTextRange(node.range, shorthands[0])
- },
+ fix: (fixer) => fixer.replaceText(node, shorthands[0]),
},
],
})
}
return {
- JSXAttribute(node) {
+ JSXAttribute(node: TSESTree.JSXAttribute) {
if (!isJSXIdentifier(node.name)) return
- if (!isPandaProp(node, context)) return
+ if (!isCachedPandaProp(node)) return
sendReport(node.name)
},
- Property(node) {
+ Property(node: TSESTree.Property) {
if (!isIdentifier(node.key)) return
- if (!isPandaAttribute(node, context)) return
- if (isRecipeVariant(node, context)) return
+ if (!isCachedPandaAttribute(node)) return
+ if (isCachedRecipeVariant(node)) return
sendReport(node.key)
},
diff --git a/plugin/src/rules/prefer-unified-property-style.ts b/plugin/src/rules/prefer-unified-property-style.ts
index d132396..e6f84ca 100644
--- a/plugin/src/rules/prefer-unified-property-style.ts
+++ b/plugin/src/rules/prefer-unified-property-style.ts
@@ -2,6 +2,7 @@ import { isPandaAttribute, isPandaProp, isRecipeVariant, isValidProperty, resolv
import { type Rule, createRule } from '../utils'
import { compositeProperties } from '../utils/composite-properties'
import { isIdentifier, isJSXIdentifier, isJSXOpeningElement, isObjectExpression } from '../utils/nodes'
+import type { TSESTree } from '@typescript-eslint/utils'
export const RULE_NAME = 'prefer-unified-property-style'
@@ -10,40 +11,101 @@ const rule: Rule = createRule({
meta: {
docs: {
description:
- 'Discourage against mixing atomic and composite forms of the same property in a style declaration. Atomic styles give more consistent results',
+ 'Discourage mixing atomic and composite forms of the same property in a style declaration. Atomic styles give more consistent results.',
},
messages: {
unify:
- "You're mixing atomic {{atomicProperties}} with composite property {{composite}}. \nPrefer atomic styling to mixing atomic and composite properties. \nRemove `{{composite}}` and use one or more of {{atomics}} instead",
+ "You're mixing atomic {{atomicProperties}} with composite property `{{composite}}`. Prefer atomic styling to mixing atomic and composite properties. Remove `{{composite}}` and use one or more of {{atomics}} instead.",
},
type: 'suggestion',
schema: [],
},
defaultOptions: [],
create(context) {
- const getLonghand = (name: string) => resolveLonghand(name, context) ?? name
+ // Cache for resolved longhand properties
+ const longhandCache = new Map()
- const resolveCompositeProperty = (name: string) => {
- if (name in compositeProperties) return name
+ const getLonghand = (name: string): string => {
+ if (longhandCache.has(name)) {
+ return longhandCache.get(name)!
+ }
+ const longhand = resolveLonghand(name, context) ?? name
+ longhandCache.set(name, longhand)
+ return longhand
+ }
+
+ // Cache for composite property resolution
+ const compositePropertyCache = new Map()
+
+ const resolveCompositeProperty = (name: string): string | undefined => {
+ if (compositePropertyCache.has(name)) {
+ return compositePropertyCache.get(name)
+ }
+
+ if (name in compositeProperties) {
+ compositePropertyCache.set(name, name)
+ return name
+ }
const longhand = getLonghand(name)
- if (isValidProperty(longhand, context) && longhand in compositeProperties) return longhand
+ if (isValidProperty(longhand, context) && longhand in compositeProperties) {
+ compositePropertyCache.set(name, longhand)
+ return longhand
+ }
+
+ compositePropertyCache.set(name, undefined)
+ return undefined
+ }
+
+ // Caches for helper functions
+ const pandaPropCache = new WeakMap()
+ const isCachedPandaProp = (node: TSESTree.JSXAttribute): boolean => {
+ if (pandaPropCache.has(node)) {
+ return pandaPropCache.get(node)!
+ }
+ const result = isPandaProp(node, context)
+ pandaPropCache.set(node, result)
+ return !!result
}
- const sendReport = (node: any, cmp: string, siblings: string[]) => {
- const _atomicProperties = siblings
- .filter((prop) => compositeProperties[cmp].includes(getLonghand(prop)))
+ const pandaAttributeCache = new WeakMap()
+ const isCachedPandaAttribute = (node: TSESTree.Property): boolean => {
+ if (pandaAttributeCache.has(node)) {
+ return pandaAttributeCache.get(node)!
+ }
+ const result = isPandaAttribute(node, context)
+ pandaAttributeCache.set(node, result)
+ return !!result
+ }
+
+ const recipeVariantCache = new WeakMap()
+ const isCachedRecipeVariant = (node: TSESTree.Property): boolean => {
+ if (recipeVariantCache.has(node)) {
+ return recipeVariantCache.get(node)!
+ }
+ const result = isRecipeVariant(node, context)
+ recipeVariantCache.set(node, result)
+ return !!result
+ }
+
+ const sendReport = (node: TSESTree.Node, composite: string, siblings: string[]) => {
+ const atomicPropertiesSet = new Set(
+ siblings.filter((prop) => compositeProperties[composite].includes(getLonghand(prop))),
+ )
+
+ if (atomicPropertiesSet.size === 0) return
+
+ const atomicProperties = Array.from(atomicPropertiesSet)
.map((prop) => `\`${prop}\``)
- if (!_atomicProperties.length) return
+ .join(', ')
- const atomicProperties = _atomicProperties.join(', ') + (_atomicProperties.length === 1 ? ' style' : ' styles')
- const atomics = compositeProperties[cmp].map((name) => `\`${name}\``).join(', ')
+ const atomics = compositeProperties[composite].map((name) => `\`${name}\``).join(', ')
context.report({
node,
messageId: 'unify',
data: {
- composite: cmp,
+ composite,
atomicProperties,
atomics,
},
@@ -51,29 +113,33 @@ const rule: Rule = createRule({
}
return {
- JSXAttribute(node) {
+ JSXAttribute(node: TSESTree.JSXAttribute) {
if (!isJSXIdentifier(node.name)) return
- if (!isPandaProp(node, context)) return
+ if (!isCachedPandaProp(node)) return
- const cmp = resolveCompositeProperty(node.name.name)
- if (!cmp) return
+ const composite = resolveCompositeProperty(node.name.name)
+ if (!composite) return
if (!isJSXOpeningElement(node.parent)) return
const siblings = node.parent.attributes.map((attr: any) => attr.name.name)
- sendReport(node, cmp, siblings)
+
+ sendReport(node, composite, siblings)
},
- Property(node) {
+ Property(node: TSESTree.Property) {
if (!isIdentifier(node.key)) return
- if (!isPandaAttribute(node, context)) return
- if (isRecipeVariant(node, context)) return
+ if (!isCachedPandaAttribute(node)) return
+ if (isCachedRecipeVariant(node)) return
- const cmp = resolveCompositeProperty(node.key.name)
- if (!cmp) return
+ const composite = resolveCompositeProperty(node.key.name)
+ if (!composite) return
if (!isObjectExpression(node.parent)) return
- const siblings = node.parent.properties.map((prop: any) => isIdentifier(prop.key) && prop.key.name)
- sendReport(node.key, cmp, siblings)
+ const siblings = node.parent.properties
+ .filter((prop): prop is TSESTree.Property => prop.type === 'Property')
+ .map((prop) => (isIdentifier(prop.key) ? prop.key.name : ''))
+
+ sendReport(node.key, composite, siblings)
},
}
},
diff --git a/plugin/src/utils/helpers.ts b/plugin/src/utils/helpers.ts
index 62774d9..dd726ba 100644
--- a/plugin/src/utils/helpers.ts
+++ b/plugin/src/utils/helpers.ts
@@ -47,7 +47,6 @@ export const getImportSpecifiers = (context: RuleContext) => {
node.specifiers.forEach((specifier) => {
if (!isImportSpecifier(specifier)) return
-
specifiers.push({ specifier, mod })
})
})
@@ -77,13 +76,20 @@ const _getImports = (context: RuleContext) => {
return imports
}
+// Caching imports per context to avoid redundant computations
+const importsCache = new WeakMap, ImportResult[]>()
+
export const getImports = (context: RuleContext) => {
+ if (importsCache.has(context)) {
+ return importsCache.get(context)!
+ }
const imports = _getImports(context)
- return imports.filter((imp) => syncAction('matchImports', getSyncOpts(context), imp))
+ const filteredImports = imports.filter((imp) => syncAction('matchImports', getSyncOpts(context), imp))
+ importsCache.set(context, filteredImports)
+ return filteredImports
}
-const isValidStyledProp = (node: T, context: RuleContext) => {
- if (typeof node === 'string') return
+const isValidStyledProp = (node: T, context: RuleContext) => {
return isJSXIdentifier(node) && isValidProperty(node.name, context)
}
@@ -103,7 +109,8 @@ const findDeclaration = (name: string, context: RuleContext) => {
?.defs.find((d) => isIdentifier(d.name) && d.name.name === name)?.node
if (isVariableDeclarator(decl)) return decl
} catch (error) {
- return
+ console.error('Error in findDeclaration:', error)
+ return undefined
}
}
@@ -123,10 +130,6 @@ export const isValidFile = (context: RuleContext) => {
return syncAction('isValidFile', getSyncOpts(context))
}
-export const isConfigFile = (context: RuleContext) => {
- return syncAction('isConfigFile', getSyncOpts(context))
-}
-
export const isValidProperty = (name: string, context: RuleContext, calleName?: string) => {
return syncAction('isValidProperty', getSyncOpts(context), name, calleName)
}
@@ -198,6 +201,7 @@ export const isInJSXProp = (node: TSESTree.Property, context: RuleContext()
+
export const extractTokens = (value: string) => {
const regex = /token\(([^"'(),]+)(?:,\s*([^"'(),]+))?\)|\{([^{}]+)\}/g
const matches = []
@@ -255,9 +262,16 @@ export const extractTokens = (value: string) => {
}
export const getInvalidTokens = (value: string, context: RuleContext) => {
+ if (invalidTokensCache.has(value)) {
+ return invalidTokensCache.get(value)!
+ }
+
const tokens = extractTokens(value)
if (!tokens.length) return []
- return syncAction('filterInvalidTokens', getSyncOpts(context), tokens)
+
+ const invalidTokens = syncAction('filterInvalidTokens', getSyncOpts(context), tokens)
+ invalidTokensCache.set(value, invalidTokens)
+ return invalidTokens
}
export const getTokenImport = (context: RuleContext) => {
diff --git a/plugin/src/utils/index.ts b/plugin/src/utils/index.ts
index 86c1f79..312071b 100644
--- a/plugin/src/utils/index.ts
+++ b/plugin/src/utils/index.ts
@@ -4,25 +4,30 @@ import { join } from 'node:path'
import { fileURLToPath } from 'node:url'
import type { run } from './worker'
-export const createRule: ReturnType<(typeof ESLintUtils)['RuleCreator']> = ESLintUtils.RuleCreator(
+// Rule creator
+export const createRule = ESLintUtils.RuleCreator(
(name) => `https://github.com/chakra-ui/eslint-plugin-panda/blob/main/docs/rules/${name}.md`,
)
-export type Rule = ReturnType>
+// Define Rule type explicitly
+export type Rule = ReturnType
+// Determine the distribution directory
const isBase = process.env.NODE_ENV !== 'test' || import.meta.url.endsWith('dist/index.js')
export const distDir = fileURLToPath(new URL(isBase ? './' : '../../dist', import.meta.url))
+// Create synchronous function using synckit
export const _syncAction = createSyncFn(join(distDir, 'utils/worker.mjs'))
-// export const _syncAction = createSyncFn(join(distDir, 'utils/worker.mjs')) as typeof run
-export const syncAction = ((...args: any) => {
+// Define syncAction with proper typing and error handling
+export const syncAction = ((...args: Parameters) => {
try {
return _syncAction(...args)
} catch (error) {
- return
+ console.error('syncAction error:', error)
+ return undefined
}
-}) as typeof run | ((...args: any) => undefined)
+}) as typeof run
export interface ImportResult {
name: string
diff --git a/plugin/src/utils/nodes.ts b/plugin/src/utils/nodes.ts
index 2df2b05..cf9f0c0 100644
--- a/plugin/src/utils/nodes.ts
+++ b/plugin/src/utils/nodes.ts
@@ -1,40 +1,23 @@
import type { TSESTree } from '@typescript-eslint/utils'
-
import { isNodeOfType } from '@typescript-eslint/utils/ast-utils'
import { AST_NODE_TYPES } from '@typescript-eslint/utils'
export type Node = TSESTree.Node
export const isIdentifier = isNodeOfType(AST_NODE_TYPES.Identifier)
-
export const isLiteral = isNodeOfType(AST_NODE_TYPES.Literal)
-
export const isTemplateLiteral = isNodeOfType(AST_NODE_TYPES.TemplateLiteral)
-
export const isArrayExpression = isNodeOfType(AST_NODE_TYPES.ArrayExpression)
-
export const isObjectExpression = isNodeOfType(AST_NODE_TYPES.ObjectExpression)
-
export const isMemberExpression = isNodeOfType(AST_NODE_TYPES.MemberExpression)
-
export const isVariableDeclarator = isNodeOfType(AST_NODE_TYPES.VariableDeclarator)
-
export const isVariableDeclaration = isNodeOfType(AST_NODE_TYPES.VariableDeclaration)
-
export const isJSXMemberExpression = isNodeOfType(AST_NODE_TYPES.JSXMemberExpression)
-
export const isJSXOpeningElement = isNodeOfType(AST_NODE_TYPES.JSXOpeningElement)
-
export const isJSXExpressionContainer = isNodeOfType(AST_NODE_TYPES.JSXExpressionContainer)
-
export const isJSXAttribute = isNodeOfType(AST_NODE_TYPES.JSXAttribute)
-
export const isJSXIdentifier = isNodeOfType(AST_NODE_TYPES.JSXIdentifier)
-
export const isCallExpression = isNodeOfType(AST_NODE_TYPES.CallExpression)
-
export const isImportDeclaration = isNodeOfType(AST_NODE_TYPES.ImportDeclaration)
-
export const isImportSpecifier = isNodeOfType(AST_NODE_TYPES.ImportSpecifier)
-
export const isProperty = isNodeOfType(AST_NODE_TYPES.Property)
diff --git a/plugin/src/utils/worker.ts b/plugin/src/utils/worker.ts
index b2fa48f..10c9311 100644
--- a/plugin/src/utils/worker.ts
+++ b/plugin/src/utils/worker.ts
@@ -6,8 +6,13 @@ import { findConfig } from '@pandacss/config'
import path from 'path'
import type { ImportResult } from '.'
-let promise: Promise | undefined
+type Opts = {
+ currentFile: string
+ configPath?: string
+}
+
let configPath: string | undefined
+let contextCache: { [configPath: string]: Promise } = {}
async function _getContext(configPath: string | undefined) {
if (!configPath) throw new Error('Invalid config path')
@@ -26,8 +31,13 @@ export async function getContext(opts: Opts) {
return ctx
} else {
configPath = configPath || findConfig({ cwd: opts.configPath ?? opts.currentFile })
- promise = promise || _getContext(configPath)
- return await promise
+
+ // Ensure that the context is refreshed when the configPath changes.
+ if (!contextCache[configPath]) {
+ contextCache[configPath] = _getContext(configPath)
+ }
+
+ return await contextCache[configPath]
}
}
@@ -53,10 +63,6 @@ const arePathsEqual = (path1: string, path2: string) => {
return normalizedPath1 === normalizedPath2
}
-async function isConfigFile(fileName: string): Promise {
- return arePathsEqual(configPath!, fileName)
-}
-
async function isValidFile(ctx: PandaContext, fileName: string): Promise {
return ctx.getFiles().some((file) => arePathsEqual(file, fileName))
}
@@ -106,15 +112,9 @@ async function matchImports(ctx: PandaContext, result: MatchImportResult) {
})
}
-type Opts = {
- currentFile: string
- configPath?: string
-}
-
export function runAsync(action: 'filterInvalidTokens', opts: Opts, paths: string[]): Promise
export function runAsync(action: 'isColorToken', opts: Opts, value: string): Promise
export function runAsync(action: 'isColorAttribute', opts: Opts, attr: string): Promise
-export function runAsync(action: 'isConfigFile', opts: Opts, fileName: string): Promise
export function runAsync(action: 'isValidFile', opts: Opts, fileName: string): Promise
export function runAsync(action: 'resolveShorthands', opts: Opts, name: string): Promise
export function runAsync(action: 'resolveLongHand', opts: Opts, name: string): Promise
@@ -140,8 +140,6 @@ export async function runAsync(action: string, opts: Opts, ...args: any): Promis
case 'resolveShorthands':
// @ts-expect-error cast
return resolveShorthands(ctx, ...args)
- case 'isConfigFile':
- return isConfigFile(opts.currentFile)
case 'isValidFile':
return isValidFile(ctx, opts.currentFile)
case 'isColorAttribute':
@@ -159,7 +157,6 @@ export async function runAsync(action: string, opts: Opts, ...args: any): Promis
export function run(action: 'filterInvalidTokens', opts: Opts, paths: string[]): string[]
export function run(action: 'isColorToken', opts: Opts, value: string): boolean
export function run(action: 'isColorAttribute', opts: Opts, attr: string): boolean
-export function run(action: 'isConfigFile', opts: Opts): boolean
export function run(action: 'isValidFile', opts: Opts): boolean
export function run(action: 'resolveShorthands', opts: Opts, name: string): string[] | undefined
export function run(action: 'resolveLongHand', opts: Opts, name: string): string | undefined
diff --git a/plugin/tests/file-not-included.test.ts b/plugin/tests/file-not-included.test.ts
index da4561c..dc39b06 100644
--- a/plugin/tests/file-not-included.test.ts
+++ b/plugin/tests/file-not-included.test.ts
@@ -28,7 +28,7 @@ tester.run(RULE_NAME, rule, {
{
code: invalidCode,
filename: 'Invalid.tsx',
- errors: 2,
+ errors: 1,
},
],
})