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.
- package/README.md +5 -0
- package/package.json +1 -1
- package/src/component-functions.js +39 -7
- package/src/exported-component-definitions.js +32 -0
- package/src/exported-components.js +22 -22
- package/src/fetch-call.js +102 -0
- package/src/helpers.js +3 -34
- package/src/index.js +5 -0
- package/src/rule-suppression.js +1 -0
- package/src/rules/argv-main-fragments.js +25 -0
- package/src/rules/argv-main-values.js +110 -19
- package/src/rules/async-ast.js +23 -0
- package/src/rules/async-call-disposition.js +39 -152
- package/src/rules/async-disposition-bindings.js +131 -0
- package/src/rules/async-disposition-expressions.js +166 -0
- package/src/rules/async-disposition-flow.js +151 -0
- package/src/rules/async-disposition-mock-bindings.js +39 -0
- package/src/rules/async-disposition-mocks.js +142 -0
- package/src/rules/async-target-bindings.js +24 -0
- package/src/rules/async-targets.js +36 -12
- package/src/rules/await-array-methods.js +21 -24
- package/src/rules/configured-runtime-import.js +28 -0
- package/src/rules/gh-api-argument-safety.js +9 -32
- package/src/rules/gh-api-wrappers.js +52 -0
- package/src/rules/logical-call.js +75 -0
- package/src/rules/module-mock-boundary.js +6 -1
- package/src/rules/module-mock-call-apply.js +18 -12
- package/src/rules/module-mock-framework.js +61 -9
- package/src/rules/module-mock-helpers.js +24 -24
- package/src/rules/module-mock-preserve-aliases.js +77 -22
- package/src/rules/module-mock-preserve-exports.js +21 -1
- package/src/rules/nextjs-static-fetch-method.js +4 -3
- package/src/rules/nextjs-static-fetch-url.js +4 -3
- package/src/rules/no-banned-import-outside-allowed-paths-calls.js +39 -0
- package/src/rules/no-banned-import-outside-allowed-paths.js +3 -29
- package/src/rules/no-consecutive-unbatched-reads.js +136 -0
- package/src/rules/no-global-fetch-outside-helper-bindings.js +9 -0
- package/src/rules/no-global-fetch-outside-helper-helpers.js +2 -2
- package/src/rules/no-global-fetch-outside-helper.js +5 -2
- package/src/rules/no-import-only-test-files.js +69 -32
- package/src/rules/no-inline-noop-promise-catch-helpers.js +30 -13
- package/src/rules/no-object-mutation-ast.js +65 -0
- package/src/rules/no-object-mutation-bind.js +24 -7
- package/src/rules/no-object-mutation-build.js +4 -0
- package/src/rules/no-object-mutation-call.js +81 -16
- package/src/rules/no-object-mutation-class.js +134 -0
- package/src/rules/no-object-mutation-expr.js +14 -4
- package/src/rules/no-object-mutation-fn.js +42 -20
- package/src/rules/no-object-mutation-forward.js +137 -0
- package/src/rules/no-object-mutation-invoke.js +155 -0
- package/src/rules/no-object-mutation-join.js +5 -0
- package/src/rules/no-object-mutation-method.js +65 -0
- package/src/rules/no-object-mutation-namespace.js +91 -0
- package/src/rules/no-object-mutation-read.js +79 -15
- package/src/rules/no-object-mutation-scan.js +21 -15
- package/src/rules/no-object-mutation-syntax.js +2 -87
- package/src/rules/no-object-mutation-values.js +11 -0
- package/src/rules/no-object-mutation-write.js +7 -1
- package/src/rules/no-vitest-sequential.js +165 -36
- package/src/rules/nullable-option-defaults-helpers.js +18 -4
- package/src/rules/playwright-assertion-timeout-cap.js +74 -14
- package/src/rules/playwright-interactive-names.js +125 -0
- package/src/rules/playwright-literals.js +8 -3
- package/src/rules/playwright-no-hoisted-unique-token.js +28 -20
- package/src/rules/playwright-no-raw-scroll-pagination.js +25 -55
- package/src/rules/playwright-prefer-get-by-test-id.js +14 -5
- package/src/rules/playwright-require-exported-component-attribute.js +8 -2
- package/src/rules/playwright-require-interactive-test-id.js +30 -65
- package/src/rules/playwright-scroll-call.js +55 -0
- package/src/rules/playwright-selector-priority.js +8 -5
- package/src/rules/playwright-static-call-alias.js +65 -0
- package/src/rules/playwright-static-call.js +155 -0
- package/src/rules/playwright-test-timeout-bindings.js +0 -1
- package/src/rules/playwright-timeout-carriers.js +0 -1
- package/src/rules/postgres-correlated-completion.js +89 -0
- package/src/rules/postgres-executor-calls.js +157 -0
- package/src/rules/postgres-executor.js +19 -3
- package/src/rules/postgres-fanout-calls.js +72 -0
- package/src/rules/postgres-no-async-sql-builder-array-map.js +51 -0
- package/src/rules/postgres-no-async-sql-builder-calls.js +39 -2
- package/src/rules/postgres-no-async-sql-builder-constants.js +93 -5
- package/src/rules/postgres-no-async-sql-builder-import.js +60 -0
- package/src/rules/postgres-no-async-sql-builder-match.js +14 -28
- package/src/rules/postgres-no-async-sql-builder-types.js +3 -2
- package/src/rules/postgres-no-correlated-sql-builder-append.js +161 -0
- package/src/rules/postgres-no-correlated-sql-builder.js +129 -0
- package/src/rules/postgres-no-executor-adapter-forward.js +32 -16
- package/src/rules/postgres-no-executor-adapter-logic.js +5 -1
- package/src/rules/postgres-no-executor-adapter-member.js +103 -1
- package/src/rules/postgres-no-executor-adapter-passed.js +3 -3
- package/src/rules/postgres-no-executor-adapter-ref.js +7 -3
- package/src/rules/postgres-no-executor-adapter.js +2 -3
- package/src/rules/postgres-no-manual-transaction.js +5 -2
- package/src/rules/postgres-no-nonliteral-sql-registry-key.js +156 -0
- package/src/rules/postgres-no-unbounded-query-fanout.js +9 -3
- package/src/rules/postgres-runtime-helpers.js +44 -10
- package/src/rules/react-no-iife-in-jsx.js +46 -17
- package/src/rules/react-no-nullish-react-node.js +10 -4
- package/src/rules/react-no-use-promise-resolve.js +126 -65
- package/src/rules/require-options-on-imported-call.js +20 -12
- package/src/rules/require-top-level-registration.js +64 -0
- package/src/rules/required-call-selector-aliases.js +45 -0
- package/src/rules/required-call-selector-compile.js +9 -3
- package/src/rules/required-call-selectors.js +35 -9
- package/src/rules/restricted-api-usage.js +195 -0
- package/src/rules/server-require-nullable-fetch-wrapper.js +63 -6
- package/src/rules/test-message-expect.js +155 -0
- package/src/rules/test-no-delayed-rejects-chains.js +5 -2
- package/src/rules/test-no-delayed-rejects-expect.js +59 -0
- package/src/rules/test-no-delayed-rejects-flow.js +7 -2
- package/src/rules/test-no-delayed-rejects.js +13 -45
- package/src/rules/test-no-error-message-matching.js +36 -43
- package/src/rules/test-no-shared-state-aliases.js +121 -1
- package/src/rules/test-no-shared-state-analysis.js +10 -2
- package/src/rules/test-no-shared-state-callees.js +64 -16
- package/src/rules/test-no-shared-state-mutations.js +3 -1
- package/src/rules/test-no-shared-state-rule-helpers.js +34 -5
- package/src/rules/test-no-shared-state.js +39 -50
- package/src/rules/test-no-skips-bindings.js +76 -24
- package/src/rules/test-no-skips.js +147 -2
- package/src/rules/ts-no-function-aliases-ast.js +65 -0
- package/src/rules/ts-no-function-aliases-contract.js +92 -0
- package/src/rules/ts-no-function-aliases-forwarding.js +46 -0
- package/src/rules/ts-no-function-aliases-legacy.js +79 -0
- package/src/rules/ts-no-function-aliases-reachable.js +76 -0
- package/src/rules/ts-no-function-aliases-self.js +132 -0
- package/src/rules/ts-no-function-aliases-signature.js +65 -0
- package/src/rules/ts-no-function-aliases.js +66 -95
- 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,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
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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(
|
|
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"),
|
package/src/rule-suppression.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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 (
|
|
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
|
|
107
|
-
return
|
|
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) =>
|
|
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 };
|
package/src/rules/async-ast.js
CHANGED
|
@@ -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
|
};
|