Skip to content

Commit

Permalink
feat: add no-invalid-nesting rule
Browse files Browse the repository at this point in the history
  • Loading branch information
anubra266 committed Feb 10, 2024
1 parent 265d068 commit 587b69a
Show file tree
Hide file tree
Showing 6 changed files with 150 additions and 13 deletions.
5 changes: 5 additions & 0 deletions .changeset/small-poets-sparkle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@pandacss/eslint-plugin": patch
---

Add `no-invalid-nesting` rule
1 change: 1 addition & 0 deletions plugin/src/configs/recommended.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export default {
'@pandacss/no-config-function-in-source': 'error',
'@pandacss/no-debug': 'warn',
'@pandacss/no-dynamic-styling': 'warn',
'@pandacss/no-invalid-nesting': 'error',
'@pandacss/no-invalid-token-paths': 'error',
'@pandacss/no-property-renaming': 'warn',
'@pandacss/prefer-unified-property-style': 'warn',
Expand Down
2 changes: 2 additions & 0 deletions plugin/src/rules/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import noDynamicStyling, { RULE_NAME as NoDynamicStyling } from './no-dynamic-st
import noEscapeHatch, { RULE_NAME as NoEscapeHatch } from './no-escape-hatch'
import noHardCodedColor, { RULE_NAME as NoHardCodedColor } from './no-hardcoded-color'
import noImportant, { RULE_NAME as NoImportant } from './no-important'
import noInvalidNesting, { RULE_NAME as NoInvalidNesting } from './no-invalid-nesting'
import noInvalidTokenPaths, { RULE_NAME as NoInvalidTokenPaths } from './no-invalid-token-paths'
import noPropertyRenaming, { RULE_NAME as NoPropertyRenaming } from './no-property-renaming'
import noUnsafeTokenUsage, { RULE_NAME as NoUnsafeTokenUsage } from './no-unsafe-token-fn-usage'
Expand All @@ -23,6 +24,7 @@ export const rules = {
[NoHardCodedColor]: noHardCodedColor,
[NoImportant]: noImportant,
[NoInvalidTokenPaths]: noInvalidTokenPaths,
[NoInvalidNesting]: noInvalidNesting,
[NoPropertyRenaming]: noPropertyRenaming,
[NoUnsafeTokenUsage]: noUnsafeTokenUsage,
[PreferLonghandProperties]: preferLonghandProperties,
Expand Down
41 changes: 41 additions & 0 deletions plugin/src/rules/no-invalid-nesting.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { isIdentifier, isLiteral, isObjectExpression, isTemplateLiteral } from '../utils/nodes'
import { type Rule, createRule } from '../utils'
import { isInJSXProp, isInPandaFunction } from '../utils/helpers'

export const RULE_NAME = 'no-invalid-nesting'

const rule: Rule = createRule({
name: RULE_NAME,
meta: {
docs: {
description: "Warn against invalid nesting. i.e. nested styles that don't contain the `&` character.",
},
messages: {
nesting: 'Invalid style nesting. Nested styles must contain the `&` character.',
},
type: 'suggestion',
schema: [],
},
defaultOptions: [],
create(context) {
return {
Property(node) {
if (!isObjectExpression(node.value) || isIdentifier(node.key)) return
if (!isInPandaFunction(node, context) && !isInJSXProp(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) {
context.report({
node: node.key,
messageId: 'nesting',
})
}
},
}
},
})

export default rule
46 changes: 33 additions & 13 deletions plugin/src/utils/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ import {
isJSXIdentifier,
isJSXMemberExpression,
isJSXOpeningElement,
isLiteral,
isMemberExpression,
isTemplateLiteral,
isVariableDeclarator,
type Node,
} from './nodes'
Expand Down Expand Up @@ -161,7 +163,22 @@ export const isPandaProp = (node: TSESTree.JSXAttribute, context: RuleContext<an
return true
}

const isInPandaFunction = (node: TSESTree.Property, context: RuleContext<any, any>) => {
export const isStyledProperty = (node: TSESTree.Property, context: RuleContext<any, any>, calleeName: string) => {
if (!isIdentifier(node.key) && !isLiteral(node.key) && !isTemplateLiteral(node.key)) return

if (isIdentifier(node.key) && !isValidProperty(node.key.name, context, calleeName)) return
if (
isLiteral(node.key) &&
typeof node.key.value === 'string' &&
!isValidProperty(node.key.value, context, calleeName)
)
return
if (isTemplateLiteral(node.key) && !isValidProperty(node.key.quasis[0].value.raw, context, calleeName)) return

return true
}

export const isInPandaFunction = (node: TSESTree.Property, context: RuleContext<any, any>) => {
const callAncestor = getAncestor(isCallExpression, node)
if (!callAncestor) return

Expand All @@ -180,20 +197,10 @@ const isInPandaFunction = (node: TSESTree.Property, context: RuleContext<any, an
if (!calleeName) return
if (!isPandaIsh(calleeName, context)) return

// Ensure attribute is a styled attribute
if (!isIdentifier(node.key)) return
const attr = node.key.name
if (!isValidProperty(attr, context, calleeName)) return

return true
return calleeName
}

export const isPandaAttribute = (node: TSESTree.Property, context: RuleContext<any, any>) => {
const callAncestor = getAncestor(isCallExpression, node)

if (callAncestor) return isInPandaFunction(node, context)

// Object could be in JSX prop value i.e css prop or a pseudo
export const isInJSXProp = (node: TSESTree.Property, context: RuleContext<any, any>) => {
const jsxExprAncestor = getAncestor(isJSXExpressionContainer, node)
const jsxAttrAncestor = getAncestor(isJSXAttribute, node)

Expand All @@ -204,6 +211,19 @@ export const isPandaAttribute = (node: TSESTree.Property, context: RuleContext<a
return true
}

export const isPandaAttribute = (node: TSESTree.Property, context: RuleContext<any, any>) => {
const callAncestor = getAncestor(isCallExpression, node)

if (callAncestor) {
const callee = isInPandaFunction(node, context)
if (!callee) return
return isStyledProperty(node, context, callee)
}

// Object could be in JSX prop value i.e css prop or a pseudo
return isInJSXProp(node, context)
}

export const resolveLonghand = (name: string, context: RuleContext<any, any>) => {
return syncAction('resolveLongHand', getSyncOpts(context), name)
}
Expand Down
68 changes: 68 additions & 0 deletions plugin/tests/no-invalid-nesting.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { tester } from '../test-utils'
import rule, { RULE_NAME } from '../src/rules/no-invalid-nesting'

const javascript = String.raw

const valids = [
{
code: javascript`
import { css } from './panda/css';
const styles = css({ '&:hover': { marginLeft: '4px' } })`,
},

{
code: javascript`
import { css } from './panda/css';
function App(){
return <div className={css({ '.dark &': { background: 'red.100' } })} />;
}`,
},

{
code: javascript`
import { Circle } from './panda/jsx';
function App(){
return <Circle css={{ '&[data-focus]': { position: 'absolute' } }} />;
}`,
},
]

const invalids = [
{
code: javascript`
import { css } from './panda/css';
const styles = css({ ':hover': { marginLeft: '4px' } })`,
},

{
code: javascript`
import { css } from './panda/css';
function App(){
return <div className={css({ '.dark ': { background: 'red.100' } })} />;
}`,
},

{
code: javascript`
import { Circle } from './panda/jsx';
function App(){
return <Circle css={{ '[data-focus]': { position: 'absolute' } }} />;
}`,
},
]

tester.run(RULE_NAME, rule, {
valid: valids.map(({ code }) => ({
code,
})),
invalid: invalids.map(({ code }) => ({
code,
errors: 1,
})),
})

0 comments on commit 587b69a

Please sign in to comment.