eslint-plugin-absolute 0.1.0 → 0.1.2
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 +56 -7
- package/package.json +1 -1
- package/src/rules/no-button-navigation.js +127 -17
package/dist/index.js
CHANGED
|
@@ -1155,7 +1155,7 @@ var no_button_navigation_default = {
|
|
|
1155
1155
|
meta: {
|
|
1156
1156
|
type: "suggestion",
|
|
1157
1157
|
docs: {
|
|
1158
|
-
description: "Enforce using anchor tags for navigation instead of buttons
|
|
1158
|
+
description: "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 + \u2026).",
|
|
1159
1159
|
category: "Best Practices",
|
|
1160
1160
|
recommended: false
|
|
1161
1161
|
},
|
|
@@ -1164,25 +1164,74 @@ var no_button_navigation_default = {
|
|
|
1164
1164
|
create(context) {
|
|
1165
1165
|
function containsWindowNavigation(node) {
|
|
1166
1166
|
let found = false;
|
|
1167
|
-
function inspect(n) {
|
|
1167
|
+
function inspect(n, parent) {
|
|
1168
1168
|
if (found || !n || typeof n !== "object")
|
|
1169
1169
|
return;
|
|
1170
|
-
if (n.type === "MemberExpression" && n.object.type === "Identifier" && n.object.name === "window" && n.property.type === "Identifier" &&
|
|
1170
|
+
if (n.type === "MemberExpression" && n.object.type === "Identifier" && n.object.name === "window" && n.property.type === "Identifier" && n.property.name === "open") {
|
|
1171
1171
|
found = true;
|
|
1172
1172
|
return;
|
|
1173
1173
|
}
|
|
1174
|
+
if (n.type === "AssignmentExpression" && n.left.type === "MemberExpression") {
|
|
1175
|
+
const left = n.left;
|
|
1176
|
+
if (left.object.type === "Identifier" && left.object.name === "window" && left.property.type === "Identifier" && left.property.name === "location") {
|
|
1177
|
+
found = true;
|
|
1178
|
+
return;
|
|
1179
|
+
}
|
|
1180
|
+
if (left.object.type === "MemberExpression" && left.object.object.type === "Identifier" && left.object.object.name === "window" && left.object.property.type === "Identifier" && left.object.property.name === "location") {
|
|
1181
|
+
found = true;
|
|
1182
|
+
return;
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
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 === "replace") {
|
|
1186
|
+
if (parent && parent.type === "CallExpression") {
|
|
1187
|
+
found = true;
|
|
1188
|
+
return;
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
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")) {
|
|
1192
|
+
if (parent && parent.type === "CallExpression" && Array.isArray(parent.arguments) && parent.arguments.length >= 3) {
|
|
1193
|
+
const urlArg = parent.arguments[2];
|
|
1194
|
+
if (!urlUsesAllowedLocation(urlArg)) {
|
|
1195
|
+
found = true;
|
|
1196
|
+
return;
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1174
1200
|
for (const key of Object.keys(n)) {
|
|
1175
1201
|
if (key === "parent")
|
|
1176
1202
|
continue;
|
|
1177
1203
|
const child = n[key];
|
|
1178
1204
|
if (Array.isArray(child)) {
|
|
1179
|
-
child.forEach(inspect);
|
|
1205
|
+
child.forEach((c) => inspect(c, n));
|
|
1180
1206
|
} else {
|
|
1181
|
-
inspect(child);
|
|
1207
|
+
inspect(child, n);
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
function urlUsesAllowedLocation(argNode) {
|
|
1212
|
+
let allowed = false;
|
|
1213
|
+
function check(n) {
|
|
1214
|
+
if (allowed || !n || typeof n !== "object")
|
|
1215
|
+
return;
|
|
1216
|
+
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")) {
|
|
1217
|
+
allowed = true;
|
|
1218
|
+
return;
|
|
1219
|
+
}
|
|
1220
|
+
for (const key of Object.keys(n)) {
|
|
1221
|
+
if (key === "parent")
|
|
1222
|
+
continue;
|
|
1223
|
+
const c = n[key];
|
|
1224
|
+
if (Array.isArray(c)) {
|
|
1225
|
+
c.forEach(check);
|
|
1226
|
+
} else {
|
|
1227
|
+
check(c);
|
|
1228
|
+
}
|
|
1182
1229
|
}
|
|
1183
1230
|
}
|
|
1231
|
+
check(argNode);
|
|
1232
|
+
return allowed;
|
|
1184
1233
|
}
|
|
1185
|
-
inspect(node.type === "ArrowFunctionExpression" || node.type === "FunctionExpression" ? node.body : node);
|
|
1234
|
+
inspect(node.type === "ArrowFunctionExpression" || node.type === "FunctionExpression" ? node.body : node, null);
|
|
1186
1235
|
return found;
|
|
1187
1236
|
}
|
|
1188
1237
|
return {
|
|
@@ -1195,7 +1244,7 @@ var no_button_navigation_default = {
|
|
|
1195
1244
|
if ((expr.type === "ArrowFunctionExpression" || expr.type === "FunctionExpression") && containsWindowNavigation(expr)) {
|
|
1196
1245
|
context.report({
|
|
1197
1246
|
node: attr,
|
|
1198
|
-
message: "Use an anchor tag for navigation instead of a button with an onClick handler that
|
|
1247
|
+
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."
|
|
1199
1248
|
});
|
|
1200
1249
|
}
|
|
1201
1250
|
}
|
package/package.json
CHANGED
|
@@ -3,50 +3,162 @@ export default {
|
|
|
3
3
|
type: "suggestion",
|
|
4
4
|
docs: {
|
|
5
5
|
description:
|
|
6
|
-
"Enforce using anchor tags for navigation instead of buttons
|
|
6
|
+
"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 + …).",
|
|
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
|
+
// 1) 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
|
+
// 2) Assignment to window.location or window.location.something (e.g. window.location = "...", window.location.href = "...")
|
|
32
|
+
if (
|
|
33
|
+
n.type === "AssignmentExpression" &&
|
|
34
|
+
n.left.type === "MemberExpression"
|
|
35
|
+
) {
|
|
36
|
+
const left = n.left;
|
|
37
|
+
|
|
38
|
+
// window.location = ...
|
|
39
|
+
if (
|
|
40
|
+
left.object.type === "Identifier" &&
|
|
41
|
+
left.object.name === "window" &&
|
|
42
|
+
left.property.type === "Identifier" &&
|
|
43
|
+
left.property.name === "location"
|
|
44
|
+
) {
|
|
45
|
+
found = true;
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// window.location.href = ... OR window.location.pathname = ...
|
|
50
|
+
if (
|
|
51
|
+
left.object.type === "MemberExpression" &&
|
|
52
|
+
left.object.object.type === "Identifier" &&
|
|
53
|
+
left.object.object.name === "window" &&
|
|
54
|
+
left.object.property.type === "Identifier" &&
|
|
55
|
+
left.object.property.name === "location"
|
|
56
|
+
) {
|
|
57
|
+
// any assignment to a sub-property of window.location (href, pathname, etc.) = navigation
|
|
58
|
+
found = true;
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// 3) window.location.replace(...) (or any call on window.location.* besides .search/.hash)
|
|
64
|
+
if (
|
|
65
|
+
n.type === "MemberExpression" &&
|
|
66
|
+
n.object.type === "MemberExpression" &&
|
|
67
|
+
n.object.object.type === "Identifier" &&
|
|
68
|
+
n.object.object.name === "window" &&
|
|
69
|
+
n.object.property.type === "Identifier" &&
|
|
70
|
+
n.object.property.name === "location" &&
|
|
71
|
+
n.property.type === "Identifier" &&
|
|
72
|
+
n.property.name === "replace"
|
|
73
|
+
) {
|
|
74
|
+
// parent should be a CallExpression
|
|
75
|
+
if (parent && parent.type === "CallExpression") {
|
|
76
|
+
found = true;
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// 4) window.history.pushState(...) or window.history.replaceState(...)
|
|
82
|
+
if (
|
|
83
|
+
n.type === "MemberExpression" &&
|
|
84
|
+
n.object.type === "MemberExpression" &&
|
|
85
|
+
n.object.object.type === "Identifier" &&
|
|
86
|
+
n.object.object.name === "window" &&
|
|
87
|
+
n.object.property.type === "Identifier" &&
|
|
88
|
+
n.object.property.name === "history" &&
|
|
89
|
+
n.property.type === "Identifier" &&
|
|
90
|
+
(n.property.name === "pushState" ||
|
|
91
|
+
n.property.name === "replaceState")
|
|
92
|
+
) {
|
|
93
|
+
if (
|
|
94
|
+
parent &&
|
|
95
|
+
parent.type === "CallExpression" &&
|
|
96
|
+
Array.isArray(parent.arguments) &&
|
|
97
|
+
parent.arguments.length >= 3
|
|
98
|
+
) {
|
|
99
|
+
const urlArg = parent.arguments[2];
|
|
100
|
+
if (!urlUsesAllowedLocation(urlArg)) {
|
|
101
|
+
found = true;
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
// If urlArg references window.location.pathname/search/hash → allowed
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Recurse into all child nodes
|
|
34
109
|
for (const key of Object.keys(n)) {
|
|
35
110
|
if (key === "parent") continue;
|
|
36
111
|
const child = n[key];
|
|
37
112
|
if (Array.isArray(child)) {
|
|
38
|
-
child.forEach(inspect);
|
|
113
|
+
child.forEach((c) => inspect(c, n));
|
|
39
114
|
} else {
|
|
40
|
-
inspect(child);
|
|
115
|
+
inspect(child, n);
|
|
41
116
|
}
|
|
42
117
|
}
|
|
43
118
|
}
|
|
44
|
-
|
|
119
|
+
|
|
120
|
+
function urlUsesAllowedLocation(argNode) {
|
|
121
|
+
let allowed = false;
|
|
122
|
+
|
|
123
|
+
function check(n) {
|
|
124
|
+
if (allowed || !n || typeof n !== "object") return;
|
|
125
|
+
// Look for window.location.pathname, .search, or .hash anywhere
|
|
126
|
+
if (
|
|
127
|
+
n.type === "MemberExpression" &&
|
|
128
|
+
n.object.type === "MemberExpression" &&
|
|
129
|
+
n.object.object.type === "Identifier" &&
|
|
130
|
+
n.object.object.name === "window" &&
|
|
131
|
+
n.object.property.type === "Identifier" &&
|
|
132
|
+
n.object.property.name === "location" &&
|
|
133
|
+
n.property.type === "Identifier" &&
|
|
134
|
+
(n.property.name === "pathname" ||
|
|
135
|
+
n.property.name === "search" ||
|
|
136
|
+
n.property.name === "hash")
|
|
137
|
+
) {
|
|
138
|
+
allowed = true;
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
for (const key of Object.keys(n)) {
|
|
142
|
+
if (key === "parent") continue;
|
|
143
|
+
const c = n[key];
|
|
144
|
+
if (Array.isArray(c)) {
|
|
145
|
+
c.forEach(check);
|
|
146
|
+
} else {
|
|
147
|
+
check(c);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
check(argNode);
|
|
153
|
+
return allowed;
|
|
154
|
+
}
|
|
155
|
+
|
|
45
156
|
inspect(
|
|
46
157
|
node.type === "ArrowFunctionExpression" ||
|
|
47
158
|
node.type === "FunctionExpression"
|
|
48
159
|
? node.body
|
|
49
|
-
: node
|
|
160
|
+
: node,
|
|
161
|
+
null
|
|
50
162
|
);
|
|
51
163
|
return found;
|
|
52
164
|
}
|
|
@@ -54,7 +166,6 @@ export default {
|
|
|
54
166
|
return {
|
|
55
167
|
JSXElement(node) {
|
|
56
168
|
const { openingElement } = node;
|
|
57
|
-
// only care about <button ...>
|
|
58
169
|
if (
|
|
59
170
|
openingElement.name.type === "JSXIdentifier" &&
|
|
60
171
|
openingElement.name.name === "button"
|
|
@@ -66,7 +177,6 @@ export default {
|
|
|
66
177
|
attr.value?.type === "JSXExpressionContainer"
|
|
67
178
|
) {
|
|
68
179
|
const expr = attr.value.expression;
|
|
69
|
-
// only inspect the inline function, not any Identifier calls
|
|
70
180
|
if (
|
|
71
181
|
(expr.type === "ArrowFunctionExpression" ||
|
|
72
182
|
expr.type === "FunctionExpression") &&
|
|
@@ -75,7 +185,7 @@ export default {
|
|
|
75
185
|
context.report({
|
|
76
186
|
node: attr,
|
|
77
187
|
message:
|
|
78
|
-
"Use an anchor tag for navigation instead of a button with an onClick handler that
|
|
188
|
+
"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
189
|
});
|
|
80
190
|
}
|
|
81
191
|
}
|