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 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: "Disallow explicit return type annotations on functions, except when using type predicates for type guards or inline object literal returns (e.g., style objects).",
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
- return {
470
- "FunctionDeclaration, FunctionExpression, ArrowFunctionExpression"(node) {
471
- if (node.returnType) {
472
- const typeAnnotation = node.returnType.typeAnnotation;
473
- if (typeAnnotation && typeAnnotation.type === "TSTypePredicate") {
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
- if (node.type === "ArrowFunctionExpression" && node.expression && node.body && node.body.type === "ObjectExpression") {
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
- if (node.body && node.body.type === "BlockStatement") {
480
- const returns = node.body.body.filter((stmt) => stmt.type === "ReturnStatement");
481
- if (returns.length === 1 && returns[0].argument && returns[0].argument.type === "ObjectExpression") {
482
- return;
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/rules/no-button-navigation.js
1193
- var no_button_navigation_default = {
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": no_button_navigation_default,
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-absolute",
3
- "version": "0.0.3",
3
+ "version": "0.1.1",
4
4
  "description": "ESLint plugin for AbsoluteJS",
5
5
  "repository": {
6
6
  "type": "git",
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
- export default {
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 use window navigation methods (e.g., window.location, window.open)",
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
- 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
+ // 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
- (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
+ // 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
- // If it's a function, start at its body; otherwise start at the node itself
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 uses window navigation methods."
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
- "Disallow explicit return type annotations on functions, except when using type predicates for type guards or inline object literal returns (e.g., style objects).",
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
- return {
18
- "FunctionDeclaration, FunctionExpression, ArrowFunctionExpression"(
19
- node
20
- ) {
21
- if (node.returnType) {
22
- // Allow type predicate annotations for type guards.
23
- const typeAnnotation = node.returnType.typeAnnotation;
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
- typeAnnotation &&
26
- typeAnnotation.type === "TSTypePredicate"
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
- // Allow if it's an arrow function that directly returns an object literal.
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
- node.type === "ArrowFunctionExpression" &&
34
- node.expression &&
35
- node.body &&
36
- node.body.type === "ObjectExpression"
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
- // Allow if the function has a block body with a single return statement that returns an object literal.
42
- if (node.body && node.body.type === "BlockStatement") {
43
- const returns = node.body.body.filter(
44
- (stmt) => stmt.type === "ReturnStatement"
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
- returns.length === 1 &&
48
- returns[0].argument &&
49
- returns[0].argument.type === "ObjectExpression"
148
+ attr.type === "JSXAttribute" &&
149
+ attr.name.name === "onClick" &&
150
+ attr.value?.type === "JSXExpressionContainer"
50
151
  ) {
51
- return;
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
- };