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