eslint-plugin-absolute 0.0.3 → 0.1.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/dist/index.js +190 -115
- package/package.json +1 -1
- package/src/index.js +0 -2
- package/src/rules/no-button-navigation.js +101 -18
- package/src/rules/no-explicit-return-types.js +142 -36
- package/src/rules/no-type-cast.js +0 -50
package/dist/index.js
CHANGED
|
@@ -1,4 +1,118 @@
|
|
|
1
1
|
// @bun
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __toESM = (mod, isNodeMode, target) => {
|
|
8
|
+
target = mod != null ? __create(__getProtoOf(mod)) : {};
|
|
9
|
+
const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
|
|
10
|
+
for (let key of __getOwnPropNames(mod))
|
|
11
|
+
if (!__hasOwnProp.call(to, key))
|
|
12
|
+
__defProp(to, key, {
|
|
13
|
+
get: () => mod[key],
|
|
14
|
+
enumerable: true
|
|
15
|
+
});
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
|
|
19
|
+
|
|
20
|
+
// src/rules/no-button-navigation.js
|
|
21
|
+
var require_no_button_navigation = __commonJS((exports, module) => {
|
|
22
|
+
module.exports = {
|
|
23
|
+
meta: {
|
|
24
|
+
type: "suggestion",
|
|
25
|
+
docs: {
|
|
26
|
+
description: "Enforce using anchor tags for navigation instead of buttons with onClick handlers that change the path. Allow only query/hash updates via window.location.search or history.replaceState(window.location.pathname + \u2026).",
|
|
27
|
+
category: "Best Practices",
|
|
28
|
+
recommended: false
|
|
29
|
+
},
|
|
30
|
+
schema: []
|
|
31
|
+
},
|
|
32
|
+
create(context) {
|
|
33
|
+
function containsWindowNavigation(node) {
|
|
34
|
+
let found = false;
|
|
35
|
+
function inspect(n, parent) {
|
|
36
|
+
if (found || !n || typeof n !== "object")
|
|
37
|
+
return;
|
|
38
|
+
if (n.type === "MemberExpression" && n.object.type === "Identifier" && n.object.name === "window" && n.property.type === "Identifier" && n.property.name === "open") {
|
|
39
|
+
found = true;
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (n.type === "MemberExpression" && n.object.type === "Identifier" && n.object.name === "window" && n.property.type === "Identifier" && n.property.name === "location") {
|
|
43
|
+
if (parent && parent.type === "MemberExpression" && parent.object === n && parent.property.type === "Identifier" && (parent.property.name === "search" || parent.property.name === "hash")) {} else {
|
|
44
|
+
found = true;
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (n.type === "MemberExpression" && n.object.type === "MemberExpression" && n.object.object.type === "Identifier" && n.object.object.name === "window" && n.object.property.type === "Identifier" && n.object.property.name === "history" && n.property.type === "Identifier" && (n.property.name === "pushState" || n.property.name === "replaceState")) {
|
|
49
|
+
if (parent && parent.type === "CallExpression" && Array.isArray(parent.arguments) && parent.arguments.length >= 3) {
|
|
50
|
+
const urlArg = parent.arguments[2];
|
|
51
|
+
if (!urlUsesAllowedLocation(urlArg)) {
|
|
52
|
+
found = true;
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
for (const key of Object.keys(n)) {
|
|
58
|
+
if (key === "parent")
|
|
59
|
+
continue;
|
|
60
|
+
const child = n[key];
|
|
61
|
+
if (Array.isArray(child)) {
|
|
62
|
+
child.forEach((c) => inspect(c, n));
|
|
63
|
+
} else {
|
|
64
|
+
inspect(child, n);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function urlUsesAllowedLocation(argNode) {
|
|
69
|
+
let allowed = false;
|
|
70
|
+
function check(n) {
|
|
71
|
+
if (allowed || !n || typeof n !== "object")
|
|
72
|
+
return;
|
|
73
|
+
if (n.type === "MemberExpression" && n.object.type === "MemberExpression" && n.object.object.type === "Identifier" && n.object.object.name === "window" && n.object.property.type === "Identifier" && n.object.property.name === "location" && n.property.type === "Identifier" && (n.property.name === "pathname" || n.property.name === "search" || n.property.name === "hash")) {
|
|
74
|
+
allowed = true;
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
for (const key of Object.keys(n)) {
|
|
78
|
+
if (key === "parent")
|
|
79
|
+
continue;
|
|
80
|
+
const c = n[key];
|
|
81
|
+
if (Array.isArray(c)) {
|
|
82
|
+
c.forEach(check);
|
|
83
|
+
} else {
|
|
84
|
+
check(c);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
check(argNode);
|
|
89
|
+
return allowed;
|
|
90
|
+
}
|
|
91
|
+
inspect(node.type === "ArrowFunctionExpression" || node.type === "FunctionExpression" ? node.body : node, null);
|
|
92
|
+
return found;
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
JSXElement(node) {
|
|
96
|
+
const { openingElement } = node;
|
|
97
|
+
if (openingElement.name.type === "JSXIdentifier" && openingElement.name.name === "button") {
|
|
98
|
+
for (const attr of openingElement.attributes) {
|
|
99
|
+
if (attr.type === "JSXAttribute" && attr.name.name === "onClick" && attr.value?.type === "JSXExpressionContainer") {
|
|
100
|
+
const expr = attr.value.expression;
|
|
101
|
+
if ((expr.type === "ArrowFunctionExpression" || expr.type === "FunctionExpression") && containsWindowNavigation(expr)) {
|
|
102
|
+
context.report({
|
|
103
|
+
node: attr,
|
|
104
|
+
message: "Use an anchor tag for navigation instead of a button with an onClick handler that changes the path. Only query/hash updates are allowed."
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
});
|
|
115
|
+
|
|
2
116
|
// src/rules/no-nested-jsx-return.js
|
|
3
117
|
var no_nested_jsx_return_default = {
|
|
4
118
|
meta: {
|
|
@@ -149,45 +263,6 @@ var explicit_object_types_default = {
|
|
|
149
263
|
}
|
|
150
264
|
};
|
|
151
265
|
|
|
152
|
-
// src/rules/no-type-cast.js
|
|
153
|
-
var no_type_cast_default = {
|
|
154
|
-
meta: {
|
|
155
|
-
type: "problem",
|
|
156
|
-
docs: {
|
|
157
|
-
description: 'Disallow type assertions using "as", angle bracket syntax, or built-in conversion functions.',
|
|
158
|
-
recommended: false
|
|
159
|
-
},
|
|
160
|
-
schema: []
|
|
161
|
-
},
|
|
162
|
-
create(context) {
|
|
163
|
-
function isBuiltInConversion(node) {
|
|
164
|
-
return node && node.type === "Identifier" && ["Number", "String", "Boolean"].includes(node.name);
|
|
165
|
-
}
|
|
166
|
-
return {
|
|
167
|
-
TSAsExpression(node) {
|
|
168
|
-
context.report({
|
|
169
|
-
node,
|
|
170
|
-
message: 'Type assertions using "as" syntax are not allowed.'
|
|
171
|
-
});
|
|
172
|
-
},
|
|
173
|
-
TSTypeAssertion(node) {
|
|
174
|
-
context.report({
|
|
175
|
-
node,
|
|
176
|
-
message: "Type assertions using angle bracket syntax are not allowed."
|
|
177
|
-
});
|
|
178
|
-
},
|
|
179
|
-
CallExpression(node) {
|
|
180
|
-
if (isBuiltInConversion(node.callee)) {
|
|
181
|
-
context.report({
|
|
182
|
-
node,
|
|
183
|
-
message: `Type assertions using "${node.callee.name}()" syntax are not allowed.`
|
|
184
|
-
});
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
};
|
|
188
|
-
}
|
|
189
|
-
};
|
|
190
|
-
|
|
191
266
|
// src/rules/sort-keys-fixable.js
|
|
192
267
|
var sort_keys_fixable_default = {
|
|
193
268
|
meta: {
|
|
@@ -457,35 +532,89 @@ var no_explicit_return_types_default = {
|
|
|
457
532
|
meta: {
|
|
458
533
|
type: "suggestion",
|
|
459
534
|
docs: {
|
|
460
|
-
description: "
|
|
535
|
+
description: "Enforce using anchor tags for navigation instead of buttons with onClick handlers that change the path. Allow only query/hash updates via window.location.search or history.replaceState(window.location.pathname + \u2026).",
|
|
536
|
+
category: "Best Practices",
|
|
461
537
|
recommended: false
|
|
462
538
|
},
|
|
463
|
-
schema: []
|
|
464
|
-
messages: {
|
|
465
|
-
noExplicitReturnType: "Explicit return types are disallowed; rely on TypeScript's inference instead."
|
|
466
|
-
}
|
|
539
|
+
schema: []
|
|
467
540
|
},
|
|
468
541
|
create(context) {
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
542
|
+
function containsWindowNavigation(node) {
|
|
543
|
+
let found = false;
|
|
544
|
+
function inspect(n, parent) {
|
|
545
|
+
if (found || !n || typeof n !== "object")
|
|
546
|
+
return;
|
|
547
|
+
if (n.type === "MemberExpression" && n.object.type === "Identifier" && n.object.name === "window" && n.property.type === "Identifier" && n.property.name === "open") {
|
|
548
|
+
found = true;
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
if (n.type === "MemberExpression" && n.object.type === "Identifier" && n.object.name === "window" && n.property.type === "Identifier" && n.property.name === "location") {
|
|
552
|
+
if (parent && parent.type === "MemberExpression" && parent.object === n && parent.property.type === "Identifier" && (parent.property.name === "search" || parent.property.name === "hash")) {} else {
|
|
553
|
+
found = true;
|
|
474
554
|
return;
|
|
475
555
|
}
|
|
476
|
-
|
|
556
|
+
}
|
|
557
|
+
if (n.type === "MemberExpression" && n.object.type === "MemberExpression" && n.object.object.type === "Identifier" && n.object.object.name === "window" && n.object.property.type === "Identifier" && n.object.property.name === "history" && n.property.type === "Identifier" && (n.property.name === "pushState" || n.property.name === "replaceState")) {
|
|
558
|
+
if (parent && parent.type === "CallExpression" && Array.isArray(parent.arguments) && parent.arguments.length >= 3) {
|
|
559
|
+
const urlArg = parent.arguments[2];
|
|
560
|
+
if (!urlUsesAllowedLocation(urlArg)) {
|
|
561
|
+
found = true;
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
for (const key of Object.keys(n)) {
|
|
567
|
+
if (key === "parent")
|
|
568
|
+
continue;
|
|
569
|
+
const child = n[key];
|
|
570
|
+
if (Array.isArray(child)) {
|
|
571
|
+
child.forEach((c) => inspect(c, n));
|
|
572
|
+
} else {
|
|
573
|
+
inspect(child, n);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
function urlUsesAllowedLocation(argNode) {
|
|
578
|
+
let allowed = false;
|
|
579
|
+
function check(n) {
|
|
580
|
+
if (allowed || !n || typeof n !== "object")
|
|
581
|
+
return;
|
|
582
|
+
if (n.type === "MemberExpression" && n.object.type === "MemberExpression" && n.object.object.type === "Identifier" && n.object.object.name === "window" && n.object.property.type === "Identifier" && n.object.property.name === "location" && n.property.type === "Identifier" && (n.property.name === "pathname" || n.property.name === "search" || n.property.name === "hash")) {
|
|
583
|
+
allowed = true;
|
|
477
584
|
return;
|
|
478
585
|
}
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
586
|
+
for (const key of Object.keys(n)) {
|
|
587
|
+
if (key === "parent")
|
|
588
|
+
continue;
|
|
589
|
+
const c = n[key];
|
|
590
|
+
if (Array.isArray(c)) {
|
|
591
|
+
c.forEach(check);
|
|
592
|
+
} else {
|
|
593
|
+
check(c);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
check(argNode);
|
|
598
|
+
return allowed;
|
|
599
|
+
}
|
|
600
|
+
inspect(node.type === "ArrowFunctionExpression" || node.type === "FunctionExpression" ? node.body : node, null);
|
|
601
|
+
return found;
|
|
602
|
+
}
|
|
603
|
+
return {
|
|
604
|
+
JSXElement(node) {
|
|
605
|
+
const { openingElement } = node;
|
|
606
|
+
if (openingElement.name.type === "JSXIdentifier" && openingElement.name.name === "button") {
|
|
607
|
+
for (const attr of openingElement.attributes) {
|
|
608
|
+
if (attr.type === "JSXAttribute" && attr.name.name === "onClick" && attr.value?.type === "JSXExpressionContainer") {
|
|
609
|
+
const expr = attr.value.expression;
|
|
610
|
+
if ((expr.type === "ArrowFunctionExpression" || expr.type === "FunctionExpression") && containsWindowNavigation(expr)) {
|
|
611
|
+
context.report({
|
|
612
|
+
node: attr,
|
|
613
|
+
message: "Use an anchor tag for navigation instead of a button with an onClick handler that changes the path. Only query/hash updates are allowed."
|
|
614
|
+
});
|
|
615
|
+
}
|
|
483
616
|
}
|
|
484
617
|
}
|
|
485
|
-
context.report({
|
|
486
|
-
node: node.returnType,
|
|
487
|
-
messageId: "noExplicitReturnType"
|
|
488
|
-
});
|
|
489
618
|
}
|
|
490
619
|
}
|
|
491
620
|
};
|
|
@@ -1189,61 +1318,8 @@ var no_or_none_component_default = {
|
|
|
1189
1318
|
}
|
|
1190
1319
|
};
|
|
1191
1320
|
|
|
1192
|
-
// src/
|
|
1193
|
-
var
|
|
1194
|
-
meta: {
|
|
1195
|
-
type: "suggestion",
|
|
1196
|
-
docs: {
|
|
1197
|
-
description: "Enforce using anchor tags for navigation instead of buttons with onClick handlers that use window navigation methods (e.g., window.location, window.open)",
|
|
1198
|
-
category: "Best Practices",
|
|
1199
|
-
recommended: false
|
|
1200
|
-
},
|
|
1201
|
-
schema: []
|
|
1202
|
-
},
|
|
1203
|
-
create(context) {
|
|
1204
|
-
function containsWindowNavigation(node) {
|
|
1205
|
-
let found = false;
|
|
1206
|
-
function inspect(n) {
|
|
1207
|
-
if (found || !n || typeof n !== "object")
|
|
1208
|
-
return;
|
|
1209
|
-
if (n.type === "MemberExpression" && n.object.type === "Identifier" && n.object.name === "window" && n.property.type === "Identifier" && (n.property.name === "open" || n.property.name === "location")) {
|
|
1210
|
-
found = true;
|
|
1211
|
-
return;
|
|
1212
|
-
}
|
|
1213
|
-
for (const key of Object.keys(n)) {
|
|
1214
|
-
if (key === "parent")
|
|
1215
|
-
continue;
|
|
1216
|
-
const child = n[key];
|
|
1217
|
-
if (Array.isArray(child)) {
|
|
1218
|
-
child.forEach(inspect);
|
|
1219
|
-
} else {
|
|
1220
|
-
inspect(child);
|
|
1221
|
-
}
|
|
1222
|
-
}
|
|
1223
|
-
}
|
|
1224
|
-
inspect(node.type === "ArrowFunctionExpression" || node.type === "FunctionExpression" ? node.body : node);
|
|
1225
|
-
return found;
|
|
1226
|
-
}
|
|
1227
|
-
return {
|
|
1228
|
-
JSXElement(node) {
|
|
1229
|
-
const { openingElement } = node;
|
|
1230
|
-
if (openingElement.name.type === "JSXIdentifier" && openingElement.name.name === "button") {
|
|
1231
|
-
for (const attr of openingElement.attributes) {
|
|
1232
|
-
if (attr.type === "JSXAttribute" && attr.name.name === "onClick" && attr.value?.type === "JSXExpressionContainer") {
|
|
1233
|
-
const expr = attr.value.expression;
|
|
1234
|
-
if ((expr.type === "ArrowFunctionExpression" || expr.type === "FunctionExpression") && containsWindowNavigation(expr)) {
|
|
1235
|
-
context.report({
|
|
1236
|
-
node: attr,
|
|
1237
|
-
message: "Use an anchor tag for navigation instead of a button with an onClick handler that uses window navigation methods."
|
|
1238
|
-
});
|
|
1239
|
-
}
|
|
1240
|
-
}
|
|
1241
|
-
}
|
|
1242
|
-
}
|
|
1243
|
-
}
|
|
1244
|
-
};
|
|
1245
|
-
}
|
|
1246
|
-
};
|
|
1321
|
+
// src/index.js
|
|
1322
|
+
var import_no_button_navigation = __toESM(require_no_button_navigation(), 1);
|
|
1247
1323
|
|
|
1248
1324
|
// src/rules/no-multi-style-objects.js
|
|
1249
1325
|
var no_multi_style_objects_default = {
|
|
@@ -1840,7 +1916,6 @@ var src_default = {
|
|
|
1840
1916
|
rules: {
|
|
1841
1917
|
"no-nested-jsx-return": no_nested_jsx_return_default,
|
|
1842
1918
|
"explicit-object-types": explicit_object_types_default,
|
|
1843
|
-
"no-type-cast": no_type_cast_default,
|
|
1844
1919
|
"sort-keys-fixable": sort_keys_fixable_default,
|
|
1845
1920
|
"no-transition-cssproperties": no_transition_cssproperties_default,
|
|
1846
1921
|
"no-explicit-return-type": no_explicit_return_types_default,
|
|
@@ -1850,7 +1925,7 @@ var src_default = {
|
|
|
1850
1925
|
"sort-exports": sort_exports_default,
|
|
1851
1926
|
"localize-react-props": localize_react_props_default,
|
|
1852
1927
|
"no-or-none-component": no_or_none_component_default,
|
|
1853
|
-
"no-button-navigation":
|
|
1928
|
+
"no-button-navigation": import_no_button_navigation.default,
|
|
1854
1929
|
"no-multi-style-objects": no_multi_style_objects_default,
|
|
1855
1930
|
"no-useless-function": no_useless_function_default,
|
|
1856
1931
|
"min-var-length": min_var_length_default,
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import noNestedJsxReturn from "./rules/no-nested-jsx-return.js";
|
|
2
2
|
import explicitObjectTypes from "./rules/explicit-object-types.js";
|
|
3
|
-
import noTypeCast from "./rules/no-type-cast.js";
|
|
4
3
|
import sortKeysFixable from "./rules/sort-keys-fixable.js";
|
|
5
4
|
import noTransitionCssproperties from "./rules/no-transition-cssproperties.js";
|
|
6
5
|
import noExplicitReturnTypes from "./rules/no-explicit-return-types.js";
|
|
@@ -24,7 +23,6 @@ export default {
|
|
|
24
23
|
rules: {
|
|
25
24
|
"no-nested-jsx-return": noNestedJsxReturn,
|
|
26
25
|
"explicit-object-types": explicitObjectTypes,
|
|
27
|
-
"no-type-cast": noTypeCast,
|
|
28
26
|
"sort-keys-fixable": sortKeysFixable,
|
|
29
27
|
"no-transition-cssproperties": noTransitionCssproperties,
|
|
30
28
|
"no-explicit-return-type": noExplicitReturnTypes,
|
|
@@ -1,52 +1,137 @@
|
|
|
1
|
-
|
|
1
|
+
module.exports = {
|
|
2
2
|
meta: {
|
|
3
3
|
type: "suggestion",
|
|
4
4
|
docs: {
|
|
5
5
|
description:
|
|
6
|
-
"Enforce using anchor tags for navigation instead of buttons with onClick handlers that
|
|
6
|
+
"Enforce using anchor tags for navigation instead of buttons with onClick handlers that change the path. Allow only query/hash updates via window.location.search or history.replaceState(window.location.pathname + …).",
|
|
7
7
|
category: "Best Practices",
|
|
8
8
|
recommended: false
|
|
9
9
|
},
|
|
10
10
|
schema: []
|
|
11
11
|
},
|
|
12
12
|
create(context) {
|
|
13
|
-
/**
|
|
14
|
-
* Inspects an AST node *only* for MemberExpressions where
|
|
15
|
-
* the object is literally `window` and the property is `open` or `location`.
|
|
16
|
-
*/
|
|
17
13
|
function containsWindowNavigation(node) {
|
|
18
14
|
let found = false;
|
|
19
|
-
|
|
15
|
+
|
|
16
|
+
function inspect(n, parent) {
|
|
20
17
|
if (found || !n || typeof n !== "object") return;
|
|
21
|
-
|
|
18
|
+
|
|
19
|
+
// Detect window.open(...)
|
|
22
20
|
if (
|
|
23
21
|
n.type === "MemberExpression" &&
|
|
24
22
|
n.object.type === "Identifier" &&
|
|
25
23
|
n.object.name === "window" &&
|
|
26
24
|
n.property.type === "Identifier" &&
|
|
27
|
-
|
|
28
|
-
n.property.name === "location")
|
|
25
|
+
n.property.name === "open"
|
|
29
26
|
) {
|
|
30
27
|
found = true;
|
|
31
28
|
return;
|
|
32
29
|
}
|
|
33
|
-
|
|
30
|
+
|
|
31
|
+
// Detect direct use of window.location (not just .search or .hash)
|
|
32
|
+
if (
|
|
33
|
+
n.type === "MemberExpression" &&
|
|
34
|
+
n.object.type === "Identifier" &&
|
|
35
|
+
n.object.name === "window" &&
|
|
36
|
+
n.property.type === "Identifier" &&
|
|
37
|
+
n.property.name === "location"
|
|
38
|
+
) {
|
|
39
|
+
if (
|
|
40
|
+
parent &&
|
|
41
|
+
parent.type === "MemberExpression" &&
|
|
42
|
+
parent.object === n &&
|
|
43
|
+
parent.property.type === "Identifier" &&
|
|
44
|
+
(parent.property.name === "search" ||
|
|
45
|
+
parent.property.name === "hash")
|
|
46
|
+
) {
|
|
47
|
+
// reading window.location.search or window.location.hash → OK
|
|
48
|
+
} else {
|
|
49
|
+
// any other use of window.location → flag
|
|
50
|
+
found = true;
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Detect window.history.pushState(...) or replaceState(...)
|
|
56
|
+
if (
|
|
57
|
+
n.type === "MemberExpression" &&
|
|
58
|
+
n.object.type === "MemberExpression" &&
|
|
59
|
+
n.object.object.type === "Identifier" &&
|
|
60
|
+
n.object.object.name === "window" &&
|
|
61
|
+
n.object.property.type === "Identifier" &&
|
|
62
|
+
n.object.property.name === "history" &&
|
|
63
|
+
n.property.type === "Identifier" &&
|
|
64
|
+
(n.property.name === "pushState" ||
|
|
65
|
+
n.property.name === "replaceState")
|
|
66
|
+
) {
|
|
67
|
+
if (
|
|
68
|
+
parent &&
|
|
69
|
+
parent.type === "CallExpression" &&
|
|
70
|
+
Array.isArray(parent.arguments) &&
|
|
71
|
+
parent.arguments.length >= 3
|
|
72
|
+
) {
|
|
73
|
+
const urlArg = parent.arguments[2];
|
|
74
|
+
if (!urlUsesAllowedLocation(urlArg)) {
|
|
75
|
+
found = true;
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
// If urlArg references window.location.pathname/search/hash → OK
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Recurse into child nodes
|
|
34
83
|
for (const key of Object.keys(n)) {
|
|
35
84
|
if (key === "parent") continue;
|
|
36
85
|
const child = n[key];
|
|
37
86
|
if (Array.isArray(child)) {
|
|
38
|
-
child.forEach(inspect);
|
|
87
|
+
child.forEach((c) => inspect(c, n));
|
|
39
88
|
} else {
|
|
40
|
-
inspect(child);
|
|
89
|
+
inspect(child, n);
|
|
41
90
|
}
|
|
42
91
|
}
|
|
43
92
|
}
|
|
44
|
-
|
|
93
|
+
|
|
94
|
+
function urlUsesAllowedLocation(argNode) {
|
|
95
|
+
let allowed = false;
|
|
96
|
+
|
|
97
|
+
function check(n) {
|
|
98
|
+
if (allowed || !n || typeof n !== "object") return;
|
|
99
|
+
if (
|
|
100
|
+
n.type === "MemberExpression" &&
|
|
101
|
+
n.object.type === "MemberExpression" &&
|
|
102
|
+
n.object.object.type === "Identifier" &&
|
|
103
|
+
n.object.object.name === "window" &&
|
|
104
|
+
n.object.property.type === "Identifier" &&
|
|
105
|
+
n.object.property.name === "location" &&
|
|
106
|
+
n.property.type === "Identifier" &&
|
|
107
|
+
(n.property.name === "pathname" ||
|
|
108
|
+
n.property.name === "search" ||
|
|
109
|
+
n.property.name === "hash")
|
|
110
|
+
) {
|
|
111
|
+
allowed = true;
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
for (const key of Object.keys(n)) {
|
|
115
|
+
if (key === "parent") continue;
|
|
116
|
+
const c = n[key];
|
|
117
|
+
if (Array.isArray(c)) {
|
|
118
|
+
c.forEach(check);
|
|
119
|
+
} else {
|
|
120
|
+
check(c);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
check(argNode);
|
|
126
|
+
return allowed;
|
|
127
|
+
}
|
|
128
|
+
|
|
45
129
|
inspect(
|
|
46
130
|
node.type === "ArrowFunctionExpression" ||
|
|
47
131
|
node.type === "FunctionExpression"
|
|
48
132
|
? node.body
|
|
49
|
-
: node
|
|
133
|
+
: node,
|
|
134
|
+
null
|
|
50
135
|
);
|
|
51
136
|
return found;
|
|
52
137
|
}
|
|
@@ -54,7 +139,6 @@ export default {
|
|
|
54
139
|
return {
|
|
55
140
|
JSXElement(node) {
|
|
56
141
|
const { openingElement } = node;
|
|
57
|
-
// only care about <button ...>
|
|
58
142
|
if (
|
|
59
143
|
openingElement.name.type === "JSXIdentifier" &&
|
|
60
144
|
openingElement.name.name === "button"
|
|
@@ -66,7 +150,6 @@ export default {
|
|
|
66
150
|
attr.value?.type === "JSXExpressionContainer"
|
|
67
151
|
) {
|
|
68
152
|
const expr = attr.value.expression;
|
|
69
|
-
// only inspect the inline function, not any Identifier calls
|
|
70
153
|
if (
|
|
71
154
|
(expr.type === "ArrowFunctionExpression" ||
|
|
72
155
|
expr.type === "FunctionExpression") &&
|
|
@@ -75,7 +158,7 @@ export default {
|
|
|
75
158
|
context.report({
|
|
76
159
|
node: attr,
|
|
77
160
|
message:
|
|
78
|
-
"Use an anchor tag for navigation instead of a button with an onClick handler that
|
|
161
|
+
"Use an anchor tag for navigation instead of a button with an onClick handler that changes the path. Only query/hash updates are allowed."
|
|
79
162
|
});
|
|
80
163
|
}
|
|
81
164
|
}
|
|
@@ -3,60 +3,166 @@ export default {
|
|
|
3
3
|
type: "suggestion",
|
|
4
4
|
docs: {
|
|
5
5
|
description:
|
|
6
|
-
"
|
|
6
|
+
"Enforce using anchor tags for navigation instead of buttons with onClick handlers that change the path. Allow only query/hash updates via window.location.search or history.replaceState(window.location.pathname + …).",
|
|
7
|
+
category: "Best Practices",
|
|
7
8
|
recommended: false
|
|
8
9
|
},
|
|
9
|
-
schema: []
|
|
10
|
-
messages: {
|
|
11
|
-
noExplicitReturnType:
|
|
12
|
-
"Explicit return types are disallowed; rely on TypeScript's inference instead."
|
|
13
|
-
}
|
|
10
|
+
schema: []
|
|
14
11
|
},
|
|
15
|
-
|
|
16
12
|
create(context) {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
) {
|
|
21
|
-
if (
|
|
22
|
-
|
|
23
|
-
|
|
13
|
+
function containsWindowNavigation(node) {
|
|
14
|
+
let found = false;
|
|
15
|
+
|
|
16
|
+
function inspect(n, parent) {
|
|
17
|
+
if (found || !n || typeof n !== "object") return;
|
|
18
|
+
|
|
19
|
+
// Detect window.open(...)
|
|
20
|
+
if (
|
|
21
|
+
n.type === "MemberExpression" &&
|
|
22
|
+
n.object.type === "Identifier" &&
|
|
23
|
+
n.object.name === "window" &&
|
|
24
|
+
n.property.type === "Identifier" &&
|
|
25
|
+
n.property.name === "open"
|
|
26
|
+
) {
|
|
27
|
+
found = true;
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Detect direct use of window.location (not just .search or .hash)
|
|
32
|
+
if (
|
|
33
|
+
n.type === "MemberExpression" &&
|
|
34
|
+
n.object.type === "Identifier" &&
|
|
35
|
+
n.object.name === "window" &&
|
|
36
|
+
n.property.type === "Identifier" &&
|
|
37
|
+
n.property.name === "location"
|
|
38
|
+
) {
|
|
24
39
|
if (
|
|
25
|
-
|
|
26
|
-
|
|
40
|
+
parent &&
|
|
41
|
+
parent.type === "MemberExpression" &&
|
|
42
|
+
parent.object === n &&
|
|
43
|
+
parent.property.type === "Identifier" &&
|
|
44
|
+
(parent.property.name === "search" ||
|
|
45
|
+
parent.property.name === "hash")
|
|
27
46
|
) {
|
|
47
|
+
// reading window.location.search or window.location.hash → OK
|
|
48
|
+
} else {
|
|
49
|
+
// any other use of window.location → flag
|
|
50
|
+
found = true;
|
|
28
51
|
return;
|
|
29
52
|
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Detect window.history.pushState(...) or replaceState(...)
|
|
56
|
+
if (
|
|
57
|
+
n.type === "MemberExpression" &&
|
|
58
|
+
n.object.type === "MemberExpression" &&
|
|
59
|
+
n.object.object.type === "Identifier" &&
|
|
60
|
+
n.object.object.name === "window" &&
|
|
61
|
+
n.object.property.type === "Identifier" &&
|
|
62
|
+
n.object.property.name === "history" &&
|
|
63
|
+
n.property.type === "Identifier" &&
|
|
64
|
+
(n.property.name === "pushState" ||
|
|
65
|
+
n.property.name === "replaceState")
|
|
66
|
+
) {
|
|
67
|
+
if (
|
|
68
|
+
parent &&
|
|
69
|
+
parent.type === "CallExpression" &&
|
|
70
|
+
Array.isArray(parent.arguments) &&
|
|
71
|
+
parent.arguments.length >= 3
|
|
72
|
+
) {
|
|
73
|
+
const urlArg = parent.arguments[2];
|
|
74
|
+
if (!urlUsesAllowedLocation(urlArg)) {
|
|
75
|
+
found = true;
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
// If urlArg references window.location.pathname/search/hash → OK
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Recurse into child nodes
|
|
83
|
+
for (const key of Object.keys(n)) {
|
|
84
|
+
if (key === "parent") continue;
|
|
85
|
+
const child = n[key];
|
|
86
|
+
if (Array.isArray(child)) {
|
|
87
|
+
child.forEach((c) => inspect(c, n));
|
|
88
|
+
} else {
|
|
89
|
+
inspect(child, n);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
30
93
|
|
|
31
|
-
|
|
94
|
+
function urlUsesAllowedLocation(argNode) {
|
|
95
|
+
let allowed = false;
|
|
96
|
+
|
|
97
|
+
function check(n) {
|
|
98
|
+
if (allowed || !n || typeof n !== "object") return;
|
|
32
99
|
if (
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
100
|
+
n.type === "MemberExpression" &&
|
|
101
|
+
n.object.type === "MemberExpression" &&
|
|
102
|
+
n.object.object.type === "Identifier" &&
|
|
103
|
+
n.object.object.name === "window" &&
|
|
104
|
+
n.object.property.type === "Identifier" &&
|
|
105
|
+
n.object.property.name === "location" &&
|
|
106
|
+
n.property.type === "Identifier" &&
|
|
107
|
+
(n.property.name === "pathname" ||
|
|
108
|
+
n.property.name === "search" ||
|
|
109
|
+
n.property.name === "hash")
|
|
37
110
|
) {
|
|
111
|
+
allowed = true;
|
|
38
112
|
return;
|
|
39
113
|
}
|
|
114
|
+
for (const key of Object.keys(n)) {
|
|
115
|
+
if (key === "parent") continue;
|
|
116
|
+
const c = n[key];
|
|
117
|
+
if (Array.isArray(c)) {
|
|
118
|
+
c.forEach(check);
|
|
119
|
+
} else {
|
|
120
|
+
check(c);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
40
124
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
125
|
+
check(argNode);
|
|
126
|
+
return allowed;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
inspect(
|
|
130
|
+
node.type === "ArrowFunctionExpression" ||
|
|
131
|
+
node.type === "FunctionExpression"
|
|
132
|
+
? node.body
|
|
133
|
+
: node,
|
|
134
|
+
null
|
|
135
|
+
);
|
|
136
|
+
return found;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
JSXElement(node) {
|
|
141
|
+
const { openingElement } = node;
|
|
142
|
+
if (
|
|
143
|
+
openingElement.name.type === "JSXIdentifier" &&
|
|
144
|
+
openingElement.name.name === "button"
|
|
145
|
+
) {
|
|
146
|
+
for (const attr of openingElement.attributes) {
|
|
46
147
|
if (
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
148
|
+
attr.type === "JSXAttribute" &&
|
|
149
|
+
attr.name.name === "onClick" &&
|
|
150
|
+
attr.value?.type === "JSXExpressionContainer"
|
|
50
151
|
) {
|
|
51
|
-
|
|
152
|
+
const expr = attr.value.expression;
|
|
153
|
+
if (
|
|
154
|
+
(expr.type === "ArrowFunctionExpression" ||
|
|
155
|
+
expr.type === "FunctionExpression") &&
|
|
156
|
+
containsWindowNavigation(expr)
|
|
157
|
+
) {
|
|
158
|
+
context.report({
|
|
159
|
+
node: attr,
|
|
160
|
+
message:
|
|
161
|
+
"Use an anchor tag for navigation instead of a button with an onClick handler that changes the path. Only query/hash updates are allowed."
|
|
162
|
+
});
|
|
163
|
+
}
|
|
52
164
|
}
|
|
53
165
|
}
|
|
54
|
-
|
|
55
|
-
// Otherwise, report an error.
|
|
56
|
-
context.report({
|
|
57
|
-
node: node.returnType,
|
|
58
|
-
messageId: "noExplicitReturnType"
|
|
59
|
-
});
|
|
60
166
|
}
|
|
61
167
|
}
|
|
62
168
|
};
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
export default {
|
|
2
|
-
meta: {
|
|
3
|
-
type: "problem",
|
|
4
|
-
docs: {
|
|
5
|
-
description:
|
|
6
|
-
'Disallow type assertions using "as", angle bracket syntax, or built-in conversion functions.',
|
|
7
|
-
recommended: false
|
|
8
|
-
},
|
|
9
|
-
schema: []
|
|
10
|
-
},
|
|
11
|
-
create(context) {
|
|
12
|
-
// Helper function to determine if a call expression is a built-in conversion.
|
|
13
|
-
function isBuiltInConversion(node) {
|
|
14
|
-
return (
|
|
15
|
-
node &&
|
|
16
|
-
node.type === "Identifier" &&
|
|
17
|
-
["Number", "String", "Boolean"].includes(node.name)
|
|
18
|
-
);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
return {
|
|
22
|
-
// Catch type assertions using "as" syntax.
|
|
23
|
-
TSAsExpression(node) {
|
|
24
|
-
context.report({
|
|
25
|
-
node,
|
|
26
|
-
message:
|
|
27
|
-
'Type assertions using "as" syntax are not allowed.'
|
|
28
|
-
});
|
|
29
|
-
},
|
|
30
|
-
// Catch type assertions using angle bracket syntax.
|
|
31
|
-
TSTypeAssertion(node) {
|
|
32
|
-
context.report({
|
|
33
|
-
node,
|
|
34
|
-
message:
|
|
35
|
-
"Type assertions using angle bracket syntax are not allowed."
|
|
36
|
-
});
|
|
37
|
-
},
|
|
38
|
-
// Catch type conversions using built-in functions like Number(prop)
|
|
39
|
-
CallExpression(node) {
|
|
40
|
-
// Check if the callee is a built-in conversion function.
|
|
41
|
-
if (isBuiltInConversion(node.callee)) {
|
|
42
|
-
context.report({
|
|
43
|
-
node,
|
|
44
|
-
message: `Type assertions using "${node.callee.name}()" syntax are not allowed.`
|
|
45
|
-
});
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
};
|