uilint-eslint 0.2.21 → 0.2.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +66 -140
- package/dist/index.js.map +1 -1
- package/dist/rules/consistent-dark-mode.js +62 -136
- package/dist/rules/consistent-dark-mode.js.map +1 -1
- package/dist/rules/enforce-absolute-imports.js.map +1 -1
- package/dist/rules/no-any-in-props.js +1 -1
- package/dist/rules/no-any-in-props.js.map +1 -1
- package/dist/rules/no-prop-drilling-depth.js +3 -16
- package/dist/rules/no-prop-drilling-depth.js.map +1 -1
- package/package.json +2 -2
- package/src/rules/consistent-dark-mode.test.ts +61 -25
- package/src/rules/consistent-dark-mode.ts +86 -160
- package/src/rules/enforce-absolute-imports.ts +3 -2
- package/src/rules/no-any-in-props.ts +5 -2
- package/src/rules/no-prop-drilling-depth.ts +4 -10
|
@@ -37,7 +37,7 @@ warns when a file uses color classes without any dark mode theming.
|
|
|
37
37
|
|
|
38
38
|
- **Prevents broken dark mode**: Catches cases where some colors change in dark mode but others don't
|
|
39
39
|
- **Encourages completeness**: Prompts you to add dark mode support where it's missing
|
|
40
|
-
- **
|
|
40
|
+
- **No false positives**: Only flags explicit Tailwind colors, not custom/CSS variable colors
|
|
41
41
|
|
|
42
42
|
## Examples
|
|
43
43
|
|
|
@@ -59,8 +59,10 @@ warns when a file uses color classes without any dark mode theming.
|
|
|
59
59
|
// All color classes have dark variants
|
|
60
60
|
<div className="bg-white dark:bg-slate-900 text-black dark:text-white">
|
|
61
61
|
|
|
62
|
-
// Using semantic colors (automatically themed)
|
|
62
|
+
// Using semantic/custom colors (automatically themed via CSS variables)
|
|
63
63
|
<div className="bg-background text-foreground">
|
|
64
|
+
<div className="bg-brand text-brand-foreground">
|
|
65
|
+
<div className="bg-primary text-primary-foreground">
|
|
64
66
|
|
|
65
67
|
// Consistent theming
|
|
66
68
|
<button className="bg-blue-500 dark:bg-blue-600 border-gray-300 dark:border-gray-600">
|
|
@@ -77,7 +79,9 @@ warns when a file uses color classes without any dark mode theming.
|
|
|
77
79
|
|
|
78
80
|
## Notes
|
|
79
81
|
|
|
80
|
-
-
|
|
82
|
+
- Only explicit Tailwind colors (like \`blue-500\`, \`white\`, \`slate-900\`) require dark variants
|
|
83
|
+
- Custom/semantic colors (\`background\`, \`foreground\`, \`brand\`, \`primary\`, etc.) are exempt
|
|
84
|
+
- These are assumed to be CSS variables that handle dark mode automatically
|
|
81
85
|
- Transparent, inherit, and current values are exempt
|
|
82
86
|
- Non-color utilities (like \`text-lg\`, \`border-2\`) are correctly ignored
|
|
83
87
|
`
|
|
@@ -108,129 +112,38 @@ var COLOR_PREFIXES = [
|
|
|
108
112
|
"to-"
|
|
109
113
|
];
|
|
110
114
|
var EXEMPT_SUFFIXES = ["transparent", "inherit", "current", "auto", "none"];
|
|
111
|
-
var
|
|
112
|
-
//
|
|
113
|
-
"
|
|
114
|
-
"
|
|
115
|
-
|
|
116
|
-
"
|
|
117
|
-
"
|
|
118
|
-
|
|
119
|
-
"
|
|
120
|
-
"
|
|
121
|
-
|
|
122
|
-
"
|
|
123
|
-
"
|
|
124
|
-
"
|
|
125
|
-
"
|
|
126
|
-
|
|
127
|
-
"
|
|
128
|
-
"
|
|
129
|
-
"
|
|
130
|
-
"
|
|
131
|
-
|
|
132
|
-
"
|
|
133
|
-
"
|
|
134
|
-
"
|
|
135
|
-
"
|
|
136
|
-
|
|
137
|
-
"
|
|
138
|
-
"
|
|
139
|
-
"
|
|
140
|
-
"
|
|
141
|
-
"
|
|
142
|
-
"ellipsis",
|
|
143
|
-
"clip",
|
|
144
|
-
// border- utilities that aren't colors
|
|
145
|
-
"0",
|
|
146
|
-
"2",
|
|
147
|
-
"4",
|
|
148
|
-
"8",
|
|
149
|
-
"solid",
|
|
150
|
-
"dashed",
|
|
151
|
-
"dotted",
|
|
152
|
-
"double",
|
|
153
|
-
"hidden",
|
|
154
|
-
"collapse",
|
|
155
|
-
"separate",
|
|
156
|
-
// shadow- utilities that aren't colors
|
|
157
|
-
// Note: "sm", "lg", "xl", "2xl" already included above
|
|
158
|
-
"md",
|
|
159
|
-
"inner",
|
|
160
|
-
// ring- utilities that aren't colors
|
|
161
|
-
// Note: "0", "2", "4", "8" already included above
|
|
162
|
-
"1",
|
|
163
|
-
"inset",
|
|
164
|
-
// outline- utilities that aren't colors
|
|
165
|
-
// Note: numeric values already included
|
|
166
|
-
"offset-0",
|
|
167
|
-
"offset-1",
|
|
168
|
-
"offset-2",
|
|
169
|
-
"offset-4",
|
|
170
|
-
"offset-8",
|
|
171
|
-
// decoration- utilities that aren't colors
|
|
172
|
-
// Note: "solid", "double", "dotted", "dashed" already included
|
|
173
|
-
"wavy",
|
|
174
|
-
"from-font",
|
|
175
|
-
"clone",
|
|
176
|
-
"slice",
|
|
177
|
-
// divide- utilities that aren't colors
|
|
178
|
-
"x",
|
|
179
|
-
"y",
|
|
180
|
-
"x-0",
|
|
181
|
-
"x-2",
|
|
182
|
-
"x-4",
|
|
183
|
-
"x-8",
|
|
184
|
-
"y-0",
|
|
185
|
-
"y-2",
|
|
186
|
-
"y-4",
|
|
187
|
-
"y-8",
|
|
188
|
-
"x-reverse",
|
|
189
|
-
"y-reverse",
|
|
190
|
-
// gradient direction utilities (from-, via-, to- prefixes)
|
|
191
|
-
"t",
|
|
192
|
-
"tr",
|
|
193
|
-
"r",
|
|
194
|
-
"br",
|
|
195
|
-
"b",
|
|
196
|
-
"bl",
|
|
197
|
-
"l",
|
|
198
|
-
"tl"
|
|
115
|
+
var TAILWIND_COLOR_NAMES = /* @__PURE__ */ new Set([
|
|
116
|
+
// Special colors
|
|
117
|
+
"white",
|
|
118
|
+
"black",
|
|
119
|
+
// Gray scale palettes
|
|
120
|
+
"slate",
|
|
121
|
+
"gray",
|
|
122
|
+
"zinc",
|
|
123
|
+
"neutral",
|
|
124
|
+
"stone",
|
|
125
|
+
// Warm colors
|
|
126
|
+
"red",
|
|
127
|
+
"orange",
|
|
128
|
+
"amber",
|
|
129
|
+
"yellow",
|
|
130
|
+
// Green colors
|
|
131
|
+
"lime",
|
|
132
|
+
"green",
|
|
133
|
+
"emerald",
|
|
134
|
+
"teal",
|
|
135
|
+
// Blue colors
|
|
136
|
+
"cyan",
|
|
137
|
+
"sky",
|
|
138
|
+
"blue",
|
|
139
|
+
"indigo",
|
|
140
|
+
// Purple/Pink colors
|
|
141
|
+
"violet",
|
|
142
|
+
"purple",
|
|
143
|
+
"fuchsia",
|
|
144
|
+
"pink",
|
|
145
|
+
"rose"
|
|
199
146
|
]);
|
|
200
|
-
var SEMANTIC_COLOR_NAMES = /* @__PURE__ */ new Set([
|
|
201
|
-
// Core shadcn colors
|
|
202
|
-
"background",
|
|
203
|
-
"foreground",
|
|
204
|
-
// Component colors
|
|
205
|
-
"card",
|
|
206
|
-
"card-foreground",
|
|
207
|
-
"popover",
|
|
208
|
-
"popover-foreground",
|
|
209
|
-
"primary",
|
|
210
|
-
"primary-foreground",
|
|
211
|
-
"secondary",
|
|
212
|
-
"secondary-foreground",
|
|
213
|
-
"muted",
|
|
214
|
-
"muted-foreground",
|
|
215
|
-
"accent",
|
|
216
|
-
"accent-foreground",
|
|
217
|
-
"destructive",
|
|
218
|
-
"destructive-foreground",
|
|
219
|
-
// Form/UI colors
|
|
220
|
-
"border",
|
|
221
|
-
"input",
|
|
222
|
-
"ring",
|
|
223
|
-
// Sidebar colors (shadcn sidebar component)
|
|
224
|
-
"sidebar",
|
|
225
|
-
"sidebar-foreground",
|
|
226
|
-
"sidebar-border",
|
|
227
|
-
"sidebar-primary",
|
|
228
|
-
"sidebar-primary-foreground",
|
|
229
|
-
"sidebar-accent",
|
|
230
|
-
"sidebar-accent-foreground",
|
|
231
|
-
"sidebar-ring"
|
|
232
|
-
]);
|
|
233
|
-
var CHART_COLOR_PATTERN = /^chart-\d+$/;
|
|
234
147
|
function hasDarkVariant(className) {
|
|
235
148
|
const parts = className.split(":");
|
|
236
149
|
const variants = parts.slice(0, -1);
|
|
@@ -246,12 +159,31 @@ function getColorPrefix(baseClass) {
|
|
|
246
159
|
);
|
|
247
160
|
return sortedPrefixes.find((p) => baseClass.startsWith(p)) || null;
|
|
248
161
|
}
|
|
249
|
-
function
|
|
250
|
-
|
|
162
|
+
function isTailwindColor(value) {
|
|
163
|
+
const valueWithoutOpacity = value.split("/")[0] || value;
|
|
164
|
+
if (TAILWIND_COLOR_NAMES.has(valueWithoutOpacity)) {
|
|
251
165
|
return true;
|
|
252
166
|
}
|
|
253
|
-
|
|
254
|
-
|
|
167
|
+
const match = valueWithoutOpacity.match(/^([a-z]+)-(\d+)$/);
|
|
168
|
+
if (match) {
|
|
169
|
+
const colorName = match[1];
|
|
170
|
+
const scale = match[2];
|
|
171
|
+
const validScales = [
|
|
172
|
+
"50",
|
|
173
|
+
"100",
|
|
174
|
+
"200",
|
|
175
|
+
"300",
|
|
176
|
+
"400",
|
|
177
|
+
"500",
|
|
178
|
+
"600",
|
|
179
|
+
"700",
|
|
180
|
+
"800",
|
|
181
|
+
"900",
|
|
182
|
+
"950"
|
|
183
|
+
];
|
|
184
|
+
if (colorName && TAILWIND_COLOR_NAMES.has(colorName) && validScales.includes(scale || "")) {
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
255
187
|
}
|
|
256
188
|
return false;
|
|
257
189
|
}
|
|
@@ -260,13 +192,7 @@ function isColorValue(baseClass, prefix) {
|
|
|
260
192
|
if (!value) {
|
|
261
193
|
return false;
|
|
262
194
|
}
|
|
263
|
-
|
|
264
|
-
return false;
|
|
265
|
-
}
|
|
266
|
-
if (NON_COLOR_UTILITIES.has(value)) {
|
|
267
|
-
return false;
|
|
268
|
-
}
|
|
269
|
-
return true;
|
|
195
|
+
return isTailwindColor(value);
|
|
270
196
|
}
|
|
271
197
|
function isExempt(baseClass) {
|
|
272
198
|
return EXEMPT_SUFFIXES.some((suffix) => baseClass.endsWith(suffix));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utils/create-rule.ts","../../src/rules/consistent-dark-mode.ts"],"sourcesContent":["/**\n * Rule creation helper using @typescript-eslint/utils\n */\n\nimport { ESLintUtils } from \"@typescript-eslint/utils\";\n\nexport const createRule = ESLintUtils.RuleCreator(\n (name) =>\n `https://github.com/peter-suggate/uilint/blob/main/packages/uilint-eslint/docs/rules/${name}.md`\n);\n\n/**\n * Schema for prompting user to configure a rule option in the CLI\n */\nexport interface OptionFieldSchema {\n /** Field name in the options object */\n key: string;\n /** Display label for the prompt */\n label: string;\n /** Prompt type */\n type: \"text\" | \"number\" | \"boolean\" | \"select\" | \"multiselect\";\n /** Default value */\n defaultValue: unknown;\n /** Placeholder text (for text/number inputs) */\n placeholder?: string;\n /** Options for select/multiselect */\n options?: Array<{ value: string | number; label: string }>;\n /** Description/hint for the field */\n description?: string;\n}\n\n/**\n * Schema describing how to prompt for rule options during installation\n */\nexport interface RuleOptionSchema {\n /** Fields that can be configured for this rule */\n fields: OptionFieldSchema[];\n}\n\n/**\n * Colocated rule metadata - exported alongside each rule\n *\n * This structure keeps all rule metadata in the same file as the rule implementation,\n * making it easy to maintain and extend as new rules are added.\n */\nexport interface RuleMeta {\n /** Rule identifier (e.g., \"no-arbitrary-tailwind\") - must match filename */\n id: string;\n\n /** Display name for CLI (e.g., \"No Arbitrary Tailwind\") */\n name: string;\n\n /** Short description for CLI selection prompts (one line) */\n description: string;\n\n /** Default severity level */\n defaultSeverity: \"error\" | \"warn\" | \"off\";\n\n /** Category for grouping in CLI */\n category: \"static\" | \"semantic\";\n\n /** Whether this rule requires a styleguide file */\n requiresStyleguide?: boolean;\n\n /** Default options for the rule (passed as second element in ESLint config) */\n defaultOptions?: unknown[];\n\n /** Schema for prompting user to configure options during install */\n optionSchema?: RuleOptionSchema;\n\n /**\n * Detailed documentation in markdown format.\n * Should include:\n * - What the rule does\n * - Why it's useful\n * - Examples of incorrect and correct code\n * - Configuration options explained\n */\n docs: string;\n}\n\n/**\n * Helper to define rule metadata with type safety\n */\nexport function defineRuleMeta(meta: RuleMeta): RuleMeta {\n return meta;\n}\n","/**\n * Rule: consistent-dark-mode\n *\n * Ensures consistent dark mode theming in Tailwind CSS classes.\n * - Error: When some color classes have dark: variants but others don't within the same element\n * - Warning: When Tailwind color classes are used in a file but no dark: theming exists\n */\n\nimport { createRule, defineRuleMeta } from \"../utils/create-rule.js\";\nimport type { TSESTree } from \"@typescript-eslint/utils\";\n\ntype MessageIds = \"inconsistentDarkMode\" | \"missingDarkMode\";\ntype Options = [\n {\n /** Whether to warn when no dark mode classes are found in a file that uses Tailwind colors. Default: true */\n warnOnMissingDarkMode?: boolean;\n }?\n];\n\n/**\n * Rule metadata - colocated with implementation for maintainability\n */\nexport const meta = defineRuleMeta({\n id: \"consistent-dark-mode\",\n name: \"Consistent Dark Mode\",\n description: \"Ensure consistent dark: theming (error on mix, warn on missing)\",\n defaultSeverity: \"error\",\n category: \"static\",\n defaultOptions: [{ warnOnMissingDarkMode: true }],\n optionSchema: {\n fields: [\n {\n key: \"warnOnMissingDarkMode\",\n label: \"Warn when elements lack dark: variant\",\n type: \"boolean\",\n defaultValue: true,\n description: \"Enable warnings for elements missing dark mode variants\",\n },\n ],\n },\n docs: `\n## What it does\n\nDetects inconsistent dark mode theming in Tailwind CSS classes. Reports errors when\nsome color classes in an element have \\`dark:\\` variants but others don't, and optionally\nwarns when a file uses color classes without any dark mode theming.\n\n## Why it's useful\n\n- **Prevents broken dark mode**: Catches cases where some colors change in dark mode but others don't\n- **Encourages completeness**: Prompts you to add dark mode support where it's missing\n- **Supports semantic colors**: Automatically ignores shadcn/CSS variable colors that handle dark mode internally\n\n## Examples\n\n### ❌ Incorrect\n\n\\`\\`\\`tsx\n// Some colors have dark variants, others don't\n<div className=\"bg-white dark:bg-slate-900 text-black\">\n// ^^^^^^^^^ missing dark: variant\n\n// Mix of themed and unthemed\n<button className=\"bg-blue-500 dark:bg-blue-600 border-gray-300\">\n// ^^^^^^^^^^^^^^^ missing dark: variant\n\\`\\`\\`\n\n### ✅ Correct\n\n\\`\\`\\`tsx\n// All color classes have dark variants\n<div className=\"bg-white dark:bg-slate-900 text-black dark:text-white\">\n\n// Using semantic colors (automatically themed)\n<div className=\"bg-background text-foreground\">\n\n// Consistent theming\n<button className=\"bg-blue-500 dark:bg-blue-600 border-gray-300 dark:border-gray-600\">\n\\`\\`\\`\n\n## Configuration\n\n\\`\\`\\`js\n// eslint.config.js\n\"uilint/consistent-dark-mode\": [\"error\", {\n warnOnMissingDarkMode: true // Warn if file uses colors without any dark mode\n}]\n\\`\\`\\`\n\n## Notes\n\n- Semantic colors (like shadcn's \\`background\\`, \\`foreground\\`, \\`primary\\`, etc.) are exempt\n- Transparent, inherit, and current values are exempt\n- Non-color utilities (like \\`text-lg\\`, \\`border-2\\`) are correctly ignored\n`,\n});\n\n// Color-related class prefixes that should have dark mode variants\nconst COLOR_PREFIXES = [\n \"bg-\",\n \"text-\",\n \"border-\",\n \"border-t-\",\n \"border-r-\",\n \"border-b-\",\n \"border-l-\",\n \"border-x-\",\n \"border-y-\",\n \"ring-\",\n \"ring-offset-\",\n \"divide-\",\n \"outline-\",\n \"shadow-\",\n \"accent-\",\n \"caret-\",\n \"fill-\",\n \"stroke-\",\n \"decoration-\",\n \"placeholder-\",\n \"from-\",\n \"via-\",\n \"to-\",\n];\n\n// Values that don't need dark variants (colorless or inherited)\nconst EXEMPT_SUFFIXES = [\"transparent\", \"inherit\", \"current\", \"auto\", \"none\"];\n\n// Known non-color utilities that use color prefixes\n// These are utilities like text-lg (font size), text-center (alignment), etc.\nconst NON_COLOR_UTILITIES = new Set([\n // Exempt values (colorless or inherited) - don't need dark variants\n \"transparent\",\n \"inherit\",\n \"current\",\n \"auto\",\n \"none\",\n // text- utilities that aren't colors\n \"xs\",\n \"sm\",\n \"base\",\n \"lg\",\n \"xl\",\n \"2xl\",\n \"3xl\",\n \"4xl\",\n \"5xl\",\n \"6xl\",\n \"7xl\",\n \"8xl\",\n \"9xl\",\n \"left\",\n \"center\",\n \"right\",\n \"justify\",\n \"start\",\n \"end\",\n \"wrap\",\n \"nowrap\",\n \"balance\",\n \"pretty\",\n \"ellipsis\",\n \"clip\",\n // border- utilities that aren't colors\n \"0\",\n \"2\",\n \"4\",\n \"8\",\n \"solid\",\n \"dashed\",\n \"dotted\",\n \"double\",\n \"hidden\",\n \"collapse\",\n \"separate\",\n // shadow- utilities that aren't colors\n // Note: \"sm\", \"lg\", \"xl\", \"2xl\" already included above\n \"md\",\n \"inner\",\n // ring- utilities that aren't colors\n // Note: \"0\", \"2\", \"4\", \"8\" already included above\n \"1\",\n \"inset\",\n // outline- utilities that aren't colors\n // Note: numeric values already included\n \"offset-0\",\n \"offset-1\",\n \"offset-2\",\n \"offset-4\",\n \"offset-8\",\n // decoration- utilities that aren't colors\n // Note: \"solid\", \"double\", \"dotted\", \"dashed\" already included\n \"wavy\",\n \"from-font\",\n \"clone\",\n \"slice\",\n // divide- utilities that aren't colors\n \"x\",\n \"y\",\n \"x-0\",\n \"x-2\",\n \"x-4\",\n \"x-8\",\n \"y-0\",\n \"y-2\",\n \"y-4\",\n \"y-8\",\n \"x-reverse\",\n \"y-reverse\",\n // gradient direction utilities (from-, via-, to- prefixes)\n \"t\",\n \"tr\",\n \"r\",\n \"br\",\n \"b\",\n \"bl\",\n \"l\",\n \"tl\",\n]);\n\n// Semantic color names used by theming systems like shadcn\n// These are CSS variable-based colors that handle dark mode automatically\nconst SEMANTIC_COLOR_NAMES = new Set([\n // Core shadcn colors\n \"background\",\n \"foreground\",\n // Component colors\n \"card\",\n \"card-foreground\",\n \"popover\",\n \"popover-foreground\",\n \"primary\",\n \"primary-foreground\",\n \"secondary\",\n \"secondary-foreground\",\n \"muted\",\n \"muted-foreground\",\n \"accent\",\n \"accent-foreground\",\n \"destructive\",\n \"destructive-foreground\",\n // Form/UI colors\n \"border\",\n \"input\",\n \"ring\",\n // Sidebar colors (shadcn sidebar component)\n \"sidebar\",\n \"sidebar-foreground\",\n \"sidebar-border\",\n \"sidebar-primary\",\n \"sidebar-primary-foreground\",\n \"sidebar-accent\",\n \"sidebar-accent-foreground\",\n \"sidebar-ring\",\n]);\n\n// Pattern for semantic chart colors (chart-1, chart-2, etc.)\nconst CHART_COLOR_PATTERN = /^chart-\\d+$/;\n\n/**\n * Check if a class has 'dark' in its variant chain\n */\nfunction hasDarkVariant(className: string): boolean {\n const parts = className.split(\":\");\n // All parts except the last are variants\n const variants = parts.slice(0, -1);\n return variants.includes(\"dark\");\n}\n\n/**\n * Get the base class (without any variants like hover:, dark:, md:, etc.)\n */\nfunction getBaseClass(className: string): string {\n const parts = className.split(\":\");\n return parts[parts.length - 1] || \"\";\n}\n\n/**\n * Find the color prefix this class uses, if any\n */\nfunction getColorPrefix(baseClass: string): string | null {\n // Sort by length descending to match more specific prefixes first\n // (e.g., \"border-t-\" before \"border-\")\n const sortedPrefixes = [...COLOR_PREFIXES].sort(\n (a, b) => b.length - a.length\n );\n return sortedPrefixes.find((p) => baseClass.startsWith(p)) || null;\n}\n\n/**\n * Check if the value is a semantic/themed color (e.g., shadcn)\n * These colors use CSS variables that automatically handle dark mode\n */\nfunction isSemanticColor(value: string): boolean {\n // Check for exact semantic color names\n if (SEMANTIC_COLOR_NAMES.has(value)) {\n return true;\n }\n\n // Check for chart colors (chart-1, chart-2, etc.)\n if (CHART_COLOR_PATTERN.test(value)) {\n return true;\n }\n\n return false;\n}\n\n/**\n * Check if the value after the prefix looks like a color value\n * Uses an exclusion-based approach: anything that's not a known non-color utility\n * and not a semantic color is treated as a potential color.\n */\nfunction isColorValue(baseClass: string, prefix: string): boolean {\n const value = baseClass.slice(prefix.length);\n\n // Empty value is not a color\n if (!value) {\n return false;\n }\n\n // Check if it's a semantic/themed color (exempt from dark mode requirements)\n if (isSemanticColor(value)) {\n return false;\n }\n\n // Check if it's a known non-color utility\n if (NON_COLOR_UTILITIES.has(value)) {\n return false;\n }\n\n // Treat everything else as a potential color\n // This catches:\n // - Standard Tailwind colors: blue-500, slate-900, white, black\n // - Custom colors defined in tailwind.config: brand, primary (non-shadcn), custom-blue\n // - Arbitrary values: [#fff], [rgb(255,0,0)], [var(--my-color)]\n // - Opacity modifiers: blue-500/50, white/80\n return true;\n}\n\n/**\n * Check if a class is exempt from dark mode requirements\n */\nfunction isExempt(baseClass: string): boolean {\n return EXEMPT_SUFFIXES.some((suffix) => baseClass.endsWith(suffix));\n}\n\nexport default createRule<Options, MessageIds>({\n name: \"consistent-dark-mode\",\n meta: {\n type: \"problem\",\n docs: {\n description: \"Ensure consistent dark mode theming in Tailwind classes\",\n },\n messages: {\n inconsistentDarkMode:\n \"Inconsistent dark mode: '{{unthemed}}' lack dark: variants while other color classes have them.\",\n missingDarkMode:\n \"No dark mode theming detected. Consider adding dark: variants for color classes.\",\n },\n schema: [\n {\n type: \"object\",\n properties: {\n warnOnMissingDarkMode: {\n type: \"boolean\",\n description:\n \"Whether to warn when no dark mode classes are found in a file that uses Tailwind colors\",\n },\n },\n additionalProperties: false,\n },\n ],\n },\n defaultOptions: [{ warnOnMissingDarkMode: true }],\n create(context) {\n const options = context.options[0] || {};\n const warnOnMissingDarkMode = options.warnOnMissingDarkMode ?? true;\n\n let fileHasColorClasses = false;\n let fileHasDarkMode = false;\n const reportedNodes = new Set<TSESTree.Node>();\n\n function checkClassString(node: TSESTree.Node, classString: string) {\n const classes = classString.split(/\\s+/).filter(Boolean);\n if (classes.length === 0) return;\n\n // Track usage per color prefix: { hasLight, hasDark, lightClasses }\n const prefixUsage = new Map<\n string,\n { hasLight: boolean; hasDark: boolean; lightClasses: string[] }\n >();\n\n for (const cls of classes) {\n const baseClass = getBaseClass(cls);\n const prefix = getColorPrefix(baseClass);\n\n if (!prefix) continue;\n if (isExempt(baseClass)) continue;\n\n // Verify this is actually a color class, not something like text-lg\n if (!isColorValue(baseClass, prefix)) continue;\n\n if (!prefixUsage.has(prefix)) {\n prefixUsage.set(prefix, {\n hasLight: false,\n hasDark: false,\n lightClasses: [],\n });\n }\n\n const usage = prefixUsage.get(prefix)!;\n\n if (hasDarkVariant(cls)) {\n usage.hasDark = true;\n fileHasDarkMode = true;\n } else {\n usage.hasLight = true;\n usage.lightClasses.push(cls);\n }\n }\n\n // Track if file uses color classes\n if (prefixUsage.size > 0) {\n fileHasColorClasses = true;\n }\n\n // Check for inconsistency: some prefixes have dark variants, others don't\n const entries = Array.from(prefixUsage.entries());\n const hasSomeDark = entries.some(([_, u]) => u.hasDark);\n\n if (hasSomeDark) {\n const unthemedEntries = entries.filter(\n ([_, usage]) => usage.hasLight && !usage.hasDark\n );\n\n if (unthemedEntries.length > 0 && !reportedNodes.has(node)) {\n reportedNodes.add(node);\n // Collect the actual class names that lack dark variants\n const unthemedClasses = unthemedEntries.flatMap(\n ([_, u]) => u.lightClasses\n );\n\n context.report({\n node,\n messageId: \"inconsistentDarkMode\",\n data: { unthemed: unthemedClasses.join(\", \") },\n });\n }\n }\n }\n\n function processStringValue(node: TSESTree.Node, value: string) {\n checkClassString(node, value);\n }\n\n function processTemplateLiteral(node: TSESTree.TemplateLiteral) {\n for (const quasi of node.quasis) {\n checkClassString(quasi, quasi.value.raw);\n }\n }\n\n return {\n // Check className attributes in JSX\n JSXAttribute(node) {\n if (\n node.name.type === \"JSXIdentifier\" &&\n (node.name.name === \"className\" || node.name.name === \"class\")\n ) {\n const value = node.value;\n\n // Handle string literal: className=\"...\"\n if (value?.type === \"Literal\" && typeof value.value === \"string\") {\n processStringValue(value, value.value);\n }\n\n // Handle JSX expression: className={...}\n if (value?.type === \"JSXExpressionContainer\") {\n const expr = value.expression;\n\n // Direct string: className={\"...\"}\n if (expr.type === \"Literal\" && typeof expr.value === \"string\") {\n processStringValue(expr, expr.value);\n }\n\n // Template literal: className={`...`}\n if (expr.type === \"TemplateLiteral\") {\n processTemplateLiteral(expr);\n }\n }\n }\n },\n\n // Check cn(), clsx(), classnames(), cva() calls\n CallExpression(node) {\n if (node.callee.type !== \"Identifier\") return;\n const name = node.callee.name;\n\n if (\n name === \"cn\" ||\n name === \"clsx\" ||\n name === \"classnames\" ||\n name === \"cva\" ||\n name === \"twMerge\"\n ) {\n for (const arg of node.arguments) {\n if (arg.type === \"Literal\" && typeof arg.value === \"string\") {\n processStringValue(arg, arg.value);\n }\n if (arg.type === \"TemplateLiteral\") {\n processTemplateLiteral(arg);\n }\n // Handle arrays of class strings\n if (arg.type === \"ArrayExpression\") {\n for (const element of arg.elements) {\n if (\n element?.type === \"Literal\" &&\n typeof element.value === \"string\"\n ) {\n processStringValue(element, element.value);\n }\n if (element?.type === \"TemplateLiteral\") {\n processTemplateLiteral(element);\n }\n }\n }\n }\n }\n },\n\n // At the end of the file, check if Tailwind colors are used without any dark mode\n \"Program:exit\"(node) {\n if (warnOnMissingDarkMode && fileHasColorClasses && !fileHasDarkMode) {\n context.report({\n node,\n messageId: \"missingDarkMode\",\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/consistent-dark-mode.ts"],"sourcesContent":["/**\n * Rule creation helper using @typescript-eslint/utils\n */\n\nimport { ESLintUtils } from \"@typescript-eslint/utils\";\n\nexport const createRule = ESLintUtils.RuleCreator(\n (name) =>\n `https://github.com/peter-suggate/uilint/blob/main/packages/uilint-eslint/docs/rules/${name}.md`\n);\n\n/**\n * Schema for prompting user to configure a rule option in the CLI\n */\nexport interface OptionFieldSchema {\n /** Field name in the options object */\n key: string;\n /** Display label for the prompt */\n label: string;\n /** Prompt type */\n type: \"text\" | \"number\" | \"boolean\" | \"select\" | \"multiselect\";\n /** Default value */\n defaultValue: unknown;\n /** Placeholder text (for text/number inputs) */\n placeholder?: string;\n /** Options for select/multiselect */\n options?: Array<{ value: string | number; label: string }>;\n /** Description/hint for the field */\n description?: string;\n}\n\n/**\n * Schema describing how to prompt for rule options during installation\n */\nexport interface RuleOptionSchema {\n /** Fields that can be configured for this rule */\n fields: OptionFieldSchema[];\n}\n\n/**\n * Colocated rule metadata - exported alongside each rule\n *\n * This structure keeps all rule metadata in the same file as the rule implementation,\n * making it easy to maintain and extend as new rules are added.\n */\nexport interface RuleMeta {\n /** Rule identifier (e.g., \"no-arbitrary-tailwind\") - must match filename */\n id: string;\n\n /** Display name for CLI (e.g., \"No Arbitrary Tailwind\") */\n name: string;\n\n /** Short description for CLI selection prompts (one line) */\n description: string;\n\n /** Default severity level */\n defaultSeverity: \"error\" | \"warn\" | \"off\";\n\n /** Category for grouping in CLI */\n category: \"static\" | \"semantic\";\n\n /** Whether this rule requires a styleguide file */\n requiresStyleguide?: boolean;\n\n /** Default options for the rule (passed as second element in ESLint config) */\n defaultOptions?: unknown[];\n\n /** Schema for prompting user to configure options during install */\n optionSchema?: RuleOptionSchema;\n\n /**\n * Detailed documentation in markdown format.\n * Should include:\n * - What the rule does\n * - Why it's useful\n * - Examples of incorrect and correct code\n * - Configuration options explained\n */\n docs: string;\n}\n\n/**\n * Helper to define rule metadata with type safety\n */\nexport function defineRuleMeta(meta: RuleMeta): RuleMeta {\n return meta;\n}\n","/**\n * Rule: consistent-dark-mode\n *\n * Ensures consistent dark mode theming in Tailwind CSS classes.\n * - Error: When some color classes have dark: variants but others don't within the same element\n * - Warning: When Tailwind color classes are used in a file but no dark: theming exists\n */\n\nimport { createRule, defineRuleMeta } from \"../utils/create-rule.js\";\nimport type { TSESTree } from \"@typescript-eslint/utils\";\n\ntype MessageIds = \"inconsistentDarkMode\" | \"missingDarkMode\";\ntype Options = [\n {\n /** Whether to warn when no dark mode classes are found in a file that uses Tailwind colors. Default: true */\n warnOnMissingDarkMode?: boolean;\n }?\n];\n\n/**\n * Rule metadata - colocated with implementation for maintainability\n */\nexport const meta = defineRuleMeta({\n id: \"consistent-dark-mode\",\n name: \"Consistent Dark Mode\",\n description: \"Ensure consistent dark: theming (error on mix, warn on missing)\",\n defaultSeverity: \"error\",\n category: \"static\",\n defaultOptions: [{ warnOnMissingDarkMode: true }],\n optionSchema: {\n fields: [\n {\n key: \"warnOnMissingDarkMode\",\n label: \"Warn when elements lack dark: variant\",\n type: \"boolean\",\n defaultValue: true,\n description: \"Enable warnings for elements missing dark mode variants\",\n },\n ],\n },\n docs: `\n## What it does\n\nDetects inconsistent dark mode theming in Tailwind CSS classes. Reports errors when\nsome color classes in an element have \\`dark:\\` variants but others don't, and optionally\nwarns when a file uses color classes without any dark mode theming.\n\n## Why it's useful\n\n- **Prevents broken dark mode**: Catches cases where some colors change in dark mode but others don't\n- **Encourages completeness**: Prompts you to add dark mode support where it's missing\n- **No false positives**: Only flags explicit Tailwind colors, not custom/CSS variable colors\n\n## Examples\n\n### ❌ Incorrect\n\n\\`\\`\\`tsx\n// Some colors have dark variants, others don't\n<div className=\"bg-white dark:bg-slate-900 text-black\">\n// ^^^^^^^^^ missing dark: variant\n\n// Mix of themed and unthemed\n<button className=\"bg-blue-500 dark:bg-blue-600 border-gray-300\">\n// ^^^^^^^^^^^^^^^ missing dark: variant\n\\`\\`\\`\n\n### ✅ Correct\n\n\\`\\`\\`tsx\n// All color classes have dark variants\n<div className=\"bg-white dark:bg-slate-900 text-black dark:text-white\">\n\n// Using semantic/custom colors (automatically themed via CSS variables)\n<div className=\"bg-background text-foreground\">\n<div className=\"bg-brand text-brand-foreground\">\n<div className=\"bg-primary text-primary-foreground\">\n\n// Consistent theming\n<button className=\"bg-blue-500 dark:bg-blue-600 border-gray-300 dark:border-gray-600\">\n\\`\\`\\`\n\n## Configuration\n\n\\`\\`\\`js\n// eslint.config.js\n\"uilint/consistent-dark-mode\": [\"error\", {\n warnOnMissingDarkMode: true // Warn if file uses colors without any dark mode\n}]\n\\`\\`\\`\n\n## Notes\n\n- Only explicit Tailwind colors (like \\`blue-500\\`, \\`white\\`, \\`slate-900\\`) require dark variants\n- Custom/semantic colors (\\`background\\`, \\`foreground\\`, \\`brand\\`, \\`primary\\`, etc.) are exempt\n- These are assumed to be CSS variables that handle dark mode automatically\n- Transparent, inherit, and current values are exempt\n- Non-color utilities (like \\`text-lg\\`, \\`border-2\\`) are correctly ignored\n`,\n});\n\n// Color-related class prefixes that should have dark mode variants\nconst COLOR_PREFIXES = [\n \"bg-\",\n \"text-\",\n \"border-\",\n \"border-t-\",\n \"border-r-\",\n \"border-b-\",\n \"border-l-\",\n \"border-x-\",\n \"border-y-\",\n \"ring-\",\n \"ring-offset-\",\n \"divide-\",\n \"outline-\",\n \"shadow-\",\n \"accent-\",\n \"caret-\",\n \"fill-\",\n \"stroke-\",\n \"decoration-\",\n \"placeholder-\",\n \"from-\",\n \"via-\",\n \"to-\",\n];\n\n// Values that don't need dark variants (colorless or inherited)\nconst EXEMPT_SUFFIXES = [\"transparent\", \"inherit\", \"current\", \"auto\", \"none\"];\n\n// Built-in Tailwind CSS color palette names\n// These are the ONLY colors that should trigger dark mode warnings.\n// Custom colors (like 'brand', 'company-primary') are assumed to be\n// CSS variables that handle dark mode automatically.\nconst TAILWIND_COLOR_NAMES = new Set([\n // Special colors\n \"white\",\n \"black\",\n // Gray scale palettes\n \"slate\",\n \"gray\",\n \"zinc\",\n \"neutral\",\n \"stone\",\n // Warm colors\n \"red\",\n \"orange\",\n \"amber\",\n \"yellow\",\n // Green colors\n \"lime\",\n \"green\",\n \"emerald\",\n \"teal\",\n // Blue colors\n \"cyan\",\n \"sky\",\n \"blue\",\n \"indigo\",\n // Purple/Pink colors\n \"violet\",\n \"purple\",\n \"fuchsia\",\n \"pink\",\n \"rose\",\n]);\n\n/**\n * Check if a class has 'dark' in its variant chain\n */\nfunction hasDarkVariant(className: string): boolean {\n const parts = className.split(\":\");\n // All parts except the last are variants\n const variants = parts.slice(0, -1);\n return variants.includes(\"dark\");\n}\n\n/**\n * Get the base class (without any variants like hover:, dark:, md:, etc.)\n */\nfunction getBaseClass(className: string): string {\n const parts = className.split(\":\");\n return parts[parts.length - 1] || \"\";\n}\n\n/**\n * Find the color prefix this class uses, if any\n */\nfunction getColorPrefix(baseClass: string): string | null {\n // Sort by length descending to match more specific prefixes first\n // (e.g., \"border-t-\" before \"border-\")\n const sortedPrefixes = [...COLOR_PREFIXES].sort(\n (a, b) => b.length - a.length\n );\n return sortedPrefixes.find((p) => baseClass.startsWith(p)) || null;\n}\n\n/**\n * Check if the value is an explicit Tailwind color.\n * Uses an allowlist approach: only built-in Tailwind color names trigger warnings.\n * Custom colors (like 'brand', 'primary', 'company-blue') are assumed to be\n * CSS variables that handle dark mode automatically and should NOT trigger.\n *\n * Matches patterns like:\n * - white, black (standalone colors)\n * - blue-500, slate-900 (color-scale)\n * - blue-500/50, gray-900/80 (with opacity modifier)\n */\nfunction isTailwindColor(value: string): boolean {\n // Remove opacity modifier if present (e.g., \"blue-500/50\" -> \"blue-500\")\n const valueWithoutOpacity = value.split(\"/\")[0] || value;\n\n // Check for standalone colors (white, black)\n if (TAILWIND_COLOR_NAMES.has(valueWithoutOpacity)) {\n return true;\n }\n\n // Check for color-scale pattern (e.g., \"blue-500\", \"slate-900\")\n // Pattern: colorName-number where number is 50, 100, 200, ..., 950\n const match = valueWithoutOpacity.match(/^([a-z]+)-(\\d+)$/);\n if (match) {\n const colorName = match[1];\n const scale = match[2];\n // Valid Tailwind scales are: 50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950\n const validScales = [\n \"50\",\n \"100\",\n \"200\",\n \"300\",\n \"400\",\n \"500\",\n \"600\",\n \"700\",\n \"800\",\n \"900\",\n \"950\",\n ];\n if (colorName && TAILWIND_COLOR_NAMES.has(colorName) && validScales.includes(scale || \"\")) {\n return true;\n }\n }\n\n return false;\n}\n\n/**\n * Check if the value after the prefix looks like an explicit Tailwind color.\n * Uses allowlist approach: only built-in Tailwind colors should trigger dark mode warnings.\n * Custom/semantic colors (brand, primary, foreground, etc.) are NOT flagged.\n */\nfunction isColorValue(baseClass: string, prefix: string): boolean {\n const value = baseClass.slice(prefix.length);\n\n // Empty value is not a color\n if (!value) {\n return false;\n }\n\n // Only flag explicit Tailwind colors\n // Custom colors, CSS variable colors, and semantic colors are exempt\n return isTailwindColor(value);\n}\n\n/**\n * Check if a class is exempt from dark mode requirements\n */\nfunction isExempt(baseClass: string): boolean {\n return EXEMPT_SUFFIXES.some((suffix) => baseClass.endsWith(suffix));\n}\n\nexport default createRule<Options, MessageIds>({\n name: \"consistent-dark-mode\",\n meta: {\n type: \"problem\",\n docs: {\n description: \"Ensure consistent dark mode theming in Tailwind classes\",\n },\n messages: {\n inconsistentDarkMode:\n \"Inconsistent dark mode: '{{unthemed}}' lack dark: variants while other color classes have them.\",\n missingDarkMode:\n \"No dark mode theming detected. Consider adding dark: variants for color classes.\",\n },\n schema: [\n {\n type: \"object\",\n properties: {\n warnOnMissingDarkMode: {\n type: \"boolean\",\n description:\n \"Whether to warn when no dark mode classes are found in a file that uses Tailwind colors\",\n },\n },\n additionalProperties: false,\n },\n ],\n },\n defaultOptions: [{ warnOnMissingDarkMode: true }],\n create(context) {\n const options = context.options[0] || {};\n const warnOnMissingDarkMode = options.warnOnMissingDarkMode ?? true;\n\n let fileHasColorClasses = false;\n let fileHasDarkMode = false;\n const reportedNodes = new Set<TSESTree.Node>();\n\n function checkClassString(node: TSESTree.Node, classString: string) {\n const classes = classString.split(/\\s+/).filter(Boolean);\n if (classes.length === 0) return;\n\n // Track usage per color prefix: { hasLight, hasDark, lightClasses }\n const prefixUsage = new Map<\n string,\n { hasLight: boolean; hasDark: boolean; lightClasses: string[] }\n >();\n\n for (const cls of classes) {\n const baseClass = getBaseClass(cls);\n const prefix = getColorPrefix(baseClass);\n\n if (!prefix) continue;\n if (isExempt(baseClass)) continue;\n\n // Verify this is actually a color class, not something like text-lg\n if (!isColorValue(baseClass, prefix)) continue;\n\n if (!prefixUsage.has(prefix)) {\n prefixUsage.set(prefix, {\n hasLight: false,\n hasDark: false,\n lightClasses: [],\n });\n }\n\n const usage = prefixUsage.get(prefix)!;\n\n if (hasDarkVariant(cls)) {\n usage.hasDark = true;\n fileHasDarkMode = true;\n } else {\n usage.hasLight = true;\n usage.lightClasses.push(cls);\n }\n }\n\n // Track if file uses color classes\n if (prefixUsage.size > 0) {\n fileHasColorClasses = true;\n }\n\n // Check for inconsistency: some prefixes have dark variants, others don't\n const entries = Array.from(prefixUsage.entries());\n const hasSomeDark = entries.some(([_, u]) => u.hasDark);\n\n if (hasSomeDark) {\n const unthemedEntries = entries.filter(\n ([_, usage]) => usage.hasLight && !usage.hasDark\n );\n\n if (unthemedEntries.length > 0 && !reportedNodes.has(node)) {\n reportedNodes.add(node);\n // Collect the actual class names that lack dark variants\n const unthemedClasses = unthemedEntries.flatMap(\n ([_, u]) => u.lightClasses\n );\n\n context.report({\n node,\n messageId: \"inconsistentDarkMode\",\n data: { unthemed: unthemedClasses.join(\", \") },\n });\n }\n }\n }\n\n function processStringValue(node: TSESTree.Node, value: string) {\n checkClassString(node, value);\n }\n\n function processTemplateLiteral(node: TSESTree.TemplateLiteral) {\n for (const quasi of node.quasis) {\n checkClassString(quasi, quasi.value.raw);\n }\n }\n\n return {\n // Check className attributes in JSX\n JSXAttribute(node) {\n if (\n node.name.type === \"JSXIdentifier\" &&\n (node.name.name === \"className\" || node.name.name === \"class\")\n ) {\n const value = node.value;\n\n // Handle string literal: className=\"...\"\n if (value?.type === \"Literal\" && typeof value.value === \"string\") {\n processStringValue(value, value.value);\n }\n\n // Handle JSX expression: className={...}\n if (value?.type === \"JSXExpressionContainer\") {\n const expr = value.expression;\n\n // Direct string: className={\"...\"}\n if (expr.type === \"Literal\" && typeof expr.value === \"string\") {\n processStringValue(expr, expr.value);\n }\n\n // Template literal: className={`...`}\n if (expr.type === \"TemplateLiteral\") {\n processTemplateLiteral(expr);\n }\n }\n }\n },\n\n // Check cn(), clsx(), classnames(), cva() calls\n CallExpression(node) {\n if (node.callee.type !== \"Identifier\") return;\n const name = node.callee.name;\n\n if (\n name === \"cn\" ||\n name === \"clsx\" ||\n name === \"classnames\" ||\n name === \"cva\" ||\n name === \"twMerge\"\n ) {\n for (const arg of node.arguments) {\n if (arg.type === \"Literal\" && typeof arg.value === \"string\") {\n processStringValue(arg, arg.value);\n }\n if (arg.type === \"TemplateLiteral\") {\n processTemplateLiteral(arg);\n }\n // Handle arrays of class strings\n if (arg.type === \"ArrayExpression\") {\n for (const element of arg.elements) {\n if (\n element?.type === \"Literal\" &&\n typeof element.value === \"string\"\n ) {\n processStringValue(element, element.value);\n }\n if (element?.type === \"TemplateLiteral\") {\n processTemplateLiteral(element);\n }\n }\n }\n }\n }\n },\n\n // At the end of the file, check if Tailwind colors are used without any dark mode\n \"Program:exit\"(node) {\n if (warnOnMissingDarkMode && fileHasColorClasses && !fileHasDarkMode) {\n context.report({\n node,\n messageId: \"missingDarkMode\",\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/enforce-absolute-imports.ts"],"sourcesContent":["/**\n * Rule creation helper using @typescript-eslint/utils\n */\n\nimport { ESLintUtils } from \"@typescript-eslint/utils\";\n\nexport const createRule = ESLintUtils.RuleCreator(\n (name) =>\n `https://github.com/peter-suggate/uilint/blob/main/packages/uilint-eslint/docs/rules/${name}.md`\n);\n\n/**\n * Schema for prompting user to configure a rule option in the CLI\n */\nexport interface OptionFieldSchema {\n /** Field name in the options object */\n key: string;\n /** Display label for the prompt */\n label: string;\n /** Prompt type */\n type: \"text\" | \"number\" | \"boolean\" | \"select\" | \"multiselect\";\n /** Default value */\n defaultValue: unknown;\n /** Placeholder text (for text/number inputs) */\n placeholder?: string;\n /** Options for select/multiselect */\n options?: Array<{ value: string | number; label: string }>;\n /** Description/hint for the field */\n description?: string;\n}\n\n/**\n * Schema describing how to prompt for rule options during installation\n */\nexport interface RuleOptionSchema {\n /** Fields that can be configured for this rule */\n fields: OptionFieldSchema[];\n}\n\n/**\n * Colocated rule metadata - exported alongside each rule\n *\n * This structure keeps all rule metadata in the same file as the rule implementation,\n * making it easy to maintain and extend as new rules are added.\n */\nexport interface RuleMeta {\n /** Rule identifier (e.g., \"no-arbitrary-tailwind\") - must match filename */\n id: string;\n\n /** Display name for CLI (e.g., \"No Arbitrary Tailwind\") */\n name: string;\n\n /** Short description for CLI selection prompts (one line) */\n description: string;\n\n /** Default severity level */\n defaultSeverity: \"error\" | \"warn\" | \"off\";\n\n /** Category for grouping in CLI */\n category: \"static\" | \"semantic\";\n\n /** Whether this rule requires a styleguide file */\n requiresStyleguide?: boolean;\n\n /** Default options for the rule (passed as second element in ESLint config) */\n defaultOptions?: unknown[];\n\n /** Schema for prompting user to configure options during install */\n optionSchema?: RuleOptionSchema;\n\n /**\n * Detailed documentation in markdown format.\n * Should include:\n * - What the rule does\n * - Why it's useful\n * - Examples of incorrect and correct code\n * - Configuration options explained\n */\n docs: string;\n}\n\n/**\n * Helper to define rule metadata with type safety\n */\nexport function defineRuleMeta(meta: RuleMeta): RuleMeta {\n return meta;\n}\n","/**\n * Rule: enforce-absolute-imports\n *\n * Requires alias imports (e.g., @/) for imports that traverse more than a\n * configurable number of directory levels. Prevents fragile relative import\n * paths like ../../../utils/helper.\n *\n * Examples:\n * - Bad: import { x } from '../../utils/helper'\n * - Good: import { x } from '@/utils/helper'\n */\n\nimport { createRule, defineRuleMeta } from \"../utils/create-rule.js\";\n\ntype MessageIds = \"preferAbsoluteImport\";\ntype Options = [\n {\n /** Maximum allowed parent directory traversals (default: 1, allows ../ but not ../../) */\n maxRelativeDepth?: number;\n /** The alias prefix to suggest (default: \"@/\") */\n aliasPrefix?: string;\n /** Patterns to ignore (e.g., [\"node_modules\", \".css\"]) */\n ignorePaths?: string[];\n }\n];\n\n/**\n * Rule metadata - colocated with implementation for maintainability\n */\nexport const meta = defineRuleMeta({\n id: \"enforce-absolute-imports\",\n name: \"Enforce Absolute Imports\",\n description:\n \"Require alias imports for paths beyond a configurable directory depth\",\n defaultSeverity: \"warn\",\n category: \"static\",\n defaultOptions: [{ maxRelativeDepth: 1, aliasPrefix: \"@/\" }],\n optionSchema: {\n fields: [\n {\n key: \"maxRelativeDepth\",\n label: \"Maximum relative depth\",\n type: \"number\",\n defaultValue: 1,\n description:\n \"Maximum number of parent directory traversals allowed (../ counts as 1)\",\n },\n {\n key: \"aliasPrefix\",\n label: \"Alias prefix\",\n type: \"text\",\n defaultValue: \"@/\",\n description: \"The path alias prefix to use (e.g., @/, ~/)\",\n },\n ],\n },\n docs: `\n## What it does\n\nEnforces the use of path aliases (like \\`@/\\`) for imports that traverse multiple\nparent directories. This prevents fragile relative imports that are hard to\nmaintain and refactor.\n\n## Why it's useful\n\n- **Maintainability**: Absolute imports don't break when files move\n- **Readability**: Clear indication of where imports come from\n- **Consistency**: Standardizes import style across the codebase\n- **Refactoring**: Easier to move files without updating import paths\n\n## Examples\n\n### ❌ Incorrect (with maxRelativeDepth: 1)\n\n\\`\\`\\`tsx\n// Too many parent traversals\nimport { Button } from '../../components/Button';\nimport { utils } from '../../../lib/utils';\n\\`\\`\\`\n\n### ✅ Correct\n\n\\`\\`\\`tsx\n// Using alias imports\nimport { Button } from '@/components/Button';\nimport { utils } from '@/lib/utils';\n\n// Single parent traversal (within threshold)\nimport { sibling } from '../sibling';\nimport { local } from './local';\n\\`\\`\\`\n\n## Configuration\n\n\\`\\`\\`js\n// eslint.config.js\n\"uilint/enforce-absolute-imports\": [\"warn\", {\n maxRelativeDepth: 1, // Allow ../ but not ../../\n aliasPrefix: \"@/\", // Suggested alias prefix\n ignorePaths: [\".css\", \".scss\", \"node_modules\"]\n}]\n\\`\\`\\`\n`,\n});\n\n/**\n * Count the number of parent directory traversals in an import path\n */\nfunction countParentTraversals(importSource: string): number {\n // Match all occurrences of ../ or ..\\\\ (Windows)\n const matches = importSource.match(/\\.\\.\\//g);\n return matches ? matches.length : 0;\n}\n\n/**\n * Check if an import is a relative path\n */\nfunction isRelativeImport(importSource: string): boolean {\n return importSource.startsWith(\"./\") || importSource.startsWith(\"../\");\n}\n\n/**\n * Check if the import should be ignored\n */\nfunction shouldIgnore(importSource: string, ignorePaths: string[]): boolean {\n return ignorePaths.some((pattern) => importSource.includes(pattern));\n}\n\nexport default createRule<Options, MessageIds>({\n name: \"enforce-absolute-imports\",\n meta: {\n type: \"suggestion\",\n docs: {\n description:\n \"Require alias imports for paths beyond a configurable directory depth\",\n },\n messages: {\n preferAbsoluteImport:\n \"Import traverses {{depth}} parent director{{plural}}. Use an alias like '{{aliasPrefix}}...' instead of '{{importSource}}'.\",\n },\n schema: [\n {\n type: \"object\",\n properties: {\n maxRelativeDepth: {\n type: \"number\",\n minimum: 0,\n description:\n \"Maximum number of parent directory traversals allowed\",\n },\n aliasPrefix: {\n type: \"string\",\n description: \"The path alias prefix to suggest\",\n },\n ignorePaths: {\n type: \"array\",\n items: { type: \"string\" },\n description: \"Patterns to ignore\",\n },\n },\n additionalProperties: false,\n },\n ],\n },\n defaultOptions: [\n {\n maxRelativeDepth: 1,\n aliasPrefix: \"@/\",\n ignorePaths: [],\n },\n ],\n create(context) {\n const options = context.options[0] || {};\n const maxRelativeDepth = options.maxRelativeDepth ?? 1;\n const aliasPrefix = options.aliasPrefix ?? \"@/\";\n const ignorePaths = options.ignorePaths ?? [];\n\n /**\n * Check an import source and report if it exceeds the depth threshold\n */\n function checkImportSource(\n source: string,\n node: { loc?: { start: { line: number; column: number } } }\n ): void {\n // Skip non-relative imports (node_modules, aliases, etc.)\n if (!isRelativeImport(source)) {\n return;\n }\n\n // Skip ignored paths\n if (shouldIgnore(source, ignorePaths)) {\n return;\n }\n\n const depth = countParentTraversals(source);\n\n if (depth > maxRelativeDepth) {\n context.report({\n node: node as Parameters<typeof context.report>[0][\"node\"],\n messageId: \"preferAbsoluteImport\",\n data: {\n depth: String(depth),\n plural: depth === 1 ? \"y\" : \"ies\",\n aliasPrefix,\n importSource: source,\n },\n });\n }\n }\n\n return {\n // Standard import declarations: import { x } from '../../utils'\n ImportDeclaration(node) {\n const source = node.source.value as string;\n checkImportSource(source, node.source);\n },\n\n // Re-exports with source: export { x } from '../../utils'\n ExportNamedDeclaration(node) {\n if (node.source) {\n const source = node.source.value as string;\n checkImportSource(source, node.source);\n }\n },\n\n // Export all: export * from '../../utils'\n ExportAllDeclaration(node) {\n const source = node.source.value as string;\n checkImportSource(source, node.source);\n },\n };\n },\n});\n"],"mappings":"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","names":["meta"]}
|
|
1
|
+
{"version":3,"sources":["../../src/utils/create-rule.ts","../../src/rules/enforce-absolute-imports.ts"],"sourcesContent":["/**\n * Rule creation helper using @typescript-eslint/utils\n */\n\nimport { ESLintUtils } from \"@typescript-eslint/utils\";\n\nexport const createRule = ESLintUtils.RuleCreator(\n (name) =>\n `https://github.com/peter-suggate/uilint/blob/main/packages/uilint-eslint/docs/rules/${name}.md`\n);\n\n/**\n * Schema for prompting user to configure a rule option in the CLI\n */\nexport interface OptionFieldSchema {\n /** Field name in the options object */\n key: string;\n /** Display label for the prompt */\n label: string;\n /** Prompt type */\n type: \"text\" | \"number\" | \"boolean\" | \"select\" | \"multiselect\";\n /** Default value */\n defaultValue: unknown;\n /** Placeholder text (for text/number inputs) */\n placeholder?: string;\n /** Options for select/multiselect */\n options?: Array<{ value: string | number; label: string }>;\n /** Description/hint for the field */\n description?: string;\n}\n\n/**\n * Schema describing how to prompt for rule options during installation\n */\nexport interface RuleOptionSchema {\n /** Fields that can be configured for this rule */\n fields: OptionFieldSchema[];\n}\n\n/**\n * Colocated rule metadata - exported alongside each rule\n *\n * This structure keeps all rule metadata in the same file as the rule implementation,\n * making it easy to maintain and extend as new rules are added.\n */\nexport interface RuleMeta {\n /** Rule identifier (e.g., \"no-arbitrary-tailwind\") - must match filename */\n id: string;\n\n /** Display name for CLI (e.g., \"No Arbitrary Tailwind\") */\n name: string;\n\n /** Short description for CLI selection prompts (one line) */\n description: string;\n\n /** Default severity level */\n defaultSeverity: \"error\" | \"warn\" | \"off\";\n\n /** Category for grouping in CLI */\n category: \"static\" | \"semantic\";\n\n /** Whether this rule requires a styleguide file */\n requiresStyleguide?: boolean;\n\n /** Default options for the rule (passed as second element in ESLint config) */\n defaultOptions?: unknown[];\n\n /** Schema for prompting user to configure options during install */\n optionSchema?: RuleOptionSchema;\n\n /**\n * Detailed documentation in markdown format.\n * Should include:\n * - What the rule does\n * - Why it's useful\n * - Examples of incorrect and correct code\n * - Configuration options explained\n */\n docs: string;\n}\n\n/**\n * Helper to define rule metadata with type safety\n */\nexport function defineRuleMeta(meta: RuleMeta): RuleMeta {\n return meta;\n}\n","/**\n * Rule: enforce-absolute-imports\n *\n * Requires alias imports (e.g., @/) for imports that traverse more than a\n * configurable number of directory levels. Prevents fragile relative import\n * paths like ../../../utils/helper.\n *\n * Examples:\n * - Bad: import { x } from '../../utils/helper'\n * - Good: import { x } from '@/utils/helper'\n */\n\nimport { createRule, defineRuleMeta } from \"../utils/create-rule.js\";\nimport type { TSESTree } from \"@typescript-eslint/utils\";\n\ntype MessageIds = \"preferAbsoluteImport\";\ntype Options = [\n {\n /** Maximum allowed parent directory traversals (default: 1, allows ../ but not ../../) */\n maxRelativeDepth?: number;\n /** The alias prefix to suggest (default: \"@/\") */\n aliasPrefix?: string;\n /** Patterns to ignore (e.g., [\"node_modules\", \".css\"]) */\n ignorePaths?: string[];\n }\n];\n\n/**\n * Rule metadata - colocated with implementation for maintainability\n */\nexport const meta = defineRuleMeta({\n id: \"enforce-absolute-imports\",\n name: \"Enforce Absolute Imports\",\n description:\n \"Require alias imports for paths beyond a configurable directory depth\",\n defaultSeverity: \"warn\",\n category: \"static\",\n defaultOptions: [{ maxRelativeDepth: 1, aliasPrefix: \"@/\" }],\n optionSchema: {\n fields: [\n {\n key: \"maxRelativeDepth\",\n label: \"Maximum relative depth\",\n type: \"number\",\n defaultValue: 1,\n description:\n \"Maximum number of parent directory traversals allowed (../ counts as 1)\",\n },\n {\n key: \"aliasPrefix\",\n label: \"Alias prefix\",\n type: \"text\",\n defaultValue: \"@/\",\n description: \"The path alias prefix to use (e.g., @/, ~/)\",\n },\n ],\n },\n docs: `\n## What it does\n\nEnforces the use of path aliases (like \\`@/\\`) for imports that traverse multiple\nparent directories. This prevents fragile relative imports that are hard to\nmaintain and refactor.\n\n## Why it's useful\n\n- **Maintainability**: Absolute imports don't break when files move\n- **Readability**: Clear indication of where imports come from\n- **Consistency**: Standardizes import style across the codebase\n- **Refactoring**: Easier to move files without updating import paths\n\n## Examples\n\n### ❌ Incorrect (with maxRelativeDepth: 1)\n\n\\`\\`\\`tsx\n// Too many parent traversals\nimport { Button } from '../../components/Button';\nimport { utils } from '../../../lib/utils';\n\\`\\`\\`\n\n### ✅ Correct\n\n\\`\\`\\`tsx\n// Using alias imports\nimport { Button } from '@/components/Button';\nimport { utils } from '@/lib/utils';\n\n// Single parent traversal (within threshold)\nimport { sibling } from '../sibling';\nimport { local } from './local';\n\\`\\`\\`\n\n## Configuration\n\n\\`\\`\\`js\n// eslint.config.js\n\"uilint/enforce-absolute-imports\": [\"warn\", {\n maxRelativeDepth: 1, // Allow ../ but not ../../\n aliasPrefix: \"@/\", // Suggested alias prefix\n ignorePaths: [\".css\", \".scss\", \"node_modules\"]\n}]\n\\`\\`\\`\n`,\n});\n\n/**\n * Count the number of parent directory traversals in an import path\n */\nfunction countParentTraversals(importSource: string): number {\n // Match all occurrences of ../ or ..\\\\ (Windows)\n const matches = importSource.match(/\\.\\.\\//g);\n return matches ? matches.length : 0;\n}\n\n/**\n * Check if an import is a relative path\n */\nfunction isRelativeImport(importSource: string): boolean {\n return importSource.startsWith(\"./\") || importSource.startsWith(\"../\");\n}\n\n/**\n * Check if the import should be ignored\n */\nfunction shouldIgnore(importSource: string, ignorePaths: string[]): boolean {\n return ignorePaths.some((pattern) => importSource.includes(pattern));\n}\n\nexport default createRule<Options, MessageIds>({\n name: \"enforce-absolute-imports\",\n meta: {\n type: \"suggestion\",\n docs: {\n description:\n \"Require alias imports for paths beyond a configurable directory depth\",\n },\n messages: {\n preferAbsoluteImport:\n \"Import traverses {{depth}} parent director{{plural}}. Use an alias like '{{aliasPrefix}}...' instead of '{{importSource}}'.\",\n },\n schema: [\n {\n type: \"object\",\n properties: {\n maxRelativeDepth: {\n type: \"number\",\n minimum: 0,\n description:\n \"Maximum number of parent directory traversals allowed\",\n },\n aliasPrefix: {\n type: \"string\",\n description: \"The path alias prefix to suggest\",\n },\n ignorePaths: {\n type: \"array\",\n items: { type: \"string\" },\n description: \"Patterns to ignore\",\n },\n },\n additionalProperties: false,\n },\n ],\n },\n defaultOptions: [\n {\n maxRelativeDepth: 1,\n aliasPrefix: \"@/\",\n ignorePaths: [],\n },\n ],\n create(context) {\n const options = context.options[0] || {};\n const maxRelativeDepth = options.maxRelativeDepth ?? 1;\n const aliasPrefix = options.aliasPrefix ?? \"@/\";\n const ignorePaths = options.ignorePaths ?? [];\n\n /**\n * Check an import source and report if it exceeds the depth threshold\n */\n function checkImportSource(\n source: string,\n node: TSESTree.StringLiteral\n ): void {\n // Skip non-relative imports (node_modules, aliases, etc.)\n if (!isRelativeImport(source)) {\n return;\n }\n\n // Skip ignored paths\n if (shouldIgnore(source, ignorePaths)) {\n return;\n }\n\n const depth = countParentTraversals(source);\n\n if (depth > maxRelativeDepth) {\n context.report({\n node,\n messageId: \"preferAbsoluteImport\",\n data: {\n depth: String(depth),\n plural: depth === 1 ? \"y\" : \"ies\",\n aliasPrefix,\n importSource: source,\n },\n });\n }\n }\n\n return {\n // Standard import declarations: import { x } from '../../utils'\n ImportDeclaration(node) {\n const source = node.source.value as string;\n checkImportSource(source, node.source);\n },\n\n // Re-exports with source: export { x } from '../../utils'\n ExportNamedDeclaration(node) {\n if (node.source) {\n const source = node.source.value as string;\n checkImportSource(source, node.source);\n }\n },\n\n // Export all: export * from '../../utils'\n ExportAllDeclaration(node) {\n const source = node.source.value as string;\n checkImportSource(source, node.source);\n },\n };\n },\n});\n"],"mappings":"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","names":["meta"]}
|
|
@@ -158,7 +158,7 @@ function containsAnyType(node, allowInGenericDefaults) {
|
|
|
158
158
|
}
|
|
159
159
|
}
|
|
160
160
|
for (const param of node.params) {
|
|
161
|
-
if (param.
|
|
161
|
+
if (param.type !== "RestElement" && param.type !== "TSParameterProperty" && "typeAnnotation" in param && param.typeAnnotation?.typeAnnotation) {
|
|
162
162
|
const result = containsAnyType(
|
|
163
163
|
param.typeAnnotation.typeAnnotation,
|
|
164
164
|
allowInGenericDefaults
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utils/create-rule.ts","../../src/rules/no-any-in-props.ts"],"sourcesContent":["/**\n * Rule creation helper using @typescript-eslint/utils\n */\n\nimport { ESLintUtils } from \"@typescript-eslint/utils\";\n\nexport const createRule = ESLintUtils.RuleCreator(\n (name) =>\n `https://github.com/peter-suggate/uilint/blob/main/packages/uilint-eslint/docs/rules/${name}.md`\n);\n\n/**\n * Schema for prompting user to configure a rule option in the CLI\n */\nexport interface OptionFieldSchema {\n /** Field name in the options object */\n key: string;\n /** Display label for the prompt */\n label: string;\n /** Prompt type */\n type: \"text\" | \"number\" | \"boolean\" | \"select\" | \"multiselect\";\n /** Default value */\n defaultValue: unknown;\n /** Placeholder text (for text/number inputs) */\n placeholder?: string;\n /** Options for select/multiselect */\n options?: Array<{ value: string | number; label: string }>;\n /** Description/hint for the field */\n description?: string;\n}\n\n/**\n * Schema describing how to prompt for rule options during installation\n */\nexport interface RuleOptionSchema {\n /** Fields that can be configured for this rule */\n fields: OptionFieldSchema[];\n}\n\n/**\n * Colocated rule metadata - exported alongside each rule\n *\n * This structure keeps all rule metadata in the same file as the rule implementation,\n * making it easy to maintain and extend as new rules are added.\n */\nexport interface RuleMeta {\n /** Rule identifier (e.g., \"no-arbitrary-tailwind\") - must match filename */\n id: string;\n\n /** Display name for CLI (e.g., \"No Arbitrary Tailwind\") */\n name: string;\n\n /** Short description for CLI selection prompts (one line) */\n description: string;\n\n /** Default severity level */\n defaultSeverity: \"error\" | \"warn\" | \"off\";\n\n /** Category for grouping in CLI */\n category: \"static\" | \"semantic\";\n\n /** Whether this rule requires a styleguide file */\n requiresStyleguide?: boolean;\n\n /** Default options for the rule (passed as second element in ESLint config) */\n defaultOptions?: unknown[];\n\n /** Schema for prompting user to configure options during install */\n optionSchema?: RuleOptionSchema;\n\n /**\n * Detailed documentation in markdown format.\n * Should include:\n * - What the rule does\n * - Why it's useful\n * - Examples of incorrect and correct code\n * - Configuration options explained\n */\n docs: string;\n}\n\n/**\n * Helper to define rule metadata with type safety\n */\nexport function defineRuleMeta(meta: RuleMeta): RuleMeta {\n return meta;\n}\n","/**\n * Rule: no-any-in-props\n *\n * Prevents React components from using `any` type in their props, ensuring\n * type safety at component boundaries.\n *\n * Examples:\n * - Bad: function Component(props: any) {}\n * - Bad: function Component({ x }: { x: any }) {}\n * - Bad: const Component: FC<any> = () => {}\n * - Good: function Component(props: { name: string }) {}\n */\n\nimport { createRule, defineRuleMeta } from \"../utils/create-rule.js\";\nimport type { TSESTree } from \"@typescript-eslint/utils\";\n\ntype MessageIds = \"anyInProps\" | \"anyInPropsProperty\";\ntype Options = [\n {\n /** Also check FC<any> and React.FC<any> patterns */\n checkFCGenerics?: boolean;\n /** Allow any in generic defaults (e.g., <T = any>) */\n allowInGenericDefaults?: boolean;\n }\n];\n\n/**\n * Rule metadata - colocated with implementation for maintainability\n */\nexport const meta = defineRuleMeta({\n id: \"no-any-in-props\",\n name: \"No Any in Props\",\n description: \"Disallow 'any' type in React component props\",\n defaultSeverity: \"error\",\n category: \"static\",\n defaultOptions: [{ checkFCGenerics: true, allowInGenericDefaults: false }],\n optionSchema: {\n fields: [\n {\n key: \"checkFCGenerics\",\n label: \"Check FC generics\",\n type: \"boolean\",\n defaultValue: true,\n description: \"Check FC<any> and React.FC<any> patterns\",\n },\n {\n key: \"allowInGenericDefaults\",\n label: \"Allow in generic defaults\",\n type: \"boolean\",\n defaultValue: false,\n description: \"Allow any in generic type parameter defaults\",\n },\n ],\n },\n docs: `\n## What it does\n\nPrevents the use of \\`any\\` type in React component props. This ensures type\nsafety at component boundaries, catching type errors at compile time rather\nthan runtime.\n\n## Why it's useful\n\n- **Type Safety**: Catches prop type errors at compile time\n- **Documentation**: Props serve as self-documenting API\n- **Refactoring**: IDE can track prop usage across codebase\n- **Code Quality**: Encourages thoughtful API design\n\n## Examples\n\n### ❌ Incorrect\n\n\\`\\`\\`tsx\n// Direct any annotation\nfunction Component(props: any) {}\n\n// Any in destructured props\nfunction Component({ data }: { data: any }) {}\n\n// FC with any generic\nconst Component: FC<any> = () => {};\n\n// Any in props interface\ninterface Props { value: any }\nfunction Component(props: Props) {}\n\\`\\`\\`\n\n### ✅ Correct\n\n\\`\\`\\`tsx\n// Properly typed props\nfunction Component(props: { name: string }) {}\n\n// Using unknown for truly unknown types\nfunction Component({ data }: { data: unknown }) {}\n\n// Typed FC\nconst Component: FC<{ count: number }> = () => {};\n\n// Generic component with constraint\nfunction List<T extends object>(props: { items: T[] }) {}\n\\`\\`\\`\n\n## Configuration\n\n\\`\\`\\`js\n// eslint.config.js\n\"uilint/no-any-in-props\": [\"error\", {\n checkFCGenerics: true, // Check FC<any> patterns\n allowInGenericDefaults: false // Disallow <T = any>\n}]\n\\`\\`\\`\n`,\n});\n\n/**\n * Check if a name is likely a React component (PascalCase, not a hook)\n */\nfunction isComponentName(name: string): boolean {\n return /^[A-Z][a-zA-Z0-9]*$/.test(name) && !name.startsWith(\"Use\");\n}\n\n/**\n * Check if a type node contains 'any'\n */\nfunction containsAnyType(\n node: TSESTree.TypeNode,\n allowInGenericDefaults: boolean\n): { hasAny: boolean; location: string | null } {\n if (!node) {\n return { hasAny: false, location: null };\n }\n\n switch (node.type) {\n case \"TSAnyKeyword\":\n return { hasAny: true, location: null };\n\n case \"TSTypeLiteral\":\n // Check each property in { prop: any }\n for (const member of node.members) {\n if (\n member.type === \"TSPropertySignature\" &&\n member.typeAnnotation?.typeAnnotation\n ) {\n const result = containsAnyType(\n member.typeAnnotation.typeAnnotation,\n allowInGenericDefaults\n );\n if (result.hasAny) {\n const propName =\n member.key.type === \"Identifier\" ? member.key.name : \"property\";\n return { hasAny: true, location: `property '${propName}'` };\n }\n }\n // Index signature [key: string]: any\n if (\n member.type === \"TSIndexSignature\" &&\n member.typeAnnotation?.typeAnnotation\n ) {\n const result = containsAnyType(\n member.typeAnnotation.typeAnnotation,\n allowInGenericDefaults\n );\n if (result.hasAny) {\n return { hasAny: true, location: \"index signature\" };\n }\n }\n }\n return { hasAny: false, location: null };\n\n case \"TSUnionType\":\n case \"TSIntersectionType\":\n for (const typeNode of node.types) {\n const result = containsAnyType(typeNode, allowInGenericDefaults);\n if (result.hasAny) {\n return result;\n }\n }\n return { hasAny: false, location: null };\n\n case \"TSArrayType\":\n return containsAnyType(node.elementType, allowInGenericDefaults);\n\n case \"TSTypeReference\":\n // Check generic arguments like Array<any>, Record<string, any>\n if (node.typeArguments) {\n for (const param of node.typeArguments.params) {\n const result = containsAnyType(param, allowInGenericDefaults);\n if (result.hasAny) {\n return { hasAny: true, location: \"generic argument\" };\n }\n }\n }\n return { hasAny: false, location: null };\n\n case \"TSFunctionType\":\n // Check return type and parameters\n if (node.returnType?.typeAnnotation) {\n const result = containsAnyType(\n node.returnType.typeAnnotation,\n allowInGenericDefaults\n );\n if (result.hasAny) {\n return { hasAny: true, location: \"function return type\" };\n }\n }\n for (const param of node.params) {\n if (\n param.typeAnnotation?.typeAnnotation &&\n param.type !== \"RestElement\"\n ) {\n const result = containsAnyType(\n param.typeAnnotation.typeAnnotation,\n allowInGenericDefaults\n );\n if (result.hasAny) {\n return { hasAny: true, location: \"function parameter\" };\n }\n }\n }\n return { hasAny: false, location: null };\n\n case \"TSTupleType\":\n for (const elementType of node.elementTypes) {\n // Handle both TSNamedTupleMember and regular type nodes\n const typeToCheck =\n elementType.type === \"TSNamedTupleMember\"\n ? elementType.elementType\n : elementType;\n const result = containsAnyType(typeToCheck, allowInGenericDefaults);\n if (result.hasAny) {\n return { hasAny: true, location: \"tuple element\" };\n }\n }\n return { hasAny: false, location: null };\n\n case \"TSConditionalType\":\n // Check all parts of conditional type\n const checkResult = containsAnyType(\n node.checkType,\n allowInGenericDefaults\n );\n if (checkResult.hasAny) return checkResult;\n const extendsResult = containsAnyType(\n node.extendsType,\n allowInGenericDefaults\n );\n if (extendsResult.hasAny) return extendsResult;\n const trueResult = containsAnyType(\n node.trueType,\n allowInGenericDefaults\n );\n if (trueResult.hasAny) return trueResult;\n const falseResult = containsAnyType(\n node.falseType,\n allowInGenericDefaults\n );\n if (falseResult.hasAny) return falseResult;\n return { hasAny: false, location: null };\n\n case \"TSMappedType\":\n if (node.typeAnnotation) {\n return containsAnyType(node.typeAnnotation, allowInGenericDefaults);\n }\n return { hasAny: false, location: null };\n\n default:\n return { hasAny: false, location: null };\n }\n}\n\n/**\n * Get the name of a function or component\n */\nfunction getComponentName(\n node:\n | TSESTree.FunctionDeclaration\n | TSESTree.ArrowFunctionExpression\n | TSESTree.FunctionExpression\n): string | null {\n // Function declaration: function Foo() {}\n if (node.type === \"FunctionDeclaration\" && node.id) {\n return node.id.name;\n }\n\n // Variable declarator: const Foo = () => {}\n const parent = node.parent;\n if (\n parent?.type === \"VariableDeclarator\" &&\n parent.id.type === \"Identifier\"\n ) {\n return parent.id.name;\n }\n\n // forwardRef/memo wrapper: const Foo = 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 Foo() {}\n if (node.type === \"FunctionExpression\" && node.id) {\n return node.id.name;\n }\n\n return null;\n}\n\nexport default createRule<Options, MessageIds>({\n name: \"no-any-in-props\",\n meta: {\n type: \"problem\",\n docs: {\n description: \"Disallow 'any' type in React component props\",\n },\n messages: {\n anyInProps:\n \"Component '{{componentName}}' has 'any' type in props. Use a specific type or 'unknown' instead.\",\n anyInPropsProperty:\n \"Component '{{componentName}}' has 'any' type in {{location}}. Use a specific type or 'unknown' instead.\",\n },\n schema: [\n {\n type: \"object\",\n properties: {\n checkFCGenerics: {\n type: \"boolean\",\n description: \"Check FC<any> and React.FC<any> patterns\",\n },\n allowInGenericDefaults: {\n type: \"boolean\",\n description: \"Allow any in generic type parameter defaults\",\n },\n },\n additionalProperties: false,\n },\n ],\n },\n defaultOptions: [\n {\n checkFCGenerics: true,\n allowInGenericDefaults: false,\n },\n ],\n create(context) {\n const options = context.options[0] || {};\n const checkFCGenerics = options.checkFCGenerics ?? true;\n const allowInGenericDefaults = options.allowInGenericDefaults ?? false;\n\n /**\n * Check a function's first parameter for any type\n */\n function checkFunctionProps(\n node:\n | TSESTree.FunctionDeclaration\n | TSESTree.ArrowFunctionExpression\n | TSESTree.FunctionExpression\n ): void {\n const componentName = getComponentName(node);\n\n // Skip if not a component (not PascalCase)\n if (!componentName || !isComponentName(componentName)) {\n return;\n }\n\n // Check first parameter (props)\n const firstParam = node.params[0];\n if (!firstParam) {\n return;\n }\n\n // Get type annotation\n let typeAnnotation: TSESTree.TypeNode | null = null;\n\n if (firstParam.type === \"Identifier\" && firstParam.typeAnnotation) {\n typeAnnotation = firstParam.typeAnnotation.typeAnnotation;\n } else if (\n firstParam.type === \"ObjectPattern\" &&\n firstParam.typeAnnotation\n ) {\n typeAnnotation = firstParam.typeAnnotation.typeAnnotation;\n }\n\n if (typeAnnotation) {\n const result = containsAnyType(typeAnnotation, allowInGenericDefaults);\n if (result.hasAny) {\n context.report({\n node: firstParam,\n messageId: result.location ? \"anyInPropsProperty\" : \"anyInProps\",\n data: {\n componentName,\n location: result.location || \"props\",\n },\n });\n }\n }\n }\n\n /**\n * Check FC<any> or React.FC<any> patterns\n */\n function checkFCGeneric(node: TSESTree.VariableDeclarator): void {\n if (!checkFCGenerics) {\n return;\n }\n\n // Get variable name\n if (node.id.type !== \"Identifier\") {\n return;\n }\n const componentName = node.id.name;\n\n // Skip if not a component name\n if (!isComponentName(componentName)) {\n return;\n }\n\n // Check type annotation\n const typeAnnotation = node.id.typeAnnotation?.typeAnnotation;\n if (!typeAnnotation || typeAnnotation.type !== \"TSTypeReference\") {\n return;\n }\n\n // Check if it's FC or React.FC\n let isFCType = false;\n if (\n typeAnnotation.typeName.type === \"Identifier\" &&\n [\"FC\", \"FunctionComponent\", \"VFC\"].includes(typeAnnotation.typeName.name)\n ) {\n isFCType = true;\n } else if (\n typeAnnotation.typeName.type === \"TSQualifiedName\" &&\n typeAnnotation.typeName.left.type === \"Identifier\" &&\n typeAnnotation.typeName.left.name === \"React\" &&\n [\"FC\", \"FunctionComponent\", \"VFC\"].includes(\n typeAnnotation.typeName.right.name\n )\n ) {\n isFCType = true;\n }\n\n if (!isFCType || !typeAnnotation.typeArguments) {\n return;\n }\n\n // Check the type argument\n const firstTypeArg = typeAnnotation.typeArguments.params[0];\n if (firstTypeArg) {\n const result = containsAnyType(firstTypeArg, allowInGenericDefaults);\n if (result.hasAny) {\n context.report({\n node: firstTypeArg,\n messageId: result.location ? \"anyInPropsProperty\" : \"anyInProps\",\n data: {\n componentName,\n location: result.location || \"FC type parameter\",\n },\n });\n }\n }\n }\n\n return {\n FunctionDeclaration(node) {\n checkFunctionProps(node);\n },\n\n ArrowFunctionExpression(node) {\n checkFunctionProps(node);\n },\n\n FunctionExpression(node) {\n checkFunctionProps(node);\n },\n\n VariableDeclarator(node) {\n checkFCGeneric(node);\n },\n };\n },\n});\n"],"mappings":"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","names":["meta"]}
|
|
1
|
+
{"version":3,"sources":["../../src/utils/create-rule.ts","../../src/rules/no-any-in-props.ts"],"sourcesContent":["/**\n * Rule creation helper using @typescript-eslint/utils\n */\n\nimport { ESLintUtils } from \"@typescript-eslint/utils\";\n\nexport const createRule = ESLintUtils.RuleCreator(\n (name) =>\n `https://github.com/peter-suggate/uilint/blob/main/packages/uilint-eslint/docs/rules/${name}.md`\n);\n\n/**\n * Schema for prompting user to configure a rule option in the CLI\n */\nexport interface OptionFieldSchema {\n /** Field name in the options object */\n key: string;\n /** Display label for the prompt */\n label: string;\n /** Prompt type */\n type: \"text\" | \"number\" | \"boolean\" | \"select\" | \"multiselect\";\n /** Default value */\n defaultValue: unknown;\n /** Placeholder text (for text/number inputs) */\n placeholder?: string;\n /** Options for select/multiselect */\n options?: Array<{ value: string | number; label: string }>;\n /** Description/hint for the field */\n description?: string;\n}\n\n/**\n * Schema describing how to prompt for rule options during installation\n */\nexport interface RuleOptionSchema {\n /** Fields that can be configured for this rule */\n fields: OptionFieldSchema[];\n}\n\n/**\n * Colocated rule metadata - exported alongside each rule\n *\n * This structure keeps all rule metadata in the same file as the rule implementation,\n * making it easy to maintain and extend as new rules are added.\n */\nexport interface RuleMeta {\n /** Rule identifier (e.g., \"no-arbitrary-tailwind\") - must match filename */\n id: string;\n\n /** Display name for CLI (e.g., \"No Arbitrary Tailwind\") */\n name: string;\n\n /** Short description for CLI selection prompts (one line) */\n description: string;\n\n /** Default severity level */\n defaultSeverity: \"error\" | \"warn\" | \"off\";\n\n /** Category for grouping in CLI */\n category: \"static\" | \"semantic\";\n\n /** Whether this rule requires a styleguide file */\n requiresStyleguide?: boolean;\n\n /** Default options for the rule (passed as second element in ESLint config) */\n defaultOptions?: unknown[];\n\n /** Schema for prompting user to configure options during install */\n optionSchema?: RuleOptionSchema;\n\n /**\n * Detailed documentation in markdown format.\n * Should include:\n * - What the rule does\n * - Why it's useful\n * - Examples of incorrect and correct code\n * - Configuration options explained\n */\n docs: string;\n}\n\n/**\n * Helper to define rule metadata with type safety\n */\nexport function defineRuleMeta(meta: RuleMeta): RuleMeta {\n return meta;\n}\n","/**\n * Rule: no-any-in-props\n *\n * Prevents React components from using `any` type in their props, ensuring\n * type safety at component boundaries.\n *\n * Examples:\n * - Bad: function Component(props: any) {}\n * - Bad: function Component({ x }: { x: any }) {}\n * - Bad: const Component: FC<any> = () => {}\n * - Good: function Component(props: { name: string }) {}\n */\n\nimport { createRule, defineRuleMeta } from \"../utils/create-rule.js\";\nimport type { TSESTree } from \"@typescript-eslint/utils\";\n\ntype MessageIds = \"anyInProps\" | \"anyInPropsProperty\";\ntype Options = [\n {\n /** Also check FC<any> and React.FC<any> patterns */\n checkFCGenerics?: boolean;\n /** Allow any in generic defaults (e.g., <T = any>) */\n allowInGenericDefaults?: boolean;\n }\n];\n\n/**\n * Rule metadata - colocated with implementation for maintainability\n */\nexport const meta = defineRuleMeta({\n id: \"no-any-in-props\",\n name: \"No Any in Props\",\n description: \"Disallow 'any' type in React component props\",\n defaultSeverity: \"error\",\n category: \"static\",\n defaultOptions: [{ checkFCGenerics: true, allowInGenericDefaults: false }],\n optionSchema: {\n fields: [\n {\n key: \"checkFCGenerics\",\n label: \"Check FC generics\",\n type: \"boolean\",\n defaultValue: true,\n description: \"Check FC<any> and React.FC<any> patterns\",\n },\n {\n key: \"allowInGenericDefaults\",\n label: \"Allow in generic defaults\",\n type: \"boolean\",\n defaultValue: false,\n description: \"Allow any in generic type parameter defaults\",\n },\n ],\n },\n docs: `\n## What it does\n\nPrevents the use of \\`any\\` type in React component props. This ensures type\nsafety at component boundaries, catching type errors at compile time rather\nthan runtime.\n\n## Why it's useful\n\n- **Type Safety**: Catches prop type errors at compile time\n- **Documentation**: Props serve as self-documenting API\n- **Refactoring**: IDE can track prop usage across codebase\n- **Code Quality**: Encourages thoughtful API design\n\n## Examples\n\n### ❌ Incorrect\n\n\\`\\`\\`tsx\n// Direct any annotation\nfunction Component(props: any) {}\n\n// Any in destructured props\nfunction Component({ data }: { data: any }) {}\n\n// FC with any generic\nconst Component: FC<any> = () => {};\n\n// Any in props interface\ninterface Props { value: any }\nfunction Component(props: Props) {}\n\\`\\`\\`\n\n### ✅ Correct\n\n\\`\\`\\`tsx\n// Properly typed props\nfunction Component(props: { name: string }) {}\n\n// Using unknown for truly unknown types\nfunction Component({ data }: { data: unknown }) {}\n\n// Typed FC\nconst Component: FC<{ count: number }> = () => {};\n\n// Generic component with constraint\nfunction List<T extends object>(props: { items: T[] }) {}\n\\`\\`\\`\n\n## Configuration\n\n\\`\\`\\`js\n// eslint.config.js\n\"uilint/no-any-in-props\": [\"error\", {\n checkFCGenerics: true, // Check FC<any> patterns\n allowInGenericDefaults: false // Disallow <T = any>\n}]\n\\`\\`\\`\n`,\n});\n\n/**\n * Check if a name is likely a React component (PascalCase, not a hook)\n */\nfunction isComponentName(name: string): boolean {\n return /^[A-Z][a-zA-Z0-9]*$/.test(name) && !name.startsWith(\"Use\");\n}\n\n/**\n * Check if a type node contains 'any'\n */\nfunction containsAnyType(\n node: TSESTree.TypeNode,\n allowInGenericDefaults: boolean\n): { hasAny: boolean; location: string | null } {\n if (!node) {\n return { hasAny: false, location: null };\n }\n\n switch (node.type) {\n case \"TSAnyKeyword\":\n return { hasAny: true, location: null };\n\n case \"TSTypeLiteral\":\n // Check each property in { prop: any }\n for (const member of node.members) {\n if (\n member.type === \"TSPropertySignature\" &&\n member.typeAnnotation?.typeAnnotation\n ) {\n const result = containsAnyType(\n member.typeAnnotation.typeAnnotation,\n allowInGenericDefaults\n );\n if (result.hasAny) {\n const propName =\n member.key.type === \"Identifier\" ? member.key.name : \"property\";\n return { hasAny: true, location: `property '${propName}'` };\n }\n }\n // Index signature [key: string]: any\n if (\n member.type === \"TSIndexSignature\" &&\n member.typeAnnotation?.typeAnnotation\n ) {\n const result = containsAnyType(\n member.typeAnnotation.typeAnnotation,\n allowInGenericDefaults\n );\n if (result.hasAny) {\n return { hasAny: true, location: \"index signature\" };\n }\n }\n }\n return { hasAny: false, location: null };\n\n case \"TSUnionType\":\n case \"TSIntersectionType\":\n for (const typeNode of node.types) {\n const result = containsAnyType(typeNode, allowInGenericDefaults);\n if (result.hasAny) {\n return result;\n }\n }\n return { hasAny: false, location: null };\n\n case \"TSArrayType\":\n return containsAnyType(node.elementType, allowInGenericDefaults);\n\n case \"TSTypeReference\":\n // Check generic arguments like Array<any>, Record<string, any>\n if (node.typeArguments) {\n for (const param of node.typeArguments.params) {\n const result = containsAnyType(param, allowInGenericDefaults);\n if (result.hasAny) {\n return { hasAny: true, location: \"generic argument\" };\n }\n }\n }\n return { hasAny: false, location: null };\n\n case \"TSFunctionType\":\n // Check return type and parameters\n if (node.returnType?.typeAnnotation) {\n const result = containsAnyType(\n node.returnType.typeAnnotation,\n allowInGenericDefaults\n );\n if (result.hasAny) {\n return { hasAny: true, location: \"function return type\" };\n }\n }\n for (const param of node.params) {\n // Skip TSParameterProperty (doesn't have typeAnnotation) and RestElement\n if (\n param.type !== \"RestElement\" &&\n param.type !== \"TSParameterProperty\" &&\n \"typeAnnotation\" in param &&\n param.typeAnnotation?.typeAnnotation\n ) {\n const result = containsAnyType(\n param.typeAnnotation.typeAnnotation,\n allowInGenericDefaults\n );\n if (result.hasAny) {\n return { hasAny: true, location: \"function parameter\" };\n }\n }\n }\n return { hasAny: false, location: null };\n\n case \"TSTupleType\":\n for (const elementType of node.elementTypes) {\n // Handle both TSNamedTupleMember and regular type nodes\n const typeToCheck =\n elementType.type === \"TSNamedTupleMember\"\n ? elementType.elementType\n : elementType;\n const result = containsAnyType(typeToCheck, allowInGenericDefaults);\n if (result.hasAny) {\n return { hasAny: true, location: \"tuple element\" };\n }\n }\n return { hasAny: false, location: null };\n\n case \"TSConditionalType\":\n // Check all parts of conditional type\n const checkResult = containsAnyType(\n node.checkType,\n allowInGenericDefaults\n );\n if (checkResult.hasAny) return checkResult;\n const extendsResult = containsAnyType(\n node.extendsType,\n allowInGenericDefaults\n );\n if (extendsResult.hasAny) return extendsResult;\n const trueResult = containsAnyType(\n node.trueType,\n allowInGenericDefaults\n );\n if (trueResult.hasAny) return trueResult;\n const falseResult = containsAnyType(\n node.falseType,\n allowInGenericDefaults\n );\n if (falseResult.hasAny) return falseResult;\n return { hasAny: false, location: null };\n\n case \"TSMappedType\":\n if (node.typeAnnotation) {\n return containsAnyType(node.typeAnnotation, allowInGenericDefaults);\n }\n return { hasAny: false, location: null };\n\n default:\n return { hasAny: false, location: null };\n }\n}\n\n/**\n * Get the name of a function or component\n */\nfunction getComponentName(\n node:\n | TSESTree.FunctionDeclaration\n | TSESTree.ArrowFunctionExpression\n | TSESTree.FunctionExpression\n): string | null {\n // Function declaration: function Foo() {}\n if (node.type === \"FunctionDeclaration\" && node.id) {\n return node.id.name;\n }\n\n // Variable declarator: const Foo = () => {}\n const parent = node.parent;\n if (\n parent?.type === \"VariableDeclarator\" &&\n parent.id.type === \"Identifier\"\n ) {\n return parent.id.name;\n }\n\n // forwardRef/memo wrapper: const Foo = 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 Foo() {}\n if (node.type === \"FunctionExpression\" && node.id) {\n return node.id.name;\n }\n\n return null;\n}\n\nexport default createRule<Options, MessageIds>({\n name: \"no-any-in-props\",\n meta: {\n type: \"problem\",\n docs: {\n description: \"Disallow 'any' type in React component props\",\n },\n messages: {\n anyInProps:\n \"Component '{{componentName}}' has 'any' type in props. Use a specific type or 'unknown' instead.\",\n anyInPropsProperty:\n \"Component '{{componentName}}' has 'any' type in {{location}}. Use a specific type or 'unknown' instead.\",\n },\n schema: [\n {\n type: \"object\",\n properties: {\n checkFCGenerics: {\n type: \"boolean\",\n description: \"Check FC<any> and React.FC<any> patterns\",\n },\n allowInGenericDefaults: {\n type: \"boolean\",\n description: \"Allow any in generic type parameter defaults\",\n },\n },\n additionalProperties: false,\n },\n ],\n },\n defaultOptions: [\n {\n checkFCGenerics: true,\n allowInGenericDefaults: false,\n },\n ],\n create(context) {\n const options = context.options[0] || {};\n const checkFCGenerics = options.checkFCGenerics ?? true;\n const allowInGenericDefaults = options.allowInGenericDefaults ?? false;\n\n /**\n * Check a function's first parameter for any type\n */\n function checkFunctionProps(\n node:\n | TSESTree.FunctionDeclaration\n | TSESTree.ArrowFunctionExpression\n | TSESTree.FunctionExpression\n ): void {\n const componentName = getComponentName(node);\n\n // Skip if not a component (not PascalCase)\n if (!componentName || !isComponentName(componentName)) {\n return;\n }\n\n // Check first parameter (props)\n const firstParam = node.params[0];\n if (!firstParam) {\n return;\n }\n\n // Get type annotation\n let typeAnnotation: TSESTree.TypeNode | null = null;\n\n if (firstParam.type === \"Identifier\" && firstParam.typeAnnotation) {\n typeAnnotation = firstParam.typeAnnotation.typeAnnotation;\n } else if (\n firstParam.type === \"ObjectPattern\" &&\n firstParam.typeAnnotation\n ) {\n typeAnnotation = firstParam.typeAnnotation.typeAnnotation;\n }\n\n if (typeAnnotation) {\n const result = containsAnyType(typeAnnotation, allowInGenericDefaults);\n if (result.hasAny) {\n context.report({\n node: firstParam,\n messageId: result.location ? \"anyInPropsProperty\" : \"anyInProps\",\n data: {\n componentName,\n location: result.location || \"props\",\n },\n });\n }\n }\n }\n\n /**\n * Check FC<any> or React.FC<any> patterns\n */\n function checkFCGeneric(node: TSESTree.VariableDeclarator): void {\n if (!checkFCGenerics) {\n return;\n }\n\n // Get variable name\n if (node.id.type !== \"Identifier\") {\n return;\n }\n const componentName = node.id.name;\n\n // Skip if not a component name\n if (!isComponentName(componentName)) {\n return;\n }\n\n // Check type annotation\n const typeAnnotation = node.id.typeAnnotation?.typeAnnotation;\n if (!typeAnnotation || typeAnnotation.type !== \"TSTypeReference\") {\n return;\n }\n\n // Check if it's FC or React.FC\n let isFCType = false;\n if (\n typeAnnotation.typeName.type === \"Identifier\" &&\n [\"FC\", \"FunctionComponent\", \"VFC\"].includes(typeAnnotation.typeName.name)\n ) {\n isFCType = true;\n } else if (\n typeAnnotation.typeName.type === \"TSQualifiedName\" &&\n typeAnnotation.typeName.left.type === \"Identifier\" &&\n typeAnnotation.typeName.left.name === \"React\" &&\n [\"FC\", \"FunctionComponent\", \"VFC\"].includes(\n typeAnnotation.typeName.right.name\n )\n ) {\n isFCType = true;\n }\n\n if (!isFCType || !typeAnnotation.typeArguments) {\n return;\n }\n\n // Check the type argument\n const firstTypeArg = typeAnnotation.typeArguments.params[0];\n if (firstTypeArg) {\n const result = containsAnyType(firstTypeArg, allowInGenericDefaults);\n if (result.hasAny) {\n context.report({\n node: firstTypeArg,\n messageId: result.location ? \"anyInPropsProperty\" : \"anyInProps\",\n data: {\n componentName,\n location: result.location || \"FC type parameter\",\n },\n });\n }\n }\n }\n\n return {\n FunctionDeclaration(node) {\n checkFunctionProps(node);\n },\n\n ArrowFunctionExpression(node) {\n checkFunctionProps(node);\n },\n\n FunctionExpression(node) {\n checkFunctionProps(node);\n },\n\n VariableDeclarator(node) {\n checkFCGeneric(node);\n },\n };\n },\n});\n"],"mappings":"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","names":["meta"]}
|
|
@@ -7,18 +7,6 @@ function defineRuleMeta(meta2) {
|
|
|
7
7
|
return meta2;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
// src/utils/export-resolver.ts
|
|
11
|
-
import { ResolverFactory } from "oxc-resolver";
|
|
12
|
-
import { parse } from "@typescript-eslint/typescript-estree";
|
|
13
|
-
var exportCache = /* @__PURE__ */ new Map();
|
|
14
|
-
var astCache = /* @__PURE__ */ new Map();
|
|
15
|
-
var resolvedPathCache = /* @__PURE__ */ new Map();
|
|
16
|
-
function clearResolverCaches() {
|
|
17
|
-
exportCache.clear();
|
|
18
|
-
astCache.clear();
|
|
19
|
-
resolvedPathCache.clear();
|
|
20
|
-
}
|
|
21
|
-
|
|
22
10
|
// src/rules/no-prop-drilling-depth.ts
|
|
23
11
|
var meta = defineRuleMeta({
|
|
24
12
|
id: "no-prop-drilling-depth",
|
|
@@ -45,9 +33,9 @@ var meta = defineRuleMeta({
|
|
|
45
33
|
{
|
|
46
34
|
key: "ignoredProps",
|
|
47
35
|
label: "Ignored props",
|
|
48
|
-
type: "
|
|
49
|
-
defaultValue:
|
|
50
|
-
description: "
|
|
36
|
+
type: "text",
|
|
37
|
+
defaultValue: "className, style, children, key, ref, id",
|
|
38
|
+
description: "Comma-separated prop names to ignore (common pass-through props)"
|
|
51
39
|
}
|
|
52
40
|
]
|
|
53
41
|
},
|
|
@@ -120,7 +108,6 @@ function Child() {
|
|
|
120
108
|
var componentPropCache = /* @__PURE__ */ new Map();
|
|
121
109
|
function clearPropCache() {
|
|
122
110
|
componentPropCache.clear();
|
|
123
|
-
clearResolverCaches();
|
|
124
111
|
}
|
|
125
112
|
function isComponentName(name) {
|
|
126
113
|
return /^[A-Z][a-zA-Z0-9]*$/.test(name);
|