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.
- package/dist/index.js +66 -140
- package/dist/index.js.map +1 -1
- package/dist/rules/consistent-dark-mode.js +62 -136
- package/dist/rules/consistent-dark-mode.js.map +1 -1
- package/dist/rules/enforce-absolute-imports.js.map +1 -1
- package/dist/rules/no-any-in-props.js +1 -1
- package/dist/rules/no-any-in-props.js.map +1 -1
- package/dist/rules/no-prop-drilling-depth.js +3 -16
- package/dist/rules/no-prop-drilling-depth.js.map +1 -1
- package/package.json +2 -2
- package/src/rules/consistent-dark-mode.test.ts +61 -25
- package/src/rules/consistent-dark-mode.ts +86 -160
- package/src/rules/enforce-absolute-imports.ts +3 -2
- package/src/rules/no-any-in-props.ts +5 -2
- package/src/rules/no-prop-drilling-depth.ts +4 -10
|
@@ -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.
|
|
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.
|
|
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
|
-
//
|
|
242
|
-
//
|
|
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
|
|
316
|
-
//
|
|
317
|
-
//
|
|
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
|
|
321
|
-
code: `<div className="bg-brand
|
|
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
|
|
325
|
-
code: `<div className="
|
|
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
|
|
329
|
-
code: `<div className="
|
|
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
|
-
//
|
|
791
|
-
// These
|
|
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: "
|
|
796
|
-
code: `<div className="bg-
|
|
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: "
|
|
805
|
-
code: `<div className="bg-
|
|
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: "
|
|
814
|
-
code: `<div className="bg-
|
|
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: "
|
|
818
|
-
data: { unthemed: "text-brand-accent" },
|
|
827
|
+
messageId: "missingDarkMode",
|
|
819
828
|
},
|
|
820
829
|
],
|
|
821
830
|
},
|
|
822
831
|
{
|
|
823
|
-
name: "
|
|
824
|
-
code: `<div className="
|
|
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
|
-
- **
|
|
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
|
-
-
|
|
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
|
-
//
|
|
129
|
-
// These are
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
"
|
|
135
|
-
"
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
"
|
|
139
|
-
"
|
|
140
|
-
"
|
|
141
|
-
"
|
|
142
|
-
|
|
143
|
-
"
|
|
144
|
-
"
|
|
145
|
-
"
|
|
146
|
-
"
|
|
147
|
-
|
|
148
|
-
"
|
|
149
|
-
"
|
|
150
|
-
"
|
|
151
|
-
"
|
|
152
|
-
|
|
153
|
-
"
|
|
154
|
-
"
|
|
155
|
-
"
|
|
156
|
-
"
|
|
157
|
-
|
|
158
|
-
"
|
|
159
|
-
"
|
|
160
|
-
"
|
|
161
|
-
"
|
|
162
|
-
"
|
|
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
|
|
291
|
-
*
|
|
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
|
|
294
|
-
//
|
|
295
|
-
|
|
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
|
|
300
|
-
|
|
301
|
-
|
|
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
|
|
309
|
-
* Uses
|
|
310
|
-
*
|
|
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
|
-
//
|
|
321
|
-
|
|
322
|
-
|
|
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:
|
|
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
|
|
200
|
+
node,
|
|
200
201
|
messageId: "preferAbsoluteImport",
|
|
201
202
|
data: {
|
|
202
203
|
depth: String(depth),
|