eslint-plugin-absolute 0.1.6 → 0.2.1
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/.absolutejs/eslint.cache.json +49 -0
- package/.absolutejs/prettier.cache.json +49 -0
- package/.absolutejs/tsconfig.tsbuildinfo +1 -0
- package/.claude/settings.local.json +10 -0
- package/dist/index.js +1787 -1457
- package/eslint.config.mjs +107 -0
- package/package.json +15 -12
- package/src/index.ts +45 -0
- package/src/rules/explicit-object-types.ts +75 -0
- package/src/rules/inline-style-limit.ts +88 -0
- package/src/rules/localize-react-props.ts +454 -0
- package/src/rules/max-depth-extended.ts +153 -0
- package/src/rules/{max-jsx-nesting.js → max-jsx-nesting.ts} +37 -38
- package/src/rules/min-var-length.ts +360 -0
- package/src/rules/no-button-navigation.ts +270 -0
- package/src/rules/no-explicit-return-types.ts +83 -0
- package/src/rules/no-inline-prop-types.ts +68 -0
- package/src/rules/no-multi-style-objects.ts +80 -0
- package/src/rules/no-nested-jsx-return.ts +205 -0
- package/src/rules/no-or-none-component.ts +63 -0
- package/src/rules/no-transition-cssproperties.ts +131 -0
- package/src/rules/no-unnecessary-div.ts +65 -0
- package/src/rules/no-unnecessary-key.ts +111 -0
- package/src/rules/no-useless-function.ts +56 -0
- package/src/rules/seperate-style-files.ts +79 -0
- package/src/rules/sort-exports.ts +424 -0
- package/src/rules/sort-keys-fixable.ts +647 -0
- package/src/rules/spring-naming-convention.ts +160 -0
- package/tsconfig.json +4 -1
- package/src/index.js +0 -45
- package/src/rules/explicit-object-types.js +0 -54
- package/src/rules/inline-style-limit.js +0 -77
- package/src/rules/localize-react-props.js +0 -418
- package/src/rules/max-depth-extended.js +0 -124
- package/src/rules/min-var-length.js +0 -300
- package/src/rules/no-button-navigation.js +0 -232
- package/src/rules/no-explicit-return-types.js +0 -64
- package/src/rules/no-inline-prop-types.js +0 -55
- package/src/rules/no-multi-style-objects.js +0 -70
- package/src/rules/no-nested-jsx-return.js +0 -154
- package/src/rules/no-or-none-component.js +0 -50
- package/src/rules/no-transition-cssproperties.js +0 -102
- package/src/rules/no-unnecessary-div.js +0 -40
- package/src/rules/no-unnecessary-key.js +0 -128
- package/src/rules/no-useless-function.js +0 -43
- package/src/rules/seperate-style-files.js +0 -62
- package/src/rules/sort-exports.js +0 -397
- package/src/rules/sort-keys-fixable.js +0 -459
- package/src/rules/spring-naming-convention.js +0 -111
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
import { TSESLint, TSESTree, AST_NODE_TYPES } from "@typescript-eslint/utils";
|
|
2
|
+
|
|
3
|
+
type Options = [];
|
|
4
|
+
type MessageIds = "stateAndSetterToChild" | "variableToChild";
|
|
5
|
+
|
|
6
|
+
type ComponentFunction =
|
|
7
|
+
| TSESTree.FunctionDeclaration
|
|
8
|
+
| TSESTree.FunctionExpression
|
|
9
|
+
| TSESTree.ArrowFunctionExpression;
|
|
10
|
+
|
|
11
|
+
type Usage = {
|
|
12
|
+
jsxUsageSet: Set<TSESTree.JSXElement>;
|
|
13
|
+
hasOutsideUsage: boolean;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
type CandidateVariable = {
|
|
17
|
+
node: TSESTree.VariableDeclarator;
|
|
18
|
+
varName: string;
|
|
19
|
+
componentName: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const localizeReactProps: TSESLint.RuleModule<MessageIds, Options> = {
|
|
23
|
+
create(context) {
|
|
24
|
+
// A list of candidate variables for reporting (for general variables only).
|
|
25
|
+
const candidateVariables: CandidateVariable[] = [];
|
|
26
|
+
|
|
27
|
+
const getSingleSetElement = <T>(set: Set<T>) => {
|
|
28
|
+
for (const value of set) {
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const getRightmostJSXIdentifier = (
|
|
35
|
+
name: TSESTree.JSXTagNameExpression
|
|
36
|
+
) => {
|
|
37
|
+
let current: TSESTree.JSXTagNameExpression = name;
|
|
38
|
+
while (current.type === AST_NODE_TYPES.JSXMemberExpression) {
|
|
39
|
+
current = current.property;
|
|
40
|
+
}
|
|
41
|
+
if (current.type === AST_NODE_TYPES.JSXIdentifier) {
|
|
42
|
+
return current;
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const getLeftmostJSXIdentifier = (
|
|
48
|
+
name: TSESTree.JSXTagNameExpression
|
|
49
|
+
) => {
|
|
50
|
+
let current: TSESTree.JSXTagNameExpression = name;
|
|
51
|
+
while (current.type === AST_NODE_TYPES.JSXMemberExpression) {
|
|
52
|
+
current = current.object;
|
|
53
|
+
}
|
|
54
|
+
if (current.type === AST_NODE_TYPES.JSXIdentifier) {
|
|
55
|
+
return current;
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// Helper: Extract the component name from a JSXElement.
|
|
61
|
+
const getJSXElementName = (jsxElement: TSESTree.JSXElement | null) => {
|
|
62
|
+
if (
|
|
63
|
+
!jsxElement ||
|
|
64
|
+
!jsxElement.openingElement ||
|
|
65
|
+
!jsxElement.openingElement.name
|
|
66
|
+
) {
|
|
67
|
+
return "";
|
|
68
|
+
}
|
|
69
|
+
const nameNode = jsxElement.openingElement.name;
|
|
70
|
+
if (nameNode.type === AST_NODE_TYPES.JSXIdentifier) {
|
|
71
|
+
return nameNode.name;
|
|
72
|
+
}
|
|
73
|
+
const rightmost = getRightmostJSXIdentifier(nameNode);
|
|
74
|
+
if (rightmost) {
|
|
75
|
+
return rightmost.name;
|
|
76
|
+
}
|
|
77
|
+
return "";
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// Helper: Check if the node is a call to useState.
|
|
81
|
+
const isUseStateCall = (
|
|
82
|
+
node: TSESTree.Node | null
|
|
83
|
+
): node is TSESTree.CallExpression =>
|
|
84
|
+
node !== null &&
|
|
85
|
+
node.type === AST_NODE_TYPES.CallExpression &&
|
|
86
|
+
node.callee !== null &&
|
|
87
|
+
((node.callee.type === AST_NODE_TYPES.Identifier &&
|
|
88
|
+
node.callee.name === "useState") ||
|
|
89
|
+
(node.callee.type === AST_NODE_TYPES.MemberExpression &&
|
|
90
|
+
node.callee.property !== null &&
|
|
91
|
+
node.callee.property.type === AST_NODE_TYPES.Identifier &&
|
|
92
|
+
node.callee.property.name === "useState"));
|
|
93
|
+
|
|
94
|
+
// Helper: Check if a call expression is a hook call (other than useState).
|
|
95
|
+
const isHookCall = (
|
|
96
|
+
node: TSESTree.Node | null
|
|
97
|
+
): node is TSESTree.CallExpression =>
|
|
98
|
+
node !== null &&
|
|
99
|
+
node.type === AST_NODE_TYPES.CallExpression &&
|
|
100
|
+
node.callee !== null &&
|
|
101
|
+
node.callee.type === AST_NODE_TYPES.Identifier &&
|
|
102
|
+
/^use[A-Z]/.test(node.callee.name) &&
|
|
103
|
+
node.callee.name !== "useState";
|
|
104
|
+
|
|
105
|
+
// Helper: Walk upward to find the closest JSXElement ancestor.
|
|
106
|
+
const getJSXAncestor = (node: TSESTree.Node) => {
|
|
107
|
+
let current: TSESTree.Node | null | undefined = node.parent;
|
|
108
|
+
while (current) {
|
|
109
|
+
if (current.type === AST_NODE_TYPES.JSXElement) {
|
|
110
|
+
return current;
|
|
111
|
+
}
|
|
112
|
+
current = current.parent;
|
|
113
|
+
}
|
|
114
|
+
return null;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const getTagNameFromOpening = (
|
|
118
|
+
openingElement: TSESTree.JSXOpeningElement
|
|
119
|
+
) => {
|
|
120
|
+
const nameNode = openingElement.name;
|
|
121
|
+
if (nameNode.type === AST_NODE_TYPES.JSXIdentifier) {
|
|
122
|
+
return nameNode.name;
|
|
123
|
+
}
|
|
124
|
+
const rightmost = getRightmostJSXIdentifier(nameNode);
|
|
125
|
+
return rightmost ? rightmost.name : null;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const isProviderOrContext = (tagName: string) =>
|
|
129
|
+
tagName.endsWith("Provider") || tagName.endsWith("Context");
|
|
130
|
+
|
|
131
|
+
const isValueAttributeOnProvider = (node: TSESTree.Node) =>
|
|
132
|
+
node.type === AST_NODE_TYPES.JSXAttribute &&
|
|
133
|
+
node.name &&
|
|
134
|
+
node.name.type === AST_NODE_TYPES.JSXIdentifier &&
|
|
135
|
+
node.name.name === "value" &&
|
|
136
|
+
node.parent &&
|
|
137
|
+
node.parent.type === AST_NODE_TYPES.JSXOpeningElement &&
|
|
138
|
+
(() => {
|
|
139
|
+
const tagName = getTagNameFromOpening(node.parent);
|
|
140
|
+
return tagName !== null && isProviderOrContext(tagName);
|
|
141
|
+
})();
|
|
142
|
+
|
|
143
|
+
// Helper: Check whether the given node is inside a JSXAttribute "value"
|
|
144
|
+
// that belongs to a context-like component (i.e. tag name ends with Provider or Context).
|
|
145
|
+
const isContextProviderValueProp = (node: TSESTree.Node) => {
|
|
146
|
+
let current: TSESTree.Node | null | undefined = node.parent;
|
|
147
|
+
while (current) {
|
|
148
|
+
if (isValueAttributeOnProvider(current)) {
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
current = current.parent;
|
|
152
|
+
}
|
|
153
|
+
return false;
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
// Helper: Determine if a JSXElement is a custom component (tag name begins with an uppercase letter).
|
|
157
|
+
const isCustomJSXElement = (jsxElement: TSESTree.JSXElement | null) => {
|
|
158
|
+
if (
|
|
159
|
+
!jsxElement ||
|
|
160
|
+
!jsxElement.openingElement ||
|
|
161
|
+
!jsxElement.openingElement.name
|
|
162
|
+
) {
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
const nameNode = jsxElement.openingElement.name;
|
|
166
|
+
if (nameNode.type === AST_NODE_TYPES.JSXIdentifier) {
|
|
167
|
+
return /^[A-Z]/.test(nameNode.name);
|
|
168
|
+
}
|
|
169
|
+
const leftmost = getLeftmostJSXIdentifier(nameNode);
|
|
170
|
+
return leftmost !== null && /^[A-Z]/.test(leftmost.name);
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
// Helper: Find the nearest enclosing function (assumed to be the component).
|
|
174
|
+
const getComponentFunction = (node: TSESTree.Node | null) => {
|
|
175
|
+
let current: TSESTree.Node | null | undefined = node;
|
|
176
|
+
while (current) {
|
|
177
|
+
if (
|
|
178
|
+
current.type === AST_NODE_TYPES.FunctionDeclaration ||
|
|
179
|
+
current.type === AST_NODE_TYPES.FunctionExpression ||
|
|
180
|
+
current.type === AST_NODE_TYPES.ArrowFunctionExpression
|
|
181
|
+
) {
|
|
182
|
+
return current;
|
|
183
|
+
}
|
|
184
|
+
current = current.parent;
|
|
185
|
+
}
|
|
186
|
+
return null;
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const findVariableForIdentifier = (identifier: TSESTree.Identifier) => {
|
|
190
|
+
let scope: TSESLint.Scope.Scope | null =
|
|
191
|
+
context.sourceCode.getScope(identifier);
|
|
192
|
+
while (scope) {
|
|
193
|
+
const found = scope.variables.find((variable) =>
|
|
194
|
+
variable.defs.some((def) => def.name === identifier)
|
|
195
|
+
);
|
|
196
|
+
if (found) {
|
|
197
|
+
return found;
|
|
198
|
+
}
|
|
199
|
+
scope = scope.upper ?? null;
|
|
200
|
+
}
|
|
201
|
+
return null;
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
// Analyze variable usage using ESLint scopes (no manual AST crawling).
|
|
205
|
+
// Only count a usage if it occurs inside a custom JSX element (and is not inside a context provider's "value" prop).
|
|
206
|
+
const classifyReference = (
|
|
207
|
+
reference: TSESLint.Scope.Reference,
|
|
208
|
+
declarationId: TSESTree.Identifier,
|
|
209
|
+
jsxUsageSet: Set<TSESTree.JSXElement>
|
|
210
|
+
) => {
|
|
211
|
+
const { identifier } = reference;
|
|
212
|
+
|
|
213
|
+
if (
|
|
214
|
+
identifier === declarationId ||
|
|
215
|
+
isContextProviderValueProp(identifier)
|
|
216
|
+
) {
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const jsxAncestor = getJSXAncestor(identifier);
|
|
221
|
+
if (jsxAncestor && isCustomJSXElement(jsxAncestor)) {
|
|
222
|
+
jsxUsageSet.add(jsxAncestor);
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
return true;
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
const analyzeVariableUsage = (
|
|
229
|
+
declarationId: TSESTree.Identifier
|
|
230
|
+
): Usage => {
|
|
231
|
+
const variable = findVariableForIdentifier(declarationId);
|
|
232
|
+
if (!variable) {
|
|
233
|
+
return {
|
|
234
|
+
hasOutsideUsage: false,
|
|
235
|
+
jsxUsageSet: new Set<TSESTree.JSXElement>()
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const jsxUsageSet = new Set<TSESTree.JSXElement>();
|
|
240
|
+
const hasOutsideUsage = variable.references.some((ref) =>
|
|
241
|
+
classifyReference(ref, declarationId, jsxUsageSet)
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
return {
|
|
245
|
+
hasOutsideUsage,
|
|
246
|
+
jsxUsageSet
|
|
247
|
+
};
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
// Manage hook-derived variables.
|
|
251
|
+
const componentHookVars = new WeakMap<ComponentFunction, Set<string>>();
|
|
252
|
+
const getHookSet = (componentFunction: ComponentFunction) => {
|
|
253
|
+
let hookSet = componentHookVars.get(componentFunction);
|
|
254
|
+
if (!hookSet) {
|
|
255
|
+
hookSet = new Set<string>();
|
|
256
|
+
componentHookVars.set(componentFunction, hookSet);
|
|
257
|
+
}
|
|
258
|
+
return hookSet;
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
const isRangeContained = (
|
|
262
|
+
refRange: [number, number],
|
|
263
|
+
nodeRange: [number, number]
|
|
264
|
+
) => refRange[0] >= nodeRange[0] && refRange[1] <= nodeRange[1];
|
|
265
|
+
|
|
266
|
+
const variableHasReferenceInRange = (
|
|
267
|
+
variable: TSESLint.Scope.Variable,
|
|
268
|
+
nodeRange: [number, number]
|
|
269
|
+
) =>
|
|
270
|
+
variable.references.some(
|
|
271
|
+
(reference) =>
|
|
272
|
+
reference.identifier.range !== undefined &&
|
|
273
|
+
isRangeContained(reference.identifier.range, nodeRange)
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
const hasHookDependency = (
|
|
277
|
+
node: TSESTree.Node,
|
|
278
|
+
hookSet: Set<string>
|
|
279
|
+
) => {
|
|
280
|
+
if (!node.range) {
|
|
281
|
+
return false;
|
|
282
|
+
}
|
|
283
|
+
const nodeRange = node.range;
|
|
284
|
+
|
|
285
|
+
let scope: TSESLint.Scope.Scope | null =
|
|
286
|
+
context.sourceCode.getScope(node);
|
|
287
|
+
|
|
288
|
+
while (scope) {
|
|
289
|
+
const hookVars = scope.variables.filter((variable) =>
|
|
290
|
+
hookSet.has(variable.name)
|
|
291
|
+
);
|
|
292
|
+
if (
|
|
293
|
+
hookVars.some((variable) =>
|
|
294
|
+
variableHasReferenceInRange(variable, nodeRange)
|
|
295
|
+
)
|
|
296
|
+
) {
|
|
297
|
+
return true;
|
|
298
|
+
}
|
|
299
|
+
scope = scope.upper ?? null;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return false;
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
const processUseStateDeclarator = (
|
|
306
|
+
node: TSESTree.VariableDeclarator
|
|
307
|
+
) => {
|
|
308
|
+
if (
|
|
309
|
+
!node.init ||
|
|
310
|
+
!isUseStateCall(node.init) ||
|
|
311
|
+
node.id.type !== AST_NODE_TYPES.ArrayPattern ||
|
|
312
|
+
node.id.elements.length < 2
|
|
313
|
+
) {
|
|
314
|
+
return false;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const [stateElem, setterElem] = node.id.elements;
|
|
318
|
+
if (
|
|
319
|
+
!stateElem ||
|
|
320
|
+
stateElem.type !== AST_NODE_TYPES.Identifier ||
|
|
321
|
+
!setterElem ||
|
|
322
|
+
setterElem.type !== AST_NODE_TYPES.Identifier
|
|
323
|
+
) {
|
|
324
|
+
return false;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const stateVarName = stateElem.name;
|
|
328
|
+
const setterVarName = setterElem.name;
|
|
329
|
+
|
|
330
|
+
const stateUsage = analyzeVariableUsage(stateElem);
|
|
331
|
+
const setterUsage = analyzeVariableUsage(setterElem);
|
|
332
|
+
|
|
333
|
+
const stateExclusivelySingleJSX =
|
|
334
|
+
!stateUsage.hasOutsideUsage &&
|
|
335
|
+
stateUsage.jsxUsageSet.size === 1;
|
|
336
|
+
const setterExclusivelySingleJSX =
|
|
337
|
+
!setterUsage.hasOutsideUsage &&
|
|
338
|
+
setterUsage.jsxUsageSet.size === 1;
|
|
339
|
+
|
|
340
|
+
if (!stateExclusivelySingleJSX || !setterExclusivelySingleJSX) {
|
|
341
|
+
return true;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const stateTarget = getSingleSetElement(stateUsage.jsxUsageSet);
|
|
345
|
+
const setterTarget = getSingleSetElement(setterUsage.jsxUsageSet);
|
|
346
|
+
if (stateTarget && stateTarget === setterTarget) {
|
|
347
|
+
context.report({
|
|
348
|
+
data: { setterVarName, stateVarName },
|
|
349
|
+
messageId: "stateAndSetterToChild",
|
|
350
|
+
node: node
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
return true;
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
const processGeneralVariable = (
|
|
357
|
+
node: TSESTree.VariableDeclarator,
|
|
358
|
+
componentFunction: ComponentFunction
|
|
359
|
+
) => {
|
|
360
|
+
if (!node.id || node.id.type !== AST_NODE_TYPES.Identifier) {
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const varName = node.id.name;
|
|
365
|
+
// Exempt variables that depend on hooks.
|
|
366
|
+
if (node.init) {
|
|
367
|
+
const hookSet = getHookSet(componentFunction);
|
|
368
|
+
if (hasHookDependency(node.init, hookSet)) {
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
const usage = analyzeVariableUsage(node.id);
|
|
373
|
+
if (!usage.hasOutsideUsage && usage.jsxUsageSet.size === 1) {
|
|
374
|
+
const target = getSingleSetElement(usage.jsxUsageSet);
|
|
375
|
+
const componentName = getJSXElementName(target);
|
|
376
|
+
candidateVariables.push({
|
|
377
|
+
componentName,
|
|
378
|
+
node,
|
|
379
|
+
varName
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
return {
|
|
385
|
+
// At the end of the traversal, group candidate variables by the target component name.
|
|
386
|
+
"Program:exit"() {
|
|
387
|
+
const groups = new Map<string, CandidateVariable[]>();
|
|
388
|
+
candidateVariables.forEach((candidate) => {
|
|
389
|
+
const key = candidate.componentName;
|
|
390
|
+
const existing = groups.get(key);
|
|
391
|
+
if (existing) {
|
|
392
|
+
existing.push(candidate);
|
|
393
|
+
} else {
|
|
394
|
+
groups.set(key, [candidate]);
|
|
395
|
+
}
|
|
396
|
+
});
|
|
397
|
+
// Only report candidates for a given component type if there is exactly one candidate.
|
|
398
|
+
groups.forEach((candidates) => {
|
|
399
|
+
if (candidates.length !== 1) {
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
const [candidate] = candidates;
|
|
403
|
+
if (!candidate) {
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
context.report({
|
|
407
|
+
data: { varName: candidate.varName },
|
|
408
|
+
messageId: "variableToChild",
|
|
409
|
+
node: candidate.node
|
|
410
|
+
});
|
|
411
|
+
});
|
|
412
|
+
},
|
|
413
|
+
VariableDeclarator(node: TSESTree.VariableDeclarator) {
|
|
414
|
+
const componentFunction = getComponentFunction(node);
|
|
415
|
+
if (!componentFunction || !componentFunction.body) return;
|
|
416
|
+
|
|
417
|
+
// Record hook-derived variables (for hooks other than useState).
|
|
418
|
+
if (
|
|
419
|
+
node.init &&
|
|
420
|
+
node.id &&
|
|
421
|
+
node.id.type === AST_NODE_TYPES.Identifier &&
|
|
422
|
+
node.init.type === AST_NODE_TYPES.CallExpression &&
|
|
423
|
+
isHookCall(node.init)
|
|
424
|
+
) {
|
|
425
|
+
const hookSet = getHookSet(componentFunction);
|
|
426
|
+
hookSet.add(node.id.name);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Case 1: useState destructuring (state & setter).
|
|
430
|
+
const wasUseState = processUseStateDeclarator(node);
|
|
431
|
+
|
|
432
|
+
// Case 2: General variable.
|
|
433
|
+
if (!wasUseState) {
|
|
434
|
+
processGeneralVariable(node, componentFunction);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
};
|
|
438
|
+
},
|
|
439
|
+
defaultOptions: [],
|
|
440
|
+
meta: {
|
|
441
|
+
docs: {
|
|
442
|
+
description:
|
|
443
|
+
"Disallow variables that are only passed to a single custom child component. For useState, only report if both the state and its setter are exclusively passed to a single custom child. For general variables, only report if a given child receives exactly one such candidate – if two or more are passed to the same component type, they're assumed to be settings that belong on the parent."
|
|
444
|
+
},
|
|
445
|
+
messages: {
|
|
446
|
+
stateAndSetterToChild:
|
|
447
|
+
"State variable '{{stateVarName}}' and its setter '{{setterVarName}}' are only passed to a single custom child component. Consider moving the state into that component.",
|
|
448
|
+
variableToChild:
|
|
449
|
+
"Variable '{{varName}}' is only passed to a single custom child component. Consider moving it to that component."
|
|
450
|
+
},
|
|
451
|
+
schema: [],
|
|
452
|
+
type: "suggestion"
|
|
453
|
+
}
|
|
454
|
+
};
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { TSESLint, TSESTree } from "@typescript-eslint/utils";
|
|
2
|
+
|
|
3
|
+
type Options = [number?];
|
|
4
|
+
type MessageIds = "tooDeep";
|
|
5
|
+
|
|
6
|
+
export const maxDepthExtended: TSESLint.RuleModule<MessageIds, Options> = {
|
|
7
|
+
create(context) {
|
|
8
|
+
const [option] = context.options;
|
|
9
|
+
const maxDepth = typeof option === "number" ? option : 1;
|
|
10
|
+
const functionStack: number[] = [];
|
|
11
|
+
|
|
12
|
+
// Helper to get ancestors of a node by walking its parent chain.
|
|
13
|
+
const getAncestors = (node: TSESTree.Node) => {
|
|
14
|
+
const ancestors: TSESTree.Node[] = [];
|
|
15
|
+
let current: TSESTree.Node | null | undefined = node.parent;
|
|
16
|
+
while (current) {
|
|
17
|
+
ancestors.push(current);
|
|
18
|
+
current = current.parent;
|
|
19
|
+
}
|
|
20
|
+
return ancestors;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// Check if a block only contains a single early exit: return or throw.
|
|
24
|
+
const isEarlyExitBlock = (node: TSESTree.BlockStatement) => {
|
|
25
|
+
if (node.body.length !== 1) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
const [first] = node.body;
|
|
29
|
+
if (!first) {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
return (
|
|
33
|
+
first.type === "ReturnStatement" ||
|
|
34
|
+
first.type === "ThrowStatement"
|
|
35
|
+
);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const isFunctionBody = (node: TSESTree.BlockStatement) => {
|
|
39
|
+
const ancestors = getAncestors(node);
|
|
40
|
+
const [parent] = ancestors;
|
|
41
|
+
return (
|
|
42
|
+
parent &&
|
|
43
|
+
(parent.type === "FunctionDeclaration" ||
|
|
44
|
+
parent.type === "FunctionExpression" ||
|
|
45
|
+
parent.type === "ArrowFunctionExpression") &&
|
|
46
|
+
node === parent.body
|
|
47
|
+
);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const incrementCurrentDepth = () => {
|
|
51
|
+
if (functionStack.length === 0) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
const index = functionStack.length - 1;
|
|
55
|
+
const currentDepth = functionStack[index];
|
|
56
|
+
if (typeof currentDepth !== "number") {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
const nextDepth = currentDepth + 1;
|
|
60
|
+
functionStack[index] = nextDepth;
|
|
61
|
+
return nextDepth;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const decrementCurrentDepth = () => {
|
|
65
|
+
if (functionStack.length === 0) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const index = functionStack.length - 1;
|
|
69
|
+
const currentDepth = functionStack[index];
|
|
70
|
+
if (typeof currentDepth !== "number") {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
functionStack[index] = currentDepth - 1;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// Report if the current depth exceeds the allowed maximum.
|
|
77
|
+
const checkDepth = (node: TSESTree.BlockStatement, depth: number) => {
|
|
78
|
+
if (depth > maxDepth) {
|
|
79
|
+
context.report({
|
|
80
|
+
data: { depth, maxDepth },
|
|
81
|
+
messageId: "tooDeep",
|
|
82
|
+
node
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
ArrowFunctionExpression() {
|
|
89
|
+
functionStack.push(0);
|
|
90
|
+
},
|
|
91
|
+
"ArrowFunctionExpression:exit"() {
|
|
92
|
+
functionStack.pop();
|
|
93
|
+
},
|
|
94
|
+
BlockStatement(node: TSESTree.BlockStatement) {
|
|
95
|
+
// Do not count if this block is the body of a function.
|
|
96
|
+
if (isFunctionBody(node)) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Skip blocks that only have an early exit.
|
|
101
|
+
if (isEarlyExitBlock(node)) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const depth = incrementCurrentDepth();
|
|
106
|
+
if (depth !== null) {
|
|
107
|
+
checkDepth(node, depth);
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
"BlockStatement:exit"(node: TSESTree.BlockStatement) {
|
|
111
|
+
if (isFunctionBody(node)) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (isEarlyExitBlock(node)) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
decrementCurrentDepth();
|
|
120
|
+
},
|
|
121
|
+
FunctionDeclaration() {
|
|
122
|
+
functionStack.push(0);
|
|
123
|
+
},
|
|
124
|
+
"FunctionDeclaration:exit"() {
|
|
125
|
+
functionStack.pop();
|
|
126
|
+
},
|
|
127
|
+
FunctionExpression() {
|
|
128
|
+
functionStack.push(0);
|
|
129
|
+
},
|
|
130
|
+
"FunctionExpression:exit"() {
|
|
131
|
+
functionStack.pop();
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
},
|
|
135
|
+
defaultOptions: [1],
|
|
136
|
+
meta: {
|
|
137
|
+
docs: {
|
|
138
|
+
description:
|
|
139
|
+
"disallow too many nested blocks except when the block only contains an early exit (return or throw)"
|
|
140
|
+
},
|
|
141
|
+
messages: {
|
|
142
|
+
tooDeep:
|
|
143
|
+
"Blocks are nested too deeply ({{depth}}). Maximum allowed is {{maxDepth}} or an early exit."
|
|
144
|
+
},
|
|
145
|
+
schema: [
|
|
146
|
+
{
|
|
147
|
+
// Accepts a single number as the maximum allowed depth.
|
|
148
|
+
type: "number"
|
|
149
|
+
}
|
|
150
|
+
],
|
|
151
|
+
type: "suggestion"
|
|
152
|
+
}
|
|
153
|
+
};
|
|
@@ -1,60 +1,59 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
schema: [
|
|
11
|
-
{
|
|
12
|
-
type: "number",
|
|
13
|
-
minimum: 1
|
|
14
|
-
}
|
|
15
|
-
],
|
|
16
|
-
messages: {
|
|
17
|
-
tooDeeplyNested:
|
|
18
|
-
"JSX element is nested too deeply ({{level}} levels, allowed is {{maxAllowed}} levels). Consider refactoring into a separate component."
|
|
19
|
-
}
|
|
20
|
-
},
|
|
1
|
+
import { TSESLint, TSESTree } from "@typescript-eslint/utils";
|
|
2
|
+
|
|
3
|
+
type Options = [number];
|
|
4
|
+
type MessageIds = "tooDeeplyNested";
|
|
5
|
+
|
|
6
|
+
const isJSXAncestor = (node: TSESTree.Node) =>
|
|
7
|
+
node.type === "JSXElement" || node.type === "JSXFragment";
|
|
8
|
+
|
|
9
|
+
export const maxJSXNesting: TSESLint.RuleModule<MessageIds, Options> = {
|
|
21
10
|
create(context) {
|
|
22
|
-
|
|
23
|
-
const maxAllowed =
|
|
11
|
+
const [option] = context.options;
|
|
12
|
+
const maxAllowed = typeof option === "number" ? option : 1;
|
|
24
13
|
|
|
25
14
|
/**
|
|
26
15
|
* Calculates the JSX nesting level for the given node by traversing the node.parent chain.
|
|
27
|
-
* The level is computed by counting the current node (level 1) plus each ancestor
|
|
28
|
-
* that is a JSXElement or JSXFragment.
|
|
29
|
-
* @param {ASTNode} node The JSX element node.
|
|
30
|
-
* @returns {number} The nesting level.
|
|
31
16
|
*/
|
|
32
|
-
|
|
17
|
+
const getJSXNestingLevel = (node: TSESTree.Node) => {
|
|
33
18
|
let level = 1; // count the current node as level 1
|
|
34
|
-
let current = node.parent;
|
|
19
|
+
let current: TSESTree.Node | null | undefined = node.parent;
|
|
35
20
|
while (current) {
|
|
36
|
-
|
|
37
|
-
current.type === "JSXElement" ||
|
|
38
|
-
current.type === "JSXFragment"
|
|
39
|
-
) {
|
|
40
|
-
level++;
|
|
41
|
-
}
|
|
21
|
+
level += isJSXAncestor(current) ? 1 : 0;
|
|
42
22
|
current = current.parent;
|
|
43
23
|
}
|
|
44
24
|
return level;
|
|
45
|
-
}
|
|
25
|
+
};
|
|
46
26
|
|
|
47
27
|
return {
|
|
48
|
-
JSXElement(node) {
|
|
28
|
+
JSXElement(node: TSESTree.JSXElement) {
|
|
49
29
|
const level = getJSXNestingLevel(node);
|
|
50
30
|
if (level > maxAllowed) {
|
|
51
31
|
context.report({
|
|
52
|
-
|
|
32
|
+
data: { level, maxAllowed },
|
|
53
33
|
messageId: "tooDeeplyNested",
|
|
54
|
-
|
|
34
|
+
node
|
|
55
35
|
});
|
|
56
36
|
}
|
|
57
37
|
}
|
|
58
38
|
};
|
|
39
|
+
},
|
|
40
|
+
defaultOptions: [1],
|
|
41
|
+
meta: {
|
|
42
|
+
docs: {
|
|
43
|
+
description:
|
|
44
|
+
"Warn when JSX elements are nested too deeply, suggesting refactoring into a separate component."
|
|
45
|
+
},
|
|
46
|
+
messages: {
|
|
47
|
+
tooDeeplyNested:
|
|
48
|
+
"JSX element is nested too deeply ({{level}} levels, allowed is {{maxAllowed}} levels). Consider refactoring into a separate component."
|
|
49
|
+
},
|
|
50
|
+
// The rule accepts a single numeric option (minimum 1)
|
|
51
|
+
schema: [
|
|
52
|
+
{
|
|
53
|
+
minimum: 1,
|
|
54
|
+
type: "number"
|
|
55
|
+
}
|
|
56
|
+
],
|
|
57
|
+
type: "suggestion"
|
|
59
58
|
}
|
|
60
59
|
};
|