Skip to content

Commit

Permalink
Apply memoization in rules (#161)
Browse files Browse the repository at this point in the history
  • Loading branch information
anubra266 authored Oct 8, 2024
2 parents 15aee80 + 121ad0f commit 021e691
Show file tree
Hide file tree
Showing 24 changed files with 1,217 additions and 560 deletions.
5 changes: 5 additions & 0 deletions .changeset/wise-seas-beg.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@pandacss/eslint-plugin': minor
---

Use memoization in rules
26 changes: 21 additions & 5 deletions plugin/src/rules/file-not-included.ts
Original file line number Diff line number Diff line change
@@ -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'

Expand All @@ -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
},
}
},
Expand Down
99 changes: 65 additions & 34 deletions plugin/src/rules/no-config-function-in-source.ts
Original file line number Diff line number Diff line change
@@ -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
},
},
],
Expand All @@ -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',
]
15 changes: 8 additions & 7 deletions plugin/src/rules/no-debug.ts
Original file line number Diff line number Diff line change
@@ -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'

Expand All @@ -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({
Expand All @@ -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({
Expand Down
98 changes: 53 additions & 45 deletions plugin/src/rules/no-dynamic-styling.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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: <Circle property={`value that could be multiline`} />
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({
Expand All @@ -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',
Expand All @@ -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
Loading

0 comments on commit 021e691

Please sign in to comment.