eslint-plugin-no-mistakes 0.94.0 → 0.96.0

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.
Files changed (129) hide show
  1. package/README.md +5 -0
  2. package/package.json +1 -1
  3. package/src/component-functions.js +39 -7
  4. package/src/exported-component-definitions.js +32 -0
  5. package/src/exported-components.js +22 -22
  6. package/src/fetch-call.js +102 -0
  7. package/src/helpers.js +3 -34
  8. package/src/index.js +5 -0
  9. package/src/rule-suppression.js +1 -0
  10. package/src/rules/argv-main-fragments.js +25 -0
  11. package/src/rules/argv-main-values.js +110 -19
  12. package/src/rules/async-ast.js +23 -0
  13. package/src/rules/async-call-disposition.js +39 -152
  14. package/src/rules/async-disposition-bindings.js +131 -0
  15. package/src/rules/async-disposition-expressions.js +166 -0
  16. package/src/rules/async-disposition-flow.js +151 -0
  17. package/src/rules/async-disposition-mock-bindings.js +39 -0
  18. package/src/rules/async-disposition-mocks.js +142 -0
  19. package/src/rules/async-target-bindings.js +24 -0
  20. package/src/rules/async-targets.js +36 -12
  21. package/src/rules/await-array-methods.js +21 -24
  22. package/src/rules/configured-runtime-import.js +28 -0
  23. package/src/rules/gh-api-argument-safety.js +9 -32
  24. package/src/rules/gh-api-wrappers.js +52 -0
  25. package/src/rules/logical-call.js +75 -0
  26. package/src/rules/module-mock-boundary.js +6 -1
  27. package/src/rules/module-mock-call-apply.js +18 -12
  28. package/src/rules/module-mock-framework.js +61 -9
  29. package/src/rules/module-mock-helpers.js +24 -24
  30. package/src/rules/module-mock-preserve-aliases.js +77 -22
  31. package/src/rules/module-mock-preserve-exports.js +21 -1
  32. package/src/rules/nextjs-static-fetch-method.js +4 -3
  33. package/src/rules/nextjs-static-fetch-url.js +4 -3
  34. package/src/rules/no-banned-import-outside-allowed-paths-calls.js +39 -0
  35. package/src/rules/no-banned-import-outside-allowed-paths.js +3 -29
  36. package/src/rules/no-consecutive-unbatched-reads.js +136 -0
  37. package/src/rules/no-global-fetch-outside-helper-bindings.js +9 -0
  38. package/src/rules/no-global-fetch-outside-helper-helpers.js +2 -2
  39. package/src/rules/no-global-fetch-outside-helper.js +5 -2
  40. package/src/rules/no-import-only-test-files.js +69 -32
  41. package/src/rules/no-inline-noop-promise-catch-helpers.js +30 -13
  42. package/src/rules/no-object-mutation-ast.js +65 -0
  43. package/src/rules/no-object-mutation-bind.js +24 -7
  44. package/src/rules/no-object-mutation-build.js +4 -0
  45. package/src/rules/no-object-mutation-call.js +81 -16
  46. package/src/rules/no-object-mutation-class.js +134 -0
  47. package/src/rules/no-object-mutation-expr.js +14 -4
  48. package/src/rules/no-object-mutation-fn.js +42 -20
  49. package/src/rules/no-object-mutation-forward.js +137 -0
  50. package/src/rules/no-object-mutation-invoke.js +155 -0
  51. package/src/rules/no-object-mutation-join.js +5 -0
  52. package/src/rules/no-object-mutation-method.js +65 -0
  53. package/src/rules/no-object-mutation-namespace.js +91 -0
  54. package/src/rules/no-object-mutation-read.js +79 -15
  55. package/src/rules/no-object-mutation-scan.js +21 -15
  56. package/src/rules/no-object-mutation-syntax.js +2 -87
  57. package/src/rules/no-object-mutation-values.js +11 -0
  58. package/src/rules/no-object-mutation-write.js +7 -1
  59. package/src/rules/no-vitest-sequential.js +165 -36
  60. package/src/rules/nullable-option-defaults-helpers.js +18 -4
  61. package/src/rules/playwright-assertion-timeout-cap.js +74 -14
  62. package/src/rules/playwright-interactive-names.js +125 -0
  63. package/src/rules/playwright-literals.js +8 -3
  64. package/src/rules/playwright-no-hoisted-unique-token.js +28 -20
  65. package/src/rules/playwright-no-raw-scroll-pagination.js +25 -55
  66. package/src/rules/playwright-prefer-get-by-test-id.js +14 -5
  67. package/src/rules/playwright-require-exported-component-attribute.js +8 -2
  68. package/src/rules/playwright-require-interactive-test-id.js +30 -65
  69. package/src/rules/playwright-scroll-call.js +55 -0
  70. package/src/rules/playwright-selector-priority.js +8 -5
  71. package/src/rules/playwright-static-call-alias.js +65 -0
  72. package/src/rules/playwright-static-call.js +155 -0
  73. package/src/rules/playwright-test-timeout-bindings.js +0 -1
  74. package/src/rules/playwright-timeout-carriers.js +0 -1
  75. package/src/rules/postgres-correlated-completion.js +89 -0
  76. package/src/rules/postgres-executor-calls.js +157 -0
  77. package/src/rules/postgres-executor.js +19 -3
  78. package/src/rules/postgres-fanout-calls.js +72 -0
  79. package/src/rules/postgres-no-async-sql-builder-array-map.js +51 -0
  80. package/src/rules/postgres-no-async-sql-builder-calls.js +39 -2
  81. package/src/rules/postgres-no-async-sql-builder-constants.js +93 -5
  82. package/src/rules/postgres-no-async-sql-builder-import.js +60 -0
  83. package/src/rules/postgres-no-async-sql-builder-match.js +14 -28
  84. package/src/rules/postgres-no-async-sql-builder-types.js +3 -2
  85. package/src/rules/postgres-no-correlated-sql-builder-append.js +161 -0
  86. package/src/rules/postgres-no-correlated-sql-builder.js +129 -0
  87. package/src/rules/postgres-no-executor-adapter-forward.js +32 -16
  88. package/src/rules/postgres-no-executor-adapter-logic.js +5 -1
  89. package/src/rules/postgres-no-executor-adapter-member.js +103 -1
  90. package/src/rules/postgres-no-executor-adapter-passed.js +3 -3
  91. package/src/rules/postgres-no-executor-adapter-ref.js +7 -3
  92. package/src/rules/postgres-no-executor-adapter.js +2 -3
  93. package/src/rules/postgres-no-manual-transaction.js +5 -2
  94. package/src/rules/postgres-no-nonliteral-sql-registry-key.js +156 -0
  95. package/src/rules/postgres-no-unbounded-query-fanout.js +9 -3
  96. package/src/rules/postgres-runtime-helpers.js +44 -10
  97. package/src/rules/react-no-iife-in-jsx.js +46 -17
  98. package/src/rules/react-no-nullish-react-node.js +10 -4
  99. package/src/rules/react-no-use-promise-resolve.js +126 -65
  100. package/src/rules/require-options-on-imported-call.js +20 -12
  101. package/src/rules/require-top-level-registration.js +64 -0
  102. package/src/rules/required-call-selector-aliases.js +45 -0
  103. package/src/rules/required-call-selector-compile.js +9 -3
  104. package/src/rules/required-call-selectors.js +35 -9
  105. package/src/rules/restricted-api-usage.js +195 -0
  106. package/src/rules/server-require-nullable-fetch-wrapper.js +63 -6
  107. package/src/rules/test-message-expect.js +155 -0
  108. package/src/rules/test-no-delayed-rejects-chains.js +5 -2
  109. package/src/rules/test-no-delayed-rejects-expect.js +59 -0
  110. package/src/rules/test-no-delayed-rejects-flow.js +7 -2
  111. package/src/rules/test-no-delayed-rejects.js +13 -45
  112. package/src/rules/test-no-error-message-matching.js +36 -43
  113. package/src/rules/test-no-shared-state-aliases.js +121 -1
  114. package/src/rules/test-no-shared-state-analysis.js +10 -2
  115. package/src/rules/test-no-shared-state-callees.js +64 -16
  116. package/src/rules/test-no-shared-state-mutations.js +3 -1
  117. package/src/rules/test-no-shared-state-rule-helpers.js +34 -5
  118. package/src/rules/test-no-shared-state.js +39 -50
  119. package/src/rules/test-no-skips-bindings.js +76 -24
  120. package/src/rules/test-no-skips.js +147 -2
  121. package/src/rules/ts-no-function-aliases-ast.js +65 -0
  122. package/src/rules/ts-no-function-aliases-contract.js +92 -0
  123. package/src/rules/ts-no-function-aliases-forwarding.js +46 -0
  124. package/src/rules/ts-no-function-aliases-legacy.js +79 -0
  125. package/src/rules/ts-no-function-aliases-reachable.js +76 -0
  126. package/src/rules/ts-no-function-aliases-self.js +132 -0
  127. package/src/rules/ts-no-function-aliases-signature.js +65 -0
  128. package/src/rules/ts-no-function-aliases.js +66 -95
  129. package/src/rules/vitest-mock-test-file-naming.js +95 -38
package/README.md CHANGED
@@ -57,3 +57,8 @@ See [options and examples](../../docs/eslint-rules/postgres-no-async-sql-builder
57
57
  `require-options-on-imported-call` requires a static options object on configured
58
58
  imports, globals with no local binding, and terminal methods with a matching receiver shape.
59
59
  See [options and examples](../../docs/eslint-rules/require-options-on-imported-call.md).
60
+
61
+ `restricted-api-usage` is an opt-in import/export-name and call-name policy.
62
+ See [options and examples](../../docs/eslint-rules/restricted-api-usage.md).
63
+
64
+ Opt-in client contracts: [`no-consecutive-unbatched-reads`](../../docs/eslint-rules/no-consecutive-unbatched-reads.md) configures adjacent awaited read batching; [`require-top-level-registration`](../../docs/eslint-rules/require-top-level-registration.md) configures owner-module lifecycle registration. Both support ESLint/Oxlint and have no product defaults.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-no-mistakes",
3
- "version": "0.94.0",
3
+ "version": "0.96.0",
4
4
  "description": "ESLint and Oxlint rules that keep code static enough for no-mistakes analyzers and coding agents",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -1,5 +1,7 @@
1
1
  "use strict";
2
2
 
3
+ const { literalString } = require("./helpers");
4
+
3
5
  function nonEmptyStrings(value, fallback) {
4
6
  if (!Array.isArray(value)) {
5
7
  return fallback;
@@ -23,7 +25,7 @@ function unwrapExpression(node, opts) {
23
25
  if (node.type !== "CallExpression" || !isConfiguredWrapper(node.callee, opts)) {
24
26
  return node;
25
27
  }
26
- for (const arg of node.arguments) {
28
+ for (const arg of wrapperArguments(node)) {
27
29
  const unwrapped = unwrapExpression(arg, opts);
28
30
  if (isFunctionNode(unwrapped)) {
29
31
  return unwrapped;
@@ -32,19 +34,49 @@ function unwrapExpression(node, opts) {
32
34
  return node;
33
35
  }
34
36
 
37
+ function wrapperArguments(node) {
38
+ const args = node.arguments || [];
39
+ const name = staticCalleeName(node.callee);
40
+ if (name !== "call" && name !== "apply") return args;
41
+ if (args.some((arg) => arg?.type === "SpreadElement")) return [];
42
+ if (name === "call") return args.slice(1);
43
+ const list = args[1];
44
+ if (
45
+ list?.type !== "ArrayExpression" ||
46
+ list.elements.some((element) => !element || element.type === "SpreadElement")
47
+ ) {
48
+ return [];
49
+ }
50
+ return list.elements;
51
+ }
52
+
35
53
  function isConfiguredWrapper(callee, opts) {
36
54
  const name = calleeName(callee);
37
55
  return Boolean(name && opts.wrappers.has(name));
38
56
  }
39
57
 
58
+ function staticCalleeName(node) {
59
+ if (!node || node.type !== "MemberExpression") return null;
60
+ if (node.computed && node.property?.type === "Identifier") return null;
61
+ if (!node.computed && typeof node.property?.name === "string") return node.property.name;
62
+ return literalString(node.property);
63
+ }
64
+
40
65
  function calleeName(node) {
41
- if (node.type === "Identifier") {
42
- return node.name;
43
- }
44
- if (node.type === "MemberExpression" && !node.computed) {
45
- return node.property.name;
66
+ let current = node;
67
+ for (let depth = 0; depth < 4; depth += 1) {
68
+ while (current?.type === "ChainExpression") current = current.expression;
69
+ if (current?.type === "MemberExpression") {
70
+ const name = staticCalleeName(current);
71
+ if (name === "call" || name === "apply") {
72
+ current = current.object;
73
+ continue;
74
+ }
75
+ return name;
76
+ }
77
+ break;
46
78
  }
47
- return null;
79
+ return current?.type === "Identifier" ? current.name : null;
48
80
  }
49
81
 
50
82
  function isFunctionNode(node) {
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+
3
+ const { functionFromExpression } = require("./component-functions");
4
+
5
+ function collectTopLevelDefinition(statement, definitions, opts, aliases, exported) {
6
+ if (statement.type === "FunctionDeclaration" && statement.id) {
7
+ definitions.set(statement.id.name, { name: statement.id.name, fn: statement });
8
+ return;
9
+ }
10
+ if (statement.type !== "VariableDeclaration") {
11
+ return;
12
+ }
13
+ for (const declaration of statement.declarations) {
14
+ if (declaration.id.type !== "Identifier") {
15
+ continue;
16
+ }
17
+ const fn = functionFromExpression(declaration.init, opts);
18
+ if (fn) {
19
+ definitions.set(declaration.id.name, { name: declaration.id.name, fn });
20
+ continue;
21
+ }
22
+ if (declaration.init?.type === "Identifier" && declaration.parent?.kind === "const") {
23
+ aliases.push({
24
+ name: declaration.id.name,
25
+ target: declaration.init.name,
26
+ exported,
27
+ });
28
+ }
29
+ }
30
+ }
31
+
32
+ module.exports = { collectTopLevelDefinition };
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
 
3
3
  const { functionFromExpression, nonEmptyStrings } = require("./component-functions");
4
+ const { collectTopLevelDefinition } = require("./exported-component-definitions");
4
5
 
5
6
  const DEFAULT_COMPONENT_NAME_PATTERN = "^[A-Z]";
6
7
  const DEFAULT_WRAPPERS = ["memo", "forwardRef", "observer"];
@@ -66,17 +67,34 @@ function collectExportedComponents(program, opts) {
66
67
  const namedExports = new Set();
67
68
  const defaultExports = new Set();
68
69
  const components = [];
70
+ const aliases = [];
69
71
 
70
72
  for (const statement of program.body) {
71
- collectTopLevelDefinition(statement, definitions, opts);
73
+ collectTopLevelDefinition(statement, definitions, opts, aliases, false);
72
74
  if (statement.type === "ExportNamedDeclaration") {
73
- collectNamedExport(statement, definitions, namedExports, defaultExports, components, opts);
75
+ collectNamedExport(
76
+ statement,
77
+ definitions,
78
+ namedExports,
79
+ defaultExports,
80
+ components,
81
+ opts,
82
+ aliases,
83
+ );
74
84
  }
75
85
  if (statement.type === "ExportDefaultDeclaration") {
76
86
  collectDefaultExport(statement, definitions, defaultExports, components, opts);
77
87
  }
78
88
  }
79
89
 
90
+ for (const alias of aliases) {
91
+ const target = definitions.get(alias.target);
92
+ if (!target) continue;
93
+ const definition = { name: alias.name, fn: target.fn };
94
+ definitions.set(alias.name, definition);
95
+ if (alias.exported && opts.exportTypes.has("named")) components.push(definition);
96
+ }
97
+
80
98
  pushExportedDefinitions(namedExports, definitions, components, opts.exportTypes.has("named"));
81
99
  pushExportedDefinitions(defaultExports, definitions, components, opts.exportTypes.has("default"));
82
100
  return uniqueComponents(components);
@@ -94,25 +112,6 @@ function pushExportedDefinitions(names, definitions, components, enabled) {
94
112
  }
95
113
  }
96
114
 
97
- function collectTopLevelDefinition(statement, definitions, opts) {
98
- if (statement.type === "FunctionDeclaration" && statement.id) {
99
- definitions.set(statement.id.name, { name: statement.id.name, fn: statement });
100
- return;
101
- }
102
- if (statement.type !== "VariableDeclaration") {
103
- return;
104
- }
105
- for (const declaration of statement.declarations) {
106
- if (declaration.id.type !== "Identifier") {
107
- continue;
108
- }
109
- const fn = functionFromExpression(declaration.init, opts);
110
- if (fn) {
111
- definitions.set(declaration.id.name, { name: declaration.id.name, fn });
112
- }
113
- }
114
- }
115
-
116
115
  function collectNamedExport(
117
116
  statement,
118
117
  definitions,
@@ -120,6 +119,7 @@ function collectNamedExport(
120
119
  defaultExports,
121
120
  components,
122
121
  opts,
122
+ aliases,
123
123
  ) {
124
124
  if (statement.source) {
125
125
  return;
@@ -133,7 +133,7 @@ function collectNamedExport(
133
133
  return;
134
134
  }
135
135
  if (statement.declaration?.type === "VariableDeclaration") {
136
- collectTopLevelDefinition(statement.declaration, definitions, opts);
136
+ collectTopLevelDefinition(statement.declaration, definitions, opts, aliases, true);
137
137
  for (const declaration of statement.declaration.declarations) {
138
138
  const definition = definitions.get(declaration.id.name);
139
139
  if (declaration.id.type === "Identifier" && definition && opts.exportTypes.has("named")) {
@@ -0,0 +1,102 @@
1
+ "use strict";
2
+
3
+ const { unwrapExpression } = require("./rules/async-ast");
4
+ const { logicalCall, memberPropertyName, propertyName } = require("./rules/module-mock-helpers");
5
+
6
+ const LOCAL_BINDING_TYPES = new Set([
7
+ "Variable",
8
+ "Parameter",
9
+ "CatchClause",
10
+ "FunctionName",
11
+ "ImportBinding",
12
+ "ClassName",
13
+ ]);
14
+ const FETCH_ROOTS = new Set(["globalThis", "window", "self", "global"]);
15
+
16
+ function isFetchShadowed(scope) {
17
+ while (scope) {
18
+ const get = scope.set?.get;
19
+ const variable = typeof get === "function" ? get.call(scope.set, "fetch") : null;
20
+ if (variable) return variable.defs.some((def) => LOCAL_BINDING_TYPES.has(def.type));
21
+ if (!scope.set || typeof get !== "function") {
22
+ const fallback = scope.variables?.find((item) => item.name === "fetch");
23
+ if (fallback) return fallback.defs.some((def) => LOCAL_BINDING_TYPES.has(def.type));
24
+ }
25
+ scope = scope.upper;
26
+ }
27
+ return false;
28
+ }
29
+
30
+ function unboundRoot(node, context) {
31
+ if (node?.type !== "Identifier" || !FETCH_ROOTS.has(node.name)) return false;
32
+ let scope = context.sourceCode.getScope(node);
33
+ while (scope) {
34
+ const variable = scope.variables?.find((item) => item.name === node.name);
35
+ if (variable) return !variable.defs?.some((def) => LOCAL_BINDING_TYPES.has(def.type));
36
+ scope = scope.upper;
37
+ }
38
+ return true;
39
+ }
40
+
41
+ function bindingVariable(node, context) {
42
+ if (!context?.sourceCode?.getScope) return null;
43
+ let scope = context.sourceCode.getScope(node);
44
+ while (scope) {
45
+ const variable = scope.variables?.find((item) => item.name === node.name);
46
+ if (variable) return variable;
47
+ scope = scope.upper;
48
+ }
49
+ return null;
50
+ }
51
+
52
+ function fetchCallee(node, context, seen) {
53
+ node = unwrapExpression(node);
54
+ if (!node) return false;
55
+ const visiting = seen || new Set();
56
+ if (node.type === "MemberExpression") {
57
+ if (memberPropertyName(node) !== "fetch") return false;
58
+ return unboundRoot(unwrapExpression(node.object), context);
59
+ }
60
+ if (node.type === "Identifier") {
61
+ if (node.name === "fetch" && !isFetchShadowed(context?.sourceCode?.getScope?.(node)))
62
+ return true;
63
+ const variable = bindingVariable(node, context);
64
+ if (!variable || visiting.has(variable) || variable.defs.length !== 1) return false;
65
+ visiting.add(variable);
66
+ const def = variable.defs[0];
67
+ if (def.type !== "Variable" || def.parent?.kind !== "const") return false;
68
+ if (def.node.id.type === "Identifier") {
69
+ if (!def.node.init) return false;
70
+ return fetchCallee(def.node.init, context, visiting);
71
+ }
72
+ if (def.node.id.type !== "ObjectPattern") return false;
73
+ for (const item of def.node.id.properties) {
74
+ if (item.type === "Property") {
75
+ const value = item.value.type === "AssignmentPattern" ? item.value.left : item.value;
76
+ // `const { [name]: schedule }` names an expression, not the fetch property.
77
+ if (item.computed && item.key?.type === "Identifier") continue;
78
+ if (
79
+ value.type === "Identifier" &&
80
+ value.name === node.name &&
81
+ propertyName(item.key) === "fetch"
82
+ ) {
83
+ return unboundRoot(unwrapExpression(def.node.init), context);
84
+ }
85
+ }
86
+ }
87
+ return false;
88
+ }
89
+ return false;
90
+ }
91
+
92
+ function fetchInvocation(node, context) {
93
+ const invoked = logicalCall(node);
94
+ if (!invoked?.callee || !fetchCallee(invoked.callee, context)) return null;
95
+ return invoked.arguments;
96
+ }
97
+
98
+ function isFetchCall(node, context) {
99
+ return fetchInvocation(node, context) !== null;
100
+ }
101
+
102
+ module.exports = { fetchInvocation, isFetchCall };
package/src/helpers.js CHANGED
@@ -1,5 +1,7 @@
1
1
  "use strict";
2
2
 
3
+ const { fetchInvocation, isFetchCall } = require("./fetch-call");
4
+
3
5
  const DEFAULT_SELECTOR_ATTRIBUTES = ["data-testid", "data-pw"];
4
6
  const DEFAULT_NAMING_PATTERN = "^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$";
5
7
  const INTERACTIVE_ELEMENTS = new Set(["button", "input", "select", "textarea"]);
@@ -134,40 +136,6 @@ function selectorValueNode(attribute) {
134
136
  return expression && expression.type !== "JSXEmptyExpression" ? expression : null;
135
137
  }
136
138
 
137
- const LOCAL_BINDING_TYPES = new Set([
138
- "Variable",
139
- "Parameter",
140
- "CatchClause",
141
- "FunctionName",
142
- "ImportBinding",
143
- "ClassName",
144
- ]);
145
-
146
- function isFetchShadowed(scope) {
147
- while (scope) {
148
- const get = scope.set?.get;
149
- const variable = typeof get === "function" ? get.call(scope.set, "fetch") : null;
150
- if (variable) {
151
- return variable.defs.some((def) => LOCAL_BINDING_TYPES.has(def.type));
152
- }
153
- if (!scope.set || typeof get !== "function") {
154
- const fallback = scope.variables?.find((item) => item.name === "fetch");
155
- if (fallback) {
156
- return fallback.defs.some((def) => LOCAL_BINDING_TYPES.has(def.type));
157
- }
158
- }
159
- scope = scope.upper;
160
- }
161
- return false;
162
- }
163
-
164
- function isFetchCall(node, context) {
165
- if (node.callee.type !== "Identifier" || node.callee.name !== "fetch") {
166
- return false;
167
- }
168
- return !isFetchShadowed(context.sourceCode.getScope(node));
169
- }
170
-
171
139
  function rule(meta, create) {
172
140
  return { meta, create };
173
141
  }
@@ -179,6 +147,7 @@ module.exports = {
179
147
  canonicalAttribute,
180
148
  cssSelectorValues,
181
149
  INTERACTIVE_ELEMENTS,
150
+ fetchInvocation,
182
151
  isFetchCall,
183
152
  isSelectorAttribute,
184
153
  isSelectorCall,
package/src/index.js CHANGED
@@ -1,6 +1,9 @@
1
1
  "use strict";
2
2
 
3
3
  const rules = {
4
+ "restricted-api-usage": require("./rules/restricted-api-usage"),
5
+ "no-consecutive-unbatched-reads": require("./rules/no-consecutive-unbatched-reads"),
6
+ "require-top-level-registration": require("./rules/require-top-level-registration"),
4
7
  "forbidden-member-chains": require("./rules/forbidden-member-chains"),
5
8
  "gh-api-argument-safety": require("./rules/gh-api-argument-safety"),
6
9
  "no-argv-main-guard": require("./rules/no-argv-main-guard"),
@@ -39,8 +42,10 @@ const rules = {
39
42
  "playwright-unique": require("./rules/playwright-unique"),
40
43
  "postgres-cursor-call-contract": require("./rules/postgres-cursor-call-contract"),
41
44
  "postgres-no-async-sql-builder": require("./rules/postgres-no-async-sql-builder"),
45
+ "postgres-no-correlated-sql-builder": require("./rules/postgres-no-correlated-sql-builder"),
42
46
  "postgres-no-executor-adapter": require("./rules/postgres-no-executor-adapter"),
43
47
  "postgres-no-manual-transaction": require("./rules/postgres-no-manual-transaction"),
48
+ "postgres-no-nonliteral-sql-registry-key": require("./rules/postgres-no-nonliteral-sql-registry-key"),
44
49
  "postgres-no-unbounded-query-fanout": require("./rules/postgres-no-unbounded-query-fanout"),
45
50
  "react-no-nullish-react-node": require("./rules/react-no-nullish-react-node"),
46
51
  "react-no-use-promise-resolve": require("./rules/react-no-use-promise-resolve"),
@@ -6,6 +6,7 @@ function ruleSuppression(context, ruleId, suppressionOptions) {
6
6
  const lines = new Set();
7
7
  let file = false;
8
8
  const source = context.sourceCode;
9
+ if (!source?.ast?.tokens || !source.getAllComments) return () => false;
9
10
  const first = source.ast.tokens[0];
10
11
  const requireReason = suppressionOptions?.requireReason === true;
11
12
  for (const comment of source.getAllComments()) {
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ const ARGV = 1;
3
+ // Strings and [prefix, suffix] fragments stay separate from path identities.
4
+ // Keeping the fragment until the comparison lets nested concatenations fold once.
5
+ function identity(value) {
6
+ if (Array.isArray(value)) return /^file:\/\/\/?$/i.test(value[0]) && value[1] === "" ? ARGV : 0;
7
+ return typeof value === "number" ? value : 0;
8
+ }
9
+ function concatenate(left, right) {
10
+ if (left === ARGV) left = ["", ""];
11
+ if (right === ARGV) right = ["", ""];
12
+ // Accepted prefixes have at most eight characters; reject before allocating.
13
+ if (typeof left === "string" && typeof right === "string")
14
+ return left.length + right.length <= 8 ? left + right : 0;
15
+ if (typeof left === "string" && Array.isArray(right))
16
+ return left.length + right[0].length <= 8 ? [left + right[0], right[1]] : 0;
17
+ if (Array.isArray(left) && typeof right === "string") return right === "" ? left : 0;
18
+ return 0;
19
+ }
20
+ function fileUrlArgument(target, read) {
21
+ if (target?.type !== "NewExpression" || target.arguments.length !== 1) return 0;
22
+ const argument = read(target.arguments[0]);
23
+ return Array.isArray(argument) && identity(argument) === ARGV ? argument : 0;
24
+ }
25
+ module.exports = { ARGV, identity, concatenate, fileUrlArgument };
@@ -1,7 +1,8 @@
1
1
  "use strict";
2
2
  const { unwrapExpression } = require("./async-ast");
3
3
  const { resolveVariable, memberPropertyName, isLocalRequire } = require("./async-target-bindings");
4
- const { literalString } = require("./module-mock-helpers");
4
+ const { literalString, logicalCall } = require("./module-mock-helpers");
5
+ const { ARGV, identity, concatenate, fileUrlArgument } = require("./argv-main-fragments");
5
6
 
6
7
  function globalName(node, name, context) {
7
8
  if (node?.type !== "Identifier" || node.name !== name) return false;
@@ -29,16 +30,18 @@ function nodeProcess(node, context) {
29
30
  ["process", "node:process"].includes(literalString(init.arguments[0]))
30
31
  );
31
32
  }
32
- const ARGV = 1;
33
33
  const MAIN = 2;
34
34
  const PROCESS = 3;
35
35
  const ARGUMENTS = 4;
36
36
  function createClassifier(context, wrappers) {
37
37
  const stable = new WeakMap();
38
38
  const aliases = new WeakMap();
39
+ const value = (node, depth) => identity(classify(node, depth));
39
40
  function classify(input, depth = 0) {
40
41
  const node = unwrapExpression(input);
41
42
  if (!node || depth > 32) return 0;
43
+ const text = literalString(node);
44
+ if (text !== null) return text;
42
45
  if (node.type === "Identifier") {
43
46
  const variable = resolveVariable(node, context);
44
47
  if (!variable) {
@@ -51,6 +54,25 @@ function createClassifier(context, wrappers) {
51
54
  if (nodeProcess(node, context)) return PROCESS;
52
55
  if (globalName(node, "__filename", context)) return MAIN;
53
56
  const def = variable.defs.length === 1 ? variable.defs[0] : null;
57
+ if (def?.type === "ImportBinding" && def.node?.type === "ImportSpecifier") {
58
+ const imported = def.node.imported;
59
+ const importedName = imported?.type === "Literal" ? String(imported.value) : imported?.name;
60
+ if (
61
+ importedName === "argv" &&
62
+ ["process", "node:process"].includes(def.parent?.source?.value)
63
+ ) {
64
+ return ARGUMENTS;
65
+ }
66
+ }
67
+ if (
68
+ def?.type === "Variable" &&
69
+ def.parent?.kind === "const" &&
70
+ def.node?.id?.type === "ObjectPattern"
71
+ ) {
72
+ const key = objectPatternKey(def.node.id, node.name);
73
+ if (key === "argv" && value(def.node.init, depth + 1) === PROCESS) return ARGUMENTS;
74
+ return 0;
75
+ }
54
76
  if (
55
77
  def?.type !== "Variable" ||
56
78
  def.parent.kind !== "const" ||
@@ -64,6 +86,29 @@ function createClassifier(context, wrappers) {
64
86
  }
65
87
  return aliases.get(variable);
66
88
  }
89
+ if (node.type === "BinaryExpression" && node.operator === "+") {
90
+ return concatenate(classify(node.left, depth + 1), classify(node.right, depth + 1));
91
+ }
92
+ if (
93
+ node.type === "LogicalExpression" &&
94
+ node.operator === "??" &&
95
+ classify(node.right, depth + 1) === ""
96
+ ) {
97
+ return classify(node.left, depth + 1);
98
+ }
99
+ if (node.type === "TemplateLiteral") {
100
+ // Only prefix substitutions may be constant; a suffix substitution is not an argv guard.
101
+ const last = node.expressions.length - 1;
102
+ const argument = classify(node.expressions[last], depth + 1);
103
+ if (identity(argument) !== ARGV || node.quasis[last + 1].value.cooked !== "") return 0;
104
+ let prefix = node.quasis[0].value.cooked;
105
+ for (let i = 0; i < last; i++) {
106
+ const part = classify(node.expressions[i], depth + 1);
107
+ if (typeof part !== "string") return 0;
108
+ prefix = concatenate(prefix, concatenate(part, node.quasis[i + 1].value.cooked));
109
+ }
110
+ return concatenate(prefix, argument);
111
+ }
67
112
  if (node.type === "MemberExpression") {
68
113
  const name = memberPropertyName(node);
69
114
  if (
@@ -73,15 +118,14 @@ function createClassifier(context, wrappers) {
73
118
  ["url", "filename"].includes(name)
74
119
  )
75
120
  return MAIN;
76
- const object = classify(node.object, depth + 1);
121
+ const target = unwrapExpression(node.object);
122
+ if (name === "href" && globalName(target?.callee, "URL", context)) {
123
+ const constructed = fileUrlArgument(target, (argument) => classify(argument, depth + 1));
124
+ if (constructed) return constructed;
125
+ }
126
+ const object = value(node.object, depth + 1);
77
127
  if (object === PROCESS && name === "argv") return ARGUMENTS;
78
- if (
79
- object === ARGUMENTS &&
80
- node.computed &&
81
- node.property.type === "Literal" &&
82
- node.property.value === 1
83
- )
84
- return ARGV;
128
+ if (object === ARGUMENTS && node.computed && staticIndexOne(node.property)) return ARGV;
85
129
  // A URL conversion's href preserves the same module/argv identity.
86
130
  if (
87
131
  name === "href" &&
@@ -91,24 +135,71 @@ function createClassifier(context, wrappers) {
91
135
  return object;
92
136
  }
93
137
  if (node.type === "CallExpression") {
94
- const callee = unwrapExpression(node.callee);
138
+ const invoked = logicalCall(node);
139
+ if (!invoked) return 0;
140
+ const callee = unwrapExpression(invoked.callee);
95
141
  if (
96
142
  callee?.type === "MemberExpression" &&
97
143
  memberPropertyName(callee) === "at" &&
98
- classify(callee.object, depth + 1) === ARGUMENTS &&
99
- node.arguments.length === 1 &&
100
- node.arguments[0].type === "Literal" &&
101
- node.arguments[0].value === 1
144
+ value(callee.object, depth + 1) === ARGUMENTS &&
145
+ invoked.arguments.length === 1 &&
146
+ staticIndexOne(invoked.arguments[0])
102
147
  )
103
148
  return ARGV;
104
- if (node.arguments.length !== 1 || node.arguments[0].type === "SpreadElement") return 0;
149
+ if (
150
+ !invoked.forwarded &&
151
+ callee?.type === "MemberExpression" &&
152
+ memberPropertyName(callee) === "toString" &&
153
+ invoked.arguments.length === 0
154
+ ) {
155
+ const url = unwrapExpression(callee.object);
156
+ if (globalName(url?.callee, "URL", context)) {
157
+ const constructed = fileUrlArgument(url, (argument) => classify(argument, depth + 1));
158
+ if (constructed) return constructed;
159
+ }
160
+ const classified = classify(callee.object, depth + 1);
161
+ return typeof classified === "string" || [ARGV, MAIN].includes(identity(classified))
162
+ ? classified
163
+ : 0;
164
+ }
165
+ if (
166
+ !invoked.forwarded &&
167
+ callee?.type === "MemberExpression" &&
168
+ memberPropertyName(callee) === "concat" &&
169
+ invoked.arguments.every((argument) => argument.type !== "SpreadElement")
170
+ )
171
+ return invoked.arguments.reduce(
172
+ (folded, argument) => concatenate(folded, classify(argument, depth + 1)),
173
+ classify(callee.object, depth + 1),
174
+ );
175
+ if (invoked.arguments.length !== 1 || invoked.arguments[0].type === "SpreadElement") return 0;
176
+ if (globalName(callee, "String", context)) return classify(invoked.arguments[0], depth + 1);
105
177
  if (globalName(callee, "Boolean", context) || wrappers.resolveCallTarget(node)) {
106
- const value = classify(node.arguments[0], depth + 1);
107
- return value === ARGV || value === MAIN ? value : 0;
178
+ const argument = value(invoked.arguments[0], depth + 1);
179
+ return argument === ARGV || argument === MAIN ? argument : 0;
108
180
  }
109
181
  }
110
182
  return 0;
111
183
  }
112
- return (node) => classify(node);
184
+ return (node) => value(node, 0);
113
185
  }
186
+ function staticIndexOne(node) {
187
+ if (!node) return false;
188
+ if (node.type === "Literal") return node.value === 1 || node.value === "1";
189
+ return literalString(node) === "1";
190
+ }
191
+
192
+ function objectPatternKey(pattern, name) {
193
+ for (const property of pattern.properties) {
194
+ if (property.type !== "Property") continue;
195
+ const value =
196
+ property.value.type === "AssignmentPattern" ? property.value.left : property.value;
197
+ if (value.type !== "Identifier" || value.name !== name) continue;
198
+ if (property.key.type === "Identifier" && !property.computed) return property.key.name;
199
+ if (property.key.type === "Literal") return String(property.key.value);
200
+ return literalString(property.key);
201
+ }
202
+ return null;
203
+ }
204
+
114
205
  module.exports = { createClassifier, ARGV, MAIN };
@@ -6,6 +6,7 @@ const transparentExpressionTypes = new Set([
6
6
  "TSAsExpression",
7
7
  "TSTypeAssertion",
8
8
  "TSSatisfiesExpression",
9
+ "TSInstantiationExpression",
9
10
  ]);
10
11
 
11
12
  function isFunction(node) {
@@ -24,6 +25,18 @@ function findContainingFunction(node) {
24
25
  }
25
26
  }
26
27
 
28
+ function findContainingRoot(node) {
29
+ return findContainingFunction(node) ?? findProgram(node);
30
+ }
31
+
32
+ function findProgram(node) {
33
+ let current = node;
34
+ while (current) {
35
+ if (current.type === "Program") return current;
36
+ current = current.parent;
37
+ }
38
+ }
39
+
27
40
  function isTransparentExpression(node) {
28
41
  return transparentExpressionTypes.has(node?.type);
29
42
  }
@@ -34,6 +47,14 @@ function unwrapExpression(node) {
34
47
  return current;
35
48
  }
36
49
 
50
+ function unwrapSequence(node) {
51
+ let current = unwrapExpression(node);
52
+ while (current?.type === "SequenceExpression") {
53
+ current = unwrapExpression(current.expressions.at(-1));
54
+ }
55
+ return current;
56
+ }
57
+
37
58
  function unwrapTransparentParent(node) {
38
59
  let current = node;
39
60
  while (isTransparentExpression(current.parent)) current = current.parent;
@@ -81,10 +102,12 @@ function isUnconditionalBeforeReturn(node, block) {
81
102
 
82
103
  module.exports = {
83
104
  findContainingFunction,
105
+ findContainingRoot,
84
106
  isFunction,
85
107
  isTransparentExpression,
86
108
  isUnconditionalBeforeReturn,
87
109
  traverse,
88
110
  unwrapExpression,
111
+ unwrapSequence,
89
112
  unwrapTransparentParent,
90
113
  };