eslint-plugin-absolute 0.2.7 → 0.2.8
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/package.json +6 -1
- package/.absolutejs/eslint.cache.json +0 -49
- package/.absolutejs/prettier.cache.json +0 -49
- package/.absolutejs/tsconfig.tsbuildinfo +0 -1
- package/.claude/settings.local.json +0 -10
- package/.codex +0 -0
- package/.prettierignore +0 -4
- package/.prettierrc.json +0 -8
- package/eslint.config.mjs +0 -107
- package/src/index.ts +0 -45
- package/src/rules/explicit-object-types.ts +0 -75
- package/src/rules/inline-style-limit.ts +0 -88
- package/src/rules/localize-react-props.ts +0 -454
- package/src/rules/max-depth-extended.ts +0 -153
- package/src/rules/max-jsx-nesting.ts +0 -59
- package/src/rules/min-var-length.ts +0 -360
- package/src/rules/no-button-navigation.ts +0 -270
- package/src/rules/no-explicit-return-types.ts +0 -83
- package/src/rules/no-inline-prop-types.ts +0 -68
- package/src/rules/no-multi-style-objects.ts +0 -80
- package/src/rules/no-nested-jsx-return.ts +0 -205
- package/src/rules/no-or-none-component.ts +0 -63
- package/src/rules/no-transition-cssproperties.ts +0 -131
- package/src/rules/no-unnecessary-div.ts +0 -65
- package/src/rules/no-unnecessary-key.ts +0 -111
- package/src/rules/no-useless-function.ts +0 -56
- package/src/rules/seperate-style-files.ts +0 -79
- package/src/rules/sort-exports.ts +0 -581
- package/src/rules/sort-keys-fixable.ts +0 -1265
- package/src/rules/spring-naming-convention.ts +0 -160
- package/tsconfig.json +0 -17
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
import { TSESLint, TSESTree } from "@typescript-eslint/utils";
|
|
2
|
-
|
|
3
|
-
type Options = [];
|
|
4
|
-
type MessageIds = "objectLiteralNeedsType" | "arrayOfObjectLiteralsNeedsType";
|
|
5
|
-
|
|
6
|
-
export const explicitObjectTypes: TSESLint.RuleModule<MessageIds, Options> = {
|
|
7
|
-
create(context) {
|
|
8
|
-
/**
|
|
9
|
-
* Returns true if the node is an object literal.
|
|
10
|
-
* @param {ASTNode} node The AST node to check.
|
|
11
|
-
*/
|
|
12
|
-
const isObjectLiteral = (
|
|
13
|
-
node: TSESTree.Node | null | undefined
|
|
14
|
-
): node is TSESTree.ObjectExpression =>
|
|
15
|
-
node !== null &&
|
|
16
|
-
node !== undefined &&
|
|
17
|
-
node.type === "ObjectExpression";
|
|
18
|
-
|
|
19
|
-
return {
|
|
20
|
-
VariableDeclarator(node: TSESTree.VariableDeclarator) {
|
|
21
|
-
// Skip if there's no initializer.
|
|
22
|
-
if (!node.init) return;
|
|
23
|
-
|
|
24
|
-
// Skip if the variable already has a type annotation.
|
|
25
|
-
if (node.id.type === "Identifier" && node.id.typeAnnotation)
|
|
26
|
-
return;
|
|
27
|
-
|
|
28
|
-
// Check if the initializer is an object literal.
|
|
29
|
-
if (
|
|
30
|
-
isObjectLiteral(node.init) &&
|
|
31
|
-
node.id.type === "Identifier"
|
|
32
|
-
) {
|
|
33
|
-
context.report({
|
|
34
|
-
messageId: "objectLiteralNeedsType",
|
|
35
|
-
node: node.id
|
|
36
|
-
});
|
|
37
|
-
return;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// Check if the initializer is an array literal containing any object literals.
|
|
41
|
-
if (node.init.type !== "ArrayExpression") {
|
|
42
|
-
return;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const hasObjectLiteral = node.init.elements.some((element) => {
|
|
46
|
-
if (!element || element.type === "SpreadElement")
|
|
47
|
-
return false;
|
|
48
|
-
return isObjectLiteral(element);
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
if (hasObjectLiteral && node.id.type === "Identifier") {
|
|
52
|
-
context.report({
|
|
53
|
-
messageId: "arrayOfObjectLiteralsNeedsType",
|
|
54
|
-
node: node.id
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
};
|
|
59
|
-
},
|
|
60
|
-
defaultOptions: [],
|
|
61
|
-
meta: {
|
|
62
|
-
docs: {
|
|
63
|
-
description:
|
|
64
|
-
"Require explicit type annotations for object literals and arrays of object literals"
|
|
65
|
-
},
|
|
66
|
-
messages: {
|
|
67
|
-
arrayOfObjectLiteralsNeedsType:
|
|
68
|
-
"Array of object literals must have an explicit type annotation.",
|
|
69
|
-
objectLiteralNeedsType:
|
|
70
|
-
"Object literal must have an explicit type annotation."
|
|
71
|
-
},
|
|
72
|
-
schema: [],
|
|
73
|
-
type: "problem"
|
|
74
|
-
}
|
|
75
|
-
};
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
import { TSESLint, TSESTree } from "@typescript-eslint/utils";
|
|
2
|
-
|
|
3
|
-
const DEFAULT_MAX_KEYS = 3;
|
|
4
|
-
|
|
5
|
-
type Options = [number | { maxKeys?: number }];
|
|
6
|
-
type MessageIds = "extractStyle";
|
|
7
|
-
|
|
8
|
-
export const inlineStyleLimit: TSESLint.RuleModule<MessageIds, Options> = {
|
|
9
|
-
create(context) {
|
|
10
|
-
const [option] = context.options;
|
|
11
|
-
// If a number is passed directly, use it as maxKeys; otherwise, extract maxKeys from the object (default to 3)
|
|
12
|
-
const maxKeys =
|
|
13
|
-
typeof option === "number"
|
|
14
|
-
? option
|
|
15
|
-
: (option && option.maxKeys) || DEFAULT_MAX_KEYS;
|
|
16
|
-
|
|
17
|
-
return {
|
|
18
|
-
JSXAttribute(node: TSESTree.JSXAttribute) {
|
|
19
|
-
// Check if the attribute name is 'style'
|
|
20
|
-
if (
|
|
21
|
-
node.name.type !== "JSXIdentifier" ||
|
|
22
|
-
node.name.name !== "style"
|
|
23
|
-
) {
|
|
24
|
-
return;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// Ensure the value is a JSX expression container with an object literal
|
|
28
|
-
if (
|
|
29
|
-
!node.value ||
|
|
30
|
-
node.value.type !== "JSXExpressionContainer" ||
|
|
31
|
-
!node.value.expression ||
|
|
32
|
-
node.value.expression.type !== "ObjectExpression"
|
|
33
|
-
) {
|
|
34
|
-
return;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const styleObject = node.value.expression;
|
|
38
|
-
|
|
39
|
-
// Count only "Property" nodes (ignoring spread elements or others)
|
|
40
|
-
const keyCount = styleObject.properties.filter(
|
|
41
|
-
(prop): prop is TSESTree.Property =>
|
|
42
|
-
prop.type === "Property"
|
|
43
|
-
).length;
|
|
44
|
-
|
|
45
|
-
// Report only if the number of keys exceeds the allowed maximum
|
|
46
|
-
if (keyCount > maxKeys) {
|
|
47
|
-
context.report({
|
|
48
|
-
data: { max: maxKeys },
|
|
49
|
-
messageId: "extractStyle",
|
|
50
|
-
node
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
};
|
|
55
|
-
},
|
|
56
|
-
defaultOptions: [DEFAULT_MAX_KEYS],
|
|
57
|
-
meta: {
|
|
58
|
-
docs: {
|
|
59
|
-
description:
|
|
60
|
-
"Disallow inline style objects with too many keys and encourage extracting them"
|
|
61
|
-
},
|
|
62
|
-
messages: {
|
|
63
|
-
extractStyle:
|
|
64
|
-
"Inline style objects should be extracted into a separate object or file when containing more than {{max}} keys."
|
|
65
|
-
},
|
|
66
|
-
schema: [
|
|
67
|
-
{
|
|
68
|
-
anyOf: [
|
|
69
|
-
{
|
|
70
|
-
type: "number"
|
|
71
|
-
},
|
|
72
|
-
{
|
|
73
|
-
additionalProperties: false,
|
|
74
|
-
properties: {
|
|
75
|
-
maxKeys: {
|
|
76
|
-
description:
|
|
77
|
-
"Maximum number of keys allowed in an inline style object before it must be extracted.",
|
|
78
|
-
type: "number"
|
|
79
|
-
}
|
|
80
|
-
},
|
|
81
|
-
type: "object"
|
|
82
|
-
}
|
|
83
|
-
]
|
|
84
|
-
}
|
|
85
|
-
],
|
|
86
|
-
type: "suggestion"
|
|
87
|
-
}
|
|
88
|
-
};
|
|
@@ -1,454 +0,0 @@
|
|
|
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
|
-
};
|