Skip to content

Commit

Permalink
fix: false positive in no-nesting rule (#99)
Browse files Browse the repository at this point in the history
  • Loading branch information
anubra266 authored Jun 16, 2024
1 parent 9dad52a commit 979d8c9
Show file tree
Hide file tree
Showing 4 changed files with 145 additions and 12 deletions.
5 changes: 5 additions & 0 deletions .changeset/new-students-leave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@pandacss/eslint-plugin": patch
---

Fix false positive in @pandacss/no-invalid-nesting
59 changes: 48 additions & 11 deletions plugin/src/rules/no-invalid-nesting.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { isIdentifier, isLiteral, isObjectExpression, isTemplateLiteral } from '../utils/nodes'
import { type Rule, createRule } from '../utils'
import { isInJSXProp, isInPandaFunction, isStyledProperty } from '../utils/helpers'
import { getImports, isInJSXProp, isInPandaFunction, isStyledProperty } from '../utils/helpers'
import type { TSESTree } from '@typescript-eslint/utils'
import type { RuleContext } from '@typescript-eslint/utils/ts-eslint'

export const RULE_NAME = 'no-invalid-nesting'

Expand All @@ -21,22 +23,57 @@ const rule: Rule = createRule({
return {
Property(node) {
if (!isObjectExpression(node.value) || isIdentifier(node.key)) return
if (!isInPandaFunction(node, context) && !isInJSXProp(node, context)) return
const caller = isInPandaFunction(node, context)
if (!caller && !isInJSXProp(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('&')
const invalidNesting = isInvalidNesting(node, context, caller)
if (!invalidNesting) return

if (invalidLiteral || invalidTemplateLiteral) {
context.report({
node: node.key,
messageId: 'nesting',
})
}
context.report({
node: node.key,
messageId: 'nesting',
})
},
}
},
})

export default rule

function isInvalidNesting(node: TSESTree.Property, context: RuleContext<any, any>, caller: string | undefined) {
// Check if the caller is either 'cva' or 'sva'
const recipe = getImports(context).find((imp) => ['cva', 'sva'].includes(imp.name) && imp.alias === caller)
if (!recipe) return checkNode(node)

//* Nesting is different here because of slots and variants. We don't want to warn about those.
let currentNode: any = node
let length = 0
let styleObjectParent = null

// Traverse up the AST
while (currentNode) {
if (currentNode.key && ['base', 'variants'].includes(currentNode.key.name)) {
styleObjectParent = currentNode.key.name
}
currentNode = currentNode.parent
if (!styleObjectParent) length++
}

// Determine the required length based on caller and styleObjectParent
const requiredLength = caller === 'cva' ? 2 : 4
const extraLength = styleObjectParent === 'base' ? 0 : 4

if (length >= requiredLength + extraLength) {
return checkNode(node)
}

return false
}

function checkNode(node: TSESTree.Property) {
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('&')

return invalidLiteral || invalidTemplateLiteral
}
2 changes: 1 addition & 1 deletion plugin/src/utils/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ const _getImports = (context: RuleContext<any, any>) => {
return imports
}

const getImports = (context: RuleContext<any, any>) => {
export const getImports = (context: RuleContext<any, any>) => {
const imports = _getImports(context)
return imports.filter((imp) => syncAction('matchImports', getSyncOpts(context), imp))
}
Expand Down
91 changes: 91 additions & 0 deletions plugin/tests/_parsing.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import rule2, { RULE_NAME as RULE_NAME2 } from '../src/rules/no-dynamic-styling'
import rule3, { RULE_NAME as RULE_NAME3 } from '../src/rules/no-escape-hatch'
import rule4, { RULE_NAME as RULE_NAME4 } from '../src/rules/no-unsafe-token-fn-usage'
import rule5, { RULE_NAME as RULE_NAME5 } from '../src/rules/no-invalid-token-paths'
import rule6, { RULE_NAME as RULE_NAME6 } from '../src/rules/no-invalid-nesting'
import { eslintTester } from '../test-utils'
import { getArbitraryValue } from '@pandacss/shared'

Expand Down Expand Up @@ -195,3 +196,93 @@ eslintTester.run(RULE_NAME5, rule5 as any, {
errors,
})),
})

//? Testing nesting in sva and cva

const imports6 = `import { cva, sva } from './panda/css';`

const valids6 = [
`const heading = cva({
base: {
color: 'red.400',
},
variants: {
size: {
'large-bold': {
color: 'red.800',
},
},
},
})`,
`const heading2 = sva({
slots: ['sm-ca'],
base: {
'sm-ca': {
color: 'red.600',
},
},
variants: {
size: {
'va-riant': {
'sm-ca': {
color: 'red.600',
},
},
},
},
})`,
]

const invalids6 = [
{
code: `const heading = cva({
base: {
'> div': {
color: 'red.500',
},
},
variants: {
size: {
'large-bold': {
'> div': {
color: 'red.500',
},
},
},
},
})`,
},
{
code: `const heading2 = sva({
slots: ['sm-ca'],
base: {
'sm-ca': {
'> div': {
color: 'red.600',
},
},
},
variants: {
size: {
'va-riant': {
'sm-ca': {
'> div': {
color: 'red.600',
},
},
},
},
},
})`,
},
]

eslintTester.run(RULE_NAME6, rule6 as any, {
valid: valids6.map((code) => ({
code: imports6 + code,
})),
invalid: invalids6.map(({ code }) => ({
code: imports6 + code,
errors: 2,
})),
})

0 comments on commit 979d8c9

Please sign in to comment.