Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Apply memoization in rules #161

Merged
merged 2 commits into from
Oct 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading