eslint-plugin-no-mistakes 0.94.0 → 0.95.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/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 +2 -0
- package/src/rule-suppression.js +1 -0
- package/src/rules/argv-main-values.js +47 -14
- package/src/rules/async-ast.js +23 -0
- package/src/rules/async-call-disposition.js +7 -5
- 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/gh-api-argument-safety.js +3 -2
- 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 +69 -8
- 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-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 +52 -0
- package/src/rules/no-object-mutation-bind.js +15 -5
- package/src/rules/no-object-mutation-build.js +4 -0
- package/src/rules/no-object-mutation-call.js +60 -7
- package/src/rules/no-object-mutation-class.js +134 -0
- package/src/rules/no-object-mutation-expr.js +5 -2
- package/src/rules/no-object-mutation-fn.js +34 -20
- package/src/rules/no-object-mutation-forward.js +146 -0
- package/src/rules/no-object-mutation-invoke.js +116 -0
- package/src/rules/no-object-mutation-namespace.js +91 -0
- package/src/rules/no-object-mutation-read.js +62 -14
- package/src/rules/no-object-mutation-scan.js +13 -14
- package/src/rules/no-object-mutation-syntax.js +2 -87
- package/src/rules/no-object-mutation-values.js +7 -0
- package/src/rules/no-object-mutation-write.js +6 -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 +79 -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 +53 -0
- package/src/rules/playwright-selector-priority.js +8 -5
- package/src/rules/playwright-static-call.js +209 -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-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-nonliteral-sql-registry-key.js +156 -0
- package/src/rules/postgres-runtime-helpers.js +35 -5
- 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/required-call-selectors.js +12 -5
- package/src/rules/server-require-nullable-fetch-wrapper.js +63 -6
- package/src/rules/test-message-expect.js +152 -0
- package/src/rules/test-no-delayed-rejects-chains.js +5 -2
- package/src/rules/test-no-delayed-rejects-flow.js +7 -2
- package/src/rules/test-no-delayed-rejects.js +24 -32
- 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 +63 -21
- package/src/rules/test-no-skips.js +142 -1
- package/src/rules/vitest-mock-test-file-naming.js +95 -38
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
|
@@ -39,8 +39,10 @@ const rules = {
|
|
|
39
39
|
"playwright-unique": require("./rules/playwright-unique"),
|
|
40
40
|
"postgres-cursor-call-contract": require("./rules/postgres-cursor-call-contract"),
|
|
41
41
|
"postgres-no-async-sql-builder": require("./rules/postgres-no-async-sql-builder"),
|
|
42
|
+
"postgres-no-correlated-sql-builder": require("./rules/postgres-no-correlated-sql-builder"),
|
|
42
43
|
"postgres-no-executor-adapter": require("./rules/postgres-no-executor-adapter"),
|
|
43
44
|
"postgres-no-manual-transaction": require("./rules/postgres-no-manual-transaction"),
|
|
45
|
+
"postgres-no-nonliteral-sql-registry-key": require("./rules/postgres-no-nonliteral-sql-registry-key"),
|
|
44
46
|
"postgres-no-unbounded-query-fanout": require("./rules/postgres-no-unbounded-query-fanout"),
|
|
45
47
|
"react-no-nullish-react-node": require("./rules/react-no-nullish-react-node"),
|
|
46
48
|
"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()) {
|
|
@@ -1,7 +1,7 @@
|
|
|
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
5
|
|
|
6
6
|
function globalName(node, name, context) {
|
|
7
7
|
if (node?.type !== "Identifier" || node.name !== name) return false;
|
|
@@ -51,6 +51,25 @@ function createClassifier(context, wrappers) {
|
|
|
51
51
|
if (nodeProcess(node, context)) return PROCESS;
|
|
52
52
|
if (globalName(node, "__filename", context)) return MAIN;
|
|
53
53
|
const def = variable.defs.length === 1 ? variable.defs[0] : null;
|
|
54
|
+
if (def?.type === "ImportBinding" && def.node?.type === "ImportSpecifier") {
|
|
55
|
+
const imported = def.node.imported;
|
|
56
|
+
const importedName = imported?.type === "Literal" ? String(imported.value) : imported?.name;
|
|
57
|
+
if (
|
|
58
|
+
importedName === "argv" &&
|
|
59
|
+
["process", "node:process"].includes(def.parent?.source?.value)
|
|
60
|
+
) {
|
|
61
|
+
return ARGUMENTS;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (
|
|
65
|
+
def?.type === "Variable" &&
|
|
66
|
+
def.parent?.kind === "const" &&
|
|
67
|
+
def.node?.id?.type === "ObjectPattern"
|
|
68
|
+
) {
|
|
69
|
+
const key = objectPatternKey(def.node.id, node.name);
|
|
70
|
+
if (key === "argv" && classify(def.node.init, depth + 1) === PROCESS) return ARGUMENTS;
|
|
71
|
+
return 0;
|
|
72
|
+
}
|
|
54
73
|
if (
|
|
55
74
|
def?.type !== "Variable" ||
|
|
56
75
|
def.parent.kind !== "const" ||
|
|
@@ -75,13 +94,7 @@ function createClassifier(context, wrappers) {
|
|
|
75
94
|
return MAIN;
|
|
76
95
|
const object = classify(node.object, depth + 1);
|
|
77
96
|
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;
|
|
97
|
+
if (object === ARGUMENTS && node.computed && staticIndexOne(node.property)) return ARGV;
|
|
85
98
|
// A URL conversion's href preserves the same module/argv identity.
|
|
86
99
|
if (
|
|
87
100
|
name === "href" &&
|
|
@@ -91,19 +104,20 @@ function createClassifier(context, wrappers) {
|
|
|
91
104
|
return object;
|
|
92
105
|
}
|
|
93
106
|
if (node.type === "CallExpression") {
|
|
94
|
-
const
|
|
107
|
+
const invoked = logicalCall(node);
|
|
108
|
+
if (!invoked) return 0;
|
|
109
|
+
const callee = unwrapExpression(invoked.callee);
|
|
95
110
|
if (
|
|
96
111
|
callee?.type === "MemberExpression" &&
|
|
97
112
|
memberPropertyName(callee) === "at" &&
|
|
98
113
|
classify(callee.object, depth + 1) === ARGUMENTS &&
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
node.arguments[0].value === 1
|
|
114
|
+
invoked.arguments.length === 1 &&
|
|
115
|
+
staticIndexOne(invoked.arguments[0])
|
|
102
116
|
)
|
|
103
117
|
return ARGV;
|
|
104
|
-
if (
|
|
118
|
+
if (invoked.arguments.length !== 1 || invoked.arguments[0].type === "SpreadElement") return 0;
|
|
105
119
|
if (globalName(callee, "Boolean", context) || wrappers.resolveCallTarget(node)) {
|
|
106
|
-
const value = classify(
|
|
120
|
+
const value = classify(invoked.arguments[0], depth + 1);
|
|
107
121
|
return value === ARGV || value === MAIN ? value : 0;
|
|
108
122
|
}
|
|
109
123
|
}
|
|
@@ -111,4 +125,23 @@ function createClassifier(context, wrappers) {
|
|
|
111
125
|
}
|
|
112
126
|
return (node) => classify(node);
|
|
113
127
|
}
|
|
128
|
+
function staticIndexOne(node) {
|
|
129
|
+
if (!node) return false;
|
|
130
|
+
if (node.type === "Literal") return node.value === 1 || node.value === "1";
|
|
131
|
+
return literalString(node) === "1";
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function objectPatternKey(pattern, name) {
|
|
135
|
+
for (const property of pattern.properties) {
|
|
136
|
+
if (property.type !== "Property") continue;
|
|
137
|
+
const value =
|
|
138
|
+
property.value.type === "AssignmentPattern" ? property.value.left : property.value;
|
|
139
|
+
if (value.type !== "Identifier" || value.name !== name) continue;
|
|
140
|
+
if (property.key.type === "Identifier" && !property.computed) return property.key.name;
|
|
141
|
+
if (property.key.type === "Literal") return String(property.key.value);
|
|
142
|
+
return literalString(property.key);
|
|
143
|
+
}
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
|
|
114
147
|
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
|
};
|
|
@@ -5,19 +5,21 @@ const {
|
|
|
5
5
|
findContainingFunction,
|
|
6
6
|
isFunction,
|
|
7
7
|
isTransparentExpression,
|
|
8
|
+
unwrapExpression,
|
|
8
9
|
unwrapTransparentParent,
|
|
9
10
|
} = require("./async-ast");
|
|
10
11
|
const { targetOptionsSchema } = require("./async-schema");
|
|
11
12
|
const { createTargetMatcher, memberPropertyName } = require("./async-targets");
|
|
12
13
|
|
|
13
14
|
function isPromiseAllCall(node) {
|
|
15
|
+
const callee = unwrapExpression(node?.callee);
|
|
16
|
+
const object = unwrapExpression(callee?.object);
|
|
14
17
|
return (
|
|
15
18
|
node?.type === "CallExpression" &&
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
memberPropertyName(node.callee) === "all"
|
|
19
|
+
callee?.type === "MemberExpression" &&
|
|
20
|
+
object?.type === "Identifier" &&
|
|
21
|
+
object.name === "Promise" &&
|
|
22
|
+
memberPropertyName(callee) === "all"
|
|
21
23
|
);
|
|
22
24
|
}
|
|
23
25
|
|
|
@@ -64,6 +64,29 @@ function recordObjectPatternBindings(pattern, source, recordDirect) {
|
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
function stableConstInitializer(node, context, seen) {
|
|
68
|
+
if (node?.type !== "Identifier") return null;
|
|
69
|
+
const variable = resolveVariable(node, context);
|
|
70
|
+
if (
|
|
71
|
+
!variable ||
|
|
72
|
+
seen.has(variable) ||
|
|
73
|
+
variable.defs.length !== 1 ||
|
|
74
|
+
isReassigned(node, context)
|
|
75
|
+
) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
const def = variable.defs[0];
|
|
79
|
+
if (
|
|
80
|
+
def.type !== "Variable" ||
|
|
81
|
+
def.parent?.kind !== "const" ||
|
|
82
|
+
def.node?.id?.type !== "Identifier"
|
|
83
|
+
) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
seen.add(variable);
|
|
87
|
+
return def.node.init;
|
|
88
|
+
}
|
|
89
|
+
|
|
67
90
|
function namespaceSourceFromInit(init, context, namespaceBindings) {
|
|
68
91
|
const expression = unwrapExpression(init);
|
|
69
92
|
if (expression?.type !== "Identifier") return null;
|
|
@@ -77,4 +100,5 @@ module.exports = {
|
|
|
77
100
|
namespaceSourceFromInit,
|
|
78
101
|
recordObjectPatternBindings,
|
|
79
102
|
resolveVariable,
|
|
103
|
+
stableConstInitializer,
|
|
80
104
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
const { unwrapExpression } = require("./async-ast");
|
|
3
|
+
const { unwrapExpression, unwrapSequence } = require("./async-ast");
|
|
4
4
|
const {
|
|
5
5
|
isLocalRequire,
|
|
6
6
|
isReassigned,
|
|
@@ -8,9 +8,10 @@ const {
|
|
|
8
8
|
namespaceSourceFromInit,
|
|
9
9
|
recordObjectPatternBindings,
|
|
10
10
|
resolveVariable,
|
|
11
|
+
stableConstInitializer,
|
|
11
12
|
} = require("./async-target-bindings");
|
|
12
13
|
const { compileTargets, matchesAny, targetMatches } = require("./async-patterns");
|
|
13
|
-
const { literalString } = require("./module-mock-helpers");
|
|
14
|
+
const { literalString, logicalCall } = require("./module-mock-helpers");
|
|
14
15
|
|
|
15
16
|
function importSpecifierName(specifier) {
|
|
16
17
|
const imported = specifier.imported;
|
|
@@ -104,6 +105,7 @@ function createTargetMatcher(context, optionKey = "targets") {
|
|
|
104
105
|
if (specifier.type === "ImportNamespaceSpecifier") {
|
|
105
106
|
recordNamespace(specifier.local, source);
|
|
106
107
|
} else if (specifier.type === "ImportDefaultSpecifier") {
|
|
108
|
+
recordNamespace(specifier.local, source);
|
|
107
109
|
recordDirect(specifier.local, source, specifier.local.name);
|
|
108
110
|
} else if (specifier.type === "ImportSpecifier") {
|
|
109
111
|
const imported = importSpecifierName(specifier);
|
|
@@ -145,22 +147,44 @@ function createTargetMatcher(context, optionKey = "targets") {
|
|
|
145
147
|
return (variable && directBindings.get(variable)) || null;
|
|
146
148
|
}
|
|
147
149
|
|
|
148
|
-
function resolveNamespaceTarget(node) {
|
|
150
|
+
function resolveNamespaceTarget(node, seen) {
|
|
149
151
|
if (node.type !== "MemberExpression") return null;
|
|
150
152
|
const name = memberPropertyName(node);
|
|
151
153
|
if (!name) return null;
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
154
|
+
let object = unwrapExpression(node.object);
|
|
155
|
+
for (;;) {
|
|
156
|
+
const source =
|
|
157
|
+
requireSource(object, context) ||
|
|
158
|
+
(object?.type === "Identifier"
|
|
159
|
+
? namespaceBindings.get(resolveVariable(object, context))
|
|
160
|
+
: null);
|
|
161
|
+
if (source) {
|
|
162
|
+
return targetMatches(targets, source, name) ? { source, calleeName: name } : null;
|
|
163
|
+
}
|
|
164
|
+
const init = stableConstInitializer(object, context, seen);
|
|
165
|
+
if (!init) return null;
|
|
166
|
+
object = unwrapSequence(init);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function resolveCallee(callee, seen = new Set()) {
|
|
171
|
+
callee = unwrapSequence(callee);
|
|
172
|
+
if (!callee) return null;
|
|
173
|
+
const found = resolveDirectTarget(callee) || resolveNamespaceTarget(callee, seen);
|
|
174
|
+
if (found) return found;
|
|
175
|
+
const init = stableConstInitializer(callee, context, seen);
|
|
176
|
+
if (!init) return null;
|
|
177
|
+
return resolveCallee(init, seen);
|
|
159
178
|
}
|
|
160
179
|
|
|
161
180
|
function resolveCallTarget(node) {
|
|
162
|
-
|
|
163
|
-
|
|
181
|
+
if (node?.type !== "CallExpression") return null;
|
|
182
|
+
const direct = resolveCallee(node.callee);
|
|
183
|
+
if (direct) return { ...direct, arguments: node.arguments };
|
|
184
|
+
const invoked = logicalCall(node);
|
|
185
|
+
if (!invoked) return null;
|
|
186
|
+
const forwarded = resolveCallee(invoked.callee);
|
|
187
|
+
return forwarded ? { ...forwarded, arguments: invoked.arguments } : null;
|
|
164
188
|
}
|
|
165
189
|
|
|
166
190
|
return {
|