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 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 with onClick handlers that use window navigation methods (e.g., window.location, window.open)",
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" && (n.property.name === "open" || n.property.name === "location")) {
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 uses window navigation methods."
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-absolute",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "ESLint plugin for AbsoluteJS",
5
5
  "repository": {
6
6
  "type": "git",
@@ -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 with onClick handlers that use window navigation methods (e.g., window.location, window.open)",
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
- function inspect(n) {
15
+
16
+ function inspect(n, parent) {
20
17
  if (found || !n || typeof n !== "object") return;
21
- // Only match MemberExpressions on the global window identifier
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
- (n.property.name === "open" ||
28
- n.property.name === "location")
25
+ n.property.name === "open"
29
26
  ) {
30
27
  found = true;
31
28
  return;
32
29
  }
33
- // recurse into children—but skip walking back up via `parent`
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
- // If it's a function, start at its body; otherwise start at the node itself
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 uses window navigation methods."
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
  }