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 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 found = false;
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 (found || !n || typeof n !== "object")
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
- found = true;
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
- found = true;
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
- found = true;
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
- found = true;
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
- 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
- }
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
- return found;
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 ((expr.type === "ArrowFunctionExpression" || expr.type === "FunctionExpression") && containsWindowNavigation(expr)) {
1245
- context.report({
1246
- node: attr,
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."
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-absolute",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "ESLint plugin for AbsoluteJS",
5
5
  "repository": {
6
6
  "type": "git",
@@ -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 found = false;
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 (found || !n || typeof n !== "object") return;
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
- found = true;
81
+ reason = "window.open";
28
82
  return;
29
83
  }
30
84
 
31
- // 2) Assignment to window.location or window.location.something (e.g. window.location = "...", window.location.href = "...")
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
- found = true;
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
- // any assignment to a sub-property of window.location (href, pathname, etc.) = navigation
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(...) (or any call on window.location.* besides .search/.hash)
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
- found = true;
128
+ reason = "window.location.replace";
77
129
  return;
78
130
  }
79
131
  }
80
132
 
81
- // 4) window.history.pushState(...) or window.history.replaceState(...)
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
- 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
- }
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 all child nodes
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
- return found;
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
- (expr.type === "ArrowFunctionExpression" ||
182
- expr.type === "FunctionExpression") &&
183
- containsWindowNavigation(expr)
212
+ expr.type === "ArrowFunctionExpression" ||
213
+ expr.type === "FunctionExpression"
184
214
  ) {
185
- context.report({
186
- node: attr,
187
- message:
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."
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
  }