eslint-plugin-absolute 0.2.7 → 0.3.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/dist/index.js +469 -11
- package/package.json +12 -2
- 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,59 +0,0 @@
|
|
|
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> = {
|
|
10
|
-
create(context) {
|
|
11
|
-
const [option] = context.options;
|
|
12
|
-
const maxAllowed = typeof option === "number" ? option : 1;
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Calculates the JSX nesting level for the given node by traversing the node.parent chain.
|
|
16
|
-
*/
|
|
17
|
-
const getJSXNestingLevel = (node: TSESTree.Node) => {
|
|
18
|
-
let level = 1; // count the current node as level 1
|
|
19
|
-
let current: TSESTree.Node | null | undefined = node.parent;
|
|
20
|
-
while (current) {
|
|
21
|
-
level += isJSXAncestor(current) ? 1 : 0;
|
|
22
|
-
current = current.parent;
|
|
23
|
-
}
|
|
24
|
-
return level;
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
return {
|
|
28
|
-
JSXElement(node: TSESTree.JSXElement) {
|
|
29
|
-
const level = getJSXNestingLevel(node);
|
|
30
|
-
if (level > maxAllowed) {
|
|
31
|
-
context.report({
|
|
32
|
-
data: { level, maxAllowed },
|
|
33
|
-
messageId: "tooDeeplyNested",
|
|
34
|
-
node
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
|
-
}
|
|
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"
|
|
58
|
-
}
|
|
59
|
-
};
|
|
@@ -1,360 +0,0 @@
|
|
|
1
|
-
import { TSESLint, TSESTree } from "@typescript-eslint/utils";
|
|
2
|
-
|
|
3
|
-
type MinVarLengthOption = {
|
|
4
|
-
minLength?: number;
|
|
5
|
-
allowedVars?: string[];
|
|
6
|
-
};
|
|
7
|
-
|
|
8
|
-
type Options = [MinVarLengthOption?];
|
|
9
|
-
type MessageIds = "variableNameTooShort";
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Recursively extract identifier names from a pattern (for destructuring)
|
|
13
|
-
*/
|
|
14
|
-
const extractIdentifiersFromPattern = (
|
|
15
|
-
pattern: TSESTree.Node | null,
|
|
16
|
-
identifiers: string[] = []
|
|
17
|
-
) => {
|
|
18
|
-
if (!pattern) return identifiers;
|
|
19
|
-
switch (pattern.type) {
|
|
20
|
-
case "Identifier":
|
|
21
|
-
identifiers.push(pattern.name);
|
|
22
|
-
break;
|
|
23
|
-
case "ObjectPattern":
|
|
24
|
-
pattern.properties.forEach((prop) => {
|
|
25
|
-
if (prop.type === "Property") {
|
|
26
|
-
extractIdentifiersFromPattern(prop.value, identifiers);
|
|
27
|
-
} else if (prop.type === "RestElement") {
|
|
28
|
-
extractIdentifiersFromPattern(prop.argument, identifiers);
|
|
29
|
-
}
|
|
30
|
-
});
|
|
31
|
-
break;
|
|
32
|
-
case "ArrayPattern":
|
|
33
|
-
pattern.elements
|
|
34
|
-
.filter(
|
|
35
|
-
(element): element is TSESTree.DestructuringPattern =>
|
|
36
|
-
element !== null
|
|
37
|
-
)
|
|
38
|
-
.forEach((element) => {
|
|
39
|
-
extractIdentifiersFromPattern(element, identifiers);
|
|
40
|
-
});
|
|
41
|
-
break;
|
|
42
|
-
case "AssignmentPattern":
|
|
43
|
-
extractIdentifiersFromPattern(pattern.left, identifiers);
|
|
44
|
-
break;
|
|
45
|
-
default:
|
|
46
|
-
break;
|
|
47
|
-
}
|
|
48
|
-
return identifiers;
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
const getDeclaratorNames = (declarations: TSESTree.VariableDeclarator[]) =>
|
|
52
|
-
declarations
|
|
53
|
-
.filter(
|
|
54
|
-
(
|
|
55
|
-
decl
|
|
56
|
-
): decl is TSESTree.VariableDeclarator & {
|
|
57
|
-
id: TSESTree.Identifier;
|
|
58
|
-
} => decl.id.type === "Identifier"
|
|
59
|
-
)
|
|
60
|
-
.map((decl) => decl.id.name);
|
|
61
|
-
|
|
62
|
-
const collectParamNames = (params: TSESTree.Parameter[]) => {
|
|
63
|
-
const names: string[] = [];
|
|
64
|
-
params.forEach((param) => {
|
|
65
|
-
extractIdentifiersFromPattern(param, names);
|
|
66
|
-
});
|
|
67
|
-
return names;
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
export const minVarLength: TSESLint.RuleModule<MessageIds, Options> = {
|
|
71
|
-
create(context) {
|
|
72
|
-
const { sourceCode } = context;
|
|
73
|
-
const [options] = context.options;
|
|
74
|
-
const configuredMinLength =
|
|
75
|
-
options && typeof options.minLength === "number"
|
|
76
|
-
? options.minLength
|
|
77
|
-
: 1;
|
|
78
|
-
const configuredAllowedVars =
|
|
79
|
-
options && Array.isArray(options.allowedVars)
|
|
80
|
-
? options.allowedVars
|
|
81
|
-
: [];
|
|
82
|
-
|
|
83
|
-
const minLength = configuredMinLength;
|
|
84
|
-
const allowedVars = configuredAllowedVars;
|
|
85
|
-
|
|
86
|
-
// Helper: walk up the node.parent chain to get ancestors.
|
|
87
|
-
const getAncestors = (node: TSESTree.Node) => {
|
|
88
|
-
const ancestors: TSESTree.Node[] = [];
|
|
89
|
-
let current: TSESTree.Node | null | undefined = node.parent;
|
|
90
|
-
while (current) {
|
|
91
|
-
ancestors.push(current);
|
|
92
|
-
current = current.parent;
|
|
93
|
-
}
|
|
94
|
-
return ancestors;
|
|
95
|
-
};
|
|
96
|
-
|
|
97
|
-
// Helper: retrieve the scope for a given node using the scopeManager.
|
|
98
|
-
const getScope = (node: TSESTree.Node) => {
|
|
99
|
-
const { scopeManager } = sourceCode;
|
|
100
|
-
if (!scopeManager) {
|
|
101
|
-
return null;
|
|
102
|
-
}
|
|
103
|
-
const acquired = scopeManager.acquire(node);
|
|
104
|
-
if (acquired) {
|
|
105
|
-
return acquired;
|
|
106
|
-
}
|
|
107
|
-
return scopeManager.globalScope ?? null;
|
|
108
|
-
};
|
|
109
|
-
|
|
110
|
-
// Fallback: get declared variable names in the nearest BlockStatement.
|
|
111
|
-
const getVariablesInNearestBlock = (node: TSESTree.Node) => {
|
|
112
|
-
let current: TSESTree.Node | null | undefined = node.parent;
|
|
113
|
-
while (current && current.type !== "BlockStatement") {
|
|
114
|
-
current = current.parent;
|
|
115
|
-
}
|
|
116
|
-
if (
|
|
117
|
-
!current ||
|
|
118
|
-
current.type !== "BlockStatement" ||
|
|
119
|
-
!Array.isArray(current.body)
|
|
120
|
-
) {
|
|
121
|
-
return [];
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
const varDeclarations = current.body.filter(
|
|
125
|
-
(stmt): stmt is TSESTree.VariableDeclaration =>
|
|
126
|
-
stmt.type === "VariableDeclaration"
|
|
127
|
-
);
|
|
128
|
-
return varDeclarations.flatMap((stmt) =>
|
|
129
|
-
getDeclaratorNames(stmt.declarations)
|
|
130
|
-
);
|
|
131
|
-
};
|
|
132
|
-
|
|
133
|
-
const isLongerMatchInScope = (shortName: string, varName: string) =>
|
|
134
|
-
varName.length >= minLength &&
|
|
135
|
-
varName.length > shortName.length &&
|
|
136
|
-
varName.startsWith(shortName);
|
|
137
|
-
|
|
138
|
-
const checkScopeVariables = (
|
|
139
|
-
shortName: string,
|
|
140
|
-
node: TSESTree.Identifier
|
|
141
|
-
) => {
|
|
142
|
-
const startingScope = getScope(node);
|
|
143
|
-
let outer =
|
|
144
|
-
startingScope && startingScope.upper
|
|
145
|
-
? startingScope.upper
|
|
146
|
-
: null;
|
|
147
|
-
while (outer) {
|
|
148
|
-
if (
|
|
149
|
-
outer.variables.some((variable) =>
|
|
150
|
-
isLongerMatchInScope(shortName, variable.name)
|
|
151
|
-
)
|
|
152
|
-
) {
|
|
153
|
-
return true;
|
|
154
|
-
}
|
|
155
|
-
outer = outer.upper;
|
|
156
|
-
}
|
|
157
|
-
return false;
|
|
158
|
-
};
|
|
159
|
-
|
|
160
|
-
const checkBlockVariables = (
|
|
161
|
-
shortName: string,
|
|
162
|
-
node: TSESTree.Identifier
|
|
163
|
-
) => {
|
|
164
|
-
const blockVars = getVariablesInNearestBlock(node);
|
|
165
|
-
return blockVars.some((varName) =>
|
|
166
|
-
isLongerMatchInScope(shortName, varName)
|
|
167
|
-
);
|
|
168
|
-
};
|
|
169
|
-
|
|
170
|
-
const checkAncestorDeclarators = (
|
|
171
|
-
shortName: string,
|
|
172
|
-
node: TSESTree.Identifier
|
|
173
|
-
) => {
|
|
174
|
-
const ancestors = getAncestors(node);
|
|
175
|
-
return ancestors.some(
|
|
176
|
-
(anc) =>
|
|
177
|
-
anc.type === "VariableDeclarator" &&
|
|
178
|
-
anc.id &&
|
|
179
|
-
anc.id.type === "Identifier" &&
|
|
180
|
-
isLongerMatchInScope(shortName, anc.id.name)
|
|
181
|
-
);
|
|
182
|
-
};
|
|
183
|
-
|
|
184
|
-
const checkFunctionAncestor = (
|
|
185
|
-
shortName: string,
|
|
186
|
-
anc:
|
|
187
|
-
| TSESTree.FunctionDeclaration
|
|
188
|
-
| TSESTree.FunctionExpression
|
|
189
|
-
| TSESTree.ArrowFunctionExpression
|
|
190
|
-
) => {
|
|
191
|
-
const names = collectParamNames(anc.params);
|
|
192
|
-
return names.some((paramName) =>
|
|
193
|
-
isLongerMatchInScope(shortName, paramName)
|
|
194
|
-
);
|
|
195
|
-
};
|
|
196
|
-
|
|
197
|
-
const checkCatchAncestor = (
|
|
198
|
-
shortName: string,
|
|
199
|
-
anc: TSESTree.CatchClause
|
|
200
|
-
) => {
|
|
201
|
-
if (!anc.param) {
|
|
202
|
-
return false;
|
|
203
|
-
}
|
|
204
|
-
const names = extractIdentifiersFromPattern(anc.param, []);
|
|
205
|
-
return names.some((paramName) =>
|
|
206
|
-
isLongerMatchInScope(shortName, paramName)
|
|
207
|
-
);
|
|
208
|
-
};
|
|
209
|
-
|
|
210
|
-
const checkAncestorParams = (
|
|
211
|
-
shortName: string,
|
|
212
|
-
node: TSESTree.Identifier
|
|
213
|
-
) => {
|
|
214
|
-
const ancestors = getAncestors(node);
|
|
215
|
-
return ancestors.some((anc) => {
|
|
216
|
-
if (
|
|
217
|
-
anc.type === "FunctionDeclaration" ||
|
|
218
|
-
anc.type === "FunctionExpression" ||
|
|
219
|
-
anc.type === "ArrowFunctionExpression"
|
|
220
|
-
) {
|
|
221
|
-
return checkFunctionAncestor(shortName, anc);
|
|
222
|
-
}
|
|
223
|
-
if (anc.type === "CatchClause") {
|
|
224
|
-
return checkCatchAncestor(shortName, anc);
|
|
225
|
-
}
|
|
226
|
-
return false;
|
|
227
|
-
});
|
|
228
|
-
};
|
|
229
|
-
|
|
230
|
-
/**
|
|
231
|
-
* Checks if there is an outer variable whose name is longer than the current short name
|
|
232
|
-
* and starts with the same characters.
|
|
233
|
-
*/
|
|
234
|
-
const hasOuterCorrespondingIdentifier = (
|
|
235
|
-
shortName: string,
|
|
236
|
-
node: TSESTree.Identifier
|
|
237
|
-
) =>
|
|
238
|
-
checkScopeVariables(shortName, node) ||
|
|
239
|
-
checkBlockVariables(shortName, node) ||
|
|
240
|
-
checkAncestorDeclarators(shortName, node) ||
|
|
241
|
-
checkAncestorParams(shortName, node);
|
|
242
|
-
|
|
243
|
-
/**
|
|
244
|
-
* Checks an Identifier node. If its name is shorter than minLength (and not in the allowed list)
|
|
245
|
-
* and no outer variable with a longer name starting with the short name is found, it reports an error.
|
|
246
|
-
*/
|
|
247
|
-
const checkIdentifier = (node: TSESTree.Identifier) => {
|
|
248
|
-
const { name } = node;
|
|
249
|
-
if (name.length >= minLength) {
|
|
250
|
-
return;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
// If the name is in the allowed list, skip.
|
|
254
|
-
if (allowedVars.includes(name)) {
|
|
255
|
-
return;
|
|
256
|
-
}
|
|
257
|
-
if (!hasOuterCorrespondingIdentifier(name, node)) {
|
|
258
|
-
context.report({
|
|
259
|
-
data: { minLength, name },
|
|
260
|
-
messageId: "variableNameTooShort",
|
|
261
|
-
node
|
|
262
|
-
});
|
|
263
|
-
}
|
|
264
|
-
};
|
|
265
|
-
|
|
266
|
-
/**
|
|
267
|
-
* Recursively checks a pattern node for identifiers.
|
|
268
|
-
*/
|
|
269
|
-
const checkPattern = (pattern: TSESTree.Node | null) => {
|
|
270
|
-
if (!pattern) return;
|
|
271
|
-
switch (pattern.type) {
|
|
272
|
-
case "Identifier":
|
|
273
|
-
checkIdentifier(pattern);
|
|
274
|
-
break;
|
|
275
|
-
case "ObjectPattern":
|
|
276
|
-
pattern.properties.forEach((prop) => {
|
|
277
|
-
if (prop.type === "Property") {
|
|
278
|
-
checkPattern(prop.value);
|
|
279
|
-
} else if (prop.type === "RestElement") {
|
|
280
|
-
checkPattern(prop.argument);
|
|
281
|
-
}
|
|
282
|
-
});
|
|
283
|
-
break;
|
|
284
|
-
case "ArrayPattern":
|
|
285
|
-
pattern.elements
|
|
286
|
-
.filter(
|
|
287
|
-
(
|
|
288
|
-
element
|
|
289
|
-
): element is TSESTree.DestructuringPattern =>
|
|
290
|
-
element !== null
|
|
291
|
-
)
|
|
292
|
-
.forEach((element) => {
|
|
293
|
-
checkPattern(element);
|
|
294
|
-
});
|
|
295
|
-
break;
|
|
296
|
-
case "AssignmentPattern":
|
|
297
|
-
checkPattern(pattern.left);
|
|
298
|
-
break;
|
|
299
|
-
default:
|
|
300
|
-
break;
|
|
301
|
-
}
|
|
302
|
-
};
|
|
303
|
-
|
|
304
|
-
return {
|
|
305
|
-
CatchClause(node: TSESTree.CatchClause) {
|
|
306
|
-
if (node.param) {
|
|
307
|
-
checkPattern(node.param);
|
|
308
|
-
}
|
|
309
|
-
},
|
|
310
|
-
"FunctionDeclaration, FunctionExpression, ArrowFunctionExpression"(
|
|
311
|
-
node:
|
|
312
|
-
| TSESTree.FunctionDeclaration
|
|
313
|
-
| TSESTree.FunctionExpression
|
|
314
|
-
| TSESTree.ArrowFunctionExpression
|
|
315
|
-
) {
|
|
316
|
-
node.params.forEach((param) => {
|
|
317
|
-
checkPattern(param);
|
|
318
|
-
});
|
|
319
|
-
},
|
|
320
|
-
VariableDeclarator(node: TSESTree.VariableDeclarator) {
|
|
321
|
-
if (node.id) {
|
|
322
|
-
checkPattern(node.id);
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
};
|
|
326
|
-
},
|
|
327
|
-
defaultOptions: [{}],
|
|
328
|
-
meta: {
|
|
329
|
-
docs: {
|
|
330
|
-
description:
|
|
331
|
-
"Disallow variable names shorter than the configured minimum length unless an outer variable with a longer name starting with the same characters exists. You can exempt specific variable names using the allowedVars option."
|
|
332
|
-
},
|
|
333
|
-
messages: {
|
|
334
|
-
variableNameTooShort:
|
|
335
|
-
"Variable '{{name}}' is too short. Minimum allowed length is {{minLength}} characters unless an outer variable with a longer name starting with '{{name}}' exists."
|
|
336
|
-
},
|
|
337
|
-
schema: [
|
|
338
|
-
{
|
|
339
|
-
additionalProperties: false,
|
|
340
|
-
properties: {
|
|
341
|
-
allowedVars: {
|
|
342
|
-
default: [],
|
|
343
|
-
items: {
|
|
344
|
-
minLength: 1,
|
|
345
|
-
type: "string"
|
|
346
|
-
// Note: The maxLength for each string should be at most the configured minLength.
|
|
347
|
-
},
|
|
348
|
-
type: "array"
|
|
349
|
-
},
|
|
350
|
-
minLength: {
|
|
351
|
-
default: 1,
|
|
352
|
-
type: "number"
|
|
353
|
-
}
|
|
354
|
-
},
|
|
355
|
-
type: "object"
|
|
356
|
-
}
|
|
357
|
-
],
|
|
358
|
-
type: "problem"
|
|
359
|
-
}
|
|
360
|
-
};
|
|
@@ -1,270 +0,0 @@
|
|
|
1
|
-
import { TSESLint, TSESTree } from "@typescript-eslint/utils";
|
|
2
|
-
|
|
3
|
-
type Options = [];
|
|
4
|
-
type MessageIds = "noButtonNavigation";
|
|
5
|
-
|
|
6
|
-
type HandlerState = {
|
|
7
|
-
attribute: TSESTree.JSXAttribute;
|
|
8
|
-
reason: string | null;
|
|
9
|
-
sawReplaceCall: boolean;
|
|
10
|
-
sawAllowedLocationRead: boolean;
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
export const noButtonNavigation: TSESLint.RuleModule<MessageIds, Options> = {
|
|
14
|
-
create(context) {
|
|
15
|
-
const handlerStack: HandlerState[] = [];
|
|
16
|
-
|
|
17
|
-
const getCurrentHandler = () => {
|
|
18
|
-
const state = handlerStack[handlerStack.length - 1];
|
|
19
|
-
if (!state) {
|
|
20
|
-
return null;
|
|
21
|
-
}
|
|
22
|
-
return state;
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
const isOnClickButtonHandler = (
|
|
26
|
-
node: TSESTree.ArrowFunctionExpression | TSESTree.FunctionExpression
|
|
27
|
-
) => {
|
|
28
|
-
const { parent } = node;
|
|
29
|
-
if (!parent || parent.type !== "JSXExpressionContainer") {
|
|
30
|
-
return null;
|
|
31
|
-
}
|
|
32
|
-
const attributeCandidate = parent.parent;
|
|
33
|
-
if (
|
|
34
|
-
!attributeCandidate ||
|
|
35
|
-
attributeCandidate.type !== "JSXAttribute"
|
|
36
|
-
) {
|
|
37
|
-
return null;
|
|
38
|
-
}
|
|
39
|
-
const attr = attributeCandidate;
|
|
40
|
-
if (
|
|
41
|
-
!attr.name ||
|
|
42
|
-
attr.name.type !== "JSXIdentifier" ||
|
|
43
|
-
attr.name.name !== "onClick"
|
|
44
|
-
) {
|
|
45
|
-
return null;
|
|
46
|
-
}
|
|
47
|
-
const openingElementCandidate = attr.parent;
|
|
48
|
-
if (
|
|
49
|
-
!openingElementCandidate ||
|
|
50
|
-
openingElementCandidate.type !== "JSXOpeningElement"
|
|
51
|
-
) {
|
|
52
|
-
return null;
|
|
53
|
-
}
|
|
54
|
-
const tagNameNode = openingElementCandidate.name;
|
|
55
|
-
if (
|
|
56
|
-
tagNameNode.type !== "JSXIdentifier" ||
|
|
57
|
-
tagNameNode.name !== "button"
|
|
58
|
-
) {
|
|
59
|
-
return null;
|
|
60
|
-
}
|
|
61
|
-
return attr;
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
const isWindowLocationMember = (member: TSESTree.MemberExpression) => {
|
|
65
|
-
const { object } = member;
|
|
66
|
-
if (object.type !== "MemberExpression") {
|
|
67
|
-
return false;
|
|
68
|
-
}
|
|
69
|
-
const outerObject = object.object;
|
|
70
|
-
const outerProperty = object.property;
|
|
71
|
-
return (
|
|
72
|
-
outerObject.type === "Identifier" &&
|
|
73
|
-
outerObject.name === "window" &&
|
|
74
|
-
outerProperty.type === "Identifier" &&
|
|
75
|
-
outerProperty.name === "location"
|
|
76
|
-
);
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
const isWindowHistoryMember = (member: TSESTree.MemberExpression) => {
|
|
80
|
-
const { object } = member;
|
|
81
|
-
if (object.type !== "MemberExpression") {
|
|
82
|
-
return false;
|
|
83
|
-
}
|
|
84
|
-
const outerObject = object.object;
|
|
85
|
-
const outerProperty = object.property;
|
|
86
|
-
return (
|
|
87
|
-
outerObject.type === "Identifier" &&
|
|
88
|
-
outerObject.name === "window" &&
|
|
89
|
-
outerProperty.type === "Identifier" &&
|
|
90
|
-
outerProperty.name === "history"
|
|
91
|
-
);
|
|
92
|
-
};
|
|
93
|
-
|
|
94
|
-
const reportHandlerExit = (state: HandlerState) => {
|
|
95
|
-
const { reason, sawReplaceCall, sawAllowedLocationRead } = state;
|
|
96
|
-
|
|
97
|
-
if (reason) {
|
|
98
|
-
context.report({
|
|
99
|
-
data: { reason },
|
|
100
|
-
messageId: "noButtonNavigation",
|
|
101
|
-
node: state.attribute
|
|
102
|
-
});
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
if (sawReplaceCall && !sawAllowedLocationRead) {
|
|
107
|
-
context.report({
|
|
108
|
-
data: {
|
|
109
|
-
reason: "history.replaceState/pushState without reading window.location"
|
|
110
|
-
},
|
|
111
|
-
messageId: "noButtonNavigation",
|
|
112
|
-
node: state.attribute
|
|
113
|
-
});
|
|
114
|
-
}
|
|
115
|
-
};
|
|
116
|
-
|
|
117
|
-
return {
|
|
118
|
-
ArrowFunctionExpression(node: TSESTree.ArrowFunctionExpression) {
|
|
119
|
-
const attr = isOnClickButtonHandler(node);
|
|
120
|
-
if (!attr) {
|
|
121
|
-
return;
|
|
122
|
-
}
|
|
123
|
-
handlerStack.push({
|
|
124
|
-
attribute: attr,
|
|
125
|
-
reason: null,
|
|
126
|
-
sawAllowedLocationRead: false,
|
|
127
|
-
sawReplaceCall: false
|
|
128
|
-
});
|
|
129
|
-
},
|
|
130
|
-
"ArrowFunctionExpression:exit"(
|
|
131
|
-
node: TSESTree.ArrowFunctionExpression
|
|
132
|
-
) {
|
|
133
|
-
const attr = isOnClickButtonHandler(node);
|
|
134
|
-
if (!attr) {
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
|
-
const state = handlerStack.pop();
|
|
138
|
-
if (!state) {
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
reportHandlerExit(state);
|
|
143
|
-
},
|
|
144
|
-
AssignmentExpression(node: TSESTree.AssignmentExpression) {
|
|
145
|
-
const state = getCurrentHandler();
|
|
146
|
-
if (!state) {
|
|
147
|
-
return;
|
|
148
|
-
}
|
|
149
|
-
if (node.left.type !== "MemberExpression") {
|
|
150
|
-
return;
|
|
151
|
-
}
|
|
152
|
-
const { left } = node;
|
|
153
|
-
|
|
154
|
-
// window.location = ...
|
|
155
|
-
if (
|
|
156
|
-
left.object.type === "Identifier" &&
|
|
157
|
-
left.object.name === "window" &&
|
|
158
|
-
left.property.type === "Identifier" &&
|
|
159
|
-
left.property.name === "location" &&
|
|
160
|
-
!state.reason
|
|
161
|
-
) {
|
|
162
|
-
state.reason = "assignment to window.location";
|
|
163
|
-
return;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// window.location.href = ... OR window.location.pathname = ...
|
|
167
|
-
if (isWindowLocationMember(left) && !state.reason) {
|
|
168
|
-
state.reason = "assignment to window.location sub-property";
|
|
169
|
-
}
|
|
170
|
-
},
|
|
171
|
-
CallExpression(node: TSESTree.CallExpression) {
|
|
172
|
-
const state = getCurrentHandler();
|
|
173
|
-
if (!state) {
|
|
174
|
-
return;
|
|
175
|
-
}
|
|
176
|
-
const { callee } = node;
|
|
177
|
-
|
|
178
|
-
if (callee.type !== "MemberExpression") {
|
|
179
|
-
return;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
// 3) window.location.replace(...)
|
|
183
|
-
if (
|
|
184
|
-
isWindowLocationMember(callee) &&
|
|
185
|
-
callee.property.type === "Identifier" &&
|
|
186
|
-
callee.property.name === "replace" &&
|
|
187
|
-
!state.reason
|
|
188
|
-
) {
|
|
189
|
-
state.reason = "window.location.replace";
|
|
190
|
-
return;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// 4) window.history.pushState(...) or replaceState(...)
|
|
194
|
-
if (
|
|
195
|
-
isWindowHistoryMember(callee) &&
|
|
196
|
-
callee.property.type === "Identifier" &&
|
|
197
|
-
(callee.property.name === "pushState" ||
|
|
198
|
-
callee.property.name === "replaceState")
|
|
199
|
-
) {
|
|
200
|
-
state.sawReplaceCall = true;
|
|
201
|
-
}
|
|
202
|
-
},
|
|
203
|
-
FunctionExpression(node: TSESTree.FunctionExpression) {
|
|
204
|
-
const attr = isOnClickButtonHandler(node);
|
|
205
|
-
if (!attr) {
|
|
206
|
-
return;
|
|
207
|
-
}
|
|
208
|
-
handlerStack.push({
|
|
209
|
-
attribute: attr,
|
|
210
|
-
reason: null,
|
|
211
|
-
sawAllowedLocationRead: false,
|
|
212
|
-
sawReplaceCall: false
|
|
213
|
-
});
|
|
214
|
-
},
|
|
215
|
-
"FunctionExpression:exit"(node: TSESTree.FunctionExpression) {
|
|
216
|
-
const attr = isOnClickButtonHandler(node);
|
|
217
|
-
if (!attr) {
|
|
218
|
-
return;
|
|
219
|
-
}
|
|
220
|
-
const state = handlerStack.pop();
|
|
221
|
-
if (!state) {
|
|
222
|
-
return;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
reportHandlerExit(state);
|
|
226
|
-
},
|
|
227
|
-
MemberExpression(node: TSESTree.MemberExpression) {
|
|
228
|
-
const state = getCurrentHandler();
|
|
229
|
-
if (!state) {
|
|
230
|
-
return;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
// 1) window.open(...)
|
|
234
|
-
if (
|
|
235
|
-
node.object.type === "Identifier" &&
|
|
236
|
-
node.object.name === "window" &&
|
|
237
|
-
node.property.type === "Identifier" &&
|
|
238
|
-
node.property.name === "open" &&
|
|
239
|
-
!state.reason
|
|
240
|
-
) {
|
|
241
|
-
state.reason = "window.open";
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
// 5) Reading window.location.search, .pathname, or .hash
|
|
245
|
-
if (
|
|
246
|
-
isWindowLocationMember(node) &&
|
|
247
|
-
node.property.type === "Identifier" &&
|
|
248
|
-
(node.property.name === "search" ||
|
|
249
|
-
node.property.name === "pathname" ||
|
|
250
|
-
node.property.name === "hash")
|
|
251
|
-
) {
|
|
252
|
-
state.sawAllowedLocationRead = true;
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
};
|
|
256
|
-
},
|
|
257
|
-
defaultOptions: [],
|
|
258
|
-
meta: {
|
|
259
|
-
docs: {
|
|
260
|
-
description:
|
|
261
|
-
"Enforce using anchor tags for navigation instead of buttons whose onClick handlers change the path. Allow only query/hash updates via window.location.search or history.replaceState(window.location.pathname + …)."
|
|
262
|
-
},
|
|
263
|
-
messages: {
|
|
264
|
-
noButtonNavigation:
|
|
265
|
-
"Use an anchor tag for navigation instead of a button whose onClick handler changes the path. Detected: {{reason}}. Only query/hash updates (reading window.location.search, .pathname, or .hash) are allowed."
|
|
266
|
-
},
|
|
267
|
-
schema: [],
|
|
268
|
-
type: "suggestion"
|
|
269
|
-
}
|
|
270
|
-
};
|