eslint-plugin-absolute 0.1.2 → 0.1.4
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 +59 -42
- package/package.json +1 -1
- package/src/rules/no-button-navigation.js +107 -72
package/dist/index.js
CHANGED
|
@@ -1162,43 +1162,69 @@ var no_button_navigation_default = {
|
|
|
1162
1162
|
schema: []
|
|
1163
1163
|
},
|
|
1164
1164
|
create(context) {
|
|
1165
|
+
function urlUsesAllowedLocation(argNode) {
|
|
1166
|
+
let allowed = false;
|
|
1167
|
+
const visited = new WeakSet;
|
|
1168
|
+
function check(n) {
|
|
1169
|
+
if (allowed || !n || typeof n !== "object" || visited.has(n))
|
|
1170
|
+
return;
|
|
1171
|
+
visited.add(n);
|
|
1172
|
+
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")) {
|
|
1173
|
+
allowed = true;
|
|
1174
|
+
return;
|
|
1175
|
+
}
|
|
1176
|
+
for (const key of Object.keys(n)) {
|
|
1177
|
+
if (key === "parent")
|
|
1178
|
+
continue;
|
|
1179
|
+
const child = n[key];
|
|
1180
|
+
if (Array.isArray(child)) {
|
|
1181
|
+
child.forEach((c) => check(c));
|
|
1182
|
+
} else {
|
|
1183
|
+
check(child);
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
check(argNode);
|
|
1188
|
+
return allowed;
|
|
1189
|
+
}
|
|
1165
1190
|
function containsWindowNavigation(node) {
|
|
1166
|
-
let
|
|
1191
|
+
let reason = null;
|
|
1192
|
+
const visited = new WeakSet;
|
|
1193
|
+
let sawReplaceCall = false;
|
|
1194
|
+
let sawAllowedLocationRead = false;
|
|
1167
1195
|
function inspect(n, parent) {
|
|
1168
|
-
if (
|
|
1196
|
+
if (reason || !n || typeof n !== "object" || visited.has(n))
|
|
1169
1197
|
return;
|
|
1198
|
+
visited.add(n);
|
|
1170
1199
|
if (n.type === "MemberExpression" && n.object.type === "Identifier" && n.object.name === "window" && n.property.type === "Identifier" && n.property.name === "open") {
|
|
1171
|
-
|
|
1200
|
+
reason = "window.open";
|
|
1172
1201
|
return;
|
|
1173
1202
|
}
|
|
1174
1203
|
if (n.type === "AssignmentExpression" && n.left.type === "MemberExpression") {
|
|
1175
1204
|
const left = n.left;
|
|
1176
1205
|
if (left.object.type === "Identifier" && left.object.name === "window" && left.property.type === "Identifier" && left.property.name === "location") {
|
|
1177
|
-
|
|
1206
|
+
reason = "assignment to window.location";
|
|
1178
1207
|
return;
|
|
1179
1208
|
}
|
|
1180
1209
|
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
|
-
|
|
1210
|
+
reason = "assignment to window.location sub-property";
|
|
1182
1211
|
return;
|
|
1183
1212
|
}
|
|
1184
1213
|
}
|
|
1185
1214
|
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
1215
|
if (parent && parent.type === "CallExpression") {
|
|
1187
|
-
|
|
1216
|
+
reason = "window.location.replace";
|
|
1188
1217
|
return;
|
|
1189
1218
|
}
|
|
1190
1219
|
}
|
|
1191
1220
|
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
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
return;
|
|
1197
|
-
}
|
|
1198
|
-
}
|
|
1221
|
+
sawReplaceCall = true;
|
|
1222
|
+
}
|
|
1223
|
+
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 === "search" || n.property.name === "pathname" || n.property.name === "hash")) {
|
|
1224
|
+
sawAllowedLocationRead = true;
|
|
1199
1225
|
}
|
|
1200
1226
|
for (const key of Object.keys(n)) {
|
|
1201
|
-
if (key === "parent")
|
|
1227
|
+
if (key === "parent" || reason)
|
|
1202
1228
|
continue;
|
|
1203
1229
|
const child = n[key];
|
|
1204
1230
|
if (Array.isArray(child)) {
|
|
@@ -1206,33 +1232,21 @@ var no_button_navigation_default = {
|
|
|
1206
1232
|
} else {
|
|
1207
1233
|
inspect(child, n);
|
|
1208
1234
|
}
|
|
1209
|
-
|
|
1210
|
-
}
|
|
1211
|
-
function urlUsesAllowedLocation(argNode) {
|
|
1212
|
-
let allowed = false;
|
|
1213
|
-
function check(n) {
|
|
1214
|
-
if (allowed || !n || typeof n !== "object")
|
|
1235
|
+
if (reason)
|
|
1215
1236
|
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
|
-
}
|
|
1229
|
-
}
|
|
1230
1237
|
}
|
|
1231
|
-
check(argNode);
|
|
1232
|
-
return allowed;
|
|
1233
1238
|
}
|
|
1234
1239
|
inspect(node.type === "ArrowFunctionExpression" || node.type === "FunctionExpression" ? node.body : node, null);
|
|
1235
|
-
|
|
1240
|
+
if (reason) {
|
|
1241
|
+
return { shouldReport: true, reason };
|
|
1242
|
+
}
|
|
1243
|
+
if (sawReplaceCall && !sawAllowedLocationRead) {
|
|
1244
|
+
return {
|
|
1245
|
+
shouldReport: true,
|
|
1246
|
+
reason: "history.replace/pushState without reading window.location"
|
|
1247
|
+
};
|
|
1248
|
+
}
|
|
1249
|
+
return { shouldReport: false, reason: null };
|
|
1236
1250
|
}
|
|
1237
1251
|
return {
|
|
1238
1252
|
JSXElement(node) {
|
|
@@ -1241,11 +1255,14 @@ var no_button_navigation_default = {
|
|
|
1241
1255
|
for (const attr of openingElement.attributes) {
|
|
1242
1256
|
if (attr.type === "JSXAttribute" && attr.name.name === "onClick" && attr.value?.type === "JSXExpressionContainer") {
|
|
1243
1257
|
const expr = attr.value.expression;
|
|
1244
|
-
if (
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1258
|
+
if (expr.type === "ArrowFunctionExpression" || expr.type === "FunctionExpression") {
|
|
1259
|
+
const { shouldReport, reason } = containsWindowNavigation(expr);
|
|
1260
|
+
if (shouldReport) {
|
|
1261
|
+
context.report({
|
|
1262
|
+
node: attr,
|
|
1263
|
+
message: `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.`
|
|
1264
|
+
});
|
|
1265
|
+
}
|
|
1249
1266
|
}
|
|
1250
1267
|
}
|
|
1251
1268
|
}
|
package/package.json
CHANGED
|
@@ -10,11 +10,65 @@ export default {
|
|
|
10
10
|
schema: []
|
|
11
11
|
},
|
|
12
12
|
create(context) {
|
|
13
|
+
/**
|
|
14
|
+
* Returns true if the given AST subtree contains a MemberExpression of
|
|
15
|
+
* the form `window.location.pathname`, `window.location.search`, or `window.location.hash`.
|
|
16
|
+
*/
|
|
17
|
+
function urlUsesAllowedLocation(argNode) {
|
|
18
|
+
let allowed = false;
|
|
19
|
+
const visited = new WeakSet();
|
|
20
|
+
|
|
21
|
+
function check(n) {
|
|
22
|
+
if (allowed || !n || typeof n !== "object" || visited.has(n))
|
|
23
|
+
return;
|
|
24
|
+
visited.add(n);
|
|
25
|
+
|
|
26
|
+
if (
|
|
27
|
+
n.type === "MemberExpression" &&
|
|
28
|
+
n.object.type === "MemberExpression" &&
|
|
29
|
+
n.object.object.type === "Identifier" &&
|
|
30
|
+
n.object.object.name === "window" &&
|
|
31
|
+
n.object.property.type === "Identifier" &&
|
|
32
|
+
n.object.property.name === "location" &&
|
|
33
|
+
n.property.type === "Identifier" &&
|
|
34
|
+
(n.property.name === "pathname" ||
|
|
35
|
+
n.property.name === "search" ||
|
|
36
|
+
n.property.name === "hash")
|
|
37
|
+
) {
|
|
38
|
+
allowed = true;
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
for (const key of Object.keys(n)) {
|
|
43
|
+
if (key === "parent") continue;
|
|
44
|
+
const child = n[key];
|
|
45
|
+
if (Array.isArray(child)) {
|
|
46
|
+
child.forEach((c) => check(c));
|
|
47
|
+
} else {
|
|
48
|
+
check(child);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
check(argNode);
|
|
54
|
+
return allowed;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Returns an object { shouldReport, reason } after inspecting the
|
|
59
|
+
* function body for forbidden patterns. - shouldReport is true if
|
|
60
|
+
* we must flag. - reason explains which pattern was found.
|
|
61
|
+
*/
|
|
13
62
|
function containsWindowNavigation(node) {
|
|
14
|
-
let
|
|
63
|
+
let reason = null;
|
|
64
|
+
const visited = new WeakSet();
|
|
65
|
+
let sawReplaceCall = false;
|
|
66
|
+
let sawAllowedLocationRead = false;
|
|
15
67
|
|
|
16
68
|
function inspect(n, parent) {
|
|
17
|
-
if (
|
|
69
|
+
if (reason || !n || typeof n !== "object" || visited.has(n))
|
|
70
|
+
return;
|
|
71
|
+
visited.add(n);
|
|
18
72
|
|
|
19
73
|
// 1) window.open(...)
|
|
20
74
|
if (
|
|
@@ -24,11 +78,11 @@ export default {
|
|
|
24
78
|
n.property.type === "Identifier" &&
|
|
25
79
|
n.property.name === "open"
|
|
26
80
|
) {
|
|
27
|
-
|
|
81
|
+
reason = "window.open";
|
|
28
82
|
return;
|
|
29
83
|
}
|
|
30
84
|
|
|
31
|
-
// 2) Assignment to window.location or window.location
|
|
85
|
+
// 2) Assignment to window.location or window.location.*
|
|
32
86
|
if (
|
|
33
87
|
n.type === "AssignmentExpression" &&
|
|
34
88
|
n.left.type === "MemberExpression"
|
|
@@ -42,11 +96,11 @@ export default {
|
|
|
42
96
|
left.property.type === "Identifier" &&
|
|
43
97
|
left.property.name === "location"
|
|
44
98
|
) {
|
|
45
|
-
|
|
99
|
+
reason = "assignment to window.location";
|
|
46
100
|
return;
|
|
47
101
|
}
|
|
48
102
|
|
|
49
|
-
// window.location.href = ... OR window.location.pathname =
|
|
103
|
+
// window.location.href = ... OR window.location.pathname =
|
|
50
104
|
if (
|
|
51
105
|
left.object.type === "MemberExpression" &&
|
|
52
106
|
left.object.object.type === "Identifier" &&
|
|
@@ -54,13 +108,12 @@ export default {
|
|
|
54
108
|
left.object.property.type === "Identifier" &&
|
|
55
109
|
left.object.property.name === "location"
|
|
56
110
|
) {
|
|
57
|
-
|
|
58
|
-
found = true;
|
|
111
|
+
reason = "assignment to window.location sub-property";
|
|
59
112
|
return;
|
|
60
113
|
}
|
|
61
114
|
}
|
|
62
115
|
|
|
63
|
-
// 3) window.location.replace(...)
|
|
116
|
+
// 3) window.location.replace(...) (or any call on window.location besides .search/.hash)
|
|
64
117
|
if (
|
|
65
118
|
n.type === "MemberExpression" &&
|
|
66
119
|
n.object.type === "MemberExpression" &&
|
|
@@ -71,14 +124,13 @@ export default {
|
|
|
71
124
|
n.property.type === "Identifier" &&
|
|
72
125
|
n.property.name === "replace"
|
|
73
126
|
) {
|
|
74
|
-
// parent should be a CallExpression
|
|
75
127
|
if (parent && parent.type === "CallExpression") {
|
|
76
|
-
|
|
128
|
+
reason = "window.location.replace";
|
|
77
129
|
return;
|
|
78
130
|
}
|
|
79
131
|
}
|
|
80
132
|
|
|
81
|
-
// 4) window.history.pushState(...) or
|
|
133
|
+
// 4) window.history.pushState(...) or replaceState(...)
|
|
82
134
|
if (
|
|
83
135
|
n.type === "MemberExpression" &&
|
|
84
136
|
n.object.type === "MemberExpression" &&
|
|
@@ -90,69 +142,38 @@ export default {
|
|
|
90
142
|
(n.property.name === "pushState" ||
|
|
91
143
|
n.property.name === "replaceState")
|
|
92
144
|
) {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
145
|
+
sawReplaceCall = true;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// 5) Reading window.location.search, .pathname, or .hash
|
|
149
|
+
if (
|
|
150
|
+
n.type === "MemberExpression" &&
|
|
151
|
+
n.object.type === "MemberExpression" &&
|
|
152
|
+
n.object.object.type === "Identifier" &&
|
|
153
|
+
n.object.object.name === "window" &&
|
|
154
|
+
n.object.property.type === "Identifier" &&
|
|
155
|
+
n.object.property.name === "location" &&
|
|
156
|
+
n.property.type === "Identifier" &&
|
|
157
|
+
(n.property.name === "search" ||
|
|
158
|
+
n.property.name === "pathname" ||
|
|
159
|
+
n.property.name === "hash")
|
|
160
|
+
) {
|
|
161
|
+
sawAllowedLocationRead = true;
|
|
106
162
|
}
|
|
107
163
|
|
|
108
|
-
// Recurse into
|
|
164
|
+
// Recurse into children
|
|
109
165
|
for (const key of Object.keys(n)) {
|
|
110
|
-
if (key === "parent") continue;
|
|
166
|
+
if (key === "parent" || reason) continue;
|
|
111
167
|
const child = n[key];
|
|
112
168
|
if (Array.isArray(child)) {
|
|
113
169
|
child.forEach((c) => inspect(c, n));
|
|
114
170
|
} else {
|
|
115
171
|
inspect(child, n);
|
|
116
172
|
}
|
|
173
|
+
if (reason) return;
|
|
117
174
|
}
|
|
118
175
|
}
|
|
119
176
|
|
|
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
|
-
|
|
156
177
|
inspect(
|
|
157
178
|
node.type === "ArrowFunctionExpression" ||
|
|
158
179
|
node.type === "FunctionExpression"
|
|
@@ -160,7 +181,17 @@ export default {
|
|
|
160
181
|
: node,
|
|
161
182
|
null
|
|
162
183
|
);
|
|
163
|
-
|
|
184
|
+
|
|
185
|
+
if (reason) {
|
|
186
|
+
return { shouldReport: true, reason };
|
|
187
|
+
}
|
|
188
|
+
if (sawReplaceCall && !sawAllowedLocationRead) {
|
|
189
|
+
return {
|
|
190
|
+
shouldReport: true,
|
|
191
|
+
reason: "history.replace/pushState without reading window.location"
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
return { shouldReport: false, reason: null };
|
|
164
195
|
}
|
|
165
196
|
|
|
166
197
|
return {
|
|
@@ -178,15 +209,19 @@ export default {
|
|
|
178
209
|
) {
|
|
179
210
|
const expr = attr.value.expression;
|
|
180
211
|
if (
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
containsWindowNavigation(expr)
|
|
212
|
+
expr.type === "ArrowFunctionExpression" ||
|
|
213
|
+
expr.type === "FunctionExpression"
|
|
184
214
|
) {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
215
|
+
const { shouldReport, reason } =
|
|
216
|
+
containsWindowNavigation(expr);
|
|
217
|
+
if (shouldReport) {
|
|
218
|
+
context.report({
|
|
219
|
+
node: attr,
|
|
220
|
+
message:
|
|
221
|
+
`Use an anchor tag for navigation instead of a button whose onClick handler changes the path. ` +
|
|
222
|
+
`Detected: ${reason}. Only query/hash updates (reading window.location.search, .pathname, or .hash) are allowed.`
|
|
223
|
+
});
|
|
224
|
+
}
|
|
190
225
|
}
|
|
191
226
|
}
|
|
192
227
|
}
|