@hatem427/code-guard-ci 2.2.8 → 3.0.0
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/config/angular.config.ts +468 -27
- package/config/guidelines.config.ts +130 -5
- package/config/nextjs.config.ts +284 -11
- package/config/react.config.ts +440 -16
- package/dist/config/angular.config.d.ts.map +1 -1
- package/dist/config/angular.config.js +468 -26
- package/dist/config/angular.config.js.map +1 -1
- package/dist/config/guidelines.config.d.ts.map +1 -1
- package/dist/config/guidelines.config.js +127 -5
- package/dist/config/guidelines.config.js.map +1 -1
- package/dist/config/nextjs.config.d.ts.map +1 -1
- package/dist/config/nextjs.config.js +284 -11
- package/dist/config/nextjs.config.js.map +1 -1
- package/dist/config/react.config.d.ts.map +1 -1
- package/dist/config/react.config.js +440 -16
- package/dist/config/react.config.js.map +1 -1
- package/dist/scripts/config-generators/ai-config-generator.d.ts.map +1 -1
- package/dist/scripts/config-generators/ai-config-generator.js +9 -71
- package/dist/scripts/config-generators/ai-config-generator.js.map +1 -1
- package/dist/scripts/config-generators/eslint-generator.d.ts.map +1 -1
- package/dist/scripts/config-generators/eslint-generator.js +517 -13
- package/dist/scripts/config-generators/eslint-generator.js.map +1 -1
- package/dist/scripts/config-generators/frameworks/angular.d.ts +6 -0
- package/dist/scripts/config-generators/frameworks/angular.d.ts.map +1 -0
- package/dist/scripts/config-generators/frameworks/angular.js +81 -0
- package/dist/scripts/config-generators/frameworks/angular.js.map +1 -0
- package/dist/scripts/config-generators/frameworks/general.d.ts +6 -0
- package/dist/scripts/config-generators/frameworks/general.d.ts.map +1 -0
- package/dist/scripts/config-generators/frameworks/general.js +15 -0
- package/dist/scripts/config-generators/frameworks/general.js.map +1 -0
- package/dist/scripts/config-generators/frameworks/index.d.ts +17 -0
- package/dist/scripts/config-generators/frameworks/index.d.ts.map +1 -0
- package/dist/scripts/config-generators/frameworks/index.js +28 -0
- package/dist/scripts/config-generators/frameworks/index.js.map +1 -0
- package/dist/scripts/config-generators/frameworks/nextjs.d.ts +6 -0
- package/dist/scripts/config-generators/frameworks/nextjs.d.ts.map +1 -0
- package/dist/scripts/config-generators/frameworks/nextjs.js +115 -0
- package/dist/scripts/config-generators/frameworks/nextjs.js.map +1 -0
- package/dist/scripts/config-generators/frameworks/node.d.ts +6 -0
- package/dist/scripts/config-generators/frameworks/node.d.ts.map +1 -0
- package/dist/scripts/config-generators/frameworks/node.js +19 -0
- package/dist/scripts/config-generators/frameworks/node.js.map +1 -0
- package/dist/scripts/config-generators/frameworks/nuxt.d.ts +6 -0
- package/dist/scripts/config-generators/frameworks/nuxt.d.ts.map +1 -0
- package/dist/scripts/config-generators/frameworks/nuxt.js +18 -0
- package/dist/scripts/config-generators/frameworks/nuxt.js.map +1 -0
- package/dist/scripts/config-generators/frameworks/react.d.ts +6 -0
- package/dist/scripts/config-generators/frameworks/react.d.ts.map +1 -0
- package/dist/scripts/config-generators/frameworks/react.js +117 -0
- package/dist/scripts/config-generators/frameworks/react.js.map +1 -0
- package/dist/scripts/config-generators/frameworks/svelte.d.ts +6 -0
- package/dist/scripts/config-generators/frameworks/svelte.d.ts.map +1 -0
- package/dist/scripts/config-generators/frameworks/svelte.js +17 -0
- package/dist/scripts/config-generators/frameworks/svelte.js.map +1 -0
- package/dist/scripts/config-generators/frameworks/vue.d.ts +6 -0
- package/dist/scripts/config-generators/frameworks/vue.d.ts.map +1 -0
- package/dist/scripts/config-generators/frameworks/vue.js +19 -0
- package/dist/scripts/config-generators/frameworks/vue.js.map +1 -0
- package/dist/scripts/utils/report-generator.js +17 -5
- package/dist/scripts/utils/report-generator.js.map +1 -1
- package/package.json +1 -1
- package/scripts/config-generators/ai-config-generator.ts +19 -78
- package/scripts/config-generators/eslint-generator.ts +511 -7
- package/scripts/config-generators/frameworks/angular.ts +78 -0
- package/scripts/config-generators/frameworks/general.ts +12 -0
- package/scripts/config-generators/frameworks/index.ts +17 -0
- package/scripts/config-generators/frameworks/nextjs.ts +112 -0
- package/scripts/config-generators/frameworks/node.ts +16 -0
- package/scripts/config-generators/frameworks/nuxt.ts +15 -0
- package/scripts/config-generators/frameworks/react.ts +114 -0
- package/scripts/config-generators/frameworks/svelte.ts +14 -0
- package/scripts/config-generators/frameworks/vue.ts +16 -0
- package/scripts/utils/report-generator.ts +19 -5
package/config/react.config.ts
CHANGED
|
@@ -16,6 +16,21 @@ import { registerRules, Rule } from './guidelines.config';
|
|
|
16
16
|
const reactRules: Rule[] = [
|
|
17
17
|
// ── NEW: React 19+ Features ────────────────────────────────────────────
|
|
18
18
|
|
|
19
|
+
// ─────────────────────────────────────────
|
|
20
|
+
// RULE: react-use-hook
|
|
21
|
+
// ROLE: Recommend React 19 use() hook for promise/context unwrapping
|
|
22
|
+
// PURPOSE: use() replaces .then() chains and useContext() with a
|
|
23
|
+
// single API that works in both Server and Client Components.
|
|
24
|
+
// It suspends the component until the promise resolves.
|
|
25
|
+
// EXAMPLE:
|
|
26
|
+
// WRONG:
|
|
27
|
+
// const data = await fetchData().then(res => res.json());
|
|
28
|
+
// const theme = useContext(ThemeContext);
|
|
29
|
+
// RIGHT:
|
|
30
|
+
// import { use } from 'react';
|
|
31
|
+
// const data = use(fetchData());
|
|
32
|
+
// const theme = use(ThemeContext);
|
|
33
|
+
// ─────────────────────────────────────────
|
|
19
34
|
{
|
|
20
35
|
id: 'react-use-hook',
|
|
21
36
|
label: 'Use use() hook for unwrapping promises and context',
|
|
@@ -27,13 +42,12 @@ const reactRules: Rule[] = [
|
|
|
27
42
|
customCheck: (file) => {
|
|
28
43
|
const violations: Array<{ line: number | null; message: string }> = [];
|
|
29
44
|
|
|
30
|
-
// Flag promise handling that could use use()
|
|
31
45
|
if (/\.then\s*\(|async\s*\(\s*\).*=>.*=>/.test(file.content) &&
|
|
32
46
|
!file.content.includes('use(')) {
|
|
33
47
|
violations.push({
|
|
34
48
|
line: null,
|
|
35
49
|
message:
|
|
36
|
-
'Found promise handling. Use React 19 use() hook to unwrap promises directly
|
|
50
|
+
'Found promise handling. Use React 19 use() hook to unwrap promises directly.',
|
|
37
51
|
});
|
|
38
52
|
}
|
|
39
53
|
return violations;
|
|
@@ -42,6 +56,26 @@ const reactRules: Rule[] = [
|
|
|
42
56
|
category: 'React 19',
|
|
43
57
|
},
|
|
44
58
|
|
|
59
|
+
// ─────────────────────────────────────────
|
|
60
|
+
// RULE: react-use-action-state
|
|
61
|
+
// ROLE: Enforce React 19 form handling pattern
|
|
62
|
+
// PURPOSE: useActionState() eliminates manual pending/error state
|
|
63
|
+
// management for forms. It handles submission state, errors,
|
|
64
|
+
// and progressive enhancement automatically.
|
|
65
|
+
// EXAMPLE:
|
|
66
|
+
// WRONG:
|
|
67
|
+
// const [pending, setPending] = useState(false);
|
|
68
|
+
// const handleSubmit = async (e) => {
|
|
69
|
+
// setPending(true);
|
|
70
|
+
// await submitForm();
|
|
71
|
+
// setPending(false);
|
|
72
|
+
// };
|
|
73
|
+
// RIGHT:
|
|
74
|
+
// const [state, formAction, isPending] = useActionState(submit, initialState);
|
|
75
|
+
// <form action={formAction}>
|
|
76
|
+
// <button disabled={isPending}>{isPending ? 'Loading...' : 'Submit'}</button>
|
|
77
|
+
// </form>
|
|
78
|
+
// ─────────────────────────────────────────
|
|
45
79
|
{
|
|
46
80
|
id: 'react-use-action-state',
|
|
47
81
|
label: 'Use useActionState() for form handling',
|
|
@@ -53,14 +87,13 @@ const reactRules: Rule[] = [
|
|
|
53
87
|
customCheck: (file) => {
|
|
54
88
|
const violations: Array<{ line: number | null; message: string }> = [];
|
|
55
89
|
|
|
56
|
-
// Flag traditional form handling in client components
|
|
57
90
|
if (/use\(["']use client['"], \{.*\}|<form\s+onSubmit|useState.*pending|useState.*loading/.test(file.content) &&
|
|
58
91
|
!file.content.includes('useActionState') &&
|
|
59
92
|
file.content.includes("'use client'")) {
|
|
60
93
|
violations.push({
|
|
61
94
|
line: null,
|
|
62
95
|
message:
|
|
63
|
-
'Form with manual state management detected. Use useActionState() for simpler form handling in React 19
|
|
96
|
+
'Form with manual state management detected. Use useActionState() for simpler form handling in React 19.',
|
|
64
97
|
});
|
|
65
98
|
}
|
|
66
99
|
return violations;
|
|
@@ -69,6 +102,24 @@ const reactRules: Rule[] = [
|
|
|
69
102
|
category: 'React 19',
|
|
70
103
|
},
|
|
71
104
|
|
|
105
|
+
// ─────────────────────────────────────────
|
|
106
|
+
// RULE: react-use-transition
|
|
107
|
+
// ROLE: Recommend non-blocking UI updates for expensive operations
|
|
108
|
+
// PURPOSE: useTransition() marks state updates as low-priority so the
|
|
109
|
+
// UI stays responsive during expensive re-renders (filtering,
|
|
110
|
+
// sorting, searching large datasets).
|
|
111
|
+
// EXAMPLE:
|
|
112
|
+
// WRONG:
|
|
113
|
+
// const [query, setQuery] = useState('');
|
|
114
|
+
// const handleChange = (e) => setQuery(e.target.value);
|
|
115
|
+
// // UI freezes during expensive filtering
|
|
116
|
+
// RIGHT:
|
|
117
|
+
// const [isPending, startTransition] = useTransition();
|
|
118
|
+
// const handleChange = (e) => {
|
|
119
|
+
// startTransition(() => setQuery(e.target.value));
|
|
120
|
+
// };
|
|
121
|
+
// // UI stays responsive, shows pending state
|
|
122
|
+
// ─────────────────────────────────────────
|
|
72
123
|
{
|
|
73
124
|
id: 'react-use-transition',
|
|
74
125
|
label: 'Use useTransition() for non-blocking updates',
|
|
@@ -80,14 +131,13 @@ const reactRules: Rule[] = [
|
|
|
80
131
|
customCheck: (file) => {
|
|
81
132
|
const violations: Array<{ line: number | null; message: string }> = [];
|
|
82
133
|
|
|
83
|
-
// Flag expensive state updates without useTransition
|
|
84
134
|
if (/filter|search|sort|debounce/.test(file.content) &&
|
|
85
135
|
/useState\s*\(/.test(file.content) &&
|
|
86
136
|
!file.content.includes('useTransition')) {
|
|
87
137
|
violations.push({
|
|
88
138
|
line: null,
|
|
89
139
|
message:
|
|
90
|
-
'Found filter/search logic. Use useTransition() to keep UI responsive during expensive updates
|
|
140
|
+
'Found filter/search logic. Use useTransition() to keep UI responsive during expensive updates.',
|
|
91
141
|
});
|
|
92
142
|
}
|
|
93
143
|
return violations;
|
|
@@ -96,6 +146,28 @@ const reactRules: Rule[] = [
|
|
|
96
146
|
category: 'React 19',
|
|
97
147
|
},
|
|
98
148
|
|
|
149
|
+
// ─────────────────────────────────────────
|
|
150
|
+
// RULE: react-use-optimistic
|
|
151
|
+
// ROLE: Recommend optimistic UI updates for async actions
|
|
152
|
+
// PURPOSE: useOptimistic() shows instant feedback to the user while
|
|
153
|
+
// the server processes the request. If the server fails, the
|
|
154
|
+
// state automatically rolls back.
|
|
155
|
+
// EXAMPLE:
|
|
156
|
+
// WRONG:
|
|
157
|
+
// const handleLike = async () => {
|
|
158
|
+
// await api.like(postId); // User waits...
|
|
159
|
+
// refetch();
|
|
160
|
+
// };
|
|
161
|
+
// RIGHT:
|
|
162
|
+
// const [optimisticLikes, addOptimistic] = useOptimistic(
|
|
163
|
+
// likes,
|
|
164
|
+
// (state, newLike) => [...state, newLike]
|
|
165
|
+
// );
|
|
166
|
+
// const handleLike = async () => {
|
|
167
|
+
// addOptimistic(newLike); // Instant feedback
|
|
168
|
+
// await api.like(postId);
|
|
169
|
+
// };
|
|
170
|
+
// ─────────────────────────────────────────
|
|
99
171
|
{
|
|
100
172
|
id: 'react-use-optimistic',
|
|
101
173
|
label: 'Use useOptimistic() for optimistic updates',
|
|
@@ -107,7 +179,6 @@ const reactRules: Rule[] = [
|
|
|
107
179
|
customCheck: (file) => {
|
|
108
180
|
const violations: Array<{ line: number | null; message: string }> = [];
|
|
109
181
|
|
|
110
|
-
// Flag async form handlers without useOptimistic
|
|
111
182
|
const hasAsyncHandler = /const\s+handle\w+\s*=\s*async\s*\(|async\s+function\s+handle/i.test(file.content);
|
|
112
183
|
const hasOptimistic = /useOptimistic/i.test(file.content);
|
|
113
184
|
|
|
@@ -116,7 +187,7 @@ const reactRules: Rule[] = [
|
|
|
116
187
|
if (/const\s+handle\w+\s*=\s*async|async\s+function\s+handle/i.test(file.lines[i])) {
|
|
117
188
|
violations.push({
|
|
118
189
|
line: i + 1,
|
|
119
|
-
message: `Async form handler found. Use useOptimistic() for instant UI feedback
|
|
190
|
+
message: `Async form handler found. Use useOptimistic() for instant UI feedback.`,
|
|
120
191
|
});
|
|
121
192
|
break;
|
|
122
193
|
}
|
|
@@ -131,6 +202,27 @@ const reactRules: Rule[] = [
|
|
|
131
202
|
|
|
132
203
|
// ── Server Components (Next.js 13+, React Canary) ────────────────────
|
|
133
204
|
|
|
205
|
+
// ─────────────────────────────────────────
|
|
206
|
+
// RULE: react-server-functions
|
|
207
|
+
// ROLE: Recommend "use server" directive for secure server logic
|
|
208
|
+
// PURPOSE: Server Functions run entirely on the server, enabling
|
|
209
|
+
// direct database access, secret handling, and mutations
|
|
210
|
+
// without creating API routes.
|
|
211
|
+
// EXAMPLE:
|
|
212
|
+
// WRONG:
|
|
213
|
+
// // pages/api/user.ts (API route approach)
|
|
214
|
+
// export async function POST(req) {
|
|
215
|
+
// const data = await req.json();
|
|
216
|
+
// await db.users.create(data);
|
|
217
|
+
// }
|
|
218
|
+
// RIGHT:
|
|
219
|
+
// // actions/user.ts
|
|
220
|
+
// 'use server';
|
|
221
|
+
// export async function createUser(formData: FormData) {
|
|
222
|
+
// await db.users.create(Object.fromEntries(formData));
|
|
223
|
+
// revalidatePath('/users');
|
|
224
|
+
// }
|
|
225
|
+
// ─────────────────────────────────────────
|
|
134
226
|
{
|
|
135
227
|
id: 'react-server-functions',
|
|
136
228
|
label: 'Use Server Functions with "use server" directive',
|
|
@@ -143,6 +235,24 @@ const reactRules: Rule[] = [
|
|
|
143
235
|
category: 'React Server Functions',
|
|
144
236
|
},
|
|
145
237
|
|
|
238
|
+
// ─────────────────────────────────────────
|
|
239
|
+
// RULE: react-server-client-boundary
|
|
240
|
+
// ROLE: Validate "use client" boundary placement
|
|
241
|
+
// PURPOSE: Unnecessarily marking components as "use client" prevents
|
|
242
|
+
// server-side rendering optimizations and increases the
|
|
243
|
+
// JavaScript bundle sent to the browser.
|
|
244
|
+
// EXAMPLE:
|
|
245
|
+
// WRONG:
|
|
246
|
+
// 'use client'; // No hooks, no interactivity!
|
|
247
|
+
// export default function StaticCard({ title }) {
|
|
248
|
+
// return <div>{title}</div>;
|
|
249
|
+
// }
|
|
250
|
+
// RIGHT:
|
|
251
|
+
// // No directive needed — Server Component by default
|
|
252
|
+
// export default function StaticCard({ title }) {
|
|
253
|
+
// return <div>{title}</div>;
|
|
254
|
+
// }
|
|
255
|
+
// ─────────────────────────────────────────
|
|
146
256
|
{
|
|
147
257
|
id: 'react-server-client-boundary',
|
|
148
258
|
label: 'Verify "use client" boundaries',
|
|
@@ -158,7 +268,6 @@ const reactRules: Rule[] = [
|
|
|
158
268
|
const hasHooks = /use(State|Effect|Context|Reducer|Callback|Memo|Ref|Transition|OptimisticOptimistic)\s*\(/.test(file.content);
|
|
159
269
|
const hasExport = /export\s+(default\s+)?function|export\s+(default\s+)?const/.test(file.content);
|
|
160
270
|
|
|
161
|
-
// If no hooks but has "use client", might be misplaced
|
|
162
271
|
if (hasUseClient && !hasHooks && hasExport) {
|
|
163
272
|
violations.push({
|
|
164
273
|
line: 1,
|
|
@@ -174,6 +283,26 @@ const reactRules: Rule[] = [
|
|
|
174
283
|
|
|
175
284
|
// ── Hooks ──────────────────────────────────────────────────────────────
|
|
176
285
|
|
|
286
|
+
// ─────────────────────────────────────────
|
|
287
|
+
// RULE: react-no-useeffect-no-deps
|
|
288
|
+
// ROLE: Block useEffect without dependency array
|
|
289
|
+
// PURPOSE: Missing dependency array causes useEffect to run after
|
|
290
|
+
// EVERY render, leading to infinite loops, stale closures,
|
|
291
|
+
// and severe performance issues.
|
|
292
|
+
// EXAMPLE:
|
|
293
|
+
// WRONG:
|
|
294
|
+
// useEffect(() => {
|
|
295
|
+
// fetchData();
|
|
296
|
+
// }); // Runs on every render!
|
|
297
|
+
// RIGHT:
|
|
298
|
+
// useEffect(() => {
|
|
299
|
+
// fetchData();
|
|
300
|
+
// }, []); // Runs once on mount
|
|
301
|
+
// // With dependencies:
|
|
302
|
+
// useEffect(() => {
|
|
303
|
+
// fetchData(id);
|
|
304
|
+
// }, [id]); // Runs when id changes
|
|
305
|
+
// ─────────────────────────────────────────
|
|
177
306
|
{
|
|
178
307
|
id: 'react-no-useeffect-no-deps',
|
|
179
308
|
label: 'useEffect must have dependency array',
|
|
@@ -189,9 +318,7 @@ const reactRules: Rule[] = [
|
|
|
189
318
|
for (let i = 0; i < lines.length; i++) {
|
|
190
319
|
const line = lines[i];
|
|
191
320
|
|
|
192
|
-
// Detect useEffect( without a closing bracket on subsequent lines containing dependency array
|
|
193
321
|
if (/useEffect\s*\(\s*\(\s*\)\s*=>\s*\{/.test(line)) {
|
|
194
|
-
// Look ahead for the closing of useEffect — find , [] or , [deps])
|
|
195
322
|
let foundDeps = false;
|
|
196
323
|
let parenDepth = 0;
|
|
197
324
|
for (let j = i; j < Math.min(i + 50, lines.length); j++) {
|
|
@@ -199,7 +326,6 @@ const reactRules: Rule[] = [
|
|
|
199
326
|
if (ch === '(') parenDepth++;
|
|
200
327
|
if (ch === ')') parenDepth--;
|
|
201
328
|
}
|
|
202
|
-
// Check if the dependency array is present
|
|
203
329
|
if (/\]\s*\)\s*;?\s*$/.test(lines[j]) || /,\s*\[/.test(lines[j])) {
|
|
204
330
|
foundDeps = true;
|
|
205
331
|
break;
|
|
@@ -210,7 +336,7 @@ const reactRules: Rule[] = [
|
|
|
210
336
|
if (!foundDeps) {
|
|
211
337
|
violations.push({
|
|
212
338
|
line: i + 1,
|
|
213
|
-
message: 'useEffect without dependency array detected. Always specify dependencies
|
|
339
|
+
message: 'useEffect without dependency array detected. Always specify dependencies.',
|
|
214
340
|
});
|
|
215
341
|
}
|
|
216
342
|
}
|
|
@@ -222,6 +348,18 @@ const reactRules: Rule[] = [
|
|
|
222
348
|
category: 'React Hooks',
|
|
223
349
|
},
|
|
224
350
|
|
|
351
|
+
// ─────────────────────────────────────────
|
|
352
|
+
// RULE: react-no-inline-styles
|
|
353
|
+
// ROLE: Block inline style objects in JSX
|
|
354
|
+
// PURPOSE: Inline style={{ }} creates a new object on every render,
|
|
355
|
+
// preventing React's reconciliation from skipping unchanged
|
|
356
|
+
// elements. Use Tailwind or CSS modules instead.
|
|
357
|
+
// EXAMPLE:
|
|
358
|
+
// WRONG:
|
|
359
|
+
// <div style={{ color: 'red', fontSize: '1rem' }}>Hello</div>
|
|
360
|
+
// RIGHT:
|
|
361
|
+
// <div className="text-red-500 text-base">Hello</div>
|
|
362
|
+
// ─────────────────────────────────────────
|
|
225
363
|
{
|
|
226
364
|
id: 'react-no-inline-styles',
|
|
227
365
|
label: 'No inline style objects',
|
|
@@ -234,6 +372,21 @@ const reactRules: Rule[] = [
|
|
|
234
372
|
category: 'Styling',
|
|
235
373
|
},
|
|
236
374
|
|
|
375
|
+
// ─────────────────────────────────────────
|
|
376
|
+
// RULE: react-no-direct-dom
|
|
377
|
+
// ROLE: Block direct DOM manipulation in React components
|
|
378
|
+
// PURPOSE: React maintains a virtual DOM — direct DOM manipulation
|
|
379
|
+
// bypasses React's reconciliation and causes state
|
|
380
|
+
// inconsistencies, hydration errors, and bugs.
|
|
381
|
+
// EXAMPLE:
|
|
382
|
+
// WRONG:
|
|
383
|
+
// document.getElementById('input').value = 'hello';
|
|
384
|
+
// document.querySelector('.modal').classList.add('open');
|
|
385
|
+
// RIGHT:
|
|
386
|
+
// const inputRef = useRef<HTMLInputElement>(null);
|
|
387
|
+
// const [isOpen, setIsOpen] = useState(false);
|
|
388
|
+
// inputRef.current!.value = 'hello';
|
|
389
|
+
// ─────────────────────────────────────────
|
|
237
390
|
{
|
|
238
391
|
id: 'react-no-direct-dom',
|
|
239
392
|
label: 'No direct DOM manipulation',
|
|
@@ -248,6 +401,20 @@ const reactRules: Rule[] = [
|
|
|
248
401
|
|
|
249
402
|
// ── Component patterns ─────────────────────────────────────────────────
|
|
250
403
|
|
|
404
|
+
// ─────────────────────────────────────────
|
|
405
|
+
// RULE: react-no-react-fc
|
|
406
|
+
// ROLE: Block deprecated React.FC type
|
|
407
|
+
// PURPOSE: React.FC implicitly includes children, has issues with
|
|
408
|
+
// generics, and doesn't work well with defaultProps. Direct
|
|
409
|
+
// prop typing is cleaner and more explicit.
|
|
410
|
+
// EXAMPLE:
|
|
411
|
+
// WRONG:
|
|
412
|
+
// const UserCard: React.FC<Props> = ({ name }) => { ... };
|
|
413
|
+
// RIGHT:
|
|
414
|
+
// function UserCard({ name }: Props): React.ReactNode { ... }
|
|
415
|
+
// // Or:
|
|
416
|
+
// const UserCard = ({ name }: Props) => { ... };
|
|
417
|
+
// ─────────────────────────────────────────
|
|
251
418
|
{
|
|
252
419
|
id: 'react-no-react-fc',
|
|
253
420
|
label: 'Do not use React.FC',
|
|
@@ -260,6 +427,17 @@ const reactRules: Rule[] = [
|
|
|
260
427
|
category: 'TypeScript',
|
|
261
428
|
},
|
|
262
429
|
|
|
430
|
+
// ─────────────────────────────────────────
|
|
431
|
+
// RULE: react-explicit-return-types
|
|
432
|
+
// ROLE: Recommend explicit return types for component functions
|
|
433
|
+
// PURPOSE: Explicit return types catch accidental non-JSX returns,
|
|
434
|
+
// prevent undefined leaks, and improve code documentation.
|
|
435
|
+
// EXAMPLE:
|
|
436
|
+
// WRONG:
|
|
437
|
+
// export function UserCard(props: Props) { ... }
|
|
438
|
+
// RIGHT:
|
|
439
|
+
// export function UserCard(props: Props): React.ReactNode { ... }
|
|
440
|
+
// ─────────────────────────────────────────
|
|
263
441
|
{
|
|
264
442
|
id: 'react-explicit-return-types',
|
|
265
443
|
label: 'Add explicit return types for components',
|
|
@@ -272,6 +450,18 @@ const reactRules: Rule[] = [
|
|
|
272
450
|
category: 'TypeScript',
|
|
273
451
|
},
|
|
274
452
|
|
|
453
|
+
// ─────────────────────────────────────────
|
|
454
|
+
// RULE: react-no-prop-spreading
|
|
455
|
+
// ROLE: Discourage prop spreading for API clarity
|
|
456
|
+
// PURPOSE: Spreading props ({...props}) hides the component API,
|
|
457
|
+
// passes unintended props, and breaks TypeScript strictness.
|
|
458
|
+
// Explicitly passing required props is safer and self-documenting.
|
|
459
|
+
// EXAMPLE:
|
|
460
|
+
// WRONG:
|
|
461
|
+
// <Button {...props} />
|
|
462
|
+
// RIGHT:
|
|
463
|
+
// <Button onClick={props.onClick} disabled={props.disabled} label={props.label} />
|
|
464
|
+
// ─────────────────────────────────────────
|
|
275
465
|
{
|
|
276
466
|
id: 'react-no-prop-spreading',
|
|
277
467
|
label: 'Avoid prop spreading ({...props})',
|
|
@@ -289,6 +479,20 @@ const reactRules: Rule[] = [
|
|
|
289
479
|
// NOTE: 'react-no-dangerously-set-html' removed — covered by ESLint 'react/no-danger' rule
|
|
290
480
|
// NOTE: 'no-eval' removed — covered by ESLint 'no-eval' rule
|
|
291
481
|
|
|
482
|
+
// ─────────────────────────────────────────
|
|
483
|
+
// RULE: no-hardcoded-credentials
|
|
484
|
+
// ROLE: Block hardcoded secrets in source code
|
|
485
|
+
// PURPOSE: Hardcoded API keys, passwords, and tokens get committed to
|
|
486
|
+
// git, leaked in public repos, and exposed in client bundles.
|
|
487
|
+
// Always use environment variables (.env).
|
|
488
|
+
// EXAMPLE:
|
|
489
|
+
// WRONG:
|
|
490
|
+
// const apiKey = 'sk-1234567890abcdef';
|
|
491
|
+
// const password = 'admin123';
|
|
492
|
+
// RIGHT:
|
|
493
|
+
// const apiKey = process.env.API_KEY;
|
|
494
|
+
// const password = process.env.ADMIN_PASSWORD;
|
|
495
|
+
// ─────────────────────────────────────────
|
|
292
496
|
{
|
|
293
497
|
id: 'no-hardcoded-credentials',
|
|
294
498
|
label: 'No hardcoded credentials',
|
|
@@ -327,6 +531,20 @@ const reactRules: Rule[] = [
|
|
|
327
531
|
category: 'Security',
|
|
328
532
|
},
|
|
329
533
|
|
|
534
|
+
// ─────────────────────────────────────────
|
|
535
|
+
// RULE: require-https
|
|
536
|
+
// ROLE: Block insecure HTTP URLs in source code
|
|
537
|
+
// PURPOSE: HTTP transmits data in plain text — passwords, tokens, and
|
|
538
|
+
// user data can be intercepted. HTTPS is mandatory for all
|
|
539
|
+
// production API calls and external resources.
|
|
540
|
+
// EXAMPLE:
|
|
541
|
+
// WRONG:
|
|
542
|
+
// fetch('http://api.example.com/users');
|
|
543
|
+
// RIGHT:
|
|
544
|
+
// fetch('https://api.example.com/users');
|
|
545
|
+
// // localhost is exempt:
|
|
546
|
+
// fetch('http://localhost:3000/api');
|
|
547
|
+
// ─────────────────────────────────────────
|
|
330
548
|
{
|
|
331
549
|
id: 'require-https',
|
|
332
550
|
label: 'Require HTTPS URLs',
|
|
@@ -341,6 +559,19 @@ const reactRules: Rule[] = [
|
|
|
341
559
|
|
|
342
560
|
// ── Performance Rules ──────────────────────────────────────────────────
|
|
343
561
|
|
|
562
|
+
// ─────────────────────────────────────────
|
|
563
|
+
// RULE: react-no-anonymous-functions-in-render
|
|
564
|
+
// ROLE: Block anonymous functions in JSX event handlers
|
|
565
|
+
// PURPOSE: Arrow functions in props create a new function reference on
|
|
566
|
+
// every render, defeating React.memo and causing child
|
|
567
|
+
// components to re-render unnecessarily.
|
|
568
|
+
// EXAMPLE:
|
|
569
|
+
// WRONG:
|
|
570
|
+
// <Button onClick={() => handleClick(id)} />
|
|
571
|
+
// RIGHT:
|
|
572
|
+
// const handleButtonClick = useCallback(() => handleClick(id), [id]);
|
|
573
|
+
// <Button onClick={handleButtonClick} />
|
|
574
|
+
// ─────────────────────────────────────────
|
|
344
575
|
{
|
|
345
576
|
id: 'react-no-anonymous-functions-in-render',
|
|
346
577
|
label: 'No anonymous functions in JSX props',
|
|
@@ -353,6 +584,19 @@ const reactRules: Rule[] = [
|
|
|
353
584
|
category: 'Performance',
|
|
354
585
|
},
|
|
355
586
|
|
|
587
|
+
// ─────────────────────────────────────────
|
|
588
|
+
// RULE: react-missing-key-prop
|
|
589
|
+
// ROLE: Block list rendering without key props
|
|
590
|
+
// PURPOSE: React uses key to identify which items changed, were added,
|
|
591
|
+
// or removed. Missing keys cause UI bugs and poor performance.
|
|
592
|
+
// Array index as key causes issues with reordering.
|
|
593
|
+
// EXAMPLE:
|
|
594
|
+
// WRONG:
|
|
595
|
+
// items.map(item => <Card title={item.name} />);
|
|
596
|
+
// items.map((item, i) => <Card key={i} title={item.name} />);
|
|
597
|
+
// RIGHT:
|
|
598
|
+
// items.map(item => <Card key={item.id} title={item.name} />);
|
|
599
|
+
// ─────────────────────────────────────────
|
|
356
600
|
{
|
|
357
601
|
id: 'react-missing-key-prop',
|
|
358
602
|
label: 'Missing key prop in list',
|
|
@@ -367,9 +611,7 @@ const reactRules: Rule[] = [
|
|
|
367
611
|
|
|
368
612
|
for (let i = 0; i < lines.length; i++) {
|
|
369
613
|
const line = lines[i];
|
|
370
|
-
// Detect .map( with JSX but no key prop nearby
|
|
371
614
|
if (/\.\s*map\s*\(/.test(line)) {
|
|
372
|
-
// Look ahead for JSX opening tag
|
|
373
615
|
let foundKey = false;
|
|
374
616
|
for (let j = i; j < Math.min(i + 10, lines.length); j++) {
|
|
375
617
|
if (/key\s*=/.test(lines[j])) {
|
|
@@ -378,7 +620,6 @@ const reactRules: Rule[] = [
|
|
|
378
620
|
}
|
|
379
621
|
}
|
|
380
622
|
|
|
381
|
-
// Check if there's JSX in the map
|
|
382
623
|
let hasJsx = false;
|
|
383
624
|
for (let j = i; j < Math.min(i + 10, lines.length); j++) {
|
|
384
625
|
if (/<[A-Z]/.test(lines[j]) || /<[a-z]/.test(lines[j])) {
|
|
@@ -402,6 +643,20 @@ const reactRules: Rule[] = [
|
|
|
402
643
|
category: 'Performance',
|
|
403
644
|
},
|
|
404
645
|
|
|
646
|
+
// ─────────────────────────────────────────
|
|
647
|
+
// RULE: no-large-bundle-imports
|
|
648
|
+
// ROLE: Block large library wildcard imports
|
|
649
|
+
// PURPOSE: Importing entire lodash (~70KB) or moment (~290KB) when
|
|
650
|
+
// you only need one function wastes bandwidth. Use specific
|
|
651
|
+
// imports for tree-shaking support.
|
|
652
|
+
// EXAMPLE:
|
|
653
|
+
// WRONG:
|
|
654
|
+
// import _ from 'lodash';
|
|
655
|
+
// import moment from 'moment';
|
|
656
|
+
// RIGHT:
|
|
657
|
+
// import get from 'lodash/get';
|
|
658
|
+
// import { format } from 'date-fns';
|
|
659
|
+
// ─────────────────────────────────────────
|
|
405
660
|
{
|
|
406
661
|
id: 'no-large-bundle-imports',
|
|
407
662
|
label: 'Avoid large library imports',
|
|
@@ -418,6 +673,19 @@ const reactRules: Rule[] = [
|
|
|
418
673
|
|
|
419
674
|
// ── Accessibility Rules ────────────────────────────────────────────────
|
|
420
675
|
|
|
676
|
+
// ─────────────────────────────────────────
|
|
677
|
+
// RULE: require-button-type
|
|
678
|
+
// ROLE: Enforce type attribute on all button elements
|
|
679
|
+
// PURPOSE: Buttons default to type="submit", which triggers form
|
|
680
|
+
// submission unexpectedly. Always specify type explicitly
|
|
681
|
+
// to prevent accidental form submissions.
|
|
682
|
+
// EXAMPLE:
|
|
683
|
+
// WRONG:
|
|
684
|
+
// <button onClick={handleClick}>Click me</button>
|
|
685
|
+
// RIGHT:
|
|
686
|
+
// <button type="button" onClick={handleClick}>Click me</button>
|
|
687
|
+
// <button type="submit">Submit</button>
|
|
688
|
+
// ─────────────────────────────────────────
|
|
421
689
|
{
|
|
422
690
|
id: 'require-button-type',
|
|
423
691
|
label: 'Buttons must have type attribute',
|
|
@@ -448,6 +716,21 @@ const reactRules: Rule[] = [
|
|
|
448
716
|
|
|
449
717
|
// ── Additional Performance Rules ───────────────────────────────────────
|
|
450
718
|
|
|
719
|
+
// ─────────────────────────────────────────
|
|
720
|
+
// RULE: react-prefer-lazy-loading
|
|
721
|
+
// ROLE: Recommend code splitting for route components
|
|
722
|
+
// PURPOSE: Eagerly importing all page components increases the initial
|
|
723
|
+
// bundle size. React.lazy() + Suspense splits code by route,
|
|
724
|
+
// loading pages only when navigated to.
|
|
725
|
+
// EXAMPLE:
|
|
726
|
+
// WRONG:
|
|
727
|
+
// import Dashboard from '../pages/Dashboard';
|
|
728
|
+
// import Settings from '../pages/Settings';
|
|
729
|
+
// RIGHT:
|
|
730
|
+
// const Dashboard = lazy(() => import('../pages/Dashboard'));
|
|
731
|
+
// const Settings = lazy(() => import('../pages/Settings'));
|
|
732
|
+
// <Suspense fallback={<Spinner />}><Dashboard /></Suspense>
|
|
733
|
+
// ─────────────────────────────────────────
|
|
451
734
|
{
|
|
452
735
|
id: 'react-prefer-lazy-loading',
|
|
453
736
|
label: 'Consider lazy loading for routes',
|
|
@@ -460,6 +743,22 @@ const reactRules: Rule[] = [
|
|
|
460
743
|
category: 'Performance',
|
|
461
744
|
},
|
|
462
745
|
|
|
746
|
+
// ─────────────────────────────────────────
|
|
747
|
+
// RULE: react-memo-expensive-components
|
|
748
|
+
// ROLE: Recommend React.memo for large render trees
|
|
749
|
+
// PURPOSE: Components with expensive render logic (>500 chars of JSX)
|
|
750
|
+
// should be wrapped in React.memo to skip re-renders when
|
|
751
|
+
// props haven't changed.
|
|
752
|
+
// EXAMPLE:
|
|
753
|
+
// WRONG:
|
|
754
|
+
// export function DataTable({ rows }) {
|
|
755
|
+
// // ... 500+ lines of complex rendering
|
|
756
|
+
// }
|
|
757
|
+
// RIGHT:
|
|
758
|
+
// export const DataTable = memo(function DataTable({ rows }) {
|
|
759
|
+
// // ... 500+ lines of complex rendering
|
|
760
|
+
// });
|
|
761
|
+
// ─────────────────────────────────────────
|
|
463
762
|
{
|
|
464
763
|
id: 'react-memo-expensive-components',
|
|
465
764
|
label: 'Consider React.memo for expensive renders',
|
|
@@ -472,6 +771,21 @@ const reactRules: Rule[] = [
|
|
|
472
771
|
category: 'Performance',
|
|
473
772
|
},
|
|
474
773
|
|
|
774
|
+
// ─────────────────────────────────────────
|
|
775
|
+
// RULE: no-object-array-in-deps
|
|
776
|
+
// ROLE: Block unstable references in hook dependency arrays
|
|
777
|
+
// PURPOSE: Objects and arrays create new references on every render.
|
|
778
|
+
// Putting them in dependency arrays triggers hooks on every
|
|
779
|
+
// render, defeating the purpose of the dependency array.
|
|
780
|
+
// EXAMPLE:
|
|
781
|
+
// WRONG:
|
|
782
|
+
// useEffect(() => { ... }, [{ id: 1 }]);
|
|
783
|
+
// useMemo(() => { ... }, [[1, 2, 3]]);
|
|
784
|
+
// RIGHT:
|
|
785
|
+
// useEffect(() => { ... }, [id]);
|
|
786
|
+
// const configRef = useRef(config); // stable reference
|
|
787
|
+
// useEffect(() => { ... }, [configRef]);
|
|
788
|
+
// ─────────────────────────────────────────
|
|
475
789
|
{
|
|
476
790
|
id: 'no-object-array-in-deps',
|
|
477
791
|
label: 'Avoid objects/arrays in dependency arrays',
|
|
@@ -486,6 +800,18 @@ const reactRules: Rule[] = [
|
|
|
486
800
|
|
|
487
801
|
// ── Additional Best Practice Rules ────────────────────────────────────
|
|
488
802
|
|
|
803
|
+
// ─────────────────────────────────────────
|
|
804
|
+
// RULE: react-use-fragments
|
|
805
|
+
// ROLE: Recommend React fragments over wrapper divs
|
|
806
|
+
// PURPOSE: Unnecessary wrapper <div>s add DOM nodes, break CSS layouts
|
|
807
|
+
// (flexbox, grid), increase memory usage, and cause
|
|
808
|
+
// accessibility issues with screen readers.
|
|
809
|
+
// EXAMPLE:
|
|
810
|
+
// WRONG:
|
|
811
|
+
// return (<div><Header /><Content /></div>);
|
|
812
|
+
// RIGHT:
|
|
813
|
+
// return (<><Header /><Content /></>);
|
|
814
|
+
// ─────────────────────────────────────────
|
|
489
815
|
{
|
|
490
816
|
id: 'react-use-fragments',
|
|
491
817
|
label: 'Use React fragments instead of divs',
|
|
@@ -498,6 +824,18 @@ const reactRules: Rule[] = [
|
|
|
498
824
|
category: 'Best Practices',
|
|
499
825
|
},
|
|
500
826
|
|
|
827
|
+
// ─────────────────────────────────────────
|
|
828
|
+
// RULE: prefer-optional-chaining
|
|
829
|
+
// ROLE: Recommend modern null-safe access patterns
|
|
830
|
+
// PURPOSE: Chained && checks are verbose and error-prone. Optional
|
|
831
|
+
// chaining (?.) is built into the language, more readable,
|
|
832
|
+
// and handles null/undefined safely.
|
|
833
|
+
// EXAMPLE:
|
|
834
|
+
// WRONG:
|
|
835
|
+
// user && user.profile && user.profile.name
|
|
836
|
+
// RIGHT:
|
|
837
|
+
// user?.profile?.name
|
|
838
|
+
// ─────────────────────────────────────────
|
|
501
839
|
{
|
|
502
840
|
id: 'prefer-optional-chaining',
|
|
503
841
|
label: 'Use optional chaining',
|
|
@@ -510,6 +848,18 @@ const reactRules: Rule[] = [
|
|
|
510
848
|
category: 'Modern Syntax',
|
|
511
849
|
},
|
|
512
850
|
|
|
851
|
+
// ─────────────────────────────────────────
|
|
852
|
+
// RULE: prefer-nullish-coalescing
|
|
853
|
+
// ROLE: Recommend ?? over || for default values
|
|
854
|
+
// PURPOSE: || treats 0, '', and false as falsy and replaces them.
|
|
855
|
+
// ?? only replaces null/undefined, preserving valid falsy
|
|
856
|
+
// values like 0 (count) and '' (empty string).
|
|
857
|
+
// EXAMPLE:
|
|
858
|
+
// WRONG:
|
|
859
|
+
// const count = data.count || 10; // 0 becomes 10!
|
|
860
|
+
// RIGHT:
|
|
861
|
+
// const count = data.count ?? 10; // 0 stays 0
|
|
862
|
+
// ─────────────────────────────────────────
|
|
513
863
|
{
|
|
514
864
|
id: 'prefer-nullish-coalescing',
|
|
515
865
|
label: 'Use nullish coalescing (??)',
|
|
@@ -522,6 +872,24 @@ const reactRules: Rule[] = [
|
|
|
522
872
|
category: 'Modern Syntax',
|
|
523
873
|
},
|
|
524
874
|
|
|
875
|
+
// ─────────────────────────────────────────
|
|
876
|
+
// RULE: react-error-boundary-usage
|
|
877
|
+
// ROLE: Recommend error boundaries for crash prevention
|
|
878
|
+
// PURPOSE: Without error boundaries, a single thrown error crashes
|
|
879
|
+
// the entire React app. Error boundaries catch render errors
|
|
880
|
+
// and display fallback UI instead.
|
|
881
|
+
// EXAMPLE:
|
|
882
|
+
// WRONG:
|
|
883
|
+
// <App>
|
|
884
|
+
// <BuggyComponent /> {/* Crash kills entire app */}
|
|
885
|
+
// </App>
|
|
886
|
+
// RIGHT:
|
|
887
|
+
// <App>
|
|
888
|
+
// <ErrorBoundary fallback={<ErrorPage />}>
|
|
889
|
+
// <BuggyComponent /> {/* Crash is contained */}
|
|
890
|
+
// </ErrorBoundary>
|
|
891
|
+
// </App>
|
|
892
|
+
// ─────────────────────────────────────────
|
|
525
893
|
{
|
|
526
894
|
id: 'react-error-boundary-usage',
|
|
527
895
|
label: 'Consider error boundaries',
|
|
@@ -534,6 +902,19 @@ const reactRules: Rule[] = [
|
|
|
534
902
|
category: 'Error Handling',
|
|
535
903
|
},
|
|
536
904
|
|
|
905
|
+
// ─────────────────────────────────────────
|
|
906
|
+
// RULE: no-sync-external-calls
|
|
907
|
+
// ROLE: Block synchronous external API calls
|
|
908
|
+
// PURPOSE: Synchronous fetch/XHR blocks the main thread, freezing
|
|
909
|
+
// the UI completely. Always use async/await for API calls,
|
|
910
|
+
// file operations, and external services.
|
|
911
|
+
// EXAMPLE:
|
|
912
|
+
// WRONG:
|
|
913
|
+
// const data = fetch('/api/data'); // No await!
|
|
914
|
+
// RIGHT:
|
|
915
|
+
// const data = await fetch('/api/data');
|
|
916
|
+
// const json = await data.json();
|
|
917
|
+
// ─────────────────────────────────────────
|
|
537
918
|
{
|
|
538
919
|
id: 'no-sync-external-calls',
|
|
539
920
|
label: 'Avoid synchronous external calls',
|
|
@@ -546,6 +927,18 @@ const reactRules: Rule[] = [
|
|
|
546
927
|
category: 'Async Patterns',
|
|
547
928
|
},
|
|
548
929
|
|
|
930
|
+
// ─────────────────────────────────────────
|
|
931
|
+
// RULE: react-prefer-controlled-inputs
|
|
932
|
+
// ROLE: Recommend controlled components over uncontrolled
|
|
933
|
+
// PURPOSE: Controlled components (value + onChange) keep React as the
|
|
934
|
+
// single source of truth. Uncontrolled (defaultValue + ref)
|
|
935
|
+
// creates two sources of truth and makes validation harder.
|
|
936
|
+
// EXAMPLE:
|
|
937
|
+
// WRONG:
|
|
938
|
+
// <input defaultValue="hello" ref={inputRef} />
|
|
939
|
+
// RIGHT:
|
|
940
|
+
// <input value={name} onChange={(e) => setName(e.target.value)} />
|
|
941
|
+
// ─────────────────────────────────────────
|
|
549
942
|
{
|
|
550
943
|
id: 'react-prefer-controlled-inputs',
|
|
551
944
|
label: 'Prefer controlled components',
|
|
@@ -560,6 +953,23 @@ const reactRules: Rule[] = [
|
|
|
560
953
|
|
|
561
954
|
// NOTE: 'no-index-as-key' removed — covered by ESLint 'react/no-array-index-key' rule
|
|
562
955
|
|
|
956
|
+
// ─────────────────────────────────────────
|
|
957
|
+
// RULE: react-cleanup-effects
|
|
958
|
+
// ROLE: Enforce cleanup functions in side-effect hooks
|
|
959
|
+
// PURPOSE: useEffect with subscriptions, timers, or event listeners
|
|
960
|
+
// must return a cleanup function. Without it, listeners
|
|
961
|
+
// accumulate on every re-render, causing memory leaks.
|
|
962
|
+
// EXAMPLE:
|
|
963
|
+
// WRONG:
|
|
964
|
+
// useEffect(() => {
|
|
965
|
+
// window.addEventListener('resize', handleResize);
|
|
966
|
+
// }, []);
|
|
967
|
+
// RIGHT:
|
|
968
|
+
// useEffect(() => {
|
|
969
|
+
// window.addEventListener('resize', handleResize);
|
|
970
|
+
// return () => window.removeEventListener('resize', handleResize);
|
|
971
|
+
// }, []);
|
|
972
|
+
// ─────────────────────────────────────────
|
|
563
973
|
{
|
|
564
974
|
id: 'react-cleanup-effects',
|
|
565
975
|
label: 'Cleanup side effects',
|
|
@@ -572,6 +982,20 @@ const reactRules: Rule[] = [
|
|
|
572
982
|
category: 'Memory Management',
|
|
573
983
|
},
|
|
574
984
|
|
|
985
|
+
// ─────────────────────────────────────────
|
|
986
|
+
// RULE: prefer-const-assertion
|
|
987
|
+
// ROLE: Recommend const assertions for literal types
|
|
988
|
+
// PURPOSE: "as const" narrows types to literal values and makes
|
|
989
|
+
// objects/arrays readonly, improving type safety and
|
|
990
|
+
// enabling discriminated unions.
|
|
991
|
+
// EXAMPLE:
|
|
992
|
+
// WRONG:
|
|
993
|
+
// const ROLES: string[] = ['admin', 'user', 'viewer'];
|
|
994
|
+
// // Type: string[] — loses literal type info
|
|
995
|
+
// RIGHT:
|
|
996
|
+
// const ROLES = ['admin', 'user', 'viewer'] as const;
|
|
997
|
+
// // Type: readonly ['admin', 'user', 'viewer']
|
|
998
|
+
// ─────────────────────────────────────────
|
|
575
999
|
{
|
|
576
1000
|
id: 'prefer-const-assertion',
|
|
577
1001
|
label: 'Use const assertions for literal types',
|