uilint-eslint 0.2.21 → 0.2.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/utils/create-rule.ts","../../src/utils/export-resolver.ts","../../src/rules/no-prop-drilling-depth.ts"],"sourcesContent":["/**\n * Rule creation helper using @typescript-eslint/utils\n */\n\nimport { ESLintUtils } from \"@typescript-eslint/utils\";\n\nexport const createRule = ESLintUtils.RuleCreator(\n (name) =>\n `https://github.com/peter-suggate/uilint/blob/main/packages/uilint-eslint/docs/rules/${name}.md`\n);\n\n/**\n * Schema for prompting user to configure a rule option in the CLI\n */\nexport interface OptionFieldSchema {\n /** Field name in the options object */\n key: string;\n /** Display label for the prompt */\n label: string;\n /** Prompt type */\n type: \"text\" | \"number\" | \"boolean\" | \"select\" | \"multiselect\";\n /** Default value */\n defaultValue: unknown;\n /** Placeholder text (for text/number inputs) */\n placeholder?: string;\n /** Options for select/multiselect */\n options?: Array<{ value: string | number; label: string }>;\n /** Description/hint for the field */\n description?: string;\n}\n\n/**\n * Schema describing how to prompt for rule options during installation\n */\nexport interface RuleOptionSchema {\n /** Fields that can be configured for this rule */\n fields: OptionFieldSchema[];\n}\n\n/**\n * Colocated rule metadata - exported alongside each rule\n *\n * This structure keeps all rule metadata in the same file as the rule implementation,\n * making it easy to maintain and extend as new rules are added.\n */\nexport interface RuleMeta {\n /** Rule identifier (e.g., \"no-arbitrary-tailwind\") - must match filename */\n id: string;\n\n /** Display name for CLI (e.g., \"No Arbitrary Tailwind\") */\n name: string;\n\n /** Short description for CLI selection prompts (one line) */\n description: string;\n\n /** Default severity level */\n defaultSeverity: \"error\" | \"warn\" | \"off\";\n\n /** Category for grouping in CLI */\n category: \"static\" | \"semantic\";\n\n /** Whether this rule requires a styleguide file */\n requiresStyleguide?: boolean;\n\n /** Default options for the rule (passed as second element in ESLint config) */\n defaultOptions?: unknown[];\n\n /** Schema for prompting user to configure options during install */\n optionSchema?: RuleOptionSchema;\n\n /**\n * Detailed documentation in markdown format.\n * Should include:\n * - What the rule does\n * - Why it's useful\n * - Examples of incorrect and correct code\n * - Configuration options explained\n */\n docs: string;\n}\n\n/**\n * Helper to define rule metadata with type safety\n */\nexport function defineRuleMeta(meta: RuleMeta): RuleMeta {\n return meta;\n}\n","/**\n * Export Resolver\n *\n * Resolves import paths and finds export definitions, following re-exports\n * to their original source files.\n */\n\nimport { ResolverFactory } from \"oxc-resolver\";\nimport { parse } from \"@typescript-eslint/typescript-estree\";\nimport { readFileSync, existsSync } from \"fs\";\nimport { dirname, join, extname } from \"path\";\nimport type { TSESTree } from \"@typescript-eslint/utils\";\n\n// Module-level resolver instance (reused across calls)\nlet resolverInstance: ReturnType<typeof ResolverFactory.prototype.sync> | null =\n null;\nlet resolverFactory: ResolverFactory | null = null;\n\n/**\n * Information about a resolved export\n */\nexport interface ResolvedExport {\n /** The name of the export (e.g., \"Button\") */\n name: string;\n /** Absolute path to the file containing the actual definition */\n filePath: string;\n /** The local name in the source file (may differ from export name) */\n localName: string;\n /** Whether this is a re-export (export { X } from './other') */\n isReexport: boolean;\n}\n\n/**\n * Cache for file exports to avoid re-parsing\n */\nconst exportCache = new Map<\n string,\n Map<string, { localName: string; reexportSource?: string }>\n>();\n\n/**\n * Cache for parsed ASTs\n */\nconst astCache = new Map<string, TSESTree.Program>();\n\n/**\n * Cache for resolved paths\n */\nconst resolvedPathCache = new Map<string, string | null>();\n\n/**\n * Get or create the resolver factory\n */\nfunction getResolverFactory(): ResolverFactory {\n if (!resolverFactory) {\n resolverFactory = new ResolverFactory({\n extensions: [\".tsx\", \".ts\", \".jsx\", \".js\"],\n mainFields: [\"module\", \"main\"],\n conditionNames: [\"import\", \"require\", \"node\", \"default\"],\n // Enable TypeScript path resolution\n tsconfig: {\n configFile: \"tsconfig.json\",\n references: \"auto\",\n },\n });\n }\n return resolverFactory;\n}\n\n/**\n * Resolve an import path to an absolute file path\n */\nexport function resolveImportPath(\n importSource: string,\n fromFile: string\n): string | null {\n const cacheKey = `${fromFile}::${importSource}`;\n\n if (resolvedPathCache.has(cacheKey)) {\n return resolvedPathCache.get(cacheKey) ?? null;\n }\n\n // Skip node_modules\n if (\n importSource.startsWith(\"react\") ||\n importSource.startsWith(\"next\") ||\n (!importSource.startsWith(\".\") &&\n !importSource.startsWith(\"@/\") &&\n !importSource.startsWith(\"~/\"))\n ) {\n // Check if it's a known external package\n if (\n importSource.includes(\"@mui/\") ||\n importSource.includes(\"@chakra-ui/\") ||\n importSource.includes(\"antd\") ||\n importSource.includes(\"@radix-ui/\")\n ) {\n // Return a marker for external packages - we don't resolve them but track them\n resolvedPathCache.set(cacheKey, null);\n return null;\n }\n resolvedPathCache.set(cacheKey, null);\n return null;\n }\n\n try {\n const factory = getResolverFactory();\n const fromDir = dirname(fromFile);\n const result = factory.sync(fromDir, importSource);\n\n if (result.path) {\n resolvedPathCache.set(cacheKey, result.path);\n return result.path;\n }\n } catch {\n // Fallback: try manual resolution for common patterns\n const resolved = manualResolve(importSource, fromFile);\n resolvedPathCache.set(cacheKey, resolved);\n return resolved;\n }\n\n resolvedPathCache.set(cacheKey, null);\n return null;\n}\n\n/**\n * Manual fallback resolution for common patterns\n */\nfunction manualResolve(importSource: string, fromFile: string): string | null {\n const fromDir = dirname(fromFile);\n const extensions = [\".tsx\", \".ts\", \".jsx\", \".js\"];\n\n // Handle @/ alias - find tsconfig and resolve\n if (importSource.startsWith(\"@/\")) {\n const projectRoot = findProjectRoot(fromFile);\n if (projectRoot) {\n const relativePath = importSource.slice(2); // Remove @/\n for (const ext of extensions) {\n const candidate = join(projectRoot, relativePath + ext);\n if (existsSync(candidate)) {\n return candidate;\n }\n // Try index file\n const indexCandidate = join(projectRoot, relativePath, `index${ext}`);\n if (existsSync(indexCandidate)) {\n return indexCandidate;\n }\n }\n }\n }\n\n // Handle relative imports\n if (importSource.startsWith(\".\")) {\n for (const ext of extensions) {\n const candidate = join(fromDir, importSource + ext);\n if (existsSync(candidate)) {\n return candidate;\n }\n // Try index file\n const indexCandidate = join(fromDir, importSource, `index${ext}`);\n if (existsSync(indexCandidate)) {\n return indexCandidate;\n }\n }\n }\n\n return null;\n}\n\n/**\n * Find the project root by looking for tsconfig.json or package.json\n */\nfunction findProjectRoot(fromFile: string): string | null {\n let dir = dirname(fromFile);\n const root = \"/\";\n\n while (dir !== root) {\n if (existsSync(join(dir, \"tsconfig.json\"))) {\n return dir;\n }\n if (existsSync(join(dir, \"package.json\"))) {\n return dir;\n }\n dir = dirname(dir);\n }\n\n return null;\n}\n\n/**\n * Parse a file and cache the AST\n */\nexport function parseFile(filePath: string): TSESTree.Program | null {\n if (astCache.has(filePath)) {\n return astCache.get(filePath)!;\n }\n\n try {\n const content = readFileSync(filePath, \"utf-8\");\n const ast = parse(content, {\n jsx: true,\n loc: true,\n range: true,\n });\n astCache.set(filePath, ast);\n return ast;\n } catch {\n return null;\n }\n}\n\n/**\n * Extract export information from a file\n */\nfunction extractExports(\n filePath: string\n): Map<string, { localName: string; reexportSource?: string }> {\n if (exportCache.has(filePath)) {\n return exportCache.get(filePath)!;\n }\n\n const exports = new Map<\n string,\n { localName: string; reexportSource?: string }\n >();\n const ast = parseFile(filePath);\n\n if (!ast) {\n exportCache.set(filePath, exports);\n return exports;\n }\n\n for (const node of ast.body) {\n // Handle: export function Button() {}\n if (\n node.type === \"ExportNamedDeclaration\" &&\n node.declaration?.type === \"FunctionDeclaration\" &&\n node.declaration.id\n ) {\n exports.set(node.declaration.id.name, {\n localName: node.declaration.id.name,\n });\n }\n\n // Handle: export const Button = () => {}\n if (\n node.type === \"ExportNamedDeclaration\" &&\n node.declaration?.type === \"VariableDeclaration\"\n ) {\n for (const decl of node.declaration.declarations) {\n if (decl.id.type === \"Identifier\") {\n exports.set(decl.id.name, { localName: decl.id.name });\n }\n }\n }\n\n // Handle: export { Button } or export { Button as Btn }\n if (node.type === \"ExportNamedDeclaration\" && node.specifiers.length > 0) {\n const source = node.source?.value as string | undefined;\n for (const spec of node.specifiers) {\n if (spec.type === \"ExportSpecifier\") {\n const exportedName =\n spec.exported.type === \"Identifier\"\n ? spec.exported.name\n : spec.exported.value;\n const localName =\n spec.local.type === \"Identifier\"\n ? spec.local.name\n : spec.local.value;\n\n exports.set(exportedName, {\n localName,\n reexportSource: source,\n });\n }\n }\n }\n\n // Handle: export default function Button() {}\n if (\n node.type === \"ExportDefaultDeclaration\" &&\n node.declaration.type === \"FunctionDeclaration\" &&\n node.declaration.id\n ) {\n exports.set(\"default\", { localName: node.declaration.id.name });\n }\n\n // Handle: export default Button\n if (\n node.type === \"ExportDefaultDeclaration\" &&\n node.declaration.type === \"Identifier\"\n ) {\n exports.set(\"default\", { localName: node.declaration.name });\n }\n }\n\n exportCache.set(filePath, exports);\n return exports;\n}\n\n/**\n * Resolve an export to its original definition, following re-exports\n */\nexport function resolveExport(\n exportName: string,\n filePath: string,\n visited = new Set<string>()\n): ResolvedExport | null {\n // Cycle detection\n const key = `${filePath}::${exportName}`;\n if (visited.has(key)) {\n return null;\n }\n visited.add(key);\n\n const exports = extractExports(filePath);\n const exportInfo = exports.get(exportName);\n\n if (!exportInfo) {\n return null;\n }\n\n // If it's a re-export, follow the chain\n if (exportInfo.reexportSource) {\n const resolvedPath = resolveImportPath(exportInfo.reexportSource, filePath);\n if (resolvedPath) {\n return resolveExport(exportInfo.localName, resolvedPath, visited);\n }\n return null;\n }\n\n // This is the actual definition\n return {\n name: exportName,\n filePath,\n localName: exportInfo.localName,\n isReexport: false,\n };\n}\n\n/**\n * Clear all caches (useful for testing or watch mode)\n */\nexport function clearResolverCaches(): void {\n exportCache.clear();\n astCache.clear();\n resolvedPathCache.clear();\n}\n","/**\n * Rule: no-prop-drilling-depth\n *\n * Warns when a prop is passed through multiple intermediate components\n * without being used, indicating prop drilling that should be refactored\n * to context or state management.\n *\n * Examples:\n * - Bad: Prop passed through 3+ components without use\n * - Good: Prop used directly in receiving component\n * - Good: Using Context or Zustand instead of drilling\n */\n\nimport type { TSESTree } from \"@typescript-eslint/utils\";\nimport { createRule, defineRuleMeta } from \"../utils/create-rule.js\";\nimport {\n resolveImportPath,\n parseFile,\n clearResolverCaches,\n} from \"../utils/export-resolver.js\";\n\ntype MessageIds = \"propDrilling\";\ntype Options = [\n {\n /** Maximum depth before warning (default: 2) */\n maxDepth?: number;\n /** Props to ignore (e.g., className, style, children) */\n ignoredProps?: string[];\n /** Component patterns to skip (regex strings) */\n ignoreComponents?: string[];\n }\n];\n\n/**\n * Rule metadata - colocated with implementation for maintainability\n */\nexport const meta = defineRuleMeta({\n id: \"no-prop-drilling-depth\",\n name: \"No Prop Drilling Depth\",\n description: \"Warn when props are drilled through too many components\",\n defaultSeverity: \"warn\",\n category: \"static\",\n defaultOptions: [\n {\n maxDepth: 2,\n ignoredProps: [\"className\", \"style\", \"children\", \"key\", \"ref\", \"id\"],\n ignoreComponents: [],\n },\n ],\n optionSchema: {\n fields: [\n {\n key: \"maxDepth\",\n label: \"Maximum drilling depth\",\n type: \"number\",\n defaultValue: 2,\n description:\n \"Maximum number of components a prop can pass through without use\",\n },\n {\n key: \"ignoredProps\",\n label: \"Ignored props\",\n type: \"array\",\n defaultValue: [\"className\", \"style\", \"children\", \"key\", \"ref\", \"id\"],\n description: \"Prop names to ignore (common pass-through props)\",\n },\n ],\n },\n docs: `\n## What it does\n\nDetects when props are passed through multiple intermediate components without\nbeing used (prop drilling). This is often a sign that you should use React\nContext, Zustand, or another state management solution.\n\n## Why it's useful\n\n- **Maintainability**: Deep prop drilling creates tight coupling\n- **Refactoring**: Changes require updates in many files\n- **Readability**: Hard to trace where props come from\n- **Performance**: Unnecessary re-renders in intermediate components\n\n## Examples\n\n### ❌ Incorrect\n\n\\`\\`\\`tsx\n// Grandparent passes user through Parent to Child\nfunction Grandparent({ user }) {\n return <Parent user={user} />;\n}\n\nfunction Parent({ user }) {\n // Parent doesn't use 'user', just passes it along\n return <Child user={user} />;\n}\n\nfunction Child({ user }) {\n return <div>{user.name}</div>;\n}\n\\`\\`\\`\n\n### ✅ Correct\n\n\\`\\`\\`tsx\n// Use Context instead\nconst UserContext = createContext();\n\nfunction Grandparent({ user }) {\n return (\n <UserContext.Provider value={user}>\n <Parent />\n </UserContext.Provider>\n );\n}\n\nfunction Child() {\n const user = useContext(UserContext);\n return <div>{user.name}</div>;\n}\n\\`\\`\\`\n\n## Configuration\n\n\\`\\`\\`js\n// eslint.config.js\n\"uilint/no-prop-drilling-depth\": [\"warn\", {\n maxDepth: 2, // Allow passing through 2 components\n ignoredProps: [\"className\", \"style\", \"children\"], // Common pass-through props\n ignoreComponents: [\"^Layout\", \"^Wrapper\"] // Skip wrapper components\n}]\n\\`\\`\\`\n`,\n});\n\n/**\n * Information about a component's prop usage\n */\ninterface ComponentPropInfo {\n /** Props received by the component */\n receivedProps: Set<string>;\n /** Props passed to child components: propName -> childComponentNames[] */\n passedProps: Map<string, string[]>;\n /** Props actually used in the component (not just passed) */\n usedProps: Set<string>;\n /** Child components that receive props from this component */\n childComponents: string[];\n}\n\n/**\n * Cache for analyzed component prop information\n */\nconst componentPropCache = new Map<string, ComponentPropInfo>();\n\n/**\n * Clear the prop analysis cache\n */\nexport function clearPropCache(): void {\n componentPropCache.clear();\n clearResolverCaches();\n}\n\n/**\n * Check if a name is a React component (PascalCase)\n */\nfunction isComponentName(name: string): boolean {\n return /^[A-Z][a-zA-Z0-9]*$/.test(name);\n}\n\n/**\n * Extract props from a function parameter\n */\nfunction extractPropsFromParam(\n param: TSESTree.Parameter\n): { propNames: Set<string>; isSpread: boolean } {\n const propNames = new Set<string>();\n let isSpread = false;\n\n if (param.type === \"ObjectPattern\") {\n for (const prop of param.properties) {\n if (prop.type === \"RestElement\") {\n isSpread = true;\n } else if (\n prop.type === \"Property\" &&\n prop.key.type === \"Identifier\"\n ) {\n propNames.add(prop.key.name);\n }\n }\n } else if (param.type === \"Identifier\") {\n // Single props parameter - assume all props accessed via props.x\n isSpread = true;\n }\n\n return { propNames, isSpread };\n}\n\n/**\n * Find all JSX elements in a function body and extract prop passing info\n */\nfunction analyzeJSXPropPassing(\n body: TSESTree.Node,\n receivedProps: Set<string>\n): { passedProps: Map<string, string[]>; usedProps: Set<string> } {\n const passedProps = new Map<string, string[]>();\n const usedProps = new Set<string>();\n\n function visit(node: TSESTree.Node): void {\n if (!node || typeof node !== \"object\") return;\n\n // Check JSX elements for prop passing\n if (node.type === \"JSXOpeningElement\") {\n const elementName = getJSXElementName(node.name);\n\n // Only care about component elements (PascalCase)\n if (elementName && isComponentName(elementName)) {\n for (const attr of node.attributes) {\n if (attr.type === \"JSXAttribute\" && attr.name.type === \"JSXIdentifier\") {\n const attrName = attr.name.name;\n const propValue = attr.value;\n\n // Check if the attribute value is a received prop\n if (propValue?.type === \"JSXExpressionContainer\") {\n const expr = propValue.expression;\n if (expr.type === \"Identifier\" && receivedProps.has(expr.name)) {\n // This prop is being passed to a child\n const existing = passedProps.get(expr.name) || [];\n existing.push(elementName);\n passedProps.set(expr.name, existing);\n } else if (\n expr.type === \"MemberExpression\" &&\n expr.object.type === \"Identifier\" &&\n expr.object.name === \"props\" &&\n expr.property.type === \"Identifier\"\n ) {\n // props.x pattern\n const propName = expr.property.name;\n if (receivedProps.has(propName) || receivedProps.size === 0) {\n const existing = passedProps.get(propName) || [];\n existing.push(elementName);\n passedProps.set(propName, existing);\n }\n }\n }\n }\n\n // Check for spread props: {...props} or {...rest}\n if (attr.type === \"JSXSpreadAttribute\") {\n if (attr.argument.type === \"Identifier\") {\n const spreadName = attr.argument.name;\n if (spreadName === \"props\" || receivedProps.has(spreadName)) {\n // All props are being spread\n for (const prop of receivedProps) {\n const existing = passedProps.get(prop) || [];\n existing.push(elementName);\n passedProps.set(prop, existing);\n }\n }\n }\n }\n }\n }\n }\n\n // Check for prop usage (not just passing)\n // e.g., {user.name} or {props.user.name} or just {user}\n if (\n node.type === \"MemberExpression\" &&\n node.object.type === \"Identifier\" &&\n receivedProps.has(node.object.name)\n ) {\n usedProps.add(node.object.name);\n }\n\n if (\n node.type === \"Identifier\" &&\n receivedProps.has(node.name) &&\n node.parent?.type !== \"JSXExpressionContainer\"\n ) {\n // Prop used in expression (but not directly passed to child)\n usedProps.add(node.name);\n }\n\n // Check for props.x.something usage\n if (\n node.type === \"MemberExpression\" &&\n node.object.type === \"MemberExpression\" &&\n node.object.object.type === \"Identifier\" &&\n node.object.object.name === \"props\" &&\n node.object.property.type === \"Identifier\"\n ) {\n usedProps.add(node.object.property.name);\n }\n\n // Recurse into children\n for (const key of Object.keys(node)) {\n if (key === \"parent\" || key === \"loc\" || key === \"range\") continue;\n const child = (node as Record<string, unknown>)[key];\n if (Array.isArray(child)) {\n for (const item of child) {\n if (item && typeof item === \"object\") {\n visit(item as TSESTree.Node);\n }\n }\n } else if (child && typeof child === \"object\") {\n visit(child as TSESTree.Node);\n }\n }\n }\n\n visit(body);\n return { passedProps, usedProps };\n}\n\n/**\n * Get the name of a JSX element\n */\nfunction getJSXElementName(node: TSESTree.JSXTagNameExpression): string | null {\n if (node.type === \"JSXIdentifier\") {\n return node.name;\n }\n if (node.type === \"JSXMemberExpression\") {\n // Get the root object for namespace components\n let current = node.object;\n while (current.type === \"JSXMemberExpression\") {\n current = current.object;\n }\n return current.type === \"JSXIdentifier\" ? current.name : null;\n }\n return null;\n}\n\n/**\n * Track prop drilling within a single file\n */\ninterface PropDrillingInfo {\n propName: string;\n component: string;\n passedTo: string[];\n usedDirectly: boolean;\n}\n\nexport default createRule<Options, MessageIds>({\n name: \"no-prop-drilling-depth\",\n meta: {\n type: \"suggestion\",\n docs: {\n description: \"Warn when props are drilled through too many components\",\n },\n messages: {\n propDrilling:\n \"Prop '{{propName}}' is passed through {{depth}} component(s) without being used. Consider using Context or state management. Path: {{path}}\",\n },\n schema: [\n {\n type: \"object\",\n properties: {\n maxDepth: {\n type: \"number\",\n minimum: 1,\n description: \"Maximum drilling depth before warning\",\n },\n ignoredProps: {\n type: \"array\",\n items: { type: \"string\" },\n description: \"Props to ignore\",\n },\n ignoreComponents: {\n type: \"array\",\n items: { type: \"string\" },\n description: \"Component patterns to skip (regex)\",\n },\n },\n additionalProperties: false,\n },\n ],\n },\n defaultOptions: [\n {\n maxDepth: 2,\n ignoredProps: [\"className\", \"style\", \"children\", \"key\", \"ref\", \"id\"],\n ignoreComponents: [],\n },\n ],\n create(context) {\n const options = context.options[0] || {};\n const maxDepth = options.maxDepth ?? 2;\n const ignoredProps = new Set(\n options.ignoredProps ?? [\n \"className\",\n \"style\",\n \"children\",\n \"key\",\n \"ref\",\n \"id\",\n ]\n );\n const ignoreComponentPatterns = (options.ignoreComponents ?? []).map(\n (p) => new RegExp(p)\n );\n\n // Track components and their prop flows within the file\n const componentProps = new Map<string, ComponentPropInfo>();\n const imports = new Map<string, string>(); // localName -> importSource\n const componentNodes = new Map<string, TSESTree.Node>(); // componentName -> node\n\n function shouldIgnoreComponent(name: string): boolean {\n return ignoreComponentPatterns.some((pattern) => pattern.test(name));\n }\n\n function shouldIgnoreProp(name: string): boolean {\n return ignoredProps.has(name);\n }\n\n /**\n * Analyze a component function for prop drilling\n */\n function analyzeComponent(\n name: string,\n node: TSESTree.FunctionDeclaration | TSESTree.ArrowFunctionExpression | TSESTree.FunctionExpression,\n reportNode: TSESTree.Node\n ): void {\n if (shouldIgnoreComponent(name)) return;\n\n const firstParam = node.params[0];\n if (!firstParam) return;\n\n const { propNames, isSpread } = extractPropsFromParam(firstParam);\n\n // If using spread without destructuring, we can't easily track props\n if (isSpread && propNames.size === 0) return;\n\n const body = node.body;\n if (!body) return;\n\n const { passedProps, usedProps } = analyzeJSXPropPassing(body, propNames);\n\n componentProps.set(name, {\n receivedProps: propNames,\n passedProps,\n usedProps,\n childComponents: [...new Set([...passedProps.values()].flat())],\n });\n\n componentNodes.set(name, reportNode);\n }\n\n return {\n // Track imports for cross-file analysis\n ImportDeclaration(node) {\n const source = node.source.value as string;\n for (const spec of node.specifiers) {\n if (spec.type === \"ImportSpecifier\" || spec.type === \"ImportDefaultSpecifier\") {\n imports.set(spec.local.name, source);\n }\n }\n },\n\n // Analyze function declarations\n FunctionDeclaration(node) {\n if (node.id && isComponentName(node.id.name)) {\n analyzeComponent(node.id.name, node, node);\n }\n },\n\n // Analyze arrow functions\n VariableDeclarator(node) {\n if (\n node.id.type === \"Identifier\" &&\n isComponentName(node.id.name) &&\n node.init?.type === \"ArrowFunctionExpression\"\n ) {\n analyzeComponent(node.id.name, node.init, node);\n }\n },\n\n // Analyze at the end of the file\n \"Program:exit\"() {\n // Find drilling chains within the file\n for (const [componentName, info] of componentProps) {\n for (const [propName, children] of info.passedProps) {\n if (shouldIgnoreProp(propName)) continue;\n\n // Check if prop is used directly\n if (info.usedProps.has(propName)) continue;\n\n // Track the drilling chain\n const chain: string[] = [componentName];\n let depth = 0;\n let current = children;\n\n while (current.length > 0 && depth < maxDepth + 1) {\n depth++;\n const nextChildren: string[] = [];\n\n for (const child of current) {\n chain.push(child);\n const childInfo = componentProps.get(child);\n\n if (childInfo) {\n // Check if child uses the prop\n if (childInfo.usedProps.has(propName)) {\n // Prop is used here, drilling stops\n break;\n }\n\n // Check if child passes the prop further\n const childPasses = childInfo.passedProps.get(propName);\n if (childPasses) {\n nextChildren.push(...childPasses);\n }\n }\n }\n\n current = nextChildren;\n }\n\n // Report if depth exceeds threshold\n if (depth > maxDepth) {\n const reportNode = componentNodes.get(componentName);\n if (reportNode) {\n context.report({\n node: reportNode,\n messageId: \"propDrilling\",\n data: {\n propName,\n depth: String(depth),\n path: chain.slice(0, maxDepth + 2).join(\" → \"),\n },\n });\n }\n }\n }\n }\n },\n };\n },\n});\n"],"mappings":"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","names":["meta"]}
1
+ {"version":3,"sources":["../../src/utils/create-rule.ts","../../src/rules/no-prop-drilling-depth.ts"],"sourcesContent":["/**\n * Rule creation helper using @typescript-eslint/utils\n */\n\nimport { ESLintUtils } from \"@typescript-eslint/utils\";\n\nexport const createRule = ESLintUtils.RuleCreator(\n (name) =>\n `https://github.com/peter-suggate/uilint/blob/main/packages/uilint-eslint/docs/rules/${name}.md`\n);\n\n/**\n * Schema for prompting user to configure a rule option in the CLI\n */\nexport interface OptionFieldSchema {\n /** Field name in the options object */\n key: string;\n /** Display label for the prompt */\n label: string;\n /** Prompt type */\n type: \"text\" | \"number\" | \"boolean\" | \"select\" | \"multiselect\";\n /** Default value */\n defaultValue: unknown;\n /** Placeholder text (for text/number inputs) */\n placeholder?: string;\n /** Options for select/multiselect */\n options?: Array<{ value: string | number; label: string }>;\n /** Description/hint for the field */\n description?: string;\n}\n\n/**\n * Schema describing how to prompt for rule options during installation\n */\nexport interface RuleOptionSchema {\n /** Fields that can be configured for this rule */\n fields: OptionFieldSchema[];\n}\n\n/**\n * Colocated rule metadata - exported alongside each rule\n *\n * This structure keeps all rule metadata in the same file as the rule implementation,\n * making it easy to maintain and extend as new rules are added.\n */\nexport interface RuleMeta {\n /** Rule identifier (e.g., \"no-arbitrary-tailwind\") - must match filename */\n id: string;\n\n /** Display name for CLI (e.g., \"No Arbitrary Tailwind\") */\n name: string;\n\n /** Short description for CLI selection prompts (one line) */\n description: string;\n\n /** Default severity level */\n defaultSeverity: \"error\" | \"warn\" | \"off\";\n\n /** Category for grouping in CLI */\n category: \"static\" | \"semantic\";\n\n /** Whether this rule requires a styleguide file */\n requiresStyleguide?: boolean;\n\n /** Default options for the rule (passed as second element in ESLint config) */\n defaultOptions?: unknown[];\n\n /** Schema for prompting user to configure options during install */\n optionSchema?: RuleOptionSchema;\n\n /**\n * Detailed documentation in markdown format.\n * Should include:\n * - What the rule does\n * - Why it's useful\n * - Examples of incorrect and correct code\n * - Configuration options explained\n */\n docs: string;\n}\n\n/**\n * Helper to define rule metadata with type safety\n */\nexport function defineRuleMeta(meta: RuleMeta): RuleMeta {\n return meta;\n}\n","/**\n * Rule: no-prop-drilling-depth\n *\n * Warns when a prop is passed through multiple intermediate components\n * without being used, indicating prop drilling that should be refactored\n * to context or state management.\n *\n * Examples:\n * - Bad: Prop passed through 3+ components without use\n * - Good: Prop used directly in receiving component\n * - Good: Using Context or Zustand instead of drilling\n */\n\nimport type { TSESTree } from \"@typescript-eslint/utils\";\nimport { createRule, defineRuleMeta } from \"../utils/create-rule.js\";\n\ntype MessageIds = \"propDrilling\";\ntype Options = [\n {\n /** Maximum depth before warning (default: 2) */\n maxDepth?: number;\n /** Props to ignore (e.g., className, style, children) */\n ignoredProps?: string[];\n /** Component patterns to skip (regex strings) */\n ignoreComponents?: string[];\n }\n];\n\n/**\n * Rule metadata - colocated with implementation for maintainability\n */\nexport const meta = defineRuleMeta({\n id: \"no-prop-drilling-depth\",\n name: \"No Prop Drilling Depth\",\n description: \"Warn when props are drilled through too many components\",\n defaultSeverity: \"warn\",\n category: \"static\",\n defaultOptions: [\n {\n maxDepth: 2,\n ignoredProps: [\"className\", \"style\", \"children\", \"key\", \"ref\", \"id\"],\n ignoreComponents: [],\n },\n ],\n optionSchema: {\n fields: [\n {\n key: \"maxDepth\",\n label: \"Maximum drilling depth\",\n type: \"number\",\n defaultValue: 2,\n description:\n \"Maximum number of components a prop can pass through without use\",\n },\n {\n key: \"ignoredProps\",\n label: \"Ignored props\",\n type: \"text\",\n defaultValue: \"className, style, children, key, ref, id\",\n description: \"Comma-separated prop names to ignore (common pass-through props)\",\n },\n ],\n },\n docs: `\n## What it does\n\nDetects when props are passed through multiple intermediate components without\nbeing used (prop drilling). This is often a sign that you should use React\nContext, Zustand, or another state management solution.\n\n## Why it's useful\n\n- **Maintainability**: Deep prop drilling creates tight coupling\n- **Refactoring**: Changes require updates in many files\n- **Readability**: Hard to trace where props come from\n- **Performance**: Unnecessary re-renders in intermediate components\n\n## Examples\n\n### ❌ Incorrect\n\n\\`\\`\\`tsx\n// Grandparent passes user through Parent to Child\nfunction Grandparent({ user }) {\n return <Parent user={user} />;\n}\n\nfunction Parent({ user }) {\n // Parent doesn't use 'user', just passes it along\n return <Child user={user} />;\n}\n\nfunction Child({ user }) {\n return <div>{user.name}</div>;\n}\n\\`\\`\\`\n\n### ✅ Correct\n\n\\`\\`\\`tsx\n// Use Context instead\nconst UserContext = createContext();\n\nfunction Grandparent({ user }) {\n return (\n <UserContext.Provider value={user}>\n <Parent />\n </UserContext.Provider>\n );\n}\n\nfunction Child() {\n const user = useContext(UserContext);\n return <div>{user.name}</div>;\n}\n\\`\\`\\`\n\n## Configuration\n\n\\`\\`\\`js\n// eslint.config.js\n\"uilint/no-prop-drilling-depth\": [\"warn\", {\n maxDepth: 2, // Allow passing through 2 components\n ignoredProps: [\"className\", \"style\", \"children\"], // Common pass-through props\n ignoreComponents: [\"^Layout\", \"^Wrapper\"] // Skip wrapper components\n}]\n\\`\\`\\`\n`,\n});\n\n/**\n * Information about a component's prop usage\n */\ninterface ComponentPropInfo {\n /** Props received by the component */\n receivedProps: Set<string>;\n /** Props passed to child components: propName -> childComponentNames[] */\n passedProps: Map<string, string[]>;\n /** Props actually used in the component (not just passed) */\n usedProps: Set<string>;\n /** Child components that receive props from this component */\n childComponents: string[];\n}\n\n/**\n * Cache for analyzed component prop information\n */\nconst componentPropCache = new Map<string, ComponentPropInfo>();\n\n/**\n * Clear the prop analysis cache\n */\nexport function clearPropCache(): void {\n componentPropCache.clear();\n}\n\n/**\n * Check if a name is a React component (PascalCase)\n */\nfunction isComponentName(name: string): boolean {\n return /^[A-Z][a-zA-Z0-9]*$/.test(name);\n}\n\n/**\n * Extract props from a function parameter\n */\nfunction extractPropsFromParam(\n param: TSESTree.Parameter\n): { propNames: Set<string>; isSpread: boolean } {\n const propNames = new Set<string>();\n let isSpread = false;\n\n if (param.type === \"ObjectPattern\") {\n for (const prop of param.properties) {\n if (prop.type === \"RestElement\") {\n isSpread = true;\n } else if (\n prop.type === \"Property\" &&\n prop.key.type === \"Identifier\"\n ) {\n propNames.add(prop.key.name);\n }\n }\n } else if (param.type === \"Identifier\") {\n // Single props parameter - assume all props accessed via props.x\n isSpread = true;\n }\n\n return { propNames, isSpread };\n}\n\n/**\n * Find all JSX elements in a function body and extract prop passing info\n */\nfunction analyzeJSXPropPassing(\n body: TSESTree.Node,\n receivedProps: Set<string>\n): { passedProps: Map<string, string[]>; usedProps: Set<string> } {\n const passedProps = new Map<string, string[]>();\n const usedProps = new Set<string>();\n\n function visit(node: TSESTree.Node): void {\n if (!node || typeof node !== \"object\") return;\n\n // Check JSX elements for prop passing\n if (node.type === \"JSXOpeningElement\") {\n const elementName = getJSXElementName(node.name);\n\n // Only care about component elements (PascalCase)\n if (elementName && isComponentName(elementName)) {\n for (const attr of node.attributes) {\n if (attr.type === \"JSXAttribute\" && attr.name.type === \"JSXIdentifier\") {\n const attrName = attr.name.name;\n const propValue = attr.value;\n\n // Check if the attribute value is a received prop\n if (propValue?.type === \"JSXExpressionContainer\") {\n const expr = propValue.expression;\n if (expr.type === \"Identifier\" && receivedProps.has(expr.name)) {\n // This prop is being passed to a child\n const existing = passedProps.get(expr.name) || [];\n existing.push(elementName);\n passedProps.set(expr.name, existing);\n } else if (\n expr.type === \"MemberExpression\" &&\n expr.object.type === \"Identifier\" &&\n expr.object.name === \"props\" &&\n expr.property.type === \"Identifier\"\n ) {\n // props.x pattern\n const propName = expr.property.name;\n if (receivedProps.has(propName) || receivedProps.size === 0) {\n const existing = passedProps.get(propName) || [];\n existing.push(elementName);\n passedProps.set(propName, existing);\n }\n }\n }\n }\n\n // Check for spread props: {...props} or {...rest}\n if (attr.type === \"JSXSpreadAttribute\") {\n if (attr.argument.type === \"Identifier\") {\n const spreadName = attr.argument.name;\n if (spreadName === \"props\" || receivedProps.has(spreadName)) {\n // All props are being spread\n for (const prop of receivedProps) {\n const existing = passedProps.get(prop) || [];\n existing.push(elementName);\n passedProps.set(prop, existing);\n }\n }\n }\n }\n }\n }\n }\n\n // Check for prop usage (not just passing)\n // e.g., {user.name} or {props.user.name} or just {user}\n if (\n node.type === \"MemberExpression\" &&\n node.object.type === \"Identifier\" &&\n receivedProps.has(node.object.name)\n ) {\n usedProps.add(node.object.name);\n }\n\n if (\n node.type === \"Identifier\" &&\n receivedProps.has(node.name) &&\n node.parent?.type !== \"JSXExpressionContainer\"\n ) {\n // Prop used in expression (but not directly passed to child)\n usedProps.add(node.name);\n }\n\n // Check for props.x.something usage\n if (\n node.type === \"MemberExpression\" &&\n node.object.type === \"MemberExpression\" &&\n node.object.object.type === \"Identifier\" &&\n node.object.object.name === \"props\" &&\n node.object.property.type === \"Identifier\"\n ) {\n usedProps.add(node.object.property.name);\n }\n\n // Recurse into children\n for (const key of Object.keys(node)) {\n if (key === \"parent\" || key === \"loc\" || key === \"range\") continue;\n const child = (node as unknown as Record<string, unknown>)[key];\n if (Array.isArray(child)) {\n for (const item of child) {\n if (item && typeof item === \"object\") {\n visit(item as TSESTree.Node);\n }\n }\n } else if (child && typeof child === \"object\") {\n visit(child as TSESTree.Node);\n }\n }\n }\n\n visit(body);\n return { passedProps, usedProps };\n}\n\n/**\n * Get the name of a JSX element\n */\nfunction getJSXElementName(node: TSESTree.JSXTagNameExpression): string | null {\n if (node.type === \"JSXIdentifier\") {\n return node.name;\n }\n if (node.type === \"JSXMemberExpression\") {\n // Get the root object for namespace components\n let current = node.object;\n while (current.type === \"JSXMemberExpression\") {\n current = current.object;\n }\n return current.type === \"JSXIdentifier\" ? current.name : null;\n }\n return null;\n}\n\n/**\n * Track prop drilling within a single file\n */\ninterface PropDrillingInfo {\n propName: string;\n component: string;\n passedTo: string[];\n usedDirectly: boolean;\n}\n\nexport default createRule<Options, MessageIds>({\n name: \"no-prop-drilling-depth\",\n meta: {\n type: \"suggestion\",\n docs: {\n description: \"Warn when props are drilled through too many components\",\n },\n messages: {\n propDrilling:\n \"Prop '{{propName}}' is passed through {{depth}} component(s) without being used. Consider using Context or state management. Path: {{path}}\",\n },\n schema: [\n {\n type: \"object\",\n properties: {\n maxDepth: {\n type: \"number\",\n minimum: 1,\n description: \"Maximum drilling depth before warning\",\n },\n ignoredProps: {\n type: \"array\",\n items: { type: \"string\" },\n description: \"Props to ignore\",\n },\n ignoreComponents: {\n type: \"array\",\n items: { type: \"string\" },\n description: \"Component patterns to skip (regex)\",\n },\n },\n additionalProperties: false,\n },\n ],\n },\n defaultOptions: [\n {\n maxDepth: 2,\n ignoredProps: [\"className\", \"style\", \"children\", \"key\", \"ref\", \"id\"],\n ignoreComponents: [],\n },\n ],\n create(context) {\n const options = context.options[0] || {};\n const maxDepth = options.maxDepth ?? 2;\n const ignoredProps = new Set(\n options.ignoredProps ?? [\n \"className\",\n \"style\",\n \"children\",\n \"key\",\n \"ref\",\n \"id\",\n ]\n );\n const ignoreComponentPatterns = (options.ignoreComponents ?? []).map(\n (p) => new RegExp(p)\n );\n\n // Track components and their prop flows within the file\n const componentProps = new Map<string, ComponentPropInfo>();\n const imports = new Map<string, string>(); // localName -> importSource\n const componentNodes = new Map<string, TSESTree.Node>(); // componentName -> node\n\n function shouldIgnoreComponent(name: string): boolean {\n return ignoreComponentPatterns.some((pattern) => pattern.test(name));\n }\n\n function shouldIgnoreProp(name: string): boolean {\n return ignoredProps.has(name);\n }\n\n /**\n * Analyze a component function for prop drilling\n */\n function analyzeComponent(\n name: string,\n node: TSESTree.FunctionDeclaration | TSESTree.ArrowFunctionExpression | TSESTree.FunctionExpression,\n reportNode: TSESTree.Node\n ): void {\n if (shouldIgnoreComponent(name)) return;\n\n const firstParam = node.params[0];\n if (!firstParam) return;\n\n const { propNames, isSpread } = extractPropsFromParam(firstParam);\n\n // If using spread without destructuring, we can't easily track props\n if (isSpread && propNames.size === 0) return;\n\n const body = node.body;\n if (!body) return;\n\n const { passedProps, usedProps } = analyzeJSXPropPassing(body, propNames);\n\n componentProps.set(name, {\n receivedProps: propNames,\n passedProps,\n usedProps,\n childComponents: [...new Set([...passedProps.values()].flat())],\n });\n\n componentNodes.set(name, reportNode);\n }\n\n return {\n // Track imports for cross-file analysis\n ImportDeclaration(node) {\n const source = node.source.value as string;\n for (const spec of node.specifiers) {\n if (spec.type === \"ImportSpecifier\" || spec.type === \"ImportDefaultSpecifier\") {\n imports.set(spec.local.name, source);\n }\n }\n },\n\n // Analyze function declarations\n FunctionDeclaration(node) {\n if (node.id && isComponentName(node.id.name)) {\n analyzeComponent(node.id.name, node, node);\n }\n },\n\n // Analyze arrow functions\n VariableDeclarator(node) {\n if (\n node.id.type === \"Identifier\" &&\n isComponentName(node.id.name) &&\n node.init?.type === \"ArrowFunctionExpression\"\n ) {\n analyzeComponent(node.id.name, node.init, node);\n }\n },\n\n // Analyze at the end of the file\n \"Program:exit\"() {\n // Find drilling chains within the file\n for (const [componentName, info] of componentProps) {\n for (const [propName, children] of info.passedProps) {\n if (shouldIgnoreProp(propName)) continue;\n\n // Check if prop is used directly\n if (info.usedProps.has(propName)) continue;\n\n // Track the drilling chain\n const chain: string[] = [componentName];\n let depth = 0;\n let current = children;\n\n while (current.length > 0 && depth < maxDepth + 1) {\n depth++;\n const nextChildren: string[] = [];\n\n for (const child of current) {\n chain.push(child);\n const childInfo = componentProps.get(child);\n\n if (childInfo) {\n // Check if child uses the prop\n if (childInfo.usedProps.has(propName)) {\n // Prop is used here, drilling stops\n break;\n }\n\n // Check if child passes the prop further\n const childPasses = childInfo.passedProps.get(propName);\n if (childPasses) {\n nextChildren.push(...childPasses);\n }\n }\n }\n\n current = nextChildren;\n }\n\n // Report if depth exceeds threshold\n if (depth > maxDepth) {\n const reportNode = componentNodes.get(componentName);\n if (reportNode) {\n context.report({\n node: reportNode,\n messageId: \"propDrilling\",\n data: {\n propName,\n depth: String(depth),\n path: chain.slice(0, maxDepth + 2).join(\" → \"),\n },\n });\n }\n }\n }\n }\n },\n };\n },\n});\n"],"mappings":"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","names":["meta"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "uilint-eslint",
3
- "version": "0.2.21",
3
+ "version": "0.2.23",
4
4
  "description": "ESLint plugin for UILint - AI-powered UI consistency checking",
5
5
  "author": "Peter Suggate",
6
6
  "repository": {
@@ -35,7 +35,7 @@
35
35
  "@typescript-eslint/utils": "^8.35.1",
36
36
  "oxc-resolver": "^11.0.0",
37
37
  "xxhash-wasm": "^1.1.0",
38
- "uilint-core": "0.2.21"
38
+ "uilint-core": "0.2.23"
39
39
  },
40
40
  "devDependencies": {
41
41
  "@types/eslint": "^9.6.1",
@@ -238,9 +238,8 @@ ruleTester.run("consistent-dark-mode", rule, {
238
238
 
239
239
  // ============================================
240
240
  // SEMANTIC/THEMED COLORS (shadcn, etc.) - EXEMPT
241
- // These tests REQUIRE the semantic color exemption.
242
- // Without it, these would trigger "missingDarkMode" errors
243
- // because the new aggressive detection treats unknown values as colors.
241
+ // CSS variable-based colors that handle dark mode automatically.
242
+ // These should NEVER trigger issues because they're not explicit Tailwind colors.
244
243
  // ============================================
245
244
  {
246
245
  name: "shadcn background/foreground semantic colors",
@@ -312,21 +311,33 @@ ruleTester.run("consistent-dark-mode", rule, {
312
311
  },
313
312
 
314
313
  // ============================================
315
- // CUSTOM COLORS (detected by aggressive detection, need dark variants)
316
- // These would NOT have been detected by the old allowlist-based approach
317
- // but ARE detected by the new exclusion-based approach.
314
+ // CUSTOM/NON-TAILWIND COLORS - SHOULD NOT TRIGGER
315
+ // Any color name that is NOT a built-in Tailwind color should be exempt.
316
+ // These are assumed to be CSS variables or custom theme colors.
318
317
  // ============================================
319
318
  {
320
- name: "custom color with dark variant - brand",
321
- code: `<div className="bg-brand dark:bg-brand-dark" />`,
319
+ name: "custom brand color - should not trigger",
320
+ code: `<div className="bg-brand text-brand-foreground" />`,
321
+ },
322
+ {
323
+ name: "custom company colors - should not trigger",
324
+ code: `<div className="bg-company-primary text-company-secondary" />`,
325
+ },
326
+ {
327
+ name: "custom gradient colors - should not trigger",
328
+ code: `<div className="from-brand-start to-brand-end" />`,
329
+ },
330
+ {
331
+ name: "arbitrary custom color names - should not trigger",
332
+ code: `<div className="bg-my-custom-color text-another-custom" />`,
322
333
  },
323
334
  {
324
- name: "custom color with dark variant - custom name",
325
- code: `<div className="text-company-blue dark:text-company-blue-light" />`,
335
+ name: "custom color mixed with themed Tailwind color",
336
+ code: `<div className="bg-brand border-blue-500 dark:border-blue-400" />`,
326
337
  },
327
338
  {
328
- name: "custom gradient colors with dark variants",
329
- code: `<div className="from-brand-start dark:from-brand-start-dark to-brand-end dark:to-brand-end-dark" />`,
339
+ name: "custom ring and fill colors - should not trigger",
340
+ code: `<div className="ring-brand-accent fill-custom-icon" />`,
330
341
  },
331
342
 
332
343
  // ============================================
@@ -787,13 +798,12 @@ ruleTester.run("consistent-dark-mode", rule, {
787
798
  },
788
799
 
789
800
  // ============================================
790
- // CUSTOM COLORS WITHOUT DARK MODE
791
- // These tests verify the aggressive detection catches custom colors.
792
- // The old allowlist approach would NOT have caught these.
801
+ // EXPLICIT TAILWIND COLORS - SHOULD TRIGGER
802
+ // These are built-in Tailwind color names that should require dark variants.
793
803
  // ============================================
794
804
  {
795
- name: "custom color without dark mode - brand",
796
- code: `<div className="bg-brand text-brand-foreground" />`,
805
+ name: "explicit Tailwind colors - white/black",
806
+ code: `<div className="bg-white text-black" />`,
797
807
  errors: [
798
808
  {
799
809
  messageId: "missingDarkMode",
@@ -801,8 +811,8 @@ ruleTester.run("consistent-dark-mode", rule, {
801
811
  ],
802
812
  },
803
813
  {
804
- name: "custom color without dark mode - company colors",
805
- code: `<div className="bg-company-primary text-company-secondary" />`,
814
+ name: "explicit Tailwind colors - slate scale",
815
+ code: `<div className="bg-slate-100 text-slate-900" />`,
806
816
  errors: [
807
817
  {
808
818
  messageId: "missingDarkMode",
@@ -810,18 +820,44 @@ ruleTester.run("consistent-dark-mode", rule, {
810
820
  ],
811
821
  },
812
822
  {
813
- name: "custom color inconsistent - one themed one not",
814
- code: `<div className="bg-brand dark:bg-brand-dark text-brand-accent" />`,
823
+ name: "explicit Tailwind colors - various palette colors",
824
+ code: `<div className="bg-red-500 text-blue-600 border-green-400" />`,
815
825
  errors: [
816
826
  {
817
- messageId: "inconsistentDarkMode",
818
- data: { unthemed: "text-brand-accent" },
827
+ messageId: "missingDarkMode",
819
828
  },
820
829
  ],
821
830
  },
822
831
  {
823
- name: "custom gradient colors without dark mode",
824
- code: `<div className="from-brand-start to-brand-end" />`,
832
+ name: "explicit Tailwind colors - all gray variants",
833
+ code: `<div className="bg-gray-50 text-zinc-900 border-neutral-200" />`,
834
+ errors: [
835
+ {
836
+ messageId: "missingDarkMode",
837
+ },
838
+ ],
839
+ },
840
+ {
841
+ name: "explicit Tailwind colors - modern palette",
842
+ code: `<div className="from-cyan-400 via-sky-500 to-indigo-600" />`,
843
+ errors: [
844
+ {
845
+ messageId: "missingDarkMode",
846
+ },
847
+ ],
848
+ },
849
+ {
850
+ name: "explicit Tailwind colors - warm palette",
851
+ code: `<div className="bg-amber-100 text-orange-800 border-yellow-300" />`,
852
+ errors: [
853
+ {
854
+ messageId: "missingDarkMode",
855
+ },
856
+ ],
857
+ },
858
+ {
859
+ name: "explicit Tailwind colors - with opacity modifier",
860
+ code: `<div className="bg-blue-500/50 text-gray-900/80" />`,
825
861
  errors: [
826
862
  {
827
863
  messageId: "missingDarkMode",
@@ -49,7 +49,7 @@ warns when a file uses color classes without any dark mode theming.
49
49
 
50
50
  - **Prevents broken dark mode**: Catches cases where some colors change in dark mode but others don't
51
51
  - **Encourages completeness**: Prompts you to add dark mode support where it's missing
52
- - **Supports semantic colors**: Automatically ignores shadcn/CSS variable colors that handle dark mode internally
52
+ - **No false positives**: Only flags explicit Tailwind colors, not custom/CSS variable colors
53
53
 
54
54
  ## Examples
55
55
 
@@ -71,8 +71,10 @@ warns when a file uses color classes without any dark mode theming.
71
71
  // All color classes have dark variants
72
72
  <div className="bg-white dark:bg-slate-900 text-black dark:text-white">
73
73
 
74
- // Using semantic colors (automatically themed)
74
+ // Using semantic/custom colors (automatically themed via CSS variables)
75
75
  <div className="bg-background text-foreground">
76
+ <div className="bg-brand text-brand-foreground">
77
+ <div className="bg-primary text-primary-foreground">
76
78
 
77
79
  // Consistent theming
78
80
  <button className="bg-blue-500 dark:bg-blue-600 border-gray-300 dark:border-gray-600">
@@ -89,7 +91,9 @@ warns when a file uses color classes without any dark mode theming.
89
91
 
90
92
  ## Notes
91
93
 
92
- - Semantic colors (like shadcn's \`background\`, \`foreground\`, \`primary\`, etc.) are exempt
94
+ - Only explicit Tailwind colors (like \`blue-500\`, \`white\`, \`slate-900\`) require dark variants
95
+ - Custom/semantic colors (\`background\`, \`foreground\`, \`brand\`, \`primary\`, etc.) are exempt
96
+ - These are assumed to be CSS variables that handle dark mode automatically
93
97
  - Transparent, inherit, and current values are exempt
94
98
  - Non-color utilities (like \`text-lg\`, \`border-2\`) are correctly ignored
95
99
  `,
@@ -125,137 +129,43 @@ const COLOR_PREFIXES = [
125
129
  // Values that don't need dark variants (colorless or inherited)
126
130
  const EXEMPT_SUFFIXES = ["transparent", "inherit", "current", "auto", "none"];
127
131
 
128
- // Known non-color utilities that use color prefixes
129
- // These are utilities like text-lg (font size), text-center (alignment), etc.
130
- const NON_COLOR_UTILITIES = new Set([
131
- // Exempt values (colorless or inherited) - don't need dark variants
132
- "transparent",
133
- "inherit",
134
- "current",
135
- "auto",
136
- "none",
137
- // text- utilities that aren't colors
138
- "xs",
139
- "sm",
140
- "base",
141
- "lg",
142
- "xl",
143
- "2xl",
144
- "3xl",
145
- "4xl",
146
- "5xl",
147
- "6xl",
148
- "7xl",
149
- "8xl",
150
- "9xl",
151
- "left",
152
- "center",
153
- "right",
154
- "justify",
155
- "start",
156
- "end",
157
- "wrap",
158
- "nowrap",
159
- "balance",
160
- "pretty",
161
- "ellipsis",
162
- "clip",
163
- // border- utilities that aren't colors
164
- "0",
165
- "2",
166
- "4",
167
- "8",
168
- "solid",
169
- "dashed",
170
- "dotted",
171
- "double",
172
- "hidden",
173
- "collapse",
174
- "separate",
175
- // shadow- utilities that aren't colors
176
- // Note: "sm", "lg", "xl", "2xl" already included above
177
- "md",
178
- "inner",
179
- // ring- utilities that aren't colors
180
- // Note: "0", "2", "4", "8" already included above
181
- "1",
182
- "inset",
183
- // outline- utilities that aren't colors
184
- // Note: numeric values already included
185
- "offset-0",
186
- "offset-1",
187
- "offset-2",
188
- "offset-4",
189
- "offset-8",
190
- // decoration- utilities that aren't colors
191
- // Note: "solid", "double", "dotted", "dashed" already included
192
- "wavy",
193
- "from-font",
194
- "clone",
195
- "slice",
196
- // divide- utilities that aren't colors
197
- "x",
198
- "y",
199
- "x-0",
200
- "x-2",
201
- "x-4",
202
- "x-8",
203
- "y-0",
204
- "y-2",
205
- "y-4",
206
- "y-8",
207
- "x-reverse",
208
- "y-reverse",
209
- // gradient direction utilities (from-, via-, to- prefixes)
210
- "t",
211
- "tr",
212
- "r",
213
- "br",
214
- "b",
215
- "bl",
216
- "l",
217
- "tl",
132
+ // Built-in Tailwind CSS color palette names
133
+ // These are the ONLY colors that should trigger dark mode warnings.
134
+ // Custom colors (like 'brand', 'company-primary') are assumed to be
135
+ // CSS variables that handle dark mode automatically.
136
+ const TAILWIND_COLOR_NAMES = new Set([
137
+ // Special colors
138
+ "white",
139
+ "black",
140
+ // Gray scale palettes
141
+ "slate",
142
+ "gray",
143
+ "zinc",
144
+ "neutral",
145
+ "stone",
146
+ // Warm colors
147
+ "red",
148
+ "orange",
149
+ "amber",
150
+ "yellow",
151
+ // Green colors
152
+ "lime",
153
+ "green",
154
+ "emerald",
155
+ "teal",
156
+ // Blue colors
157
+ "cyan",
158
+ "sky",
159
+ "blue",
160
+ "indigo",
161
+ // Purple/Pink colors
162
+ "violet",
163
+ "purple",
164
+ "fuchsia",
165
+ "pink",
166
+ "rose",
218
167
  ]);
219
168
 
220
- // Semantic color names used by theming systems like shadcn
221
- // These are CSS variable-based colors that handle dark mode automatically
222
- const SEMANTIC_COLOR_NAMES = new Set([
223
- // Core shadcn colors
224
- "background",
225
- "foreground",
226
- // Component colors
227
- "card",
228
- "card-foreground",
229
- "popover",
230
- "popover-foreground",
231
- "primary",
232
- "primary-foreground",
233
- "secondary",
234
- "secondary-foreground",
235
- "muted",
236
- "muted-foreground",
237
- "accent",
238
- "accent-foreground",
239
- "destructive",
240
- "destructive-foreground",
241
- // Form/UI colors
242
- "border",
243
- "input",
244
- "ring",
245
- // Sidebar colors (shadcn sidebar component)
246
- "sidebar",
247
- "sidebar-foreground",
248
- "sidebar-border",
249
- "sidebar-primary",
250
- "sidebar-primary-foreground",
251
- "sidebar-accent",
252
- "sidebar-accent-foreground",
253
- "sidebar-ring",
254
- ]);
255
-
256
- // Pattern for semantic chart colors (chart-1, chart-2, etc.)
257
- const CHART_COLOR_PATTERN = /^chart-\d+$/;
258
-
259
169
  /**
260
170
  * Check if a class has 'dark' in its variant chain
261
171
  */
@@ -287,27 +197,57 @@ function getColorPrefix(baseClass: string): string | null {
287
197
  }
288
198
 
289
199
  /**
290
- * Check if the value is a semantic/themed color (e.g., shadcn)
291
- * These colors use CSS variables that automatically handle dark mode
200
+ * Check if the value is an explicit Tailwind color.
201
+ * Uses an allowlist approach: only built-in Tailwind color names trigger warnings.
202
+ * Custom colors (like 'brand', 'primary', 'company-blue') are assumed to be
203
+ * CSS variables that handle dark mode automatically and should NOT trigger.
204
+ *
205
+ * Matches patterns like:
206
+ * - white, black (standalone colors)
207
+ * - blue-500, slate-900 (color-scale)
208
+ * - blue-500/50, gray-900/80 (with opacity modifier)
292
209
  */
293
- function isSemanticColor(value: string): boolean {
294
- // Check for exact semantic color names
295
- if (SEMANTIC_COLOR_NAMES.has(value)) {
210
+ function isTailwindColor(value: string): boolean {
211
+ // Remove opacity modifier if present (e.g., "blue-500/50" -> "blue-500")
212
+ const valueWithoutOpacity = value.split("/")[0] || value;
213
+
214
+ // Check for standalone colors (white, black)
215
+ if (TAILWIND_COLOR_NAMES.has(valueWithoutOpacity)) {
296
216
  return true;
297
217
  }
298
218
 
299
- // Check for chart colors (chart-1, chart-2, etc.)
300
- if (CHART_COLOR_PATTERN.test(value)) {
301
- return true;
219
+ // Check for color-scale pattern (e.g., "blue-500", "slate-900")
220
+ // Pattern: colorName-number where number is 50, 100, 200, ..., 950
221
+ const match = valueWithoutOpacity.match(/^([a-z]+)-(\d+)$/);
222
+ if (match) {
223
+ const colorName = match[1];
224
+ const scale = match[2];
225
+ // Valid Tailwind scales are: 50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950
226
+ const validScales = [
227
+ "50",
228
+ "100",
229
+ "200",
230
+ "300",
231
+ "400",
232
+ "500",
233
+ "600",
234
+ "700",
235
+ "800",
236
+ "900",
237
+ "950",
238
+ ];
239
+ if (colorName && TAILWIND_COLOR_NAMES.has(colorName) && validScales.includes(scale || "")) {
240
+ return true;
241
+ }
302
242
  }
303
243
 
304
244
  return false;
305
245
  }
306
246
 
307
247
  /**
308
- * Check if the value after the prefix looks like a color value
309
- * Uses an exclusion-based approach: anything that's not a known non-color utility
310
- * and not a semantic color is treated as a potential color.
248
+ * Check if the value after the prefix looks like an explicit Tailwind color.
249
+ * Uses allowlist approach: only built-in Tailwind colors should trigger dark mode warnings.
250
+ * Custom/semantic colors (brand, primary, foreground, etc.) are NOT flagged.
311
251
  */
312
252
  function isColorValue(baseClass: string, prefix: string): boolean {
313
253
  const value = baseClass.slice(prefix.length);
@@ -317,23 +257,9 @@ function isColorValue(baseClass: string, prefix: string): boolean {
317
257
  return false;
318
258
  }
319
259
 
320
- // Check if it's a semantic/themed color (exempt from dark mode requirements)
321
- if (isSemanticColor(value)) {
322
- return false;
323
- }
324
-
325
- // Check if it's a known non-color utility
326
- if (NON_COLOR_UTILITIES.has(value)) {
327
- return false;
328
- }
329
-
330
- // Treat everything else as a potential color
331
- // This catches:
332
- // - Standard Tailwind colors: blue-500, slate-900, white, black
333
- // - Custom colors defined in tailwind.config: brand, primary (non-shadcn), custom-blue
334
- // - Arbitrary values: [#fff], [rgb(255,0,0)], [var(--my-color)]
335
- // - Opacity modifiers: blue-500/50, white/80
336
- return true;
260
+ // Only flag explicit Tailwind colors
261
+ // Custom colors, CSS variable colors, and semantic colors are exempt
262
+ return isTailwindColor(value);
337
263
  }
338
264
 
339
265
  /**
@@ -11,6 +11,7 @@
11
11
  */
12
12
 
13
13
  import { createRule, defineRuleMeta } from "../utils/create-rule.js";
14
+ import type { TSESTree } from "@typescript-eslint/utils";
14
15
 
15
16
  type MessageIds = "preferAbsoluteImport";
16
17
  type Options = [
@@ -180,7 +181,7 @@ export default createRule<Options, MessageIds>({
180
181
  */
181
182
  function checkImportSource(
182
183
  source: string,
183
- node: { loc?: { start: { line: number; column: number } } }
184
+ node: TSESTree.StringLiteral
184
185
  ): void {
185
186
  // Skip non-relative imports (node_modules, aliases, etc.)
186
187
  if (!isRelativeImport(source)) {
@@ -196,7 +197,7 @@ export default createRule<Options, MessageIds>({
196
197
 
197
198
  if (depth > maxRelativeDepth) {
198
199
  context.report({
199
- node: node as Parameters<typeof context.report>[0]["node"],
200
+ node,
200
201
  messageId: "preferAbsoluteImport",
201
202
  data: {
202
203
  depth: String(depth),