uilint-eslint 0.2.76 → 0.2.78

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/rules/no-semantic-duplicates.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 * External requirement that a rule needs to function\n */\nexport interface RuleRequirement {\n /** Requirement type for programmatic checks */\n type: \"ollama\" | \"git\" | \"coverage\" | \"semantic-index\" | \"styleguide\";\n /** Human-readable description */\n description: string;\n /** Optional: how to satisfy the requirement */\n setupHint?: string;\n}\n\n/**\n * Rule migration definition for updating rule options between versions\n */\nexport interface RuleMigration {\n /** Source version (semver) */\n from: string;\n /** Target version (semver) */\n to: string;\n /** Human-readable description of what changed */\n description: string;\n /** Function to migrate options from old format to new format */\n migrate: (oldOptions: unknown[]) => unknown[];\n /** Whether this migration contains breaking changes */\n breaking?: boolean;\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., \"consistent-dark-mode\") - must match filename */\n id: string;\n\n /** Semantic version of the rule (e.g., \"1.0.0\") */\n version: 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 /** Icon for display in CLI/UI (emoji or icon name) */\n icon?: string;\n\n /** Short hint about the rule type/requirements */\n hint?: string;\n\n /** Whether rule is enabled by default during install */\n defaultEnabled?: boolean;\n\n /** External requirements the rule needs */\n requirements?: RuleRequirement[];\n\n /** Instructions to show after installation */\n postInstallInstructions?: string;\n\n /** Framework compatibility */\n frameworks?: (\"next\" | \"vite\" | \"cra\" | \"remix\")[];\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 * Internal utility dependencies that this rule requires.\n * When the rule is copied to a target project, these utilities\n * will be transformed to import from \"uilint-eslint\" instead\n * of relative paths.\n *\n * Example: [\"coverage-aggregator\", \"dependency-graph\"]\n */\n internalDependencies?: string[];\n\n /**\n * Whether this rule is directory-based (has lib/ folder with utilities).\n * Directory-based rules are installed as folders with index.ts and lib/ subdirectory.\n * Single-file rules are installed as single .ts files.\n *\n * When true, ESLint config imports will use:\n * ./.uilint/rules/rule-id/index.js\n * When false (default):\n * ./.uilint/rules/rule-id.js\n */\n isDirectoryBased?: boolean;\n\n /**\n * Migrations for updating rule options between versions.\n * Migrations are applied in order to transform options from older versions.\n */\n migrations?: RuleMigration[];\n\n /**\n * Which UI plugin should handle this rule.\n * Defaults based on category:\n * - \"static\" category → \"eslint\" plugin\n * - \"semantic\" category → \"semantic\" plugin\n *\n * Special cases:\n * - \"vision\" for semantic-vision rule\n */\n plugin?: \"eslint\" | \"vision\" | \"semantic\";\n\n /**\n * Custom inspector panel ID to use for this rule's issues.\n * If not specified, uses the plugin's default issue inspector.\n *\n * Examples:\n * - \"vision-issue\" for VisionIssueInspector\n * - \"duplicates\" for DuplicatesInspector\n * - \"semantic-issue\" for SemanticIssueInspector\n */\n customInspector?: string;\n\n /**\n * Custom heatmap color for this rule's issues.\n * CSS color value (hex, rgb, hsl, or named color).\n * If not specified, uses severity-based coloring.\n */\n heatmapColor?: 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-semantic-duplicates\n *\n * Warns when code is semantically similar to existing indexed code.\n * This rule queries a pre-built semantic index (from uilint duplicates index)\n * rather than calling the LLM during linting - making it fast.\n *\n * Prerequisites:\n * - Run `uilint duplicates index` to build the semantic index first\n * - The index is stored at .uilint/.duplicates-index/\n */\n\nimport { createRule, defineRuleMeta } from \"../utils/create-rule.js\";\nimport type { TSESTree } from \"@typescript-eslint/utils\";\nimport { existsSync, readFileSync, appendFileSync, writeFileSync } from \"fs\";\nimport { dirname, join, relative } from \"path\";\n\n// Debug logging - writes to .uilint/no-semantic-duplicates.log in the project root\nlet logFile: string | null = null;\nlet logInitialized = false;\n\nfunction initLog(projectRoot: string): void {\n if (logFile) return;\n const uilintDir = join(projectRoot, \".uilint\");\n if (existsSync(uilintDir)) {\n logFile = join(uilintDir, \"no-semantic-duplicates.log\");\n }\n}\n\nfunction log(message: string): void {\n if (!logFile) return;\n try {\n const timestamp = new Date().toISOString();\n const line = `[${timestamp}] ${message}\\n`;\n if (!logInitialized) {\n writeFileSync(logFile, line);\n logInitialized = true;\n } else {\n appendFileSync(logFile, line);\n }\n } catch {\n // Ignore logging errors\n }\n}\n\ntype MessageIds = \"semanticDuplicate\" | \"noIndex\";\ntype Options = [\n {\n /** Similarity threshold (0-1). Default: 0.85 */\n threshold?: number;\n /** Path to the index directory */\n indexPath?: string;\n /** Minimum number of lines for a chunk to be reported (default: 3) */\n minLines?: number;\n }\n];\n\n/**\n * Rule metadata\n */\nexport const meta = defineRuleMeta({\n id: \"no-semantic-duplicates\",\n version: \"1.0.0\",\n name: \"No Semantic Duplicates\",\n description: \"Warn when code is semantically similar to existing code\",\n defaultSeverity: \"warn\",\n category: \"semantic\",\n icon: \"🔍\",\n hint: \"Finds similar code via embeddings\",\n defaultEnabled: false,\n plugin: \"semantic\",\n customInspector: \"duplicates\",\n requirements: [\n {\n type: \"semantic-index\",\n description: \"Requires semantic index for duplicate detection\",\n setupHint: \"Run: uilint duplicates index\",\n },\n ],\n postInstallInstructions: \"Run 'uilint duplicates index' to build the semantic index before using this rule.\",\n defaultOptions: [{ threshold: 0.85, indexPath: \".uilint/.duplicates-index\", minLines: 3 }],\n optionSchema: {\n fields: [\n {\n key: \"threshold\",\n label: \"Similarity threshold\",\n type: \"number\",\n defaultValue: 0.85,\n description:\n \"Minimum similarity score (0-1) to report as duplicate. Higher = stricter.\",\n },\n {\n key: \"indexPath\",\n label: \"Index path\",\n type: \"text\",\n defaultValue: \".uilint/.duplicates-index\",\n description: \"Path to the semantic duplicates index directory\",\n },\n {\n key: \"minLines\",\n label: \"Minimum lines\",\n type: \"number\",\n defaultValue: 3,\n description:\n \"Minimum number of lines for a chunk to be reported as a potential duplicate.\",\n },\n ],\n },\n docs: `\n## What it does\n\nWarns when code (components, hooks, functions) is semantically similar to other\ncode in the codebase. Unlike syntactic duplicate detection, this finds code that\nimplements similar functionality even if written differently.\n\n## Prerequisites\n\nBefore using this rule, you must build the semantic index:\n\n\\`\\`\\`bash\nuilint duplicates index\n\\`\\`\\`\n\nThis creates an embedding-based index at \\`.uilint/.duplicates-index/\\`.\n\n## Why it's useful\n\n- **Reduce Duplication**: Find components/hooks that could be consolidated\n- **Discover Patterns**: Identify similar code that could be abstracted\n- **Code Quality**: Encourage reuse over reimplementation\n- **Fast**: Queries pre-built index, no LLM calls during linting\n\n## How it works\n\n1. The rule checks if the current file is in the semantic index\n2. For each indexed code chunk, it looks up similar chunks\n3. If similar chunks exist above the threshold, it reports a warning\n\n## Examples\n\n### Semantic duplicates detected:\n\n\\`\\`\\`tsx\n// UserCard.tsx - Original component\nexport function UserCard({ user }) {\n return (\n <div className=\"card\">\n <img src={user.avatar} />\n <h3>{user.name}</h3>\n </div>\n );\n}\n\n// ProfileCard.tsx - Semantically similar (warning!)\nexport function ProfileCard({ profile }) {\n return (\n <article className=\"profile\">\n <img src={profile.avatarUrl} />\n <h2>{profile.displayName}</h2>\n </article>\n );\n}\n\\`\\`\\`\n\n## Configuration\n\n\\`\\`\\`js\n// eslint.config.js\n\"uilint/no-semantic-duplicates\": [\"warn\", {\n threshold: 0.85, // Similarity threshold (0-1)\n indexPath: \".uilint/.duplicates-index\",\n minLines: 3 // Minimum lines to report (default: 3)\n}]\n\\`\\`\\`\n\n## Notes\n\n- Run \\`uilint duplicates index\\` after significant code changes\n- Use \\`uilint duplicates find\\` to explore all duplicate groups\n- The rule only reports if the file is in the index\n`,\n});\n\n// Cache for loaded index data across files in a single ESLint run\nlet indexCache: {\n projectRoot: string;\n vectorStore: Map<string, number[]>;\n metadataStore: Map<\n string,\n {\n filePath: string;\n startLine: number;\n endLine: number;\n startColumn: number;\n endColumn: number;\n name: string | null;\n kind: string;\n }\n >;\n fileToChunks: Map<string, string[]>;\n} | null = null;\n\n/**\n * Clear the index cache (useful for testing)\n */\nexport function clearIndexCache(): void {\n indexCache = null;\n}\n\n/**\n * Find project root by looking for the .uilint directory (preferred)\n * or falling back to the root package.json (monorepo root)\n */\nfunction findProjectRoot(startPath: string, indexPath: string): string {\n let current = startPath;\n let lastPackageJson: string | null = null;\n\n // Walk up the directory tree\n while (current !== dirname(current)) {\n // Check for .uilint directory with index (highest priority)\n const uilintDir = join(current, indexPath);\n if (existsSync(join(uilintDir, \"manifest.json\"))) {\n return current;\n }\n\n // Track package.json locations\n if (existsSync(join(current, \"package.json\"))) {\n lastPackageJson = current;\n }\n\n current = dirname(current);\n }\n\n // Return the topmost package.json location (monorepo root) or start path\n return lastPackageJson || startPath;\n}\n\n/**\n * Load the index into memory (cached across files)\n */\nfunction loadIndex(\n projectRoot: string,\n indexPath: string\n): typeof indexCache | null {\n const fullIndexPath = join(projectRoot, indexPath);\n log(`loadIndex called: projectRoot=${projectRoot}, indexPath=${indexPath}`);\n log(`fullIndexPath=${fullIndexPath}`);\n\n // Check if we already have a cached index for this project\n if (indexCache && indexCache.projectRoot === projectRoot) {\n log(`Using cached index (${indexCache.vectorStore.size} vectors, ${indexCache.fileToChunks.size} files)`);\n return indexCache;\n }\n\n // Check if index exists\n const manifestPath = join(fullIndexPath, \"manifest.json\");\n if (!existsSync(manifestPath)) {\n log(`Index not found: manifest.json missing at ${manifestPath}`);\n return null;\n }\n\n try {\n // Load metadata\n const metadataPath = join(fullIndexPath, \"metadata.json\");\n if (!existsSync(metadataPath)) {\n log(`Index not found: metadata.json missing at ${metadataPath}`);\n return null;\n }\n\n const metadataContent = readFileSync(metadataPath, \"utf-8\");\n const metadataJson = JSON.parse(metadataContent);\n\n // Support both formats: { entries: {...} } and direct { chunkId: {...} }\n const entries = metadataJson.entries || metadataJson;\n log(`Loaded metadata.json: ${Object.keys(entries).length} entries`);\n\n const metadataStore = new Map<\n string,\n {\n filePath: string;\n startLine: number;\n endLine: number;\n startColumn: number;\n endColumn: number;\n name: string | null;\n kind: string;\n }\n >();\n const fileToChunks = new Map<string, string[]>();\n\n for (const [id, meta] of Object.entries(entries)) {\n const m = meta as {\n filePath: string;\n startLine: number;\n endLine: number;\n startColumn: number;\n endColumn: number;\n name: string | null;\n kind: string;\n };\n metadataStore.set(id, {\n filePath: m.filePath,\n startLine: m.startLine,\n endLine: m.endLine,\n startColumn: m.startColumn ?? 0,\n endColumn: m.endColumn ?? 0,\n name: m.name,\n kind: m.kind,\n });\n\n // Build file -> chunks mapping\n const chunks = fileToChunks.get(m.filePath) || [];\n chunks.push(id);\n fileToChunks.set(m.filePath, chunks);\n }\n\n log(`File to chunks mapping:`);\n for (const [filePath, chunks] of fileToChunks.entries()) {\n log(` ${filePath}: ${chunks.length} chunks (${chunks.join(\", \")})`);\n }\n\n // Load vectors (binary format)\n const vectorsPath = join(fullIndexPath, \"embeddings.bin\");\n const idsPath = join(fullIndexPath, \"ids.json\");\n const vectorStore = new Map<string, number[]>();\n\n if (existsSync(vectorsPath) && existsSync(idsPath)) {\n const idsContent = readFileSync(idsPath, \"utf-8\");\n const ids = JSON.parse(idsContent) as string[];\n log(`Loaded ids.json: ${ids.length} IDs`);\n\n const buffer = readFileSync(vectorsPath);\n // Must use byteOffset and byteLength because Node's Buffer uses pooling\n // and buffer.buffer may contain data from other buffers at different offsets\n const view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength);\n\n // Read header\n const dimension = view.getUint32(0, true);\n const count = view.getUint32(4, true);\n log(`Embeddings binary: dimension=${dimension}, count=${count}`);\n\n // Read vectors\n let offset = 8;\n for (let i = 0; i < count && i < ids.length; i++) {\n const vector: number[] = [];\n for (let j = 0; j < dimension; j++) {\n vector.push(view.getFloat32(offset, true));\n offset += 4;\n }\n vectorStore.set(ids[i], vector);\n }\n log(`Loaded ${vectorStore.size} vectors into store`);\n } else {\n log(`Missing vectors or ids files: vectorsPath=${existsSync(vectorsPath)}, idsPath=${existsSync(idsPath)}`);\n }\n\n indexCache = {\n projectRoot,\n vectorStore,\n metadataStore,\n fileToChunks,\n };\n\n log(`Index loaded successfully: ${vectorStore.size} vectors, ${metadataStore.size} metadata entries, ${fileToChunks.size} files`);\n return indexCache;\n } catch (err) {\n log(`Error loading index: ${err}`);\n return null;\n }\n}\n\n/**\n * Calculate cosine similarity between two vectors\n */\nfunction cosineSimilarity(a: number[], b: number[]): number {\n if (a.length !== b.length) return 0;\n\n let dotProduct = 0;\n let normA = 0;\n let normB = 0;\n\n for (let i = 0; i < a.length; i++) {\n dotProduct += a[i] * b[i];\n normA += a[i] * a[i];\n normB += b[i] * b[i];\n }\n\n const denominator = Math.sqrt(normA) * Math.sqrt(normB);\n return denominator === 0 ? 0 : dotProduct / denominator;\n}\n\n/**\n * Find similar chunks to a given chunk\n */\nfunction findSimilarChunks(\n index: NonNullable<typeof indexCache>,\n chunkId: string,\n threshold: number\n): Array<{ id: string; score: number }> {\n log(`findSimilarChunks: chunkId=${chunkId}, threshold=${threshold}`);\n\n const vector = index.vectorStore.get(chunkId);\n if (!vector) {\n log(` No vector found for chunk ${chunkId}`);\n return [];\n }\n log(` Vector found: dimension=${vector.length}`);\n\n const results: Array<{ id: string; score: number }> = [];\n const allScores: Array<{ id: string; score: number }> = [];\n\n for (const [id, vec] of index.vectorStore.entries()) {\n if (id === chunkId) continue;\n\n const score = cosineSimilarity(vector, vec);\n allScores.push({ id, score });\n if (score >= threshold) {\n results.push({ id, score });\n }\n }\n\n // Log top 10 scores regardless of threshold\n const sortedAll = allScores.sort((a, b) => b.score - a.score).slice(0, 10);\n log(` Top 10 similarity scores (threshold=${threshold}):`);\n for (const { id, score } of sortedAll) {\n const meta = index.metadataStore.get(id);\n const meetsThreshold = score >= threshold ? \"✓\" : \"✗\";\n log(` ${meetsThreshold} ${(score * 100).toFixed(1)}% - ${id} (${meta?.name || \"anonymous\"} in ${meta?.filePath})`);\n }\n\n log(` Found ${results.length} chunks above threshold`);\n return results.sort((a, b) => b.score - a.score);\n}\n\nexport default createRule<Options, MessageIds>({\n name: \"no-semantic-duplicates\",\n meta: {\n type: \"suggestion\",\n docs: {\n description: \"Warn when code is semantically similar to existing code\",\n },\n messages: {\n semanticDuplicate:\n \"This {{kind}} '{{name}}' is {{similarity}}% similar to '{{otherName}}' at {{otherLocation}}. Consider consolidating.\",\n noIndex:\n \"Semantic duplicates index not found. Run 'uilint duplicates index' first.\",\n },\n schema: [\n {\n type: \"object\",\n properties: {\n threshold: {\n type: \"number\",\n minimum: 0,\n maximum: 1,\n description: \"Similarity threshold (0-1)\",\n },\n indexPath: {\n type: \"string\",\n description: \"Path to the index directory\",\n },\n minLines: {\n type: \"integer\",\n minimum: 1,\n description: \"Minimum number of lines for a chunk to be reported\",\n },\n },\n additionalProperties: false,\n },\n ],\n },\n defaultOptions: [\n {\n threshold: 0.85,\n indexPath: \".uilint/.duplicates-index\",\n minLines: 3,\n },\n ],\n create(context) {\n const options = context.options[0] || {};\n const threshold = options.threshold ?? 0.85;\n const indexPath = options.indexPath ?? \".uilint/.duplicates-index\";\n const minLines = options.minLines ?? 3;\n\n const filename = context.filename || context.getFilename();\n const projectRoot = findProjectRoot(dirname(filename), indexPath);\n\n // Initialize logging to .uilint folder\n initLog(projectRoot);\n\n log(`\\n========== Rule create() ==========`);\n log(`Filename: ${filename}`);\n log(`Threshold: ${threshold}`);\n log(`Index path: ${indexPath}`);\n log(`Min lines: ${minLines}`);\n log(`Project root: ${projectRoot}`);\n\n const index = loadIndex(projectRoot, indexPath);\n\n // Track which chunks we've already reported to avoid duplicates\n const reportedChunks = new Set<string>();\n\n /**\n * Check if a node location corresponds to an indexed chunk\n * and if so, check for similar chunks\n */\n function checkForDuplicates(\n node: TSESTree.Node,\n name: string | null\n ): void {\n log(`checkForDuplicates: name=${name}, file=${filename}`);\n\n if (!index) {\n log(` No index loaded`);\n return;\n }\n\n // Get chunks for this file\n const fileChunks = index.fileToChunks.get(filename);\n log(` Looking for chunks for file: ${filename}`);\n log(` Files in index: ${Array.from(index.fileToChunks.keys()).join(\", \")}`);\n\n if (!fileChunks || fileChunks.length === 0) {\n log(` No chunks found for this file`);\n return;\n }\n log(` Found ${fileChunks.length} chunks: ${fileChunks.join(\", \")}`);\n\n // Find the chunk that contains this node's location\n const nodeLine = node.loc?.start.line;\n if (!nodeLine) {\n log(` No node line number`);\n return;\n }\n log(` Node starts at line ${nodeLine}`);\n\n for (const chunkId of fileChunks) {\n if (reportedChunks.has(chunkId)) {\n log(` Chunk ${chunkId} already reported, skipping`);\n continue;\n }\n\n const meta = index.metadataStore.get(chunkId);\n if (!meta) {\n log(` No metadata for chunk ${chunkId}`);\n continue;\n }\n\n log(` Checking chunk ${chunkId}: lines ${meta.startLine}-${meta.endLine} (node at line ${nodeLine})`);\n\n // Check if this node is within the chunk's line range\n if (nodeLine >= meta.startLine && nodeLine <= meta.endLine) {\n log(` Node is within chunk range, searching for similar chunks...`);\n\n // Find similar chunks\n const similar = findSimilarChunks(index, chunkId, threshold);\n\n if (similar.length > 0) {\n const best = similar[0];\n const bestMeta = index.metadataStore.get(best.id);\n\n if (bestMeta) {\n // Check minimum lines threshold\n const chunkLines = meta.endLine - meta.startLine + 1;\n if (chunkLines < minLines) {\n log(` Skipping: chunk has ${chunkLines} lines, below minLines=${minLines}`);\n continue;\n }\n\n reportedChunks.add(chunkId);\n\n const relPath = relative(projectRoot, bestMeta.filePath);\n const similarity = Math.round(best.score * 100);\n\n log(` REPORTING: ${meta.kind} '${name || meta.name}' is ${similarity}% similar to '${bestMeta.name}' at ${relPath}:${bestMeta.startLine}`);\n\n context.report({\n node,\n loc: {\n start: { line: meta.startLine, column: meta.startColumn },\n end: { line: meta.endLine, column: meta.endColumn },\n },\n messageId: \"semanticDuplicate\",\n data: {\n kind: meta.kind,\n name: name || meta.name || \"(anonymous)\",\n similarity: String(similarity),\n otherName: bestMeta.name || \"(anonymous)\",\n otherLocation: `${relPath}:${bestMeta.startLine}`,\n },\n });\n }\n } else {\n log(` No similar chunks found above threshold`);\n }\n } else {\n log(` Node line ${nodeLine} not in chunk range ${meta.startLine}-${meta.endLine}`);\n }\n }\n }\n\n return {\n // Check function declarations\n FunctionDeclaration(node) {\n const name = node.id?.name || null;\n checkForDuplicates(node, name);\n },\n\n // Check arrow functions assigned to variables\n \"VariableDeclarator[init.type='ArrowFunctionExpression']\"(\n node: TSESTree.VariableDeclarator\n ) {\n const name =\n node.id.type === \"Identifier\" ? node.id.name : null;\n if (node.init) {\n checkForDuplicates(node.init, name);\n }\n },\n\n // Check function expressions\n \"VariableDeclarator[init.type='FunctionExpression']\"(\n node: TSESTree.VariableDeclarator\n ) {\n const name =\n node.id.type === \"Identifier\" ? node.id.name : null;\n if (node.init) {\n checkForDuplicates(node.init, name);\n }\n },\n };\n },\n});\n"],"mappings":"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","names":["meta","meta"]}
1
+ {"version":3,"sources":["../../src/utils/create-rule.ts","../../src/rules/no-semantic-duplicates.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 * External requirement that a rule needs to function\n */\nexport interface RuleRequirement {\n /** Requirement type for programmatic checks */\n type: \"ollama\" | \"git\" | \"coverage\" | \"semantic-index\" | \"styleguide\";\n /** Human-readable description */\n description: string;\n /** Optional: how to satisfy the requirement */\n setupHint?: string;\n}\n\n/**\n * Rule migration definition for updating rule options between versions\n */\nexport interface RuleMigration {\n /** Source version (semver) */\n from: string;\n /** Target version (semver) */\n to: string;\n /** Human-readable description of what changed */\n description: string;\n /** Function to migrate options from old format to new format */\n migrate: (oldOptions: unknown[]) => unknown[];\n /** Whether this migration contains breaking changes */\n breaking?: boolean;\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., \"consistent-dark-mode\") - must match filename */\n id: string;\n\n /** Semantic version of the rule (e.g., \"1.0.0\") */\n version: 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 /** Icon for display in CLI/UI (emoji or icon name) */\n icon?: string;\n\n /** Short hint about the rule type/requirements */\n hint?: string;\n\n /** Whether rule is enabled by default during install */\n defaultEnabled?: boolean;\n\n /** External requirements the rule needs */\n requirements?: RuleRequirement[];\n\n /**\n * NPM packages that must be installed for this rule to work.\n * These will be added to the target project's dependencies during installation.\n *\n * Example: [\"xxhash-wasm\"] for rules using the xxhash library\n */\n npmDependencies?: string[];\n\n /** Instructions to show after installation */\n postInstallInstructions?: string;\n\n /** Framework compatibility */\n frameworks?: (\"next\" | \"vite\" | \"cra\" | \"remix\")[];\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 * Internal utility dependencies that this rule requires.\n * When the rule is copied to a target project, these utilities\n * will be transformed to import from \"uilint-eslint\" instead\n * of relative paths.\n *\n * Example: [\"coverage-aggregator\", \"dependency-graph\"]\n */\n internalDependencies?: string[];\n\n /**\n * Whether this rule is directory-based (has lib/ folder with utilities).\n * Directory-based rules are installed as folders with index.ts and lib/ subdirectory.\n * Single-file rules are installed as single .ts files.\n *\n * When true, ESLint config imports will use:\n * ./.uilint/rules/rule-id/index.js\n * When false (default):\n * ./.uilint/rules/rule-id.js\n */\n isDirectoryBased?: boolean;\n\n /**\n * Migrations for updating rule options between versions.\n * Migrations are applied in order to transform options from older versions.\n */\n migrations?: RuleMigration[];\n\n /**\n * Which UI plugin should handle this rule.\n * Defaults based on category:\n * - \"static\" category → \"eslint\" plugin\n * - \"semantic\" category → \"semantic\" plugin\n *\n * Special cases:\n * - \"vision\" for semantic-vision rule\n */\n plugin?: \"eslint\" | \"vision\" | \"semantic\";\n\n /**\n * Custom inspector panel ID to use for this rule's issues.\n * If not specified, uses the plugin's default issue inspector.\n *\n * Examples:\n * - \"vision-issue\" for VisionIssueInspector\n * - \"duplicates\" for DuplicatesInspector\n * - \"semantic-issue\" for SemanticIssueInspector\n */\n customInspector?: string;\n\n /**\n * Custom heatmap color for this rule's issues.\n * CSS color value (hex, rgb, hsl, or named color).\n * If not specified, uses severity-based coloring.\n */\n heatmapColor?: 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-semantic-duplicates\n *\n * Warns when code is semantically similar to existing indexed code.\n * This rule queries a pre-built semantic index (from uilint duplicates index)\n * rather than calling the LLM during linting - making it fast.\n *\n * Prerequisites:\n * - Run `uilint duplicates index` to build the semantic index first\n * - The index is stored at .uilint/.duplicates-index/\n */\n\nimport { createRule, defineRuleMeta } from \"../utils/create-rule.js\";\nimport type { TSESTree } from \"@typescript-eslint/utils\";\nimport { existsSync, readFileSync, appendFileSync, writeFileSync } from \"fs\";\nimport { dirname, join, relative } from \"path\";\n\n// Debug logging - writes to .uilint/no-semantic-duplicates.log in the project root\nlet logFile: string | null = null;\nlet logInitialized = false;\n\nfunction initLog(projectRoot: string): void {\n if (logFile) return;\n const uilintDir = join(projectRoot, \".uilint\");\n if (existsSync(uilintDir)) {\n logFile = join(uilintDir, \"no-semantic-duplicates.log\");\n }\n}\n\nfunction log(message: string): void {\n if (!logFile) return;\n try {\n const timestamp = new Date().toISOString();\n const line = `[${timestamp}] ${message}\\n`;\n if (!logInitialized) {\n writeFileSync(logFile, line);\n logInitialized = true;\n } else {\n appendFileSync(logFile, line);\n }\n } catch {\n // Ignore logging errors\n }\n}\n\ntype MessageIds = \"semanticDuplicate\" | \"noIndex\";\ntype Options = [\n {\n /** Similarity threshold (0-1). Default: 0.85 */\n threshold?: number;\n /** Path to the index directory */\n indexPath?: string;\n /** Minimum number of lines for a chunk to be reported (default: 3) */\n minLines?: number;\n }\n];\n\n/**\n * Rule metadata\n */\nexport const meta = defineRuleMeta({\n id: \"no-semantic-duplicates\",\n version: \"1.0.0\",\n name: \"No Semantic Duplicates\",\n description: \"Warn when code is semantically similar to existing code\",\n defaultSeverity: \"warn\",\n category: \"semantic\",\n icon: \"🔍\",\n hint: \"Finds similar code via embeddings\",\n defaultEnabled: false,\n plugin: \"semantic\",\n customInspector: \"duplicates\",\n requirements: [\n {\n type: \"semantic-index\",\n description: \"Requires semantic index for duplicate detection\",\n setupHint: \"Run: uilint duplicates index\",\n },\n ],\n postInstallInstructions: \"Run 'uilint duplicates index' to build the semantic index before using this rule.\",\n defaultOptions: [{ threshold: 0.85, indexPath: \".uilint/.duplicates-index\", minLines: 3 }],\n optionSchema: {\n fields: [\n {\n key: \"threshold\",\n label: \"Similarity threshold\",\n type: \"number\",\n defaultValue: 0.85,\n description:\n \"Minimum similarity score (0-1) to report as duplicate. Higher = stricter.\",\n },\n {\n key: \"indexPath\",\n label: \"Index path\",\n type: \"text\",\n defaultValue: \".uilint/.duplicates-index\",\n description: \"Path to the semantic duplicates index directory\",\n },\n {\n key: \"minLines\",\n label: \"Minimum lines\",\n type: \"number\",\n defaultValue: 3,\n description:\n \"Minimum number of lines for a chunk to be reported as a potential duplicate.\",\n },\n ],\n },\n docs: `\n## What it does\n\nWarns when code (components, hooks, functions) is semantically similar to other\ncode in the codebase. Unlike syntactic duplicate detection, this finds code that\nimplements similar functionality even if written differently.\n\n## Prerequisites\n\nBefore using this rule, you must build the semantic index:\n\n\\`\\`\\`bash\nuilint duplicates index\n\\`\\`\\`\n\nThis creates an embedding-based index at \\`.uilint/.duplicates-index/\\`.\n\n## Why it's useful\n\n- **Reduce Duplication**: Find components/hooks that could be consolidated\n- **Discover Patterns**: Identify similar code that could be abstracted\n- **Code Quality**: Encourage reuse over reimplementation\n- **Fast**: Queries pre-built index, no LLM calls during linting\n\n## How it works\n\n1. The rule checks if the current file is in the semantic index\n2. For each indexed code chunk, it looks up similar chunks\n3. If similar chunks exist above the threshold, it reports a warning\n\n## Examples\n\n### Semantic duplicates detected:\n\n\\`\\`\\`tsx\n// UserCard.tsx - Original component\nexport function UserCard({ user }) {\n return (\n <div className=\"card\">\n <img src={user.avatar} />\n <h3>{user.name}</h3>\n </div>\n );\n}\n\n// ProfileCard.tsx - Semantically similar (warning!)\nexport function ProfileCard({ profile }) {\n return (\n <article className=\"profile\">\n <img src={profile.avatarUrl} />\n <h2>{profile.displayName}</h2>\n </article>\n );\n}\n\\`\\`\\`\n\n## Configuration\n\n\\`\\`\\`js\n// eslint.config.js\n\"uilint/no-semantic-duplicates\": [\"warn\", {\n threshold: 0.85, // Similarity threshold (0-1)\n indexPath: \".uilint/.duplicates-index\",\n minLines: 3 // Minimum lines to report (default: 3)\n}]\n\\`\\`\\`\n\n## Notes\n\n- Run \\`uilint duplicates index\\` after significant code changes\n- Use \\`uilint duplicates find\\` to explore all duplicate groups\n- The rule only reports if the file is in the index\n`,\n});\n\n// Cache for loaded index data across files in a single ESLint run\nlet indexCache: {\n projectRoot: string;\n vectorStore: Map<string, number[]>;\n metadataStore: Map<\n string,\n {\n filePath: string;\n startLine: number;\n endLine: number;\n startColumn: number;\n endColumn: number;\n name: string | null;\n kind: string;\n }\n >;\n fileToChunks: Map<string, string[]>;\n} | null = null;\n\n/**\n * Clear the index cache (useful for testing)\n */\nexport function clearIndexCache(): void {\n indexCache = null;\n}\n\n/**\n * Find project root by looking for the .uilint directory (preferred)\n * or falling back to the root package.json (monorepo root)\n */\nfunction findProjectRoot(startPath: string, indexPath: string): string {\n let current = startPath;\n let lastPackageJson: string | null = null;\n\n // Walk up the directory tree\n while (current !== dirname(current)) {\n // Check for .uilint directory with index (highest priority)\n const uilintDir = join(current, indexPath);\n if (existsSync(join(uilintDir, \"manifest.json\"))) {\n return current;\n }\n\n // Track package.json locations\n if (existsSync(join(current, \"package.json\"))) {\n lastPackageJson = current;\n }\n\n current = dirname(current);\n }\n\n // Return the topmost package.json location (monorepo root) or start path\n return lastPackageJson || startPath;\n}\n\n/**\n * Load the index into memory (cached across files)\n */\nfunction loadIndex(\n projectRoot: string,\n indexPath: string\n): typeof indexCache | null {\n const fullIndexPath = join(projectRoot, indexPath);\n log(`loadIndex called: projectRoot=${projectRoot}, indexPath=${indexPath}`);\n log(`fullIndexPath=${fullIndexPath}`);\n\n // Check if we already have a cached index for this project\n if (indexCache && indexCache.projectRoot === projectRoot) {\n log(`Using cached index (${indexCache.vectorStore.size} vectors, ${indexCache.fileToChunks.size} files)`);\n return indexCache;\n }\n\n // Check if index exists\n const manifestPath = join(fullIndexPath, \"manifest.json\");\n if (!existsSync(manifestPath)) {\n log(`Index not found: manifest.json missing at ${manifestPath}`);\n return null;\n }\n\n try {\n // Load metadata\n const metadataPath = join(fullIndexPath, \"metadata.json\");\n if (!existsSync(metadataPath)) {\n log(`Index not found: metadata.json missing at ${metadataPath}`);\n return null;\n }\n\n const metadataContent = readFileSync(metadataPath, \"utf-8\");\n const metadataJson = JSON.parse(metadataContent);\n\n // Support both formats: { entries: {...} } and direct { chunkId: {...} }\n const entries = metadataJson.entries || metadataJson;\n log(`Loaded metadata.json: ${Object.keys(entries).length} entries`);\n\n const metadataStore = new Map<\n string,\n {\n filePath: string;\n startLine: number;\n endLine: number;\n startColumn: number;\n endColumn: number;\n name: string | null;\n kind: string;\n }\n >();\n const fileToChunks = new Map<string, string[]>();\n\n for (const [id, meta] of Object.entries(entries)) {\n const m = meta as {\n filePath: string;\n startLine: number;\n endLine: number;\n startColumn: number;\n endColumn: number;\n name: string | null;\n kind: string;\n };\n metadataStore.set(id, {\n filePath: m.filePath,\n startLine: m.startLine,\n endLine: m.endLine,\n startColumn: m.startColumn ?? 0,\n endColumn: m.endColumn ?? 0,\n name: m.name,\n kind: m.kind,\n });\n\n // Build file -> chunks mapping\n const chunks = fileToChunks.get(m.filePath) || [];\n chunks.push(id);\n fileToChunks.set(m.filePath, chunks);\n }\n\n log(`File to chunks mapping:`);\n for (const [filePath, chunks] of fileToChunks.entries()) {\n log(` ${filePath}: ${chunks.length} chunks (${chunks.join(\", \")})`);\n }\n\n // Load vectors (binary format)\n const vectorsPath = join(fullIndexPath, \"embeddings.bin\");\n const idsPath = join(fullIndexPath, \"ids.json\");\n const vectorStore = new Map<string, number[]>();\n\n if (existsSync(vectorsPath) && existsSync(idsPath)) {\n const idsContent = readFileSync(idsPath, \"utf-8\");\n const ids = JSON.parse(idsContent) as string[];\n log(`Loaded ids.json: ${ids.length} IDs`);\n\n const buffer = readFileSync(vectorsPath);\n // Must use byteOffset and byteLength because Node's Buffer uses pooling\n // and buffer.buffer may contain data from other buffers at different offsets\n const view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength);\n\n // Read header\n const dimension = view.getUint32(0, true);\n const count = view.getUint32(4, true);\n log(`Embeddings binary: dimension=${dimension}, count=${count}`);\n\n // Read vectors\n let offset = 8;\n for (let i = 0; i < count && i < ids.length; i++) {\n const vector: number[] = [];\n for (let j = 0; j < dimension; j++) {\n vector.push(view.getFloat32(offset, true));\n offset += 4;\n }\n vectorStore.set(ids[i], vector);\n }\n log(`Loaded ${vectorStore.size} vectors into store`);\n } else {\n log(`Missing vectors or ids files: vectorsPath=${existsSync(vectorsPath)}, idsPath=${existsSync(idsPath)}`);\n }\n\n indexCache = {\n projectRoot,\n vectorStore,\n metadataStore,\n fileToChunks,\n };\n\n log(`Index loaded successfully: ${vectorStore.size} vectors, ${metadataStore.size} metadata entries, ${fileToChunks.size} files`);\n return indexCache;\n } catch (err) {\n log(`Error loading index: ${err}`);\n return null;\n }\n}\n\n/**\n * Calculate cosine similarity between two vectors\n */\nfunction cosineSimilarity(a: number[], b: number[]): number {\n if (a.length !== b.length) return 0;\n\n let dotProduct = 0;\n let normA = 0;\n let normB = 0;\n\n for (let i = 0; i < a.length; i++) {\n dotProduct += a[i] * b[i];\n normA += a[i] * a[i];\n normB += b[i] * b[i];\n }\n\n const denominator = Math.sqrt(normA) * Math.sqrt(normB);\n return denominator === 0 ? 0 : dotProduct / denominator;\n}\n\n/**\n * Find similar chunks to a given chunk\n */\nfunction findSimilarChunks(\n index: NonNullable<typeof indexCache>,\n chunkId: string,\n threshold: number\n): Array<{ id: string; score: number }> {\n log(`findSimilarChunks: chunkId=${chunkId}, threshold=${threshold}`);\n\n const vector = index.vectorStore.get(chunkId);\n if (!vector) {\n log(` No vector found for chunk ${chunkId}`);\n return [];\n }\n log(` Vector found: dimension=${vector.length}`);\n\n const results: Array<{ id: string; score: number }> = [];\n const allScores: Array<{ id: string; score: number }> = [];\n\n for (const [id, vec] of index.vectorStore.entries()) {\n if (id === chunkId) continue;\n\n const score = cosineSimilarity(vector, vec);\n allScores.push({ id, score });\n if (score >= threshold) {\n results.push({ id, score });\n }\n }\n\n // Log top 10 scores regardless of threshold\n const sortedAll = allScores.sort((a, b) => b.score - a.score).slice(0, 10);\n log(` Top 10 similarity scores (threshold=${threshold}):`);\n for (const { id, score } of sortedAll) {\n const meta = index.metadataStore.get(id);\n const meetsThreshold = score >= threshold ? \"✓\" : \"✗\";\n log(` ${meetsThreshold} ${(score * 100).toFixed(1)}% - ${id} (${meta?.name || \"anonymous\"} in ${meta?.filePath})`);\n }\n\n log(` Found ${results.length} chunks above threshold`);\n return results.sort((a, b) => b.score - a.score);\n}\n\nexport default createRule<Options, MessageIds>({\n name: \"no-semantic-duplicates\",\n meta: {\n type: \"suggestion\",\n docs: {\n description: \"Warn when code is semantically similar to existing code\",\n },\n messages: {\n semanticDuplicate:\n \"This {{kind}} '{{name}}' is {{similarity}}% similar to '{{otherName}}' at {{otherLocation}}. Consider consolidating.\",\n noIndex:\n \"Semantic duplicates index not found. Run 'uilint duplicates index' first.\",\n },\n schema: [\n {\n type: \"object\",\n properties: {\n threshold: {\n type: \"number\",\n minimum: 0,\n maximum: 1,\n description: \"Similarity threshold (0-1)\",\n },\n indexPath: {\n type: \"string\",\n description: \"Path to the index directory\",\n },\n minLines: {\n type: \"integer\",\n minimum: 1,\n description: \"Minimum number of lines for a chunk to be reported\",\n },\n },\n additionalProperties: false,\n },\n ],\n },\n defaultOptions: [\n {\n threshold: 0.85,\n indexPath: \".uilint/.duplicates-index\",\n minLines: 3,\n },\n ],\n create(context) {\n const options = context.options[0] || {};\n const threshold = options.threshold ?? 0.85;\n const indexPath = options.indexPath ?? \".uilint/.duplicates-index\";\n const minLines = options.minLines ?? 3;\n\n const filename = context.filename || context.getFilename();\n const projectRoot = findProjectRoot(dirname(filename), indexPath);\n\n // Initialize logging to .uilint folder\n initLog(projectRoot);\n\n log(`\\n========== Rule create() ==========`);\n log(`Filename: ${filename}`);\n log(`Threshold: ${threshold}`);\n log(`Index path: ${indexPath}`);\n log(`Min lines: ${minLines}`);\n log(`Project root: ${projectRoot}`);\n\n const index = loadIndex(projectRoot, indexPath);\n\n // Track which chunks we've already reported to avoid duplicates\n const reportedChunks = new Set<string>();\n\n /**\n * Check if a node location corresponds to an indexed chunk\n * and if so, check for similar chunks\n */\n function checkForDuplicates(\n node: TSESTree.Node,\n name: string | null\n ): void {\n log(`checkForDuplicates: name=${name}, file=${filename}`);\n\n if (!index) {\n log(` No index loaded`);\n return;\n }\n\n // Get chunks for this file\n const fileChunks = index.fileToChunks.get(filename);\n log(` Looking for chunks for file: ${filename}`);\n log(` Files in index: ${Array.from(index.fileToChunks.keys()).join(\", \")}`);\n\n if (!fileChunks || fileChunks.length === 0) {\n log(` No chunks found for this file`);\n return;\n }\n log(` Found ${fileChunks.length} chunks: ${fileChunks.join(\", \")}`);\n\n // Find the chunk that contains this node's location\n const nodeLine = node.loc?.start.line;\n if (!nodeLine) {\n log(` No node line number`);\n return;\n }\n log(` Node starts at line ${nodeLine}`);\n\n for (const chunkId of fileChunks) {\n if (reportedChunks.has(chunkId)) {\n log(` Chunk ${chunkId} already reported, skipping`);\n continue;\n }\n\n const meta = index.metadataStore.get(chunkId);\n if (!meta) {\n log(` No metadata for chunk ${chunkId}`);\n continue;\n }\n\n log(` Checking chunk ${chunkId}: lines ${meta.startLine}-${meta.endLine} (node at line ${nodeLine})`);\n\n // Check if this node is within the chunk's line range\n if (nodeLine >= meta.startLine && nodeLine <= meta.endLine) {\n log(` Node is within chunk range, searching for similar chunks...`);\n\n // Find similar chunks\n const similar = findSimilarChunks(index, chunkId, threshold);\n\n if (similar.length > 0) {\n const best = similar[0];\n const bestMeta = index.metadataStore.get(best.id);\n\n if (bestMeta) {\n // Check minimum lines threshold\n const chunkLines = meta.endLine - meta.startLine + 1;\n if (chunkLines < minLines) {\n log(` Skipping: chunk has ${chunkLines} lines, below minLines=${minLines}`);\n continue;\n }\n\n reportedChunks.add(chunkId);\n\n const relPath = relative(projectRoot, bestMeta.filePath);\n const similarity = Math.round(best.score * 100);\n\n log(` REPORTING: ${meta.kind} '${name || meta.name}' is ${similarity}% similar to '${bestMeta.name}' at ${relPath}:${bestMeta.startLine}`);\n\n context.report({\n node,\n loc: {\n start: { line: meta.startLine, column: meta.startColumn },\n end: { line: meta.endLine, column: meta.endColumn },\n },\n messageId: \"semanticDuplicate\",\n data: {\n kind: meta.kind,\n name: name || meta.name || \"(anonymous)\",\n similarity: String(similarity),\n otherName: bestMeta.name || \"(anonymous)\",\n otherLocation: `${relPath}:${bestMeta.startLine}`,\n },\n });\n }\n } else {\n log(` No similar chunks found above threshold`);\n }\n } else {\n log(` Node line ${nodeLine} not in chunk range ${meta.startLine}-${meta.endLine}`);\n }\n }\n }\n\n return {\n // Check function declarations\n FunctionDeclaration(node) {\n const name = node.id?.name || null;\n checkForDuplicates(node, name);\n },\n\n // Check arrow functions assigned to variables\n \"VariableDeclarator[init.type='ArrowFunctionExpression']\"(\n node: TSESTree.VariableDeclarator\n ) {\n const name =\n node.id.type === \"Identifier\" ? node.id.name : null;\n if (node.init) {\n checkForDuplicates(node.init, name);\n }\n },\n\n // Check function expressions\n \"VariableDeclarator[init.type='FunctionExpression']\"(\n node: TSESTree.VariableDeclarator\n ) {\n const name =\n node.id.type === \"Identifier\" ? node.id.name : null;\n if (node.init) {\n checkForDuplicates(node.init, name);\n }\n },\n };\n },\n});\n"],"mappings":"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","names":["meta","meta"]}
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/utils/create-rule.ts","../../src/rules/prefer-tailwind.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 * External requirement that a rule needs to function\n */\nexport interface RuleRequirement {\n /** Requirement type for programmatic checks */\n type: \"ollama\" | \"git\" | \"coverage\" | \"semantic-index\" | \"styleguide\";\n /** Human-readable description */\n description: string;\n /** Optional: how to satisfy the requirement */\n setupHint?: string;\n}\n\n/**\n * Rule migration definition for updating rule options between versions\n */\nexport interface RuleMigration {\n /** Source version (semver) */\n from: string;\n /** Target version (semver) */\n to: string;\n /** Human-readable description of what changed */\n description: string;\n /** Function to migrate options from old format to new format */\n migrate: (oldOptions: unknown[]) => unknown[];\n /** Whether this migration contains breaking changes */\n breaking?: boolean;\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., \"consistent-dark-mode\") - must match filename */\n id: string;\n\n /** Semantic version of the rule (e.g., \"1.0.0\") */\n version: 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 /** Icon for display in CLI/UI (emoji or icon name) */\n icon?: string;\n\n /** Short hint about the rule type/requirements */\n hint?: string;\n\n /** Whether rule is enabled by default during install */\n defaultEnabled?: boolean;\n\n /** External requirements the rule needs */\n requirements?: RuleRequirement[];\n\n /** Instructions to show after installation */\n postInstallInstructions?: string;\n\n /** Framework compatibility */\n frameworks?: (\"next\" | \"vite\" | \"cra\" | \"remix\")[];\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 * Internal utility dependencies that this rule requires.\n * When the rule is copied to a target project, these utilities\n * will be transformed to import from \"uilint-eslint\" instead\n * of relative paths.\n *\n * Example: [\"coverage-aggregator\", \"dependency-graph\"]\n */\n internalDependencies?: string[];\n\n /**\n * Whether this rule is directory-based (has lib/ folder with utilities).\n * Directory-based rules are installed as folders with index.ts and lib/ subdirectory.\n * Single-file rules are installed as single .ts files.\n *\n * When true, ESLint config imports will use:\n * ./.uilint/rules/rule-id/index.js\n * When false (default):\n * ./.uilint/rules/rule-id.js\n */\n isDirectoryBased?: boolean;\n\n /**\n * Migrations for updating rule options between versions.\n * Migrations are applied in order to transform options from older versions.\n */\n migrations?: RuleMigration[];\n\n /**\n * Which UI plugin should handle this rule.\n * Defaults based on category:\n * - \"static\" category → \"eslint\" plugin\n * - \"semantic\" category → \"semantic\" plugin\n *\n * Special cases:\n * - \"vision\" for semantic-vision rule\n */\n plugin?: \"eslint\" | \"vision\" | \"semantic\";\n\n /**\n * Custom inspector panel ID to use for this rule's issues.\n * If not specified, uses the plugin's default issue inspector.\n *\n * Examples:\n * - \"vision-issue\" for VisionIssueInspector\n * - \"duplicates\" for DuplicatesInspector\n * - \"semantic-issue\" for SemanticIssueInspector\n */\n customInspector?: string;\n\n /**\n * Custom heatmap color for this rule's issues.\n * CSS color value (hex, rgb, hsl, or named color).\n * If not specified, uses severity-based coloring.\n */\n heatmapColor?: 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: prefer-tailwind\n *\n * Encourages using Tailwind className over inline style attributes.\n * - Detects files with a high ratio of inline `style` vs `className` usage\n * - Warns at each element using style without className when ratio exceeds threshold\n */\n\nimport { createRule, defineRuleMeta } from \"../utils/create-rule.js\";\nimport type { TSESTree } from \"@typescript-eslint/utils\";\n\ntype MessageIds = \"preferTailwind\" | \"preferSemanticColors\";\ntype Options = [\n {\n /** Minimum ratio of style-only elements before warnings trigger (0-1). Default: 0.3 */\n styleRatioThreshold?: number;\n /** Don't warn if file has fewer than N JSX elements with styling. Default: 3 */\n minElementsForAnalysis?: number;\n /** Style properties to ignore (e.g., [\"transform\", \"animation\"] for dynamic values). Default: [] */\n allowedStyleProperties?: string[];\n /** Component names to skip (e.g., [\"motion.div\", \"animated.View\"]). Default: [] */\n ignoreComponents?: string[];\n /** Prefer semantic colors (bg-destructive) over hard-coded (bg-red-500). Default: false */\n preferSemanticColors?: boolean;\n /** Hard-coded color names to allow when preferSemanticColors is enabled. Default: [] */\n allowedHardCodedColors?: string[];\n }?\n];\n\n/**\n * Rule metadata - colocated with implementation for maintainability\n */\nexport const meta = defineRuleMeta({\n id: \"prefer-tailwind\",\n version: \"1.0.0\",\n name: \"Prefer Tailwind\",\n description: \"Encourage Tailwind className over inline style attributes\",\n defaultSeverity: \"warn\",\n category: \"static\",\n icon: \"🎨\",\n hint: \"Prefers className over inline styles\",\n defaultEnabled: true,\n defaultOptions: [\n {\n styleRatioThreshold: 0.3,\n minElementsForAnalysis: 3,\n allowedStyleProperties: [],\n ignoreComponents: [],\n preferSemanticColors: true,\n allowedHardCodedColors: [],\n },\n ],\n optionSchema: {\n fields: [\n {\n key: \"styleRatioThreshold\",\n label: \"Style ratio threshold\",\n type: \"number\",\n defaultValue: 0.3,\n description:\n \"Minimum ratio (0-1) of style-only elements before warnings trigger\",\n },\n {\n key: \"minElementsForAnalysis\",\n label: \"Minimum elements\",\n type: \"number\",\n defaultValue: 3,\n description: \"Don't warn if file has fewer styled elements than this\",\n },\n {\n key: \"allowedStyleProperties\",\n label: \"Allowed style properties\",\n type: \"text\",\n defaultValue: \"\",\n description:\n \"Comma-separated list of style properties to allow (e.g., transform,animation)\",\n },\n {\n key: \"ignoreComponents\",\n label: \"Ignored components\",\n type: \"text\",\n defaultValue: \"\",\n description:\n \"Comma-separated component names to skip (e.g., motion.div,animated.View)\",\n },\n {\n key: \"preferSemanticColors\",\n label: \"Prefer semantic colors\",\n type: \"boolean\",\n defaultValue: true,\n description:\n \"Warn against hard-coded colors (bg-red-500) in favor of semantic theme colors (bg-destructive)\",\n },\n {\n key: \"allowedHardCodedColors\",\n label: \"Allowed hard-coded colors\",\n type: \"text\",\n defaultValue: \"\",\n description:\n \"Comma-separated color names to allow when preferSemanticColors is enabled (e.g., gray,slate)\",\n },\n ],\n },\n docs: `\n## What it does\n\nDetects files with a high ratio of inline \\`style\\` attributes versus \\`className\\` usage\nin JSX elements. Reports warnings on elements that use \\`style\\` without \\`className\\`,\nbut only when the file exceeds a configurable threshold ratio.\n\n## Why it's useful\n\n- **Consistency**: Encourages using Tailwind's utility classes for styling\n- **Maintainability**: Tailwind classes are easier to read and maintain than inline styles\n- **Performance**: Tailwind generates optimized CSS; inline styles can't be deduplicated\n- **Theming**: Tailwind classes work with dark mode and responsive variants\n\n## Examples\n\n### ❌ Incorrect (when file exceeds threshold)\n\n\\`\\`\\`tsx\n// Many elements using style without className\n<div style={{ color: 'red' }}>Red text</div>\n<span style={{ marginTop: '10px' }}>Spaced</span>\n<p style={{ fontSize: '16px' }}>Paragraph</p>\n\\`\\`\\`\n\n### ✅ Correct\n\n\\`\\`\\`tsx\n// Using Tailwind className\n<div className=\"text-red-500\">Red text</div>\n<span className=\"mt-2\">Spaced</span>\n<p className=\"text-base\">Paragraph</p>\n\n// Both style and className (acceptable for dynamic values)\n<div className=\"p-4\" style={{ backgroundColor: dynamicColor }}>Mixed</div>\n\\`\\`\\`\n\n## Configuration\n\n\\`\\`\\`js\n// eslint.config.js\n\"uilint/prefer-tailwind\": [\"warn\", {\n styleRatioThreshold: 0.3, // Warn when >30% of elements are style-only\n minElementsForAnalysis: 3, // Need at least 3 styled elements to analyze\n allowedStyleProperties: [\"transform\", \"animation\"], // Skip these properties\n ignoreComponents: [\"motion.div\", \"animated.View\"], // Skip animation libraries\n preferSemanticColors: true, // Warn on hard-coded colors like bg-red-500\n allowedHardCodedColors: [\"gray\", \"slate\"] // Allow specific color palettes\n}]\n\\`\\`\\`\n\n## Semantic Colors\n\nWhen \\`preferSemanticColors\\` is enabled, the rule warns against hard-coded Tailwind color classes\nin favor of semantic theme colors:\n\n### ❌ Hard-coded colors (when enabled)\n\n\\`\\`\\`tsx\n<div className=\"bg-red-500 text-white\">Error</div>\n<button className=\"hover:bg-blue-600\">Click</button>\n\\`\\`\\`\n\n### ✅ Semantic colors (preferred)\n\n\\`\\`\\`tsx\n<div className=\"bg-destructive text-destructive-foreground\">Error</div>\n<button className=\"hover:bg-primary\">Click</button>\n\\`\\`\\`\n\nSemantic colors like \\`bg-background\\`, \\`text-foreground\\`, \\`bg-primary\\`, \\`bg-destructive\\`,\n\\`bg-muted\\`, etc. work better with theming and dark mode.\n\nColors that are always allowed: \\`white\\`, \\`black\\`, \\`transparent\\`, \\`inherit\\`, \\`current\\`.\n\n## Notes\n\n- Elements with BOTH \\`style\\` and \\`className\\` are considered acceptable\n- Files with few styled elements are not analyzed (prevents false positives)\n- The rule uses a ratio-based approach to catch systematic patterns, not isolated cases\n- Use \\`allowedStyleProperties\\` for dynamic values that can't use Tailwind\n- Use \\`ignoreComponents\\` for animation libraries that require inline styles\n`,\n});\n\n/**\n * Get the component name from a JSX opening element\n */\nfunction getComponentName(node: TSESTree.JSXOpeningElement): string {\n const name = node.name;\n\n if (name.type === \"JSXIdentifier\") {\n return name.name;\n }\n\n if (name.type === \"JSXMemberExpression\") {\n // Handle motion.div, animated.View, etc.\n const parts: string[] = [];\n let current: TSESTree.JSXMemberExpression | TSESTree.JSXIdentifier = name;\n\n while (current.type === \"JSXMemberExpression\") {\n if (current.property.type === \"JSXIdentifier\") {\n parts.unshift(current.property.name);\n }\n current = current.object as\n | TSESTree.JSXMemberExpression\n | TSESTree.JSXIdentifier;\n }\n\n if (current.type === \"JSXIdentifier\") {\n parts.unshift(current.name);\n }\n\n return parts.join(\".\");\n }\n\n return \"\";\n}\n\n/**\n * Extract property names from a style object expression\n */\nfunction getStylePropertyNames(\n value: TSESTree.JSXExpressionContainer\n): string[] {\n const expr = value.expression;\n\n // Handle style={{ prop: value }}\n if (expr.type === \"ObjectExpression\") {\n return expr.properties\n .filter((prop): prop is TSESTree.Property => prop.type === \"Property\")\n .map((prop) => {\n if (prop.key.type === \"Identifier\") {\n return prop.key.name;\n }\n if (prop.key.type === \"Literal\" && typeof prop.key.value === \"string\") {\n return prop.key.value;\n }\n return \"\";\n })\n .filter(Boolean);\n }\n\n // For style={variable} or style={{...spread}}, we can't determine properties\n return [];\n}\n\n/**\n * Check if all style properties are in the allowed list\n */\nfunction hasOnlyAllowedProperties(\n styleProperties: string[],\n allowedProperties: string[]\n): boolean {\n if (allowedProperties.length === 0 || styleProperties.length === 0) {\n return false;\n }\n\n return styleProperties.every((prop) => allowedProperties.includes(prop));\n}\n\ninterface ElementInfo {\n node: TSESTree.JSXOpeningElement;\n hasStyle: boolean;\n hasClassName: boolean;\n styleProperties: string[];\n}\n\n/**\n * Tailwind color names that should use semantic alternatives\n * Excludes neutral colors that are often acceptable\n */\nconst HARD_CODED_COLOR_NAMES = [\n \"red\",\n \"orange\",\n \"amber\",\n \"yellow\",\n \"lime\",\n \"green\",\n \"emerald\",\n \"teal\",\n \"cyan\",\n \"sky\",\n \"blue\",\n \"indigo\",\n \"violet\",\n \"purple\",\n \"fuchsia\",\n \"pink\",\n \"rose\",\n \"slate\",\n \"gray\",\n \"zinc\",\n \"neutral\",\n \"stone\",\n];\n\n/**\n * Colors that are always allowed (not theme-dependent)\n */\nconst ALWAYS_ALLOWED_COLORS = [\n \"white\",\n \"black\",\n \"transparent\",\n \"inherit\",\n \"current\",\n];\n\n/**\n * Regex to match hard-coded Tailwind color classes\n * Matches patterns like: bg-red-500, text-blue-600/50, hover:bg-green-400, dark:text-slate-100\n * Color utilities: bg, text, border, ring, outline, decoration, accent, fill, stroke,\n * from, via, to (gradients), divide, placeholder, caret, shadow\n */\nfunction createHardCodedColorRegex(colorNames: string[]): RegExp {\n const colorPattern = colorNames.join(\"|\");\n // Match color utilities with color-shade pattern, optional opacity, with optional variant prefixes\n return new RegExp(\n `(?:^|\\\\s)(?:[a-z-]+:)*(?:bg|text|border|ring|outline|decoration|accent|fill|stroke|from|via|to|divide|placeholder|caret|shadow)-(${colorPattern})-\\\\d{1,3}(?:/\\\\d{1,3})?(?=\\\\s|$)`,\n \"g\"\n );\n}\n\n/**\n * Extract className value from a JSX attribute\n */\nfunction getClassNameValue(attr: TSESTree.JSXAttribute): string | null {\n if (!attr.value) return null;\n\n // className=\"...\"\n if (attr.value.type === \"Literal\" && typeof attr.value.value === \"string\") {\n return attr.value.value;\n }\n\n // className={\"...\"}\n if (\n attr.value.type === \"JSXExpressionContainer\" &&\n attr.value.expression.type === \"Literal\" &&\n typeof attr.value.expression.value === \"string\"\n ) {\n return attr.value.expression.value;\n }\n\n // className={`...`}\n if (\n attr.value.type === \"JSXExpressionContainer\" &&\n attr.value.expression.type === \"TemplateLiteral\"\n ) {\n // Extract static parts of template literal\n return attr.value.expression.quasis.map((q) => q.value.raw).join(\" \");\n }\n\n return null;\n}\n\n/**\n * Check if a className string contains hard-coded color classes\n */\nfunction findHardCodedColors(\n className: string,\n allowedColors: string[]\n): string[] {\n const disallowedColorNames = HARD_CODED_COLOR_NAMES.filter(\n (c) => !allowedColors.includes(c)\n );\n\n if (disallowedColorNames.length === 0) return [];\n\n const regex = createHardCodedColorRegex(disallowedColorNames);\n const matches: string[] = [];\n let match;\n\n while ((match = regex.exec(className)) !== null) {\n matches.push(match[0].trim());\n }\n\n return matches;\n}\n\nexport default createRule<Options, MessageIds>({\n name: \"prefer-tailwind\",\n meta: {\n type: \"suggestion\",\n docs: {\n description: \"Encourage Tailwind className over inline style attributes\",\n },\n messages: {\n preferTailwind:\n \"Prefer Tailwind className over inline style. This element uses style attribute without className.\",\n preferSemanticColors:\n \"Prefer semantic color classes (e.g., bg-destructive, text-primary) over hard-coded colors (e.g., bg-red-500).\",\n },\n schema: [\n {\n type: \"object\",\n properties: {\n styleRatioThreshold: {\n type: \"number\",\n minimum: 0,\n maximum: 1,\n description:\n \"Minimum ratio of style-only elements to trigger warnings\",\n },\n minElementsForAnalysis: {\n type: \"number\",\n minimum: 1,\n description: \"Minimum styled elements required for analysis\",\n },\n allowedStyleProperties: {\n type: \"array\",\n items: { type: \"string\" },\n description: \"Style properties to ignore\",\n },\n ignoreComponents: {\n type: \"array\",\n items: { type: \"string\" },\n description: \"Component names to skip\",\n },\n preferSemanticColors: {\n type: \"boolean\",\n description:\n \"Warn against hard-coded colors in favor of semantic theme colors\",\n },\n allowedHardCodedColors: {\n type: \"array\",\n items: { type: \"string\" },\n description:\n \"Hard-coded color names to allow when preferSemanticColors is enabled\",\n },\n },\n additionalProperties: false,\n },\n ],\n },\n defaultOptions: [\n {\n styleRatioThreshold: 0.3,\n minElementsForAnalysis: 3,\n allowedStyleProperties: [],\n ignoreComponents: [],\n preferSemanticColors: false,\n allowedHardCodedColors: [],\n },\n ],\n create(context) {\n const options = context.options[0] || {};\n const styleRatioThreshold = options.styleRatioThreshold ?? 0.3;\n const minElementsForAnalysis = options.minElementsForAnalysis ?? 3;\n const allowedStyleProperties = options.allowedStyleProperties ?? [];\n const ignoreComponents = options.ignoreComponents ?? [];\n const preferSemanticColors = options.preferSemanticColors ?? false;\n const allowedHardCodedColors = options.allowedHardCodedColors ?? [];\n\n // Tracking state for file-level analysis\n const styledElements: ElementInfo[] = [];\n\n /**\n * Check if a JSXAttribute is a style attribute with an expression\n */\n function isStyleAttribute(attr: TSESTree.JSXAttribute): boolean {\n return (\n attr.name.type === \"JSXIdentifier\" &&\n attr.name.name === \"style\" &&\n attr.value?.type === \"JSXExpressionContainer\"\n );\n }\n\n /**\n * Check if a JSXAttribute is a className attribute\n */\n function isClassNameAttribute(attr: TSESTree.JSXAttribute): boolean {\n return (\n attr.name.type === \"JSXIdentifier\" &&\n (attr.name.name === \"className\" || attr.name.name === \"class\")\n );\n }\n\n return {\n JSXOpeningElement(node) {\n // Check if component should be ignored\n const componentName = getComponentName(node);\n if (ignoreComponents.includes(componentName)) {\n return;\n }\n\n let hasStyle = false;\n let hasClassName = false;\n let styleProperties: string[] = [];\n\n for (const attr of node.attributes) {\n if (attr.type === \"JSXAttribute\") {\n if (isStyleAttribute(attr)) {\n hasStyle = true;\n styleProperties = getStylePropertyNames(\n attr.value as TSESTree.JSXExpressionContainer\n );\n }\n if (isClassNameAttribute(attr)) {\n hasClassName = true;\n\n // Check for hard-coded colors if preferSemanticColors is enabled\n if (preferSemanticColors) {\n const classNameValue = getClassNameValue(attr);\n if (classNameValue) {\n const hardCodedColors = findHardCodedColors(\n classNameValue,\n allowedHardCodedColors\n );\n if (hardCodedColors.length > 0) {\n context.report({\n node,\n messageId: \"preferSemanticColors\",\n });\n }\n }\n }\n }\n }\n }\n\n // Only track elements that have style OR className (or both)\n if (hasStyle || hasClassName) {\n styledElements.push({\n node,\n hasStyle,\n hasClassName,\n styleProperties,\n });\n }\n },\n\n \"Program:exit\"() {\n // Don't analyze if not enough styled elements\n if (styledElements.length < minElementsForAnalysis) {\n return;\n }\n\n // Filter out elements where all style properties are allowed\n const styleOnlyElements = styledElements.filter((el) => {\n if (!el.hasStyle || el.hasClassName) {\n return false;\n }\n\n // If all style properties are in the allowed list, don't count this element\n if (\n hasOnlyAllowedProperties(el.styleProperties, allowedStyleProperties)\n ) {\n return false;\n }\n\n return true;\n });\n\n const ratio = styleOnlyElements.length / styledElements.length;\n\n // Only report if ratio exceeds threshold\n if (ratio > styleRatioThreshold) {\n for (const element of styleOnlyElements) {\n context.report({\n node: element.node,\n messageId: \"preferTailwind\",\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/prefer-tailwind.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 * External requirement that a rule needs to function\n */\nexport interface RuleRequirement {\n /** Requirement type for programmatic checks */\n type: \"ollama\" | \"git\" | \"coverage\" | \"semantic-index\" | \"styleguide\";\n /** Human-readable description */\n description: string;\n /** Optional: how to satisfy the requirement */\n setupHint?: string;\n}\n\n/**\n * Rule migration definition for updating rule options between versions\n */\nexport interface RuleMigration {\n /** Source version (semver) */\n from: string;\n /** Target version (semver) */\n to: string;\n /** Human-readable description of what changed */\n description: string;\n /** Function to migrate options from old format to new format */\n migrate: (oldOptions: unknown[]) => unknown[];\n /** Whether this migration contains breaking changes */\n breaking?: boolean;\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., \"consistent-dark-mode\") - must match filename */\n id: string;\n\n /** Semantic version of the rule (e.g., \"1.0.0\") */\n version: 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 /** Icon for display in CLI/UI (emoji or icon name) */\n icon?: string;\n\n /** Short hint about the rule type/requirements */\n hint?: string;\n\n /** Whether rule is enabled by default during install */\n defaultEnabled?: boolean;\n\n /** External requirements the rule needs */\n requirements?: RuleRequirement[];\n\n /**\n * NPM packages that must be installed for this rule to work.\n * These will be added to the target project's dependencies during installation.\n *\n * Example: [\"xxhash-wasm\"] for rules using the xxhash library\n */\n npmDependencies?: string[];\n\n /** Instructions to show after installation */\n postInstallInstructions?: string;\n\n /** Framework compatibility */\n frameworks?: (\"next\" | \"vite\" | \"cra\" | \"remix\")[];\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 * Internal utility dependencies that this rule requires.\n * When the rule is copied to a target project, these utilities\n * will be transformed to import from \"uilint-eslint\" instead\n * of relative paths.\n *\n * Example: [\"coverage-aggregator\", \"dependency-graph\"]\n */\n internalDependencies?: string[];\n\n /**\n * Whether this rule is directory-based (has lib/ folder with utilities).\n * Directory-based rules are installed as folders with index.ts and lib/ subdirectory.\n * Single-file rules are installed as single .ts files.\n *\n * When true, ESLint config imports will use:\n * ./.uilint/rules/rule-id/index.js\n * When false (default):\n * ./.uilint/rules/rule-id.js\n */\n isDirectoryBased?: boolean;\n\n /**\n * Migrations for updating rule options between versions.\n * Migrations are applied in order to transform options from older versions.\n */\n migrations?: RuleMigration[];\n\n /**\n * Which UI plugin should handle this rule.\n * Defaults based on category:\n * - \"static\" category → \"eslint\" plugin\n * - \"semantic\" category → \"semantic\" plugin\n *\n * Special cases:\n * - \"vision\" for semantic-vision rule\n */\n plugin?: \"eslint\" | \"vision\" | \"semantic\";\n\n /**\n * Custom inspector panel ID to use for this rule's issues.\n * If not specified, uses the plugin's default issue inspector.\n *\n * Examples:\n * - \"vision-issue\" for VisionIssueInspector\n * - \"duplicates\" for DuplicatesInspector\n * - \"semantic-issue\" for SemanticIssueInspector\n */\n customInspector?: string;\n\n /**\n * Custom heatmap color for this rule's issues.\n * CSS color value (hex, rgb, hsl, or named color).\n * If not specified, uses severity-based coloring.\n */\n heatmapColor?: 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: prefer-tailwind\n *\n * Encourages using Tailwind className over inline style attributes.\n * - Detects files with a high ratio of inline `style` vs `className` usage\n * - Warns at each element using style without className when ratio exceeds threshold\n */\n\nimport { createRule, defineRuleMeta } from \"../utils/create-rule.js\";\nimport type { TSESTree } from \"@typescript-eslint/utils\";\n\ntype MessageIds = \"preferTailwind\" | \"preferSemanticColors\";\ntype Options = [\n {\n /** Minimum ratio of style-only elements before warnings trigger (0-1). Default: 0.3 */\n styleRatioThreshold?: number;\n /** Don't warn if file has fewer than N JSX elements with styling. Default: 3 */\n minElementsForAnalysis?: number;\n /** Style properties to ignore (e.g., [\"transform\", \"animation\"] for dynamic values). Default: [] */\n allowedStyleProperties?: string[];\n /** Component names to skip (e.g., [\"motion.div\", \"animated.View\"]). Default: [] */\n ignoreComponents?: string[];\n /** Prefer semantic colors (bg-destructive) over hard-coded (bg-red-500). Default: false */\n preferSemanticColors?: boolean;\n /** Hard-coded color names to allow when preferSemanticColors is enabled. Default: [] */\n allowedHardCodedColors?: string[];\n }?\n];\n\n/**\n * Rule metadata - colocated with implementation for maintainability\n */\nexport const meta = defineRuleMeta({\n id: \"prefer-tailwind\",\n version: \"1.0.0\",\n name: \"Prefer Tailwind\",\n description: \"Encourage Tailwind className over inline style attributes\",\n defaultSeverity: \"warn\",\n category: \"static\",\n icon: \"🎨\",\n hint: \"Prefers className over inline styles\",\n defaultEnabled: true,\n defaultOptions: [\n {\n styleRatioThreshold: 0.3,\n minElementsForAnalysis: 3,\n allowedStyleProperties: [],\n ignoreComponents: [],\n preferSemanticColors: true,\n allowedHardCodedColors: [],\n },\n ],\n optionSchema: {\n fields: [\n {\n key: \"styleRatioThreshold\",\n label: \"Style ratio threshold\",\n type: \"number\",\n defaultValue: 0.3,\n description:\n \"Minimum ratio (0-1) of style-only elements before warnings trigger\",\n },\n {\n key: \"minElementsForAnalysis\",\n label: \"Minimum elements\",\n type: \"number\",\n defaultValue: 3,\n description: \"Don't warn if file has fewer styled elements than this\",\n },\n {\n key: \"allowedStyleProperties\",\n label: \"Allowed style properties\",\n type: \"text\",\n defaultValue: \"\",\n description:\n \"Comma-separated list of style properties to allow (e.g., transform,animation)\",\n },\n {\n key: \"ignoreComponents\",\n label: \"Ignored components\",\n type: \"text\",\n defaultValue: \"\",\n description:\n \"Comma-separated component names to skip (e.g., motion.div,animated.View)\",\n },\n {\n key: \"preferSemanticColors\",\n label: \"Prefer semantic colors\",\n type: \"boolean\",\n defaultValue: true,\n description:\n \"Warn against hard-coded colors (bg-red-500) in favor of semantic theme colors (bg-destructive)\",\n },\n {\n key: \"allowedHardCodedColors\",\n label: \"Allowed hard-coded colors\",\n type: \"text\",\n defaultValue: \"\",\n description:\n \"Comma-separated color names to allow when preferSemanticColors is enabled (e.g., gray,slate)\",\n },\n ],\n },\n docs: `\n## What it does\n\nDetects files with a high ratio of inline \\`style\\` attributes versus \\`className\\` usage\nin JSX elements. Reports warnings on elements that use \\`style\\` without \\`className\\`,\nbut only when the file exceeds a configurable threshold ratio.\n\n## Why it's useful\n\n- **Consistency**: Encourages using Tailwind's utility classes for styling\n- **Maintainability**: Tailwind classes are easier to read and maintain than inline styles\n- **Performance**: Tailwind generates optimized CSS; inline styles can't be deduplicated\n- **Theming**: Tailwind classes work with dark mode and responsive variants\n\n## Examples\n\n### ❌ Incorrect (when file exceeds threshold)\n\n\\`\\`\\`tsx\n// Many elements using style without className\n<div style={{ color: 'red' }}>Red text</div>\n<span style={{ marginTop: '10px' }}>Spaced</span>\n<p style={{ fontSize: '16px' }}>Paragraph</p>\n\\`\\`\\`\n\n### ✅ Correct\n\n\\`\\`\\`tsx\n// Using Tailwind className\n<div className=\"text-red-500\">Red text</div>\n<span className=\"mt-2\">Spaced</span>\n<p className=\"text-base\">Paragraph</p>\n\n// Both style and className (acceptable for dynamic values)\n<div className=\"p-4\" style={{ backgroundColor: dynamicColor }}>Mixed</div>\n\\`\\`\\`\n\n## Configuration\n\n\\`\\`\\`js\n// eslint.config.js\n\"uilint/prefer-tailwind\": [\"warn\", {\n styleRatioThreshold: 0.3, // Warn when >30% of elements are style-only\n minElementsForAnalysis: 3, // Need at least 3 styled elements to analyze\n allowedStyleProperties: [\"transform\", \"animation\"], // Skip these properties\n ignoreComponents: [\"motion.div\", \"animated.View\"], // Skip animation libraries\n preferSemanticColors: true, // Warn on hard-coded colors like bg-red-500\n allowedHardCodedColors: [\"gray\", \"slate\"] // Allow specific color palettes\n}]\n\\`\\`\\`\n\n## Semantic Colors\n\nWhen \\`preferSemanticColors\\` is enabled, the rule warns against hard-coded Tailwind color classes\nin favor of semantic theme colors:\n\n### ❌ Hard-coded colors (when enabled)\n\n\\`\\`\\`tsx\n<div className=\"bg-red-500 text-white\">Error</div>\n<button className=\"hover:bg-blue-600\">Click</button>\n\\`\\`\\`\n\n### ✅ Semantic colors (preferred)\n\n\\`\\`\\`tsx\n<div className=\"bg-destructive text-destructive-foreground\">Error</div>\n<button className=\"hover:bg-primary\">Click</button>\n\\`\\`\\`\n\nSemantic colors like \\`bg-background\\`, \\`text-foreground\\`, \\`bg-primary\\`, \\`bg-destructive\\`,\n\\`bg-muted\\`, etc. work better with theming and dark mode.\n\nColors that are always allowed: \\`white\\`, \\`black\\`, \\`transparent\\`, \\`inherit\\`, \\`current\\`.\n\n## Notes\n\n- Elements with BOTH \\`style\\` and \\`className\\` are considered acceptable\n- Files with few styled elements are not analyzed (prevents false positives)\n- The rule uses a ratio-based approach to catch systematic patterns, not isolated cases\n- Use \\`allowedStyleProperties\\` for dynamic values that can't use Tailwind\n- Use \\`ignoreComponents\\` for animation libraries that require inline styles\n`,\n});\n\n/**\n * Get the component name from a JSX opening element\n */\nfunction getComponentName(node: TSESTree.JSXOpeningElement): string {\n const name = node.name;\n\n if (name.type === \"JSXIdentifier\") {\n return name.name;\n }\n\n if (name.type === \"JSXMemberExpression\") {\n // Handle motion.div, animated.View, etc.\n const parts: string[] = [];\n let current: TSESTree.JSXMemberExpression | TSESTree.JSXIdentifier = name;\n\n while (current.type === \"JSXMemberExpression\") {\n if (current.property.type === \"JSXIdentifier\") {\n parts.unshift(current.property.name);\n }\n current = current.object as\n | TSESTree.JSXMemberExpression\n | TSESTree.JSXIdentifier;\n }\n\n if (current.type === \"JSXIdentifier\") {\n parts.unshift(current.name);\n }\n\n return parts.join(\".\");\n }\n\n return \"\";\n}\n\n/**\n * Extract property names from a style object expression\n */\nfunction getStylePropertyNames(\n value: TSESTree.JSXExpressionContainer\n): string[] {\n const expr = value.expression;\n\n // Handle style={{ prop: value }}\n if (expr.type === \"ObjectExpression\") {\n return expr.properties\n .filter((prop): prop is TSESTree.Property => prop.type === \"Property\")\n .map((prop) => {\n if (prop.key.type === \"Identifier\") {\n return prop.key.name;\n }\n if (prop.key.type === \"Literal\" && typeof prop.key.value === \"string\") {\n return prop.key.value;\n }\n return \"\";\n })\n .filter(Boolean);\n }\n\n // For style={variable} or style={{...spread}}, we can't determine properties\n return [];\n}\n\n/**\n * Check if all style properties are in the allowed list\n */\nfunction hasOnlyAllowedProperties(\n styleProperties: string[],\n allowedProperties: string[]\n): boolean {\n if (allowedProperties.length === 0 || styleProperties.length === 0) {\n return false;\n }\n\n return styleProperties.every((prop) => allowedProperties.includes(prop));\n}\n\ninterface ElementInfo {\n node: TSESTree.JSXOpeningElement;\n hasStyle: boolean;\n hasClassName: boolean;\n styleProperties: string[];\n}\n\n/**\n * Tailwind color names that should use semantic alternatives\n * Excludes neutral colors that are often acceptable\n */\nconst HARD_CODED_COLOR_NAMES = [\n \"red\",\n \"orange\",\n \"amber\",\n \"yellow\",\n \"lime\",\n \"green\",\n \"emerald\",\n \"teal\",\n \"cyan\",\n \"sky\",\n \"blue\",\n \"indigo\",\n \"violet\",\n \"purple\",\n \"fuchsia\",\n \"pink\",\n \"rose\",\n \"slate\",\n \"gray\",\n \"zinc\",\n \"neutral\",\n \"stone\",\n];\n\n/**\n * Colors that are always allowed (not theme-dependent)\n */\nconst ALWAYS_ALLOWED_COLORS = [\n \"white\",\n \"black\",\n \"transparent\",\n \"inherit\",\n \"current\",\n];\n\n/**\n * Regex to match hard-coded Tailwind color classes\n * Matches patterns like: bg-red-500, text-blue-600/50, hover:bg-green-400, dark:text-slate-100\n * Color utilities: bg, text, border, ring, outline, decoration, accent, fill, stroke,\n * from, via, to (gradients), divide, placeholder, caret, shadow\n */\nfunction createHardCodedColorRegex(colorNames: string[]): RegExp {\n const colorPattern = colorNames.join(\"|\");\n // Match color utilities with color-shade pattern, optional opacity, with optional variant prefixes\n return new RegExp(\n `(?:^|\\\\s)(?:[a-z-]+:)*(?:bg|text|border|ring|outline|decoration|accent|fill|stroke|from|via|to|divide|placeholder|caret|shadow)-(${colorPattern})-\\\\d{1,3}(?:/\\\\d{1,3})?(?=\\\\s|$)`,\n \"g\"\n );\n}\n\n/**\n * Extract className value from a JSX attribute\n */\nfunction getClassNameValue(attr: TSESTree.JSXAttribute): string | null {\n if (!attr.value) return null;\n\n // className=\"...\"\n if (attr.value.type === \"Literal\" && typeof attr.value.value === \"string\") {\n return attr.value.value;\n }\n\n // className={\"...\"}\n if (\n attr.value.type === \"JSXExpressionContainer\" &&\n attr.value.expression.type === \"Literal\" &&\n typeof attr.value.expression.value === \"string\"\n ) {\n return attr.value.expression.value;\n }\n\n // className={`...`}\n if (\n attr.value.type === \"JSXExpressionContainer\" &&\n attr.value.expression.type === \"TemplateLiteral\"\n ) {\n // Extract static parts of template literal\n return attr.value.expression.quasis.map((q) => q.value.raw).join(\" \");\n }\n\n return null;\n}\n\n/**\n * Check if a className string contains hard-coded color classes\n */\nfunction findHardCodedColors(\n className: string,\n allowedColors: string[]\n): string[] {\n const disallowedColorNames = HARD_CODED_COLOR_NAMES.filter(\n (c) => !allowedColors.includes(c)\n );\n\n if (disallowedColorNames.length === 0) return [];\n\n const regex = createHardCodedColorRegex(disallowedColorNames);\n const matches: string[] = [];\n let match;\n\n while ((match = regex.exec(className)) !== null) {\n matches.push(match[0].trim());\n }\n\n return matches;\n}\n\nexport default createRule<Options, MessageIds>({\n name: \"prefer-tailwind\",\n meta: {\n type: \"suggestion\",\n docs: {\n description: \"Encourage Tailwind className over inline style attributes\",\n },\n messages: {\n preferTailwind:\n \"Prefer Tailwind className over inline style. This element uses style attribute without className.\",\n preferSemanticColors:\n \"Prefer semantic color classes (e.g., bg-destructive, text-primary) over hard-coded colors (e.g., bg-red-500).\",\n },\n schema: [\n {\n type: \"object\",\n properties: {\n styleRatioThreshold: {\n type: \"number\",\n minimum: 0,\n maximum: 1,\n description:\n \"Minimum ratio of style-only elements to trigger warnings\",\n },\n minElementsForAnalysis: {\n type: \"number\",\n minimum: 1,\n description: \"Minimum styled elements required for analysis\",\n },\n allowedStyleProperties: {\n type: \"array\",\n items: { type: \"string\" },\n description: \"Style properties to ignore\",\n },\n ignoreComponents: {\n type: \"array\",\n items: { type: \"string\" },\n description: \"Component names to skip\",\n },\n preferSemanticColors: {\n type: \"boolean\",\n description:\n \"Warn against hard-coded colors in favor of semantic theme colors\",\n },\n allowedHardCodedColors: {\n type: \"array\",\n items: { type: \"string\" },\n description:\n \"Hard-coded color names to allow when preferSemanticColors is enabled\",\n },\n },\n additionalProperties: false,\n },\n ],\n },\n defaultOptions: [\n {\n styleRatioThreshold: 0.3,\n minElementsForAnalysis: 3,\n allowedStyleProperties: [],\n ignoreComponents: [],\n preferSemanticColors: false,\n allowedHardCodedColors: [],\n },\n ],\n create(context) {\n const options = context.options[0] || {};\n const styleRatioThreshold = options.styleRatioThreshold ?? 0.3;\n const minElementsForAnalysis = options.minElementsForAnalysis ?? 3;\n const allowedStyleProperties = options.allowedStyleProperties ?? [];\n const ignoreComponents = options.ignoreComponents ?? [];\n const preferSemanticColors = options.preferSemanticColors ?? false;\n const allowedHardCodedColors = options.allowedHardCodedColors ?? [];\n\n // Tracking state for file-level analysis\n const styledElements: ElementInfo[] = [];\n\n /**\n * Check if a JSXAttribute is a style attribute with an expression\n */\n function isStyleAttribute(attr: TSESTree.JSXAttribute): boolean {\n return (\n attr.name.type === \"JSXIdentifier\" &&\n attr.name.name === \"style\" &&\n attr.value?.type === \"JSXExpressionContainer\"\n );\n }\n\n /**\n * Check if a JSXAttribute is a className attribute\n */\n function isClassNameAttribute(attr: TSESTree.JSXAttribute): boolean {\n return (\n attr.name.type === \"JSXIdentifier\" &&\n (attr.name.name === \"className\" || attr.name.name === \"class\")\n );\n }\n\n return {\n JSXOpeningElement(node) {\n // Check if component should be ignored\n const componentName = getComponentName(node);\n if (ignoreComponents.includes(componentName)) {\n return;\n }\n\n let hasStyle = false;\n let hasClassName = false;\n let styleProperties: string[] = [];\n\n for (const attr of node.attributes) {\n if (attr.type === \"JSXAttribute\") {\n if (isStyleAttribute(attr)) {\n hasStyle = true;\n styleProperties = getStylePropertyNames(\n attr.value as TSESTree.JSXExpressionContainer\n );\n }\n if (isClassNameAttribute(attr)) {\n hasClassName = true;\n\n // Check for hard-coded colors if preferSemanticColors is enabled\n if (preferSemanticColors) {\n const classNameValue = getClassNameValue(attr);\n if (classNameValue) {\n const hardCodedColors = findHardCodedColors(\n classNameValue,\n allowedHardCodedColors\n );\n if (hardCodedColors.length > 0) {\n context.report({\n node,\n messageId: \"preferSemanticColors\",\n });\n }\n }\n }\n }\n }\n }\n\n // Only track elements that have style OR className (or both)\n if (hasStyle || hasClassName) {\n styledElements.push({\n node,\n hasStyle,\n hasClassName,\n styleProperties,\n });\n }\n },\n\n \"Program:exit\"() {\n // Don't analyze if not enough styled elements\n if (styledElements.length < minElementsForAnalysis) {\n return;\n }\n\n // Filter out elements where all style properties are allowed\n const styleOnlyElements = styledElements.filter((el) => {\n if (!el.hasStyle || el.hasClassName) {\n return false;\n }\n\n // If all style properties are in the allowed list, don't count this element\n if (\n hasOnlyAllowedProperties(el.styleProperties, allowedStyleProperties)\n ) {\n return false;\n }\n\n return true;\n });\n\n const ratio = styleOnlyElements.length / styledElements.length;\n\n // Only report if ratio exceeds threshold\n if (ratio > styleRatioThreshold) {\n for (const element of styleOnlyElements) {\n context.report({\n node: element.node,\n messageId: \"preferTailwind\",\n });\n }\n }\n },\n };\n },\n});\n"],"mappings":"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","names":["meta"]}
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/utils/create-rule.ts","../../src/rules/prefer-zustand-state-management.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 * External requirement that a rule needs to function\n */\nexport interface RuleRequirement {\n /** Requirement type for programmatic checks */\n type: \"ollama\" | \"git\" | \"coverage\" | \"semantic-index\" | \"styleguide\";\n /** Human-readable description */\n description: string;\n /** Optional: how to satisfy the requirement */\n setupHint?: string;\n}\n\n/**\n * Rule migration definition for updating rule options between versions\n */\nexport interface RuleMigration {\n /** Source version (semver) */\n from: string;\n /** Target version (semver) */\n to: string;\n /** Human-readable description of what changed */\n description: string;\n /** Function to migrate options from old format to new format */\n migrate: (oldOptions: unknown[]) => unknown[];\n /** Whether this migration contains breaking changes */\n breaking?: boolean;\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., \"consistent-dark-mode\") - must match filename */\n id: string;\n\n /** Semantic version of the rule (e.g., \"1.0.0\") */\n version: 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 /** Icon for display in CLI/UI (emoji or icon name) */\n icon?: string;\n\n /** Short hint about the rule type/requirements */\n hint?: string;\n\n /** Whether rule is enabled by default during install */\n defaultEnabled?: boolean;\n\n /** External requirements the rule needs */\n requirements?: RuleRequirement[];\n\n /** Instructions to show after installation */\n postInstallInstructions?: string;\n\n /** Framework compatibility */\n frameworks?: (\"next\" | \"vite\" | \"cra\" | \"remix\")[];\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 * Internal utility dependencies that this rule requires.\n * When the rule is copied to a target project, these utilities\n * will be transformed to import from \"uilint-eslint\" instead\n * of relative paths.\n *\n * Example: [\"coverage-aggregator\", \"dependency-graph\"]\n */\n internalDependencies?: string[];\n\n /**\n * Whether this rule is directory-based (has lib/ folder with utilities).\n * Directory-based rules are installed as folders with index.ts and lib/ subdirectory.\n * Single-file rules are installed as single .ts files.\n *\n * When true, ESLint config imports will use:\n * ./.uilint/rules/rule-id/index.js\n * When false (default):\n * ./.uilint/rules/rule-id.js\n */\n isDirectoryBased?: boolean;\n\n /**\n * Migrations for updating rule options between versions.\n * Migrations are applied in order to transform options from older versions.\n */\n migrations?: RuleMigration[];\n\n /**\n * Which UI plugin should handle this rule.\n * Defaults based on category:\n * - \"static\" category → \"eslint\" plugin\n * - \"semantic\" category → \"semantic\" plugin\n *\n * Special cases:\n * - \"vision\" for semantic-vision rule\n */\n plugin?: \"eslint\" | \"vision\" | \"semantic\";\n\n /**\n * Custom inspector panel ID to use for this rule's issues.\n * If not specified, uses the plugin's default issue inspector.\n *\n * Examples:\n * - \"vision-issue\" for VisionIssueInspector\n * - \"duplicates\" for DuplicatesInspector\n * - \"semantic-issue\" for SemanticIssueInspector\n */\n customInspector?: string;\n\n /**\n * Custom heatmap color for this rule's issues.\n * CSS color value (hex, rgb, hsl, or named color).\n * If not specified, uses severity-based coloring.\n */\n heatmapColor?: 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: prefer-zustand-state-management\n *\n * Detects excessive use of React state hooks (useState, useReducer, useContext)\n * in components and suggests using Zustand stores for better state management.\n */\n\nimport { createRule, defineRuleMeta } from \"../utils/create-rule.js\";\nimport type { TSESTree } from \"@typescript-eslint/utils\";\n\ntype MessageIds = \"excessiveStateHooks\";\ntype Options = [\n {\n /** Maximum number of state hooks before warning. Default: 3 */\n maxStateHooks?: number;\n /** Whether to count useState calls. Default: true */\n countUseState?: boolean;\n /** Whether to count useReducer calls. Default: true */\n countUseReducer?: boolean;\n /** Whether to count useContext calls. Default: true */\n countUseContext?: boolean;\n }?\n];\n\n/**\n * Rule metadata - colocated with implementation for maintainability\n */\nexport const meta = defineRuleMeta({\n id: \"prefer-zustand-state-management\",\n version: \"1.0.0\",\n name: \"Prefer Zustand State Management\",\n description: \"Detect excessive useState/useReducer/useContext; suggest Zustand\",\n defaultSeverity: \"warn\",\n category: \"static\",\n icon: \"🐻\",\n hint: \"Suggests centralized state management\",\n defaultEnabled: true,\n defaultOptions: [\n {\n maxStateHooks: 3,\n countUseState: true,\n countUseReducer: true,\n countUseContext: true,\n },\n ],\n optionSchema: {\n fields: [\n {\n key: \"maxStateHooks\",\n label: \"Max state hooks before warning\",\n type: \"number\",\n defaultValue: 3,\n placeholder: \"3\",\n description: \"Maximum number of state hooks allowed before warning\",\n },\n {\n key: \"countUseState\",\n label: \"Count useState hooks\",\n type: \"boolean\",\n defaultValue: true,\n },\n {\n key: \"countUseReducer\",\n label: \"Count useReducer hooks\",\n type: \"boolean\",\n defaultValue: true,\n },\n {\n key: \"countUseContext\",\n label: \"Count useContext hooks\",\n type: \"boolean\",\n defaultValue: true,\n },\n ],\n },\n docs: `\n## What it does\n\nDetects components that use many React state hooks (\\`useState\\`, \\`useReducer\\`, \\`useContext\\`)\nand suggests consolidating state into a Zustand store for better maintainability.\n\n## Why it's useful\n\n- **Simplifies components**: Fewer hooks means less cognitive overhead\n- **Centralizes state**: Related state lives together in a store\n- **Better performance**: Zustand's selector pattern prevents unnecessary re-renders\n- **Easier testing**: Store logic can be tested independently of components\n\n## Examples\n\n### ❌ Incorrect (with default maxStateHooks: 3)\n\n\\`\\`\\`tsx\nfunction UserProfile() {\n const [name, setName] = useState('');\n const [email, setEmail] = useState('');\n const [avatar, setAvatar] = useState(null);\n const [isLoading, setIsLoading] = useState(false); // 4 hooks = warning\n // ...\n}\n\\`\\`\\`\n\n### ✅ Correct\n\n\\`\\`\\`tsx\n// Using a Zustand store\nconst useUserStore = create((set) => ({\n name: '',\n email: '',\n avatar: null,\n isLoading: false,\n setName: (name) => set({ name }),\n // ...\n}));\n\nfunction UserProfile() {\n const { name, email, avatar, isLoading } = useUserStore();\n // Much cleaner!\n}\n\\`\\`\\`\n\n## Configuration\n\n\\`\\`\\`js\n// eslint.config.js\n\"uilint/prefer-zustand-state-management\": [\"warn\", {\n maxStateHooks: 3, // Warn when exceeding this count\n countUseState: true, // Include useState in count\n countUseReducer: true, // Include useReducer in count\n countUseContext: true // Include useContext in count\n}]\n\\`\\`\\`\n\n## Notes\n\n- Custom hooks (starting with \\`use\\` lowercase) are exempt from this rule\n- Only counts hooks at the top level of the component, not in nested functions\n- Adjust \\`maxStateHooks\\` based on your team's preferences\n`,\n});\n\ninterface ComponentInfo {\n name: string;\n node: TSESTree.Node;\n hookCount: number;\n functionNode:\n | TSESTree.FunctionDeclaration\n | TSESTree.FunctionExpression\n | TSESTree.ArrowFunctionExpression;\n}\n\nconst STATE_HOOKS = new Set([\"useState\", \"useReducer\", \"useContext\"]);\n\nexport default createRule<Options, MessageIds>({\n name: \"prefer-zustand-state-management\",\n meta: {\n type: \"suggestion\",\n docs: {\n description:\n \"Detect excessive use of React state hooks and suggest Zustand stores\",\n },\n messages: {\n excessiveStateHooks:\n \"Component '{{component}}' has {{count}} state hooks (max: {{max}}). Consider using a Zustand store for state management.\",\n },\n schema: [\n {\n type: \"object\",\n properties: {\n maxStateHooks: {\n type: \"number\",\n minimum: 1,\n description: \"Maximum number of state hooks before warning\",\n },\n countUseState: {\n type: \"boolean\",\n description: \"Whether to count useState calls\",\n },\n countUseReducer: {\n type: \"boolean\",\n description: \"Whether to count useReducer calls\",\n },\n countUseContext: {\n type: \"boolean\",\n description: \"Whether to count useContext calls\",\n },\n },\n additionalProperties: false,\n },\n ],\n },\n defaultOptions: [\n {\n maxStateHooks: 3,\n countUseState: true,\n countUseReducer: true,\n countUseContext: true,\n },\n ],\n create(context) {\n const options = context.options[0] || {};\n const maxStateHooks = options.maxStateHooks ?? 3;\n const countUseState = options.countUseState ?? true;\n const countUseReducer = options.countUseReducer ?? true;\n const countUseContext = options.countUseContext ?? true;\n\n // Stack to track current component context\n const componentStack: ComponentInfo[] = [];\n\n // Set of function nodes we've identified as components to check\n const componentFunctions = new Map<TSESTree.Node, ComponentInfo>();\n\n /**\n * Check if a function name indicates a React component (PascalCase)\n */\n function isComponentName(name: string | null | undefined): boolean {\n if (!name) return false;\n // Components start with uppercase, hooks start with lowercase \"use\"\n return /^[A-Z]/.test(name);\n }\n\n /**\n * Check if a function name indicates a custom hook (starts with \"use\")\n */\n function isCustomHookName(name: string | null | undefined): boolean {\n if (!name) return false;\n return /^use[A-Z]/.test(name);\n }\n\n /**\n * Get the name of a function from various declaration patterns\n */\n function getFunctionName(\n node:\n | TSESTree.FunctionDeclaration\n | TSESTree.FunctionExpression\n | TSESTree.ArrowFunctionExpression\n ): string | null {\n // Function declaration: function MyComponent() {}\n if (node.type === \"FunctionDeclaration\" && node.id) {\n return node.id.name;\n }\n\n // Check parent for variable declaration: const MyComponent = () => {}\n const parent = node.parent;\n\n if (\n parent?.type === \"VariableDeclarator\" &&\n parent.id.type === \"Identifier\"\n ) {\n return parent.id.name;\n }\n\n // Check for forwardRef/memo: const MyComponent = forwardRef(function MyComponent() {})\n // or const MyComponent = forwardRef(() => {})\n if (parent?.type === \"CallExpression\") {\n const callParent = parent.parent;\n if (\n callParent?.type === \"VariableDeclarator\" &&\n callParent.id.type === \"Identifier\"\n ) {\n return callParent.id.name;\n }\n }\n\n // Named function expression: const x = function MyComponent() {}\n if (node.type === \"FunctionExpression\" && node.id) {\n return node.id.name;\n }\n\n return null;\n }\n\n /**\n * Check if a hook call should be counted based on options\n */\n function shouldCountHook(hookName: string): boolean {\n switch (hookName) {\n case \"useState\":\n return countUseState;\n case \"useReducer\":\n return countUseReducer;\n case \"useContext\":\n return countUseContext;\n default:\n return false;\n }\n }\n\n /**\n * Get hook name from a call expression (handles both useState and React.useState)\n */\n function getHookName(callee: TSESTree.Expression): string | null {\n // Direct call: useState()\n if (callee.type === \"Identifier\" && STATE_HOOKS.has(callee.name)) {\n return callee.name;\n }\n\n // Member expression: React.useState()\n if (\n callee.type === \"MemberExpression\" &&\n callee.object.type === \"Identifier\" &&\n callee.object.name === \"React\" &&\n callee.property.type === \"Identifier\" &&\n STATE_HOOKS.has(callee.property.name)\n ) {\n return callee.property.name;\n }\n\n return null;\n }\n\n /**\n * Check if we're directly inside a component function (not in a nested function)\n */\n function isDirectChildOfComponent(\n node: TSESTree.Node,\n componentNode: TSESTree.Node\n ): boolean {\n let current: TSESTree.Node | undefined = node.parent;\n\n while (current) {\n // If we hit the component node, we're a direct child\n if (current === componentNode) {\n return true;\n }\n\n // If we hit another function first, we're nested\n if (\n current.type === \"FunctionDeclaration\" ||\n current.type === \"FunctionExpression\" ||\n current.type === \"ArrowFunctionExpression\"\n ) {\n return false;\n }\n\n current = current.parent;\n }\n\n return false;\n }\n\n /**\n * Enter a function that might be a component\n */\n function enterFunction(\n node:\n | TSESTree.FunctionDeclaration\n | TSESTree.FunctionExpression\n | TSESTree.ArrowFunctionExpression\n ) {\n const name = getFunctionName(node);\n\n // Skip custom hooks - they're allowed to have many state hooks\n if (isCustomHookName(name)) {\n return;\n }\n\n // Skip non-component functions (lowercase names that aren't anonymous)\n if (name && !isComponentName(name)) {\n return;\n }\n\n // Track this as a potential component\n const componentInfo: ComponentInfo = {\n name: name || \"AnonymousComponent\",\n node,\n hookCount: 0,\n functionNode: node,\n };\n\n componentStack.push(componentInfo);\n componentFunctions.set(node, componentInfo);\n }\n\n /**\n * Exit a function and report if it had too many hooks\n */\n function exitFunction(\n node:\n | TSESTree.FunctionDeclaration\n | TSESTree.FunctionExpression\n | TSESTree.ArrowFunctionExpression\n ) {\n const componentInfo = componentFunctions.get(node);\n\n if (!componentInfo) {\n return;\n }\n\n // Remove from stack\n const index = componentStack.findIndex((c) => c.functionNode === node);\n if (index !== -1) {\n componentStack.splice(index, 1);\n }\n\n // Check if exceeded threshold\n if (componentInfo.hookCount > maxStateHooks) {\n context.report({\n node: componentInfo.node,\n messageId: \"excessiveStateHooks\",\n data: {\n component: componentInfo.name,\n count: componentInfo.hookCount,\n max: maxStateHooks,\n },\n });\n }\n\n componentFunctions.delete(node);\n }\n\n return {\n FunctionDeclaration: enterFunction,\n FunctionExpression: enterFunction,\n ArrowFunctionExpression: enterFunction,\n \"FunctionDeclaration:exit\": exitFunction,\n \"FunctionExpression:exit\": exitFunction,\n \"ArrowFunctionExpression:exit\": exitFunction,\n\n CallExpression(node) {\n const hookName = getHookName(node.callee);\n\n if (!hookName || !shouldCountHook(hookName)) {\n return;\n }\n\n // Find the innermost component this hook belongs to\n // We iterate from the end to find the most recent (innermost) component\n for (let i = componentStack.length - 1; i >= 0; i--) {\n const component = componentStack[i];\n\n if (isDirectChildOfComponent(node, component.functionNode)) {\n component.hookCount++;\n break;\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/prefer-zustand-state-management.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 * External requirement that a rule needs to function\n */\nexport interface RuleRequirement {\n /** Requirement type for programmatic checks */\n type: \"ollama\" | \"git\" | \"coverage\" | \"semantic-index\" | \"styleguide\";\n /** Human-readable description */\n description: string;\n /** Optional: how to satisfy the requirement */\n setupHint?: string;\n}\n\n/**\n * Rule migration definition for updating rule options between versions\n */\nexport interface RuleMigration {\n /** Source version (semver) */\n from: string;\n /** Target version (semver) */\n to: string;\n /** Human-readable description of what changed */\n description: string;\n /** Function to migrate options from old format to new format */\n migrate: (oldOptions: unknown[]) => unknown[];\n /** Whether this migration contains breaking changes */\n breaking?: boolean;\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., \"consistent-dark-mode\") - must match filename */\n id: string;\n\n /** Semantic version of the rule (e.g., \"1.0.0\") */\n version: 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 /** Icon for display in CLI/UI (emoji or icon name) */\n icon?: string;\n\n /** Short hint about the rule type/requirements */\n hint?: string;\n\n /** Whether rule is enabled by default during install */\n defaultEnabled?: boolean;\n\n /** External requirements the rule needs */\n requirements?: RuleRequirement[];\n\n /**\n * NPM packages that must be installed for this rule to work.\n * These will be added to the target project's dependencies during installation.\n *\n * Example: [\"xxhash-wasm\"] for rules using the xxhash library\n */\n npmDependencies?: string[];\n\n /** Instructions to show after installation */\n postInstallInstructions?: string;\n\n /** Framework compatibility */\n frameworks?: (\"next\" | \"vite\" | \"cra\" | \"remix\")[];\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 * Internal utility dependencies that this rule requires.\n * When the rule is copied to a target project, these utilities\n * will be transformed to import from \"uilint-eslint\" instead\n * of relative paths.\n *\n * Example: [\"coverage-aggregator\", \"dependency-graph\"]\n */\n internalDependencies?: string[];\n\n /**\n * Whether this rule is directory-based (has lib/ folder with utilities).\n * Directory-based rules are installed as folders with index.ts and lib/ subdirectory.\n * Single-file rules are installed as single .ts files.\n *\n * When true, ESLint config imports will use:\n * ./.uilint/rules/rule-id/index.js\n * When false (default):\n * ./.uilint/rules/rule-id.js\n */\n isDirectoryBased?: boolean;\n\n /**\n * Migrations for updating rule options between versions.\n * Migrations are applied in order to transform options from older versions.\n */\n migrations?: RuleMigration[];\n\n /**\n * Which UI plugin should handle this rule.\n * Defaults based on category:\n * - \"static\" category → \"eslint\" plugin\n * - \"semantic\" category → \"semantic\" plugin\n *\n * Special cases:\n * - \"vision\" for semantic-vision rule\n */\n plugin?: \"eslint\" | \"vision\" | \"semantic\";\n\n /**\n * Custom inspector panel ID to use for this rule's issues.\n * If not specified, uses the plugin's default issue inspector.\n *\n * Examples:\n * - \"vision-issue\" for VisionIssueInspector\n * - \"duplicates\" for DuplicatesInspector\n * - \"semantic-issue\" for SemanticIssueInspector\n */\n customInspector?: string;\n\n /**\n * Custom heatmap color for this rule's issues.\n * CSS color value (hex, rgb, hsl, or named color).\n * If not specified, uses severity-based coloring.\n */\n heatmapColor?: 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: prefer-zustand-state-management\n *\n * Detects excessive use of React state hooks (useState, useReducer, useContext)\n * in components and suggests using Zustand stores for better state management.\n */\n\nimport { createRule, defineRuleMeta } from \"../utils/create-rule.js\";\nimport type { TSESTree } from \"@typescript-eslint/utils\";\n\ntype MessageIds = \"excessiveStateHooks\";\ntype Options = [\n {\n /** Maximum number of state hooks before warning. Default: 3 */\n maxStateHooks?: number;\n /** Whether to count useState calls. Default: true */\n countUseState?: boolean;\n /** Whether to count useReducer calls. Default: true */\n countUseReducer?: boolean;\n /** Whether to count useContext calls. Default: true */\n countUseContext?: boolean;\n }?\n];\n\n/**\n * Rule metadata - colocated with implementation for maintainability\n */\nexport const meta = defineRuleMeta({\n id: \"prefer-zustand-state-management\",\n version: \"1.0.0\",\n name: \"Prefer Zustand State Management\",\n description: \"Detect excessive useState/useReducer/useContext; suggest Zustand\",\n defaultSeverity: \"warn\",\n category: \"static\",\n icon: \"🐻\",\n hint: \"Suggests centralized state management\",\n defaultEnabled: true,\n defaultOptions: [\n {\n maxStateHooks: 3,\n countUseState: true,\n countUseReducer: true,\n countUseContext: true,\n },\n ],\n optionSchema: {\n fields: [\n {\n key: \"maxStateHooks\",\n label: \"Max state hooks before warning\",\n type: \"number\",\n defaultValue: 3,\n placeholder: \"3\",\n description: \"Maximum number of state hooks allowed before warning\",\n },\n {\n key: \"countUseState\",\n label: \"Count useState hooks\",\n type: \"boolean\",\n defaultValue: true,\n },\n {\n key: \"countUseReducer\",\n label: \"Count useReducer hooks\",\n type: \"boolean\",\n defaultValue: true,\n },\n {\n key: \"countUseContext\",\n label: \"Count useContext hooks\",\n type: \"boolean\",\n defaultValue: true,\n },\n ],\n },\n docs: `\n## What it does\n\nDetects components that use many React state hooks (\\`useState\\`, \\`useReducer\\`, \\`useContext\\`)\nand suggests consolidating state into a Zustand store for better maintainability.\n\n## Why it's useful\n\n- **Simplifies components**: Fewer hooks means less cognitive overhead\n- **Centralizes state**: Related state lives together in a store\n- **Better performance**: Zustand's selector pattern prevents unnecessary re-renders\n- **Easier testing**: Store logic can be tested independently of components\n\n## Examples\n\n### ❌ Incorrect (with default maxStateHooks: 3)\n\n\\`\\`\\`tsx\nfunction UserProfile() {\n const [name, setName] = useState('');\n const [email, setEmail] = useState('');\n const [avatar, setAvatar] = useState(null);\n const [isLoading, setIsLoading] = useState(false); // 4 hooks = warning\n // ...\n}\n\\`\\`\\`\n\n### ✅ Correct\n\n\\`\\`\\`tsx\n// Using a Zustand store\nconst useUserStore = create((set) => ({\n name: '',\n email: '',\n avatar: null,\n isLoading: false,\n setName: (name) => set({ name }),\n // ...\n}));\n\nfunction UserProfile() {\n const { name, email, avatar, isLoading } = useUserStore();\n // Much cleaner!\n}\n\\`\\`\\`\n\n## Configuration\n\n\\`\\`\\`js\n// eslint.config.js\n\"uilint/prefer-zustand-state-management\": [\"warn\", {\n maxStateHooks: 3, // Warn when exceeding this count\n countUseState: true, // Include useState in count\n countUseReducer: true, // Include useReducer in count\n countUseContext: true // Include useContext in count\n}]\n\\`\\`\\`\n\n## Notes\n\n- Custom hooks (starting with \\`use\\` lowercase) are exempt from this rule\n- Only counts hooks at the top level of the component, not in nested functions\n- Adjust \\`maxStateHooks\\` based on your team's preferences\n`,\n});\n\ninterface ComponentInfo {\n name: string;\n node: TSESTree.Node;\n hookCount: number;\n functionNode:\n | TSESTree.FunctionDeclaration\n | TSESTree.FunctionExpression\n | TSESTree.ArrowFunctionExpression;\n}\n\nconst STATE_HOOKS = new Set([\"useState\", \"useReducer\", \"useContext\"]);\n\nexport default createRule<Options, MessageIds>({\n name: \"prefer-zustand-state-management\",\n meta: {\n type: \"suggestion\",\n docs: {\n description:\n \"Detect excessive use of React state hooks and suggest Zustand stores\",\n },\n messages: {\n excessiveStateHooks:\n \"Component '{{component}}' has {{count}} state hooks (max: {{max}}). Consider using a Zustand store for state management.\",\n },\n schema: [\n {\n type: \"object\",\n properties: {\n maxStateHooks: {\n type: \"number\",\n minimum: 1,\n description: \"Maximum number of state hooks before warning\",\n },\n countUseState: {\n type: \"boolean\",\n description: \"Whether to count useState calls\",\n },\n countUseReducer: {\n type: \"boolean\",\n description: \"Whether to count useReducer calls\",\n },\n countUseContext: {\n type: \"boolean\",\n description: \"Whether to count useContext calls\",\n },\n },\n additionalProperties: false,\n },\n ],\n },\n defaultOptions: [\n {\n maxStateHooks: 3,\n countUseState: true,\n countUseReducer: true,\n countUseContext: true,\n },\n ],\n create(context) {\n const options = context.options[0] || {};\n const maxStateHooks = options.maxStateHooks ?? 3;\n const countUseState = options.countUseState ?? true;\n const countUseReducer = options.countUseReducer ?? true;\n const countUseContext = options.countUseContext ?? true;\n\n // Stack to track current component context\n const componentStack: ComponentInfo[] = [];\n\n // Set of function nodes we've identified as components to check\n const componentFunctions = new Map<TSESTree.Node, ComponentInfo>();\n\n /**\n * Check if a function name indicates a React component (PascalCase)\n */\n function isComponentName(name: string | null | undefined): boolean {\n if (!name) return false;\n // Components start with uppercase, hooks start with lowercase \"use\"\n return /^[A-Z]/.test(name);\n }\n\n /**\n * Check if a function name indicates a custom hook (starts with \"use\")\n */\n function isCustomHookName(name: string | null | undefined): boolean {\n if (!name) return false;\n return /^use[A-Z]/.test(name);\n }\n\n /**\n * Get the name of a function from various declaration patterns\n */\n function getFunctionName(\n node:\n | TSESTree.FunctionDeclaration\n | TSESTree.FunctionExpression\n | TSESTree.ArrowFunctionExpression\n ): string | null {\n // Function declaration: function MyComponent() {}\n if (node.type === \"FunctionDeclaration\" && node.id) {\n return node.id.name;\n }\n\n // Check parent for variable declaration: const MyComponent = () => {}\n const parent = node.parent;\n\n if (\n parent?.type === \"VariableDeclarator\" &&\n parent.id.type === \"Identifier\"\n ) {\n return parent.id.name;\n }\n\n // Check for forwardRef/memo: const MyComponent = forwardRef(function MyComponent() {})\n // or const MyComponent = forwardRef(() => {})\n if (parent?.type === \"CallExpression\") {\n const callParent = parent.parent;\n if (\n callParent?.type === \"VariableDeclarator\" &&\n callParent.id.type === \"Identifier\"\n ) {\n return callParent.id.name;\n }\n }\n\n // Named function expression: const x = function MyComponent() {}\n if (node.type === \"FunctionExpression\" && node.id) {\n return node.id.name;\n }\n\n return null;\n }\n\n /**\n * Check if a hook call should be counted based on options\n */\n function shouldCountHook(hookName: string): boolean {\n switch (hookName) {\n case \"useState\":\n return countUseState;\n case \"useReducer\":\n return countUseReducer;\n case \"useContext\":\n return countUseContext;\n default:\n return false;\n }\n }\n\n /**\n * Get hook name from a call expression (handles both useState and React.useState)\n */\n function getHookName(callee: TSESTree.Expression): string | null {\n // Direct call: useState()\n if (callee.type === \"Identifier\" && STATE_HOOKS.has(callee.name)) {\n return callee.name;\n }\n\n // Member expression: React.useState()\n if (\n callee.type === \"MemberExpression\" &&\n callee.object.type === \"Identifier\" &&\n callee.object.name === \"React\" &&\n callee.property.type === \"Identifier\" &&\n STATE_HOOKS.has(callee.property.name)\n ) {\n return callee.property.name;\n }\n\n return null;\n }\n\n /**\n * Check if we're directly inside a component function (not in a nested function)\n */\n function isDirectChildOfComponent(\n node: TSESTree.Node,\n componentNode: TSESTree.Node\n ): boolean {\n let current: TSESTree.Node | undefined = node.parent;\n\n while (current) {\n // If we hit the component node, we're a direct child\n if (current === componentNode) {\n return true;\n }\n\n // If we hit another function first, we're nested\n if (\n current.type === \"FunctionDeclaration\" ||\n current.type === \"FunctionExpression\" ||\n current.type === \"ArrowFunctionExpression\"\n ) {\n return false;\n }\n\n current = current.parent;\n }\n\n return false;\n }\n\n /**\n * Enter a function that might be a component\n */\n function enterFunction(\n node:\n | TSESTree.FunctionDeclaration\n | TSESTree.FunctionExpression\n | TSESTree.ArrowFunctionExpression\n ) {\n const name = getFunctionName(node);\n\n // Skip custom hooks - they're allowed to have many state hooks\n if (isCustomHookName(name)) {\n return;\n }\n\n // Skip non-component functions (lowercase names that aren't anonymous)\n if (name && !isComponentName(name)) {\n return;\n }\n\n // Track this as a potential component\n const componentInfo: ComponentInfo = {\n name: name || \"AnonymousComponent\",\n node,\n hookCount: 0,\n functionNode: node,\n };\n\n componentStack.push(componentInfo);\n componentFunctions.set(node, componentInfo);\n }\n\n /**\n * Exit a function and report if it had too many hooks\n */\n function exitFunction(\n node:\n | TSESTree.FunctionDeclaration\n | TSESTree.FunctionExpression\n | TSESTree.ArrowFunctionExpression\n ) {\n const componentInfo = componentFunctions.get(node);\n\n if (!componentInfo) {\n return;\n }\n\n // Remove from stack\n const index = componentStack.findIndex((c) => c.functionNode === node);\n if (index !== -1) {\n componentStack.splice(index, 1);\n }\n\n // Check if exceeded threshold\n if (componentInfo.hookCount > maxStateHooks) {\n context.report({\n node: componentInfo.node,\n messageId: \"excessiveStateHooks\",\n data: {\n component: componentInfo.name,\n count: componentInfo.hookCount,\n max: maxStateHooks,\n },\n });\n }\n\n componentFunctions.delete(node);\n }\n\n return {\n FunctionDeclaration: enterFunction,\n FunctionExpression: enterFunction,\n ArrowFunctionExpression: enterFunction,\n \"FunctionDeclaration:exit\": exitFunction,\n \"FunctionExpression:exit\": exitFunction,\n \"ArrowFunctionExpression:exit\": exitFunction,\n\n CallExpression(node) {\n const hookName = getHookName(node.callee);\n\n if (!hookName || !shouldCountHook(hookName)) {\n return;\n }\n\n // Find the innermost component this hook belongs to\n // We iterate from the end to find the most recent (innermost) component\n for (let i = componentStack.length - 1; i >= 0; i--) {\n const component = componentStack[i];\n\n if (isDirectChildOfComponent(node, component.functionNode)) {\n component.hookCount++;\n break;\n }\n }\n },\n };\n },\n});\n"],"mappings":"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","names":["meta"]}
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/utils/create-rule.ts","../../src/rules/require-input-validation.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 * External requirement that a rule needs to function\n */\nexport interface RuleRequirement {\n /** Requirement type for programmatic checks */\n type: \"ollama\" | \"git\" | \"coverage\" | \"semantic-index\" | \"styleguide\";\n /** Human-readable description */\n description: string;\n /** Optional: how to satisfy the requirement */\n setupHint?: string;\n}\n\n/**\n * Rule migration definition for updating rule options between versions\n */\nexport interface RuleMigration {\n /** Source version (semver) */\n from: string;\n /** Target version (semver) */\n to: string;\n /** Human-readable description of what changed */\n description: string;\n /** Function to migrate options from old format to new format */\n migrate: (oldOptions: unknown[]) => unknown[];\n /** Whether this migration contains breaking changes */\n breaking?: boolean;\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., \"consistent-dark-mode\") - must match filename */\n id: string;\n\n /** Semantic version of the rule (e.g., \"1.0.0\") */\n version: 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 /** Icon for display in CLI/UI (emoji or icon name) */\n icon?: string;\n\n /** Short hint about the rule type/requirements */\n hint?: string;\n\n /** Whether rule is enabled by default during install */\n defaultEnabled?: boolean;\n\n /** External requirements the rule needs */\n requirements?: RuleRequirement[];\n\n /** Instructions to show after installation */\n postInstallInstructions?: string;\n\n /** Framework compatibility */\n frameworks?: (\"next\" | \"vite\" | \"cra\" | \"remix\")[];\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 * Internal utility dependencies that this rule requires.\n * When the rule is copied to a target project, these utilities\n * will be transformed to import from \"uilint-eslint\" instead\n * of relative paths.\n *\n * Example: [\"coverage-aggregator\", \"dependency-graph\"]\n */\n internalDependencies?: string[];\n\n /**\n * Whether this rule is directory-based (has lib/ folder with utilities).\n * Directory-based rules are installed as folders with index.ts and lib/ subdirectory.\n * Single-file rules are installed as single .ts files.\n *\n * When true, ESLint config imports will use:\n * ./.uilint/rules/rule-id/index.js\n * When false (default):\n * ./.uilint/rules/rule-id.js\n */\n isDirectoryBased?: boolean;\n\n /**\n * Migrations for updating rule options between versions.\n * Migrations are applied in order to transform options from older versions.\n */\n migrations?: RuleMigration[];\n\n /**\n * Which UI plugin should handle this rule.\n * Defaults based on category:\n * - \"static\" category → \"eslint\" plugin\n * - \"semantic\" category → \"semantic\" plugin\n *\n * Special cases:\n * - \"vision\" for semantic-vision rule\n */\n plugin?: \"eslint\" | \"vision\" | \"semantic\";\n\n /**\n * Custom inspector panel ID to use for this rule's issues.\n * If not specified, uses the plugin's default issue inspector.\n *\n * Examples:\n * - \"vision-issue\" for VisionIssueInspector\n * - \"duplicates\" for DuplicatesInspector\n * - \"semantic-issue\" for SemanticIssueInspector\n */\n customInspector?: string;\n\n /**\n * Custom heatmap color for this rule's issues.\n * CSS color value (hex, rgb, hsl, or named color).\n * If not specified, uses severity-based coloring.\n */\n heatmapColor?: 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: require-input-validation\n *\n * Requires API route handlers to validate request body using schema validation\n * libraries like Zod, Yup, or Joi before accessing request data.\n *\n * Examples:\n * - Bad: const { name } = await req.json()\n * - Good: const data = schema.parse(await req.json())\n */\n\nimport { createRule, defineRuleMeta } from \"../utils/create-rule.js\";\nimport type { TSESTree } from \"@typescript-eslint/utils\";\n\ntype MessageIds = \"missingValidation\" | \"unvalidatedBodyAccess\";\ntype Options = [\n {\n /** HTTP methods that require validation (default: POST, PUT, PATCH, DELETE) */\n httpMethods?: string[];\n /** File patterns that indicate API routes */\n routePatterns?: string[];\n /** Allow manual type guards/if-checks as validation */\n allowManualValidation?: boolean;\n }\n];\n\n/**\n * Rule metadata - colocated with implementation for maintainability\n */\nexport const meta = defineRuleMeta({\n id: \"require-input-validation\",\n version: \"1.0.0\",\n name: \"Require Input Validation\",\n description: \"Require schema validation in API route handlers\",\n defaultSeverity: \"warn\",\n category: \"static\",\n icon: \"✅\",\n hint: \"Enforces input validation in APIs\",\n defaultEnabled: true,\n defaultOptions: [\n {\n httpMethods: [\"POST\", \"PUT\", \"PATCH\", \"DELETE\"],\n routePatterns: [\"route.ts\", \"route.tsx\", \"/api/\", \"/app/api/\"],\n allowManualValidation: false,\n },\n ],\n optionSchema: {\n fields: [\n {\n key: \"httpMethods\",\n label: \"HTTP methods requiring validation\",\n type: \"multiselect\",\n defaultValue: [\"POST\", \"PUT\", \"PATCH\", \"DELETE\"],\n options: [\n { value: \"GET\", label: \"GET\" },\n { value: \"POST\", label: \"POST\" },\n { value: \"PUT\", label: \"PUT\" },\n { value: \"PATCH\", label: \"PATCH\" },\n { value: \"DELETE\", label: \"DELETE\" },\n ],\n description: \"HTTP methods that require request body validation\",\n },\n {\n key: \"allowManualValidation\",\n label: \"Allow manual validation\",\n type: \"boolean\",\n defaultValue: false,\n description: \"Allow if-checks and type guards instead of schema validation\",\n },\n ],\n },\n docs: `\n## What it does\n\nEnsures that API route handlers validate request body data using a schema\nvalidation library (Zod, Yup, Joi, etc.) before accessing it.\n\n## Why it's useful\n\n- **Security**: Prevents injection attacks and malformed data\n- **Type Safety**: Ensures runtime data matches expected types\n- **Error Handling**: Provides clear validation error messages\n- **Best Practice**: Follows defense-in-depth principles\n\n## Supported Validation Libraries\n\n- Zod: \\`parse()\\`, \\`safeParse()\\`, \\`parseAsync()\\`\n- Yup: \\`validate()\\`, \\`validateSync()\\`\n- Joi: \\`validate()\\`\n- Superstruct: \\`create()\\`, \\`assert()\\`\n- io-ts: \\`decode()\\`\n- Valibot: \\`parse()\\`, \\`safeParse()\\`\n\n## Examples\n\n### ❌ Incorrect\n\n\\`\\`\\`tsx\n// Next.js App Router\nexport async function POST(request: Request) {\n const body = await request.json();\n // Body accessed without validation\n await db.users.create({ name: body.name });\n}\n\n// Next.js Pages API\nexport default function handler(req, res) {\n const { email } = req.body; // Unvalidated\n sendEmail(email);\n}\n\\`\\`\\`\n\n### ✅ Correct\n\n\\`\\`\\`tsx\nimport { z } from 'zod';\n\nconst CreateUserSchema = z.object({\n name: z.string().min(1),\n email: z.string().email(),\n});\n\nexport async function POST(request: Request) {\n const body = await request.json();\n const data = CreateUserSchema.parse(body); // Validated!\n await db.users.create(data);\n}\n\\`\\`\\`\n\n## Configuration\n\n\\`\\`\\`js\n// eslint.config.js\n\"uilint/require-input-validation\": [\"warn\", {\n httpMethods: [\"POST\", \"PUT\", \"PATCH\", \"DELETE\"],\n routePatterns: [\"route.ts\", \"/api/\"],\n allowManualValidation: false\n}]\n\\`\\`\\`\n`,\n});\n\n/**\n * HTTP method names (Next.js App Router style)\n */\nconst HTTP_METHODS = [\"GET\", \"POST\", \"PUT\", \"PATCH\", \"DELETE\", \"HEAD\", \"OPTIONS\"];\n\n/**\n * Validation method names from common libraries\n */\nconst VALIDATION_METHODS = [\n // Zod\n \"parse\",\n \"safeParse\",\n \"parseAsync\",\n \"safeParseAsync\",\n // Yup\n \"validate\",\n \"validateSync\",\n \"validateAt\",\n \"validateSyncAt\",\n // Joi\n \"validate\",\n \"validateAsync\",\n // Superstruct\n \"create\",\n \"assert\",\n // io-ts\n \"decode\",\n // Valibot\n \"parse\",\n \"safeParse\",\n // Generic\n \"validateBody\",\n \"validateRequest\",\n \"validateInput\",\n];\n\n/**\n * Check if file matches route patterns\n */\nfunction isApiRouteFile(filename: string, patterns: string[]): boolean {\n return patterns.some((pattern) => filename.includes(pattern));\n}\n\n/**\n * Check if a function is an HTTP method handler\n */\nfunction isHttpMethodHandler(\n node: TSESTree.Node,\n methods: string[]\n): { isHandler: boolean; method: string | null } {\n // Check export function GET/POST/etc\n if (\n node.type === \"ExportNamedDeclaration\" &&\n node.declaration?.type === \"FunctionDeclaration\" &&\n node.declaration.id\n ) {\n const name = node.declaration.id.name.toUpperCase();\n if (methods.includes(name)) {\n return { isHandler: true, method: name };\n }\n }\n\n // Check export const GET = async () => {}\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 const name = decl.id.name.toUpperCase();\n if (methods.includes(name)) {\n return { isHandler: true, method: name };\n }\n }\n }\n }\n\n return { isHandler: false, method: null };\n}\n\n/**\n * Check if a call expression is a validation call\n */\nfunction isValidationCall(node: TSESTree.CallExpression): boolean {\n // Check method calls: schema.parse(), schema.validate()\n if (\n node.callee.type === \"MemberExpression\" &&\n node.callee.property.type === \"Identifier\"\n ) {\n const methodName = node.callee.property.name;\n\n // Exclude JSON.parse as it's not schema validation\n if (\n node.callee.object.type === \"Identifier\" &&\n node.callee.object.name === \"JSON\" &&\n methodName === \"parse\"\n ) {\n return false;\n }\n\n return VALIDATION_METHODS.includes(methodName);\n }\n\n // Check direct calls: validate(schema, data)\n if (node.callee.type === \"Identifier\") {\n const funcName = node.callee.name;\n return VALIDATION_METHODS.includes(funcName);\n }\n\n return false;\n}\n\n/**\n * Check if a node is a body access pattern\n */\nfunction isBodyAccess(node: TSESTree.Node): {\n isAccess: boolean;\n accessType: string | null;\n} {\n // req.body\n if (\n node.type === \"MemberExpression\" &&\n node.property.type === \"Identifier\" &&\n node.property.name === \"body\"\n ) {\n return { isAccess: true, accessType: \"req.body\" };\n }\n\n // request.json() or req.json()\n if (\n node.type === \"CallExpression\" &&\n node.callee.type === \"MemberExpression\" &&\n node.callee.property.type === \"Identifier\" &&\n node.callee.property.name === \"json\"\n ) {\n return { isAccess: true, accessType: \"request.json()\" };\n }\n\n // request.formData()\n if (\n node.type === \"CallExpression\" &&\n node.callee.type === \"MemberExpression\" &&\n node.callee.property.type === \"Identifier\" &&\n node.callee.property.name === \"formData\"\n ) {\n return { isAccess: true, accessType: \"request.formData()\" };\n }\n\n // request.text()\n if (\n node.type === \"CallExpression\" &&\n node.callee.type === \"MemberExpression\" &&\n node.callee.property.type === \"Identifier\" &&\n node.callee.property.name === \"text\"\n ) {\n return { isAccess: true, accessType: \"request.text()\" };\n }\n\n return { isAccess: false, accessType: null };\n}\n\n/**\n * Track if we're inside a validation context\n */\ninterface ValidationContext {\n hasValidation: boolean;\n bodyAccessNodes: Array<{ node: TSESTree.Node; accessType: string }>;\n}\n\nexport default createRule<Options, MessageIds>({\n name: \"require-input-validation\",\n meta: {\n type: \"problem\",\n docs: {\n description: \"Require schema validation in API route handlers\",\n },\n messages: {\n missingValidation:\n \"API route handler '{{method}}' accesses request body without validation. Use a schema validation library like Zod.\",\n unvalidatedBodyAccess:\n \"Accessing '{{accessType}}' without prior validation. Validate the data first using a schema.\",\n },\n schema: [\n {\n type: \"object\",\n properties: {\n httpMethods: {\n type: \"array\",\n items: { type: \"string\" },\n description: \"HTTP methods that require validation\",\n },\n routePatterns: {\n type: \"array\",\n items: { type: \"string\" },\n description: \"File patterns that indicate API routes\",\n },\n allowManualValidation: {\n type: \"boolean\",\n description: \"Allow manual type guards as validation\",\n },\n },\n additionalProperties: false,\n },\n ],\n },\n defaultOptions: [\n {\n httpMethods: [\"POST\", \"PUT\", \"PATCH\", \"DELETE\"],\n routePatterns: [\"route.ts\", \"route.tsx\", \"/api/\", \"/app/api/\"],\n allowManualValidation: false,\n },\n ],\n create(context) {\n const options = context.options[0] || {};\n const httpMethods = (options.httpMethods ?? [\"POST\", \"PUT\", \"PATCH\", \"DELETE\"]).map(\n (m) => m.toUpperCase()\n );\n const routePatterns = options.routePatterns ?? [\n \"route.ts\",\n \"route.tsx\",\n \"/api/\",\n \"/app/api/\",\n ];\n\n const filename = context.filename || context.getFilename?.() || \"\";\n\n // Only check API route files\n if (!isApiRouteFile(filename, routePatterns)) {\n return {};\n }\n\n // Track handlers and their validation status\n const handlerContexts = new Map<TSESTree.Node, ValidationContext>();\n let currentHandler: TSESTree.Node | null = null;\n let currentMethod: string | null = null;\n\n return {\n // Detect HTTP method handlers\n ExportNamedDeclaration(node) {\n const { isHandler, method } = isHttpMethodHandler(node, httpMethods);\n if (isHandler) {\n currentHandler = node;\n currentMethod = method;\n handlerContexts.set(node, {\n hasValidation: false,\n bodyAccessNodes: [],\n });\n }\n },\n\n // Track body access within handlers\n MemberExpression(node) {\n if (!currentHandler) return;\n\n const ctx = handlerContexts.get(currentHandler);\n if (!ctx) return;\n\n const { isAccess, accessType } = isBodyAccess(node);\n if (isAccess && accessType) {\n ctx.bodyAccessNodes.push({ node, accessType });\n }\n },\n\n CallExpression(node) {\n if (!currentHandler) return;\n\n const ctx = handlerContexts.get(currentHandler);\n if (!ctx) return;\n\n // Check for body access\n const { isAccess, accessType } = isBodyAccess(node);\n if (isAccess && accessType) {\n // Check if this is inside a validation call\n // e.g., schema.parse(await request.json())\n if (\n node.parent?.type === \"AwaitExpression\" &&\n node.parent.parent?.type === \"CallExpression\" &&\n isValidationCall(node.parent.parent)\n ) {\n ctx.hasValidation = true;\n return;\n }\n\n // Check if this is directly wrapped in validation\n if (\n node.parent?.type === \"CallExpression\" &&\n isValidationCall(node.parent)\n ) {\n ctx.hasValidation = true;\n return;\n }\n\n ctx.bodyAccessNodes.push({ node, accessType });\n }\n\n // Check for validation calls\n if (isValidationCall(node)) {\n ctx.hasValidation = true;\n }\n },\n\n \"ExportNamedDeclaration:exit\"(node: TSESTree.ExportNamedDeclaration) {\n const ctx = handlerContexts.get(node);\n if (!ctx) return;\n\n // If we have body access but no validation, report\n if (ctx.bodyAccessNodes.length > 0 && !ctx.hasValidation) {\n // Report on the first body access\n const firstAccess = ctx.bodyAccessNodes[0];\n context.report({\n node: firstAccess.node,\n messageId: \"unvalidatedBodyAccess\",\n data: {\n accessType: firstAccess.accessType,\n },\n });\n }\n\n // Clean up\n if (currentHandler === node) {\n currentHandler = null;\n currentMethod = null;\n }\n handlerContexts.delete(node);\n },\n };\n },\n});\n"],"mappings":"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","names":["meta"]}
1
+ {"version":3,"sources":["../../src/utils/create-rule.ts","../../src/rules/require-input-validation.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 * External requirement that a rule needs to function\n */\nexport interface RuleRequirement {\n /** Requirement type for programmatic checks */\n type: \"ollama\" | \"git\" | \"coverage\" | \"semantic-index\" | \"styleguide\";\n /** Human-readable description */\n description: string;\n /** Optional: how to satisfy the requirement */\n setupHint?: string;\n}\n\n/**\n * Rule migration definition for updating rule options between versions\n */\nexport interface RuleMigration {\n /** Source version (semver) */\n from: string;\n /** Target version (semver) */\n to: string;\n /** Human-readable description of what changed */\n description: string;\n /** Function to migrate options from old format to new format */\n migrate: (oldOptions: unknown[]) => unknown[];\n /** Whether this migration contains breaking changes */\n breaking?: boolean;\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., \"consistent-dark-mode\") - must match filename */\n id: string;\n\n /** Semantic version of the rule (e.g., \"1.0.0\") */\n version: 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 /** Icon for display in CLI/UI (emoji or icon name) */\n icon?: string;\n\n /** Short hint about the rule type/requirements */\n hint?: string;\n\n /** Whether rule is enabled by default during install */\n defaultEnabled?: boolean;\n\n /** External requirements the rule needs */\n requirements?: RuleRequirement[];\n\n /**\n * NPM packages that must be installed for this rule to work.\n * These will be added to the target project's dependencies during installation.\n *\n * Example: [\"xxhash-wasm\"] for rules using the xxhash library\n */\n npmDependencies?: string[];\n\n /** Instructions to show after installation */\n postInstallInstructions?: string;\n\n /** Framework compatibility */\n frameworks?: (\"next\" | \"vite\" | \"cra\" | \"remix\")[];\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 * Internal utility dependencies that this rule requires.\n * When the rule is copied to a target project, these utilities\n * will be transformed to import from \"uilint-eslint\" instead\n * of relative paths.\n *\n * Example: [\"coverage-aggregator\", \"dependency-graph\"]\n */\n internalDependencies?: string[];\n\n /**\n * Whether this rule is directory-based (has lib/ folder with utilities).\n * Directory-based rules are installed as folders with index.ts and lib/ subdirectory.\n * Single-file rules are installed as single .ts files.\n *\n * When true, ESLint config imports will use:\n * ./.uilint/rules/rule-id/index.js\n * When false (default):\n * ./.uilint/rules/rule-id.js\n */\n isDirectoryBased?: boolean;\n\n /**\n * Migrations for updating rule options between versions.\n * Migrations are applied in order to transform options from older versions.\n */\n migrations?: RuleMigration[];\n\n /**\n * Which UI plugin should handle this rule.\n * Defaults based on category:\n * - \"static\" category → \"eslint\" plugin\n * - \"semantic\" category → \"semantic\" plugin\n *\n * Special cases:\n * - \"vision\" for semantic-vision rule\n */\n plugin?: \"eslint\" | \"vision\" | \"semantic\";\n\n /**\n * Custom inspector panel ID to use for this rule's issues.\n * If not specified, uses the plugin's default issue inspector.\n *\n * Examples:\n * - \"vision-issue\" for VisionIssueInspector\n * - \"duplicates\" for DuplicatesInspector\n * - \"semantic-issue\" for SemanticIssueInspector\n */\n customInspector?: string;\n\n /**\n * Custom heatmap color for this rule's issues.\n * CSS color value (hex, rgb, hsl, or named color).\n * If not specified, uses severity-based coloring.\n */\n heatmapColor?: 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: require-input-validation\n *\n * Requires API route handlers to validate request body using schema validation\n * libraries like Zod, Yup, or Joi before accessing request data.\n *\n * Examples:\n * - Bad: const { name } = await req.json()\n * - Good: const data = schema.parse(await req.json())\n */\n\nimport { createRule, defineRuleMeta } from \"../utils/create-rule.js\";\nimport type { TSESTree } from \"@typescript-eslint/utils\";\n\ntype MessageIds = \"missingValidation\" | \"unvalidatedBodyAccess\";\ntype Options = [\n {\n /** HTTP methods that require validation (default: POST, PUT, PATCH, DELETE) */\n httpMethods?: string[];\n /** File patterns that indicate API routes */\n routePatterns?: string[];\n /** Allow manual type guards/if-checks as validation */\n allowManualValidation?: boolean;\n }\n];\n\n/**\n * Rule metadata - colocated with implementation for maintainability\n */\nexport const meta = defineRuleMeta({\n id: \"require-input-validation\",\n version: \"1.0.0\",\n name: \"Require Input Validation\",\n description: \"Require schema validation in API route handlers\",\n defaultSeverity: \"warn\",\n category: \"static\",\n icon: \"✅\",\n hint: \"Enforces input validation in APIs\",\n defaultEnabled: true,\n defaultOptions: [\n {\n httpMethods: [\"POST\", \"PUT\", \"PATCH\", \"DELETE\"],\n routePatterns: [\"route.ts\", \"route.tsx\", \"/api/\", \"/app/api/\"],\n allowManualValidation: false,\n },\n ],\n optionSchema: {\n fields: [\n {\n key: \"httpMethods\",\n label: \"HTTP methods requiring validation\",\n type: \"multiselect\",\n defaultValue: [\"POST\", \"PUT\", \"PATCH\", \"DELETE\"],\n options: [\n { value: \"GET\", label: \"GET\" },\n { value: \"POST\", label: \"POST\" },\n { value: \"PUT\", label: \"PUT\" },\n { value: \"PATCH\", label: \"PATCH\" },\n { value: \"DELETE\", label: \"DELETE\" },\n ],\n description: \"HTTP methods that require request body validation\",\n },\n {\n key: \"allowManualValidation\",\n label: \"Allow manual validation\",\n type: \"boolean\",\n defaultValue: false,\n description: \"Allow if-checks and type guards instead of schema validation\",\n },\n ],\n },\n docs: `\n## What it does\n\nEnsures that API route handlers validate request body data using a schema\nvalidation library (Zod, Yup, Joi, etc.) before accessing it.\n\n## Why it's useful\n\n- **Security**: Prevents injection attacks and malformed data\n- **Type Safety**: Ensures runtime data matches expected types\n- **Error Handling**: Provides clear validation error messages\n- **Best Practice**: Follows defense-in-depth principles\n\n## Supported Validation Libraries\n\n- Zod: \\`parse()\\`, \\`safeParse()\\`, \\`parseAsync()\\`\n- Yup: \\`validate()\\`, \\`validateSync()\\`\n- Joi: \\`validate()\\`\n- Superstruct: \\`create()\\`, \\`assert()\\`\n- io-ts: \\`decode()\\`\n- Valibot: \\`parse()\\`, \\`safeParse()\\`\n\n## Examples\n\n### ❌ Incorrect\n\n\\`\\`\\`tsx\n// Next.js App Router\nexport async function POST(request: Request) {\n const body = await request.json();\n // Body accessed without validation\n await db.users.create({ name: body.name });\n}\n\n// Next.js Pages API\nexport default function handler(req, res) {\n const { email } = req.body; // Unvalidated\n sendEmail(email);\n}\n\\`\\`\\`\n\n### ✅ Correct\n\n\\`\\`\\`tsx\nimport { z } from 'zod';\n\nconst CreateUserSchema = z.object({\n name: z.string().min(1),\n email: z.string().email(),\n});\n\nexport async function POST(request: Request) {\n const body = await request.json();\n const data = CreateUserSchema.parse(body); // Validated!\n await db.users.create(data);\n}\n\\`\\`\\`\n\n## Configuration\n\n\\`\\`\\`js\n// eslint.config.js\n\"uilint/require-input-validation\": [\"warn\", {\n httpMethods: [\"POST\", \"PUT\", \"PATCH\", \"DELETE\"],\n routePatterns: [\"route.ts\", \"/api/\"],\n allowManualValidation: false\n}]\n\\`\\`\\`\n`,\n});\n\n/**\n * HTTP method names (Next.js App Router style)\n */\nconst HTTP_METHODS = [\"GET\", \"POST\", \"PUT\", \"PATCH\", \"DELETE\", \"HEAD\", \"OPTIONS\"];\n\n/**\n * Validation method names from common libraries\n */\nconst VALIDATION_METHODS = [\n // Zod\n \"parse\",\n \"safeParse\",\n \"parseAsync\",\n \"safeParseAsync\",\n // Yup\n \"validate\",\n \"validateSync\",\n \"validateAt\",\n \"validateSyncAt\",\n // Joi\n \"validate\",\n \"validateAsync\",\n // Superstruct\n \"create\",\n \"assert\",\n // io-ts\n \"decode\",\n // Valibot\n \"parse\",\n \"safeParse\",\n // Generic\n \"validateBody\",\n \"validateRequest\",\n \"validateInput\",\n];\n\n/**\n * Check if file matches route patterns\n */\nfunction isApiRouteFile(filename: string, patterns: string[]): boolean {\n return patterns.some((pattern) => filename.includes(pattern));\n}\n\n/**\n * Check if a function is an HTTP method handler\n */\nfunction isHttpMethodHandler(\n node: TSESTree.Node,\n methods: string[]\n): { isHandler: boolean; method: string | null } {\n // Check export function GET/POST/etc\n if (\n node.type === \"ExportNamedDeclaration\" &&\n node.declaration?.type === \"FunctionDeclaration\" &&\n node.declaration.id\n ) {\n const name = node.declaration.id.name.toUpperCase();\n if (methods.includes(name)) {\n return { isHandler: true, method: name };\n }\n }\n\n // Check export const GET = async () => {}\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 const name = decl.id.name.toUpperCase();\n if (methods.includes(name)) {\n return { isHandler: true, method: name };\n }\n }\n }\n }\n\n return { isHandler: false, method: null };\n}\n\n/**\n * Check if a call expression is a validation call\n */\nfunction isValidationCall(node: TSESTree.CallExpression): boolean {\n // Check method calls: schema.parse(), schema.validate()\n if (\n node.callee.type === \"MemberExpression\" &&\n node.callee.property.type === \"Identifier\"\n ) {\n const methodName = node.callee.property.name;\n\n // Exclude JSON.parse as it's not schema validation\n if (\n node.callee.object.type === \"Identifier\" &&\n node.callee.object.name === \"JSON\" &&\n methodName === \"parse\"\n ) {\n return false;\n }\n\n return VALIDATION_METHODS.includes(methodName);\n }\n\n // Check direct calls: validate(schema, data)\n if (node.callee.type === \"Identifier\") {\n const funcName = node.callee.name;\n return VALIDATION_METHODS.includes(funcName);\n }\n\n return false;\n}\n\n/**\n * Check if a node is a body access pattern\n */\nfunction isBodyAccess(node: TSESTree.Node): {\n isAccess: boolean;\n accessType: string | null;\n} {\n // req.body\n if (\n node.type === \"MemberExpression\" &&\n node.property.type === \"Identifier\" &&\n node.property.name === \"body\"\n ) {\n return { isAccess: true, accessType: \"req.body\" };\n }\n\n // request.json() or req.json()\n if (\n node.type === \"CallExpression\" &&\n node.callee.type === \"MemberExpression\" &&\n node.callee.property.type === \"Identifier\" &&\n node.callee.property.name === \"json\"\n ) {\n return { isAccess: true, accessType: \"request.json()\" };\n }\n\n // request.formData()\n if (\n node.type === \"CallExpression\" &&\n node.callee.type === \"MemberExpression\" &&\n node.callee.property.type === \"Identifier\" &&\n node.callee.property.name === \"formData\"\n ) {\n return { isAccess: true, accessType: \"request.formData()\" };\n }\n\n // request.text()\n if (\n node.type === \"CallExpression\" &&\n node.callee.type === \"MemberExpression\" &&\n node.callee.property.type === \"Identifier\" &&\n node.callee.property.name === \"text\"\n ) {\n return { isAccess: true, accessType: \"request.text()\" };\n }\n\n return { isAccess: false, accessType: null };\n}\n\n/**\n * Track if we're inside a validation context\n */\ninterface ValidationContext {\n hasValidation: boolean;\n bodyAccessNodes: Array<{ node: TSESTree.Node; accessType: string }>;\n}\n\nexport default createRule<Options, MessageIds>({\n name: \"require-input-validation\",\n meta: {\n type: \"problem\",\n docs: {\n description: \"Require schema validation in API route handlers\",\n },\n messages: {\n missingValidation:\n \"API route handler '{{method}}' accesses request body without validation. Use a schema validation library like Zod.\",\n unvalidatedBodyAccess:\n \"Accessing '{{accessType}}' without prior validation. Validate the data first using a schema.\",\n },\n schema: [\n {\n type: \"object\",\n properties: {\n httpMethods: {\n type: \"array\",\n items: { type: \"string\" },\n description: \"HTTP methods that require validation\",\n },\n routePatterns: {\n type: \"array\",\n items: { type: \"string\" },\n description: \"File patterns that indicate API routes\",\n },\n allowManualValidation: {\n type: \"boolean\",\n description: \"Allow manual type guards as validation\",\n },\n },\n additionalProperties: false,\n },\n ],\n },\n defaultOptions: [\n {\n httpMethods: [\"POST\", \"PUT\", \"PATCH\", \"DELETE\"],\n routePatterns: [\"route.ts\", \"route.tsx\", \"/api/\", \"/app/api/\"],\n allowManualValidation: false,\n },\n ],\n create(context) {\n const options = context.options[0] || {};\n const httpMethods = (options.httpMethods ?? [\"POST\", \"PUT\", \"PATCH\", \"DELETE\"]).map(\n (m) => m.toUpperCase()\n );\n const routePatterns = options.routePatterns ?? [\n \"route.ts\",\n \"route.tsx\",\n \"/api/\",\n \"/app/api/\",\n ];\n\n const filename = context.filename || context.getFilename?.() || \"\";\n\n // Only check API route files\n if (!isApiRouteFile(filename, routePatterns)) {\n return {};\n }\n\n // Track handlers and their validation status\n const handlerContexts = new Map<TSESTree.Node, ValidationContext>();\n let currentHandler: TSESTree.Node | null = null;\n let currentMethod: string | null = null;\n\n return {\n // Detect HTTP method handlers\n ExportNamedDeclaration(node) {\n const { isHandler, method } = isHttpMethodHandler(node, httpMethods);\n if (isHandler) {\n currentHandler = node;\n currentMethod = method;\n handlerContexts.set(node, {\n hasValidation: false,\n bodyAccessNodes: [],\n });\n }\n },\n\n // Track body access within handlers\n MemberExpression(node) {\n if (!currentHandler) return;\n\n const ctx = handlerContexts.get(currentHandler);\n if (!ctx) return;\n\n const { isAccess, accessType } = isBodyAccess(node);\n if (isAccess && accessType) {\n ctx.bodyAccessNodes.push({ node, accessType });\n }\n },\n\n CallExpression(node) {\n if (!currentHandler) return;\n\n const ctx = handlerContexts.get(currentHandler);\n if (!ctx) return;\n\n // Check for body access\n const { isAccess, accessType } = isBodyAccess(node);\n if (isAccess && accessType) {\n // Check if this is inside a validation call\n // e.g., schema.parse(await request.json())\n if (\n node.parent?.type === \"AwaitExpression\" &&\n node.parent.parent?.type === \"CallExpression\" &&\n isValidationCall(node.parent.parent)\n ) {\n ctx.hasValidation = true;\n return;\n }\n\n // Check if this is directly wrapped in validation\n if (\n node.parent?.type === \"CallExpression\" &&\n isValidationCall(node.parent)\n ) {\n ctx.hasValidation = true;\n return;\n }\n\n ctx.bodyAccessNodes.push({ node, accessType });\n }\n\n // Check for validation calls\n if (isValidationCall(node)) {\n ctx.hasValidation = true;\n }\n },\n\n \"ExportNamedDeclaration:exit\"(node: TSESTree.ExportNamedDeclaration) {\n const ctx = handlerContexts.get(node);\n if (!ctx) return;\n\n // If we have body access but no validation, report\n if (ctx.bodyAccessNodes.length > 0 && !ctx.hasValidation) {\n // Report on the first body access\n const firstAccess = ctx.bodyAccessNodes[0];\n context.report({\n node: firstAccess.node,\n messageId: \"unvalidatedBodyAccess\",\n data: {\n accessType: firstAccess.accessType,\n },\n });\n }\n\n // Clean up\n if (currentHandler === node) {\n currentHandler = null;\n currentMethod = null;\n }\n handlerContexts.delete(node);\n },\n };\n },\n});\n"],"mappings":"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","names":["meta"]}