eslint-plugin-no-mistakes 0.7.0 → 0.9.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 +76 -0
- package/src/exported-components.js +198 -0
- package/src/helpers.js +1 -1
- package/src/index.js +1 -0
- package/src/returned-jsx.js +119 -0
- package/src/rules/nextjs-no-manual-script-tags.js +54 -2
- package/src/rules/playwright-require-exported-component-attribute.js +77 -0
- package/src/rules/test-no-error-message-matching.js +24 -3
- package/src/rules/test-no-shared-state-analysis.js +130 -0
- package/src/rules/test-no-shared-state-helpers.js +106 -6
- package/src/rules/test-no-shared-state.js +140 -57
- package/src/rules/ts-no-export-renaming.js +60 -14
- package/src/rules/ts-no-function-aliases.js +31 -0
- package/src/rules/vitest-mock-test-file-naming.js +2 -1
package/package.json
CHANGED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
function nonEmptyStrings(value, fallback) {
|
|
4
|
+
if (!Array.isArray(value)) {
|
|
5
|
+
return fallback;
|
|
6
|
+
}
|
|
7
|
+
const strings = value.filter((item) => typeof item === "string" && item.length > 0);
|
|
8
|
+
return strings.length > 0 ? strings : fallback;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function functionFromExpression(node, opts) {
|
|
12
|
+
const unwrapped = unwrapExpression(node, opts);
|
|
13
|
+
return isFunctionNode(unwrapped) ? unwrapped : null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function unwrapExpression(node, opts) {
|
|
17
|
+
if (!node) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
if (isExpressionWrapper(node)) {
|
|
21
|
+
return unwrapExpression(node.expression, opts);
|
|
22
|
+
}
|
|
23
|
+
if (node.type !== "CallExpression" || !isConfiguredWrapper(node.callee, opts)) {
|
|
24
|
+
return node;
|
|
25
|
+
}
|
|
26
|
+
for (const arg of node.arguments) {
|
|
27
|
+
const unwrapped = unwrapExpression(arg, opts);
|
|
28
|
+
if (isFunctionNode(unwrapped)) {
|
|
29
|
+
return unwrapped;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return node;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function isConfiguredWrapper(callee, opts) {
|
|
36
|
+
const name = calleeName(callee);
|
|
37
|
+
return Boolean(name && opts.wrappers.has(name));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
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;
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function isFunctionNode(node) {
|
|
51
|
+
return Boolean(
|
|
52
|
+
node &&
|
|
53
|
+
(node.type === "FunctionDeclaration" ||
|
|
54
|
+
node.type === "FunctionExpression" ||
|
|
55
|
+
node.type === "ArrowFunctionExpression"),
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function isExpressionWrapper(node) {
|
|
60
|
+
return [
|
|
61
|
+
"ChainExpression",
|
|
62
|
+
"ParenthesizedExpression",
|
|
63
|
+
"TSAsExpression",
|
|
64
|
+
"TSSatisfiesExpression",
|
|
65
|
+
"TSNonNullExpression",
|
|
66
|
+
"TSTypeAssertion",
|
|
67
|
+
"TypeCastExpression",
|
|
68
|
+
].includes(node.type);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
module.exports = {
|
|
72
|
+
functionFromExpression,
|
|
73
|
+
isExpressionWrapper,
|
|
74
|
+
isFunctionNode,
|
|
75
|
+
nonEmptyStrings,
|
|
76
|
+
};
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { functionFromExpression, nonEmptyStrings } = require("./component-functions");
|
|
4
|
+
|
|
5
|
+
const DEFAULT_COMPONENT_NAME_PATTERN = "^[A-Z]";
|
|
6
|
+
const DEFAULT_WRAPPERS = ["memo", "forwardRef", "observer"];
|
|
7
|
+
const DEFAULT_EXPORT_TYPES = ["named", "default"];
|
|
8
|
+
|
|
9
|
+
function normalizedComponentOptions(option) {
|
|
10
|
+
return {
|
|
11
|
+
componentNamePattern: compilePattern(
|
|
12
|
+
option.componentNamePattern,
|
|
13
|
+
DEFAULT_COMPONENT_NAME_PATTERN,
|
|
14
|
+
),
|
|
15
|
+
components: compileMatchers(option.components),
|
|
16
|
+
ignoreComponents: compileMatchers(option.ignoreComponents),
|
|
17
|
+
wrappers: new Set(nonEmptyStrings(option.wrappers, DEFAULT_WRAPPERS)),
|
|
18
|
+
exportTypes: new Set(nonEmptyStrings(option.exportTypes, DEFAULT_EXPORT_TYPES)),
|
|
19
|
+
checkAnonymousDefault: option.checkAnonymousDefault === true,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function compilePattern(value, fallback) {
|
|
24
|
+
return new RegExp(typeof value === "string" && value.length > 0 ? value : fallback);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function compileMatchers(values) {
|
|
28
|
+
if (!Array.isArray(values)) {
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
31
|
+
return values
|
|
32
|
+
.filter((value) => typeof value === "string" && value.length > 0)
|
|
33
|
+
.map((value) => {
|
|
34
|
+
const regex = regexLiteral(value);
|
|
35
|
+
return regex ? { regex } : { exact: value };
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function regexLiteral(value) {
|
|
40
|
+
if (!value.startsWith("/") || value.lastIndexOf("/") === 0) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
const lastSlash = value.lastIndexOf("/");
|
|
44
|
+
return new RegExp(value.slice(1, lastSlash), value.slice(lastSlash + 1));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function shouldCheckComponent(component, opts) {
|
|
48
|
+
if (component.anonymousDefault) {
|
|
49
|
+
return opts.checkAnonymousDefault;
|
|
50
|
+
}
|
|
51
|
+
if (opts.ignoreComponents.some((matcher) => matchesName(component.name, matcher))) {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
if (opts.components.length > 0) {
|
|
55
|
+
return opts.components.some((matcher) => matchesName(component.name, matcher));
|
|
56
|
+
}
|
|
57
|
+
return opts.componentNamePattern.test(component.name);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function matchesName(name, matcher) {
|
|
61
|
+
return matcher.regex ? matcher.regex.test(name) : matcher.exact === name;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function collectExportedComponents(program, opts) {
|
|
65
|
+
const definitions = new Map();
|
|
66
|
+
const namedExports = new Set();
|
|
67
|
+
const defaultExports = new Set();
|
|
68
|
+
const components = [];
|
|
69
|
+
|
|
70
|
+
for (const statement of program.body) {
|
|
71
|
+
collectTopLevelDefinition(statement, definitions, opts);
|
|
72
|
+
if (statement.type === "ExportNamedDeclaration") {
|
|
73
|
+
collectNamedExport(statement, definitions, namedExports, defaultExports, components, opts);
|
|
74
|
+
}
|
|
75
|
+
if (statement.type === "ExportDefaultDeclaration") {
|
|
76
|
+
collectDefaultExport(statement, definitions, defaultExports, components, opts);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
pushExportedDefinitions(namedExports, definitions, components, opts.exportTypes.has("named"));
|
|
81
|
+
pushExportedDefinitions(defaultExports, definitions, components, opts.exportTypes.has("default"));
|
|
82
|
+
return uniqueComponents(components);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function pushExportedDefinitions(names, definitions, components, enabled) {
|
|
86
|
+
if (!enabled) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
for (const name of names) {
|
|
90
|
+
const definition = definitions.get(name);
|
|
91
|
+
if (definition) {
|
|
92
|
+
components.push(definition);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
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
|
+
function collectNamedExport(
|
|
117
|
+
statement,
|
|
118
|
+
definitions,
|
|
119
|
+
namedExports,
|
|
120
|
+
defaultExports,
|
|
121
|
+
components,
|
|
122
|
+
opts,
|
|
123
|
+
) {
|
|
124
|
+
if (statement.source) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (statement.declaration?.type === "FunctionDeclaration" && statement.declaration.id) {
|
|
128
|
+
const name = statement.declaration.id.name;
|
|
129
|
+
definitions.set(name, { name, fn: statement.declaration });
|
|
130
|
+
if (opts.exportTypes.has("named")) {
|
|
131
|
+
components.push(definitions.get(name));
|
|
132
|
+
}
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (statement.declaration?.type === "VariableDeclaration") {
|
|
136
|
+
collectTopLevelDefinition(statement.declaration, definitions, opts);
|
|
137
|
+
for (const declaration of statement.declaration.declarations) {
|
|
138
|
+
const definition = definitions.get(declaration.id.name);
|
|
139
|
+
if (declaration.id.type === "Identifier" && definition && opts.exportTypes.has("named")) {
|
|
140
|
+
components.push(definition);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
for (const specifier of statement.specifiers || []) {
|
|
146
|
+
if (specifier.local?.type === "Identifier") {
|
|
147
|
+
if (specifier.exported?.name === "default") {
|
|
148
|
+
defaultExports.add(specifier.local.name);
|
|
149
|
+
} else {
|
|
150
|
+
namedExports.add(specifier.local.name);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function collectDefaultExport(statement, definitions, defaultExports, components, opts) {
|
|
157
|
+
if (!opts.exportTypes.has("default")) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
const declaration = statement.declaration;
|
|
161
|
+
if (declaration.type === "Identifier") {
|
|
162
|
+
defaultExports.add(declaration.name);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
if (declaration.type === "FunctionDeclaration") {
|
|
166
|
+
const name = declaration.id?.name || "default";
|
|
167
|
+
if (declaration.id || opts.checkAnonymousDefault) {
|
|
168
|
+
components.push({ name, fn: declaration, anonymousDefault: !declaration.id });
|
|
169
|
+
}
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
const fn = functionFromExpression(declaration, opts);
|
|
173
|
+
if (fn && opts.checkAnonymousDefault) {
|
|
174
|
+
components.push({ name: fn.id?.name || "default", fn, anonymousDefault: !fn.id });
|
|
175
|
+
}
|
|
176
|
+
if (fn?.id) {
|
|
177
|
+
definitions.set(fn.id.name, { name: fn.id.name, fn });
|
|
178
|
+
defaultExports.add(fn.id.name);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function uniqueComponents(components) {
|
|
183
|
+
const seen = new Set();
|
|
184
|
+
return components.filter((component) => {
|
|
185
|
+
const key = `${component.name}:${component.fn.range?.[0] ?? component.fn.loc?.start?.line ?? ""}`;
|
|
186
|
+
if (seen.has(key)) {
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
seen.add(key);
|
|
190
|
+
return true;
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
module.exports = {
|
|
195
|
+
collectExportedComponents,
|
|
196
|
+
normalizedComponentOptions,
|
|
197
|
+
shouldCheckComponent,
|
|
198
|
+
};
|
package/src/helpers.js
CHANGED
|
@@ -109,7 +109,7 @@ function cssSelectorValues(source, attrs) {
|
|
|
109
109
|
for (const attr of attrs) {
|
|
110
110
|
const escaped = attr.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
111
111
|
const regex = new RegExp(
|
|
112
|
-
`\\[\\s*${escaped}\\s*([*^$]?=)\\s*(?:"([^"]*)"|'([^']*)'|([^\\s\\]]+))
|
|
112
|
+
`\\[\\s*${escaped}\\s*([*^$]?=)\\s*(?:"([^"]*)"|'([^']*)'|([^\\s\\]]+))(?:[is]|\\s+[is])?\\s*\\]`,
|
|
113
113
|
"g",
|
|
114
114
|
);
|
|
115
115
|
let match = regex.exec(source);
|
package/src/index.js
CHANGED
|
@@ -18,6 +18,7 @@ const rules = {
|
|
|
18
18
|
"playwright-no-empty": require("./rules/playwright-no-empty"),
|
|
19
19
|
"playwright-no-set-timeout": require("./rules/playwright-no-set-timeout"),
|
|
20
20
|
"playwright-prefer-get-by-test-id": require("./rules/playwright-prefer-get-by-test-id"),
|
|
21
|
+
"playwright-require-exported-component-attribute": require("./rules/playwright-require-exported-component-attribute"),
|
|
21
22
|
"playwright-require-interactive-test-id": require("./rules/playwright-require-interactive-test-id"),
|
|
22
23
|
"playwright-selector-priority": require("./rules/playwright-selector-priority"),
|
|
23
24
|
"react-no-iife-in-jsx": require("./rules/react-no-iife-in-jsx"),
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { attributeName } = require("./helpers");
|
|
4
|
+
const { isExpressionWrapper, isFunctionNode } = require("./component-functions");
|
|
5
|
+
|
|
6
|
+
function returnedJsxBranches(fn) {
|
|
7
|
+
if (fn.type === "ArrowFunctionExpression" && fn.body.type !== "BlockStatement") {
|
|
8
|
+
return jsxBranches(fn.body);
|
|
9
|
+
}
|
|
10
|
+
const branches = [];
|
|
11
|
+
collectReturnBranches(fn.body, branches);
|
|
12
|
+
return branches;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function collectReturnBranches(node, branches) {
|
|
16
|
+
if (!node || (node.type !== "BlockStatement" && isFunctionNode(node))) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
if (node.type === "ReturnStatement") {
|
|
20
|
+
branches.push(...jsxBranches(node.argument));
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
if (node.type === "ClassDeclaration" || node.type === "ClassExpression") {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
for (const child of childNodes(node)) {
|
|
27
|
+
collectReturnBranches(child, branches);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function jsxBranches(node) {
|
|
32
|
+
const unwrapped = unwrapSyntaxExpression(node);
|
|
33
|
+
if (!unwrapped || isNullLiteral(unwrapped)) {
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
if (isJsxNode(unwrapped)) {
|
|
37
|
+
return [unwrapped];
|
|
38
|
+
}
|
|
39
|
+
if (unwrapped.type === "ConditionalExpression") {
|
|
40
|
+
return [...jsxBranches(unwrapped.consequent), ...jsxBranches(unwrapped.alternate)];
|
|
41
|
+
}
|
|
42
|
+
if (unwrapped.type === "LogicalExpression") {
|
|
43
|
+
return [...jsxBranches(unwrapped.left), ...jsxBranches(unwrapped.right)];
|
|
44
|
+
}
|
|
45
|
+
if (unwrapped.type === "SequenceExpression") {
|
|
46
|
+
return jsxBranches(unwrapped.expressions.at(-1));
|
|
47
|
+
}
|
|
48
|
+
if (unwrapped.type === "ArrayExpression") {
|
|
49
|
+
return unwrapped.elements.flatMap((element) => jsxBranches(element));
|
|
50
|
+
}
|
|
51
|
+
return [];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function unwrapSyntaxExpression(node) {
|
|
55
|
+
let current = node;
|
|
56
|
+
while (current && isExpressionWrapper(current)) {
|
|
57
|
+
current = current.expression;
|
|
58
|
+
}
|
|
59
|
+
return current;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function isNullLiteral(node) {
|
|
63
|
+
return node.type === "Literal" && node.value === null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function isJsxNode(node) {
|
|
67
|
+
return node.type === "JSXElement" || node.type === "JSXFragment";
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function jsxTreeHasAttribute(node, opts) {
|
|
71
|
+
let found = false;
|
|
72
|
+
visitNode(node, (current) => {
|
|
73
|
+
if (found || current.type !== "JSXOpeningElement") {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
found = current.attributes.some((attribute) => {
|
|
77
|
+
if (attribute.type === "JSXSpreadAttribute") {
|
|
78
|
+
return opts.allowSpreadAttributes;
|
|
79
|
+
}
|
|
80
|
+
const name = attributeName(attribute);
|
|
81
|
+
return Boolean(name && opts.attributes.includes(name));
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
return found;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function visitNode(node, callback) {
|
|
88
|
+
if (!node || typeof node.type !== "string") {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
callback(node);
|
|
92
|
+
for (const child of childNodes(node)) {
|
|
93
|
+
visitNode(child, callback);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function childNodes(node) {
|
|
98
|
+
const children = [];
|
|
99
|
+
for (const [key, value] of Object.entries(node)) {
|
|
100
|
+
if (key === "parent" || key === "tokens" || key === "comments") {
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (Array.isArray(value)) {
|
|
104
|
+
children.push(...value.filter(isAstNode));
|
|
105
|
+
} else if (isAstNode(value)) {
|
|
106
|
+
children.push(value);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return children;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function isAstNode(value) {
|
|
113
|
+
return Boolean(value && typeof value.type === "string");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
module.exports = {
|
|
117
|
+
jsxTreeHasAttribute,
|
|
118
|
+
returnedJsxBranches,
|
|
119
|
+
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
const { rule } = require("../helpers");
|
|
3
|
+
const { literalString, rule } = require("../helpers");
|
|
4
4
|
|
|
5
5
|
const NEXT_FILE_PATTERN = /(?:^|[/\\])(?:app|pages)(?:[/\\]|$)/;
|
|
6
6
|
|
|
@@ -25,14 +25,65 @@ function isJsonLdScript(node) {
|
|
|
25
25
|
});
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
function jsxAttribute(node, name) {
|
|
29
|
+
return node.attributes.find(
|
|
30
|
+
(attribute) =>
|
|
31
|
+
attribute.type === "JSXAttribute" &&
|
|
32
|
+
attribute.name.type === "JSXIdentifier" &&
|
|
33
|
+
attribute.name.name === name,
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function attributeValue(attribute) {
|
|
38
|
+
const value =
|
|
39
|
+
attribute?.value?.type === "JSXExpressionContainer"
|
|
40
|
+
? attribute.value.expression
|
|
41
|
+
: attribute?.value;
|
|
42
|
+
return value ? literalString(value) : null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function hasAttribute(node, name) {
|
|
46
|
+
return Boolean(jsxAttribute(node, name));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function allowedIdPatterns(options) {
|
|
50
|
+
return (options.allowInlineScriptIdPatterns || []).flatMap((pattern) => {
|
|
51
|
+
try {
|
|
52
|
+
return [new RegExp(pattern)];
|
|
53
|
+
} catch {
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function isAllowedInlineScript(node, options, patterns) {
|
|
60
|
+
if (!hasAttribute(node, "dangerouslySetInnerHTML")) return false;
|
|
61
|
+
const id = attributeValue(jsxAttribute(node, "id"));
|
|
62
|
+
if (!id) return false;
|
|
63
|
+
return (
|
|
64
|
+
(options.allowInlineScriptIds || []).includes(id) || patterns.some((regex) => regex.test(id))
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
28
68
|
module.exports = rule(
|
|
29
69
|
{
|
|
30
70
|
type: "problem",
|
|
31
71
|
docs: { description: "prefer next/script over raw script JSX tags", recommended: false },
|
|
32
|
-
schema: [
|
|
72
|
+
schema: [
|
|
73
|
+
{
|
|
74
|
+
type: "object",
|
|
75
|
+
properties: {
|
|
76
|
+
allowInlineScriptIds: { type: "array", items: { type: "string" } },
|
|
77
|
+
allowInlineScriptIdPatterns: { type: "array", items: { type: "string" } },
|
|
78
|
+
},
|
|
79
|
+
additionalProperties: false,
|
|
80
|
+
},
|
|
81
|
+
],
|
|
33
82
|
messages: { script: "Use next/script instead of a raw <script> tag." },
|
|
34
83
|
},
|
|
35
84
|
(context) => {
|
|
85
|
+
const options = context.options[0] || {};
|
|
86
|
+
const patterns = allowedIdPatterns(options);
|
|
36
87
|
let isNextFile = isNextPath(context.filename);
|
|
37
88
|
return {
|
|
38
89
|
ImportDeclaration(node) {
|
|
@@ -44,6 +95,7 @@ module.exports = rule(
|
|
|
44
95
|
if (!isNextFile) return;
|
|
45
96
|
if (node.name.type !== "JSXIdentifier" || node.name.name !== "script") return;
|
|
46
97
|
if (isJsonLdScript(node)) return;
|
|
98
|
+
if (isAllowedInlineScript(node, options, patterns)) return;
|
|
47
99
|
context.report({ node, messageId: "script" });
|
|
48
100
|
},
|
|
49
101
|
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
collectExportedComponents,
|
|
5
|
+
normalizedComponentOptions,
|
|
6
|
+
shouldCheckComponent,
|
|
7
|
+
} = require("../exported-components");
|
|
8
|
+
const { nonEmptyStrings } = require("../component-functions");
|
|
9
|
+
const { options, rule } = require("../helpers");
|
|
10
|
+
const { jsxTreeHasAttribute, returnedJsxBranches } = require("../returned-jsx");
|
|
11
|
+
|
|
12
|
+
const DEFAULT_ATTRIBUTES = ["data-pw"];
|
|
13
|
+
|
|
14
|
+
module.exports = rule(
|
|
15
|
+
{
|
|
16
|
+
type: "suggestion",
|
|
17
|
+
docs: {
|
|
18
|
+
description: "require configured attributes in exported component JSX trees",
|
|
19
|
+
recommended: false,
|
|
20
|
+
},
|
|
21
|
+
schema: [
|
|
22
|
+
{
|
|
23
|
+
type: "object",
|
|
24
|
+
properties: {
|
|
25
|
+
attributes: { type: "array", items: { type: "string" } },
|
|
26
|
+
componentNamePattern: { type: "string" },
|
|
27
|
+
components: { type: "array", items: { type: "string" } },
|
|
28
|
+
ignoreComponents: { type: "array", items: { type: "string" } },
|
|
29
|
+
wrappers: { type: "array", items: { type: "string" } },
|
|
30
|
+
allowSpreadAttributes: { type: "boolean" },
|
|
31
|
+
exportTypes: {
|
|
32
|
+
type: "array",
|
|
33
|
+
items: { enum: ["named", "default"] },
|
|
34
|
+
},
|
|
35
|
+
checkAnonymousDefault: { type: "boolean" },
|
|
36
|
+
},
|
|
37
|
+
additionalProperties: false,
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
messages: {
|
|
41
|
+
missing: "Exported component '{{name}}' must return JSX containing one of: {{attributes}}.",
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
(context) => ({
|
|
45
|
+
Program(node) {
|
|
46
|
+
const opts = normalizedOptions(options(context));
|
|
47
|
+
for (const component of collectExportedComponents(node, opts)) {
|
|
48
|
+
reportMissingBranches(context, component, opts);
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
}),
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
function normalizedOptions(option) {
|
|
55
|
+
return {
|
|
56
|
+
...normalizedComponentOptions(option),
|
|
57
|
+
attributes: nonEmptyStrings(option.attributes, DEFAULT_ATTRIBUTES),
|
|
58
|
+
allowSpreadAttributes: option.allowSpreadAttributes === true,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function reportMissingBranches(context, component, opts) {
|
|
63
|
+
if (shouldCheckComponent(component, opts)) {
|
|
64
|
+
for (const jsx of returnedJsxBranches(component.fn)) {
|
|
65
|
+
if (!jsxTreeHasAttribute(jsx, opts)) {
|
|
66
|
+
context.report({
|
|
67
|
+
node: jsx,
|
|
68
|
+
messageId: "missing",
|
|
69
|
+
data: {
|
|
70
|
+
name: component.name,
|
|
71
|
+
attributes: opts.attributes.join(", "),
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -24,8 +24,8 @@ const MODIFIERS = new Set(
|
|
|
24
24
|
const TEST_CALLEES = new Set(["it", "test"]);
|
|
25
25
|
|
|
26
26
|
function propertyName(node) {
|
|
27
|
-
if (node
|
|
28
|
-
return node
|
|
27
|
+
if (node?.type === "Identifier") return node.name;
|
|
28
|
+
return node?.type === "Literal" ? String(node.value) : null;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
function unwrap(node) {
|
|
@@ -43,7 +43,27 @@ function unwrap(node) {
|
|
|
43
43
|
|
|
44
44
|
function isMessageMember(node) {
|
|
45
45
|
const current = unwrap(node);
|
|
46
|
-
return
|
|
46
|
+
return (
|
|
47
|
+
current?.type === "MemberExpression" &&
|
|
48
|
+
propertyName(current.property) === "message" &&
|
|
49
|
+
!isBodyMember(current.object)
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function isBodyMember(node) {
|
|
54
|
+
const current = unwrap(node);
|
|
55
|
+
return current?.type === "MemberExpression" && propertyName(current.property) === "body";
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function isStaticAnalysisPath(filename) {
|
|
59
|
+
const normalized = filename.replace(/\\/g, "/");
|
|
60
|
+
return (
|
|
61
|
+
normalized.startsWith("static-code-analysis/") || normalized.includes("/static-code-analysis/")
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function isGeneratedTestPath(filename) {
|
|
66
|
+
return filename.replace(/\\/g, "/").includes(".generated.");
|
|
47
67
|
}
|
|
48
68
|
|
|
49
69
|
function isExpectCall(node) {
|
|
@@ -83,6 +103,7 @@ module.exports = rule(
|
|
|
83
103
|
messages: { message: "Do not assert on err.message; check the error type or code instead." },
|
|
84
104
|
},
|
|
85
105
|
(context) => {
|
|
106
|
+
if (isStaticAnalysisPath(context.filename) || isGeneratedTestPath(context.filename)) return {};
|
|
86
107
|
let testDepth = 0;
|
|
87
108
|
return {
|
|
88
109
|
CallExpression(node) {
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
childNodes,
|
|
5
|
+
isCalledFunction,
|
|
6
|
+
isFunctionNode,
|
|
7
|
+
isInlineSetupCallback,
|
|
8
|
+
isInlineTestCallback,
|
|
9
|
+
} = require("./test-no-shared-state-helpers");
|
|
10
|
+
|
|
11
|
+
function isInsideUncalledNestedFunction(node, testDepth, setupDepth) {
|
|
12
|
+
if (testDepth === 0 && setupDepth === 0) return false;
|
|
13
|
+
let current = node.parent;
|
|
14
|
+
while (current) {
|
|
15
|
+
const isUncalledFunction =
|
|
16
|
+
isFunctionNode(current) &&
|
|
17
|
+
!isInlineSetupCallback(current) &&
|
|
18
|
+
!isInlineTestCallback(current) &&
|
|
19
|
+
!isCalledFunction(current);
|
|
20
|
+
if (isUncalledFunction) return true;
|
|
21
|
+
current = current.parent;
|
|
22
|
+
}
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function isModuleMutable(context, mutableTopLevel, node, name) {
|
|
27
|
+
let scope = context.sourceCode.getScope(node);
|
|
28
|
+
while (scope) {
|
|
29
|
+
const variable = scope.variables.find((candidate) => candidate.name === name);
|
|
30
|
+
if (variable) {
|
|
31
|
+
return (
|
|
32
|
+
mutableTopLevel.has(variable.name) &&
|
|
33
|
+
(variable.scope.type === "module" || variable.scope.block.type === "Program")
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
scope = scope.upper;
|
|
37
|
+
}
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function isResetAssignment(node, isMutableInitializer) {
|
|
42
|
+
if (node.left.type === "Identifier") return isMutableInitializer(node.right);
|
|
43
|
+
return (
|
|
44
|
+
node.left.type === "MemberExpression" &&
|
|
45
|
+
!node.left.computed &&
|
|
46
|
+
node.left.property.type === "Identifier" &&
|
|
47
|
+
node.left.property.name === "length" &&
|
|
48
|
+
node.right.type === "Literal" &&
|
|
49
|
+
node.right.value === 0
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function walkSharedMutations(node, handlers) {
|
|
54
|
+
if (isFunctionNode(node) && !isCalledFunction(node)) return;
|
|
55
|
+
if (node.type === "AssignmentExpression") {
|
|
56
|
+
handlers.onAssignment(node);
|
|
57
|
+
} else if (node.type === "UpdateExpression") {
|
|
58
|
+
handlers.onUpdate(node);
|
|
59
|
+
} else if (node.type === "CallExpression") {
|
|
60
|
+
handlers.onCall(node);
|
|
61
|
+
}
|
|
62
|
+
for (const child of childNodes(node)) walkSharedMutations(child, handlers);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function createViMockTracker(context, mutableTopLevel) {
|
|
66
|
+
const factoryReferences = new Set();
|
|
67
|
+
const capturedMutables = new Set();
|
|
68
|
+
|
|
69
|
+
function markIfCaptured(name) {
|
|
70
|
+
if (name.startsWith("mock") && mutableTopLevel.has(name) && factoryReferences.has(name)) {
|
|
71
|
+
capturedMutables.add(name);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
collectFactoryReferences(node) {
|
|
77
|
+
if (
|
|
78
|
+
node.callee.type !== "MemberExpression" ||
|
|
79
|
+
node.callee.object.type !== "Identifier" ||
|
|
80
|
+
node.callee.object.name !== "vi" ||
|
|
81
|
+
node.callee.property.type !== "Identifier" ||
|
|
82
|
+
node.callee.property.name !== "mock"
|
|
83
|
+
) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const factory = node.arguments[1];
|
|
87
|
+
if (!isFunctionNode(factory)) return;
|
|
88
|
+
for (const { identifier } of context.sourceCode.getScope(factory).through) {
|
|
89
|
+
factoryReferences.add(identifier.name);
|
|
90
|
+
markIfCaptured(identifier.name);
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
isCaptured(name) {
|
|
94
|
+
return capturedMutables.has(name);
|
|
95
|
+
},
|
|
96
|
+
markIfCaptured,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function createRegistryReports(context, mutableTopLevel, cleanupTracker, isCaptured) {
|
|
101
|
+
const pending = [];
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
flush() {
|
|
105
|
+
for (const { node, path, suiteKey } of pending) {
|
|
106
|
+
if (!cleanupTracker.has(path, suiteKey)) context.report({ node, messageId: "shared" });
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
remember(node, name, path, testDepth, setupDepth) {
|
|
110
|
+
if (
|
|
111
|
+
name &&
|
|
112
|
+
testDepth > 0 &&
|
|
113
|
+
setupDepth === 0 &&
|
|
114
|
+
!isCaptured(name) &&
|
|
115
|
+
isModuleMutable(context, mutableTopLevel, node, name)
|
|
116
|
+
) {
|
|
117
|
+
pending.push({ node, path, suiteKey: cleanupTracker.currentSuiteKey() });
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
module.exports = {
|
|
124
|
+
createRegistryReports,
|
|
125
|
+
createViMockTracker,
|
|
126
|
+
isInsideUncalledNestedFunction,
|
|
127
|
+
isModuleMutable,
|
|
128
|
+
isResetAssignment,
|
|
129
|
+
walkSharedMutations,
|
|
130
|
+
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
const TEST_CALLEES = new Set(["it", "test", "describe"]);
|
|
4
|
+
const SETUP_CALLEES = new Set(["beforeEach", "afterEach", "beforeAll", "afterAll"]);
|
|
4
5
|
const MUTATING_METHODS = new Set(
|
|
5
6
|
"add clear copyWithin delete fill pop push reverse set shift sort splice unshift".split(" "),
|
|
6
7
|
);
|
|
@@ -22,6 +23,19 @@ function isTestCall(node) {
|
|
|
22
23
|
return TEST_CALLEES.has(calleeName(node.callee));
|
|
23
24
|
}
|
|
24
25
|
|
|
26
|
+
function setupCallbackKind(node) {
|
|
27
|
+
const name = calleeName(node.callee);
|
|
28
|
+
return name === "beforeEach" || name === "afterEach"
|
|
29
|
+
? "per-test"
|
|
30
|
+
: name === "beforeAll" || name === "afterAll"
|
|
31
|
+
? "once"
|
|
32
|
+
: null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function isSetupCall(node) {
|
|
36
|
+
return SETUP_CALLEES.has(calleeName(node.callee));
|
|
37
|
+
}
|
|
38
|
+
|
|
25
39
|
function collectPatternNames(node, names = new Set()) {
|
|
26
40
|
if (!node) return names;
|
|
27
41
|
if (node.type === "Identifier") {
|
|
@@ -52,11 +66,30 @@ function propertyName(node) {
|
|
|
52
66
|
return node.type === "Literal" ? String(node.value) : node.name;
|
|
53
67
|
}
|
|
54
68
|
|
|
55
|
-
function
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
69
|
+
function mutationPath(node) {
|
|
70
|
+
if (node.type === "Identifier") return node.name;
|
|
71
|
+
if (node.type !== "MemberExpression") return null;
|
|
72
|
+
const objectPath = mutationPath(node.object);
|
|
73
|
+
const property =
|
|
74
|
+
node.computed && node.property.type !== "Literal" ? null : propertyName(node.property);
|
|
75
|
+
return objectPath && property ? `${objectPath}.${property}` : null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function mutatingCallTarget(node) {
|
|
79
|
+
if (
|
|
80
|
+
node.callee.type !== "MemberExpression" ||
|
|
81
|
+
!MUTATING_METHODS.has(propertyName(node.callee.property))
|
|
82
|
+
) {
|
|
83
|
+
return { name: null, path: null };
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
name: mutationRootName(node.callee.object),
|
|
87
|
+
path: mutationPath(node.callee.object),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function mutatingCallPropertyName(node) {
|
|
92
|
+
return node.callee.type === "MemberExpression" ? propertyName(node.callee.property) : null;
|
|
60
93
|
}
|
|
61
94
|
|
|
62
95
|
function isFunctionNode(node) {
|
|
@@ -67,6 +100,10 @@ function isInlineTestCallback(node) {
|
|
|
67
100
|
return node.parent?.type === "CallExpression" && isTestCall(node.parent);
|
|
68
101
|
}
|
|
69
102
|
|
|
103
|
+
function isInlineSetupCallback(node) {
|
|
104
|
+
return node.parent?.type === "CallExpression" && isSetupCall(node.parent);
|
|
105
|
+
}
|
|
106
|
+
|
|
70
107
|
function isCalledFunction(node) {
|
|
71
108
|
if (node.parent?.type === "CallExpression" && node.parent.callee === node) return true;
|
|
72
109
|
const declarator = node.parent?.type === "VariableDeclarator" ? node.parent : null;
|
|
@@ -111,15 +148,78 @@ function namedCallbackArgument(args) {
|
|
|
111
148
|
}
|
|
112
149
|
}
|
|
113
150
|
|
|
151
|
+
function firstNamedCallbackArgument(args) {
|
|
152
|
+
return args[0]?.type === "Identifier" ? args[0] : undefined;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function createCleanupTracker() {
|
|
156
|
+
const pathsBySuite = new Map();
|
|
157
|
+
const suiteStack = [];
|
|
158
|
+
let activeSuiteKey;
|
|
159
|
+
let replaySuiteKey;
|
|
160
|
+
let nextSuiteId = 0;
|
|
161
|
+
|
|
162
|
+
function currentSuiteKey() {
|
|
163
|
+
return replaySuiteKey ?? suiteStack.join("/");
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function has(path, suiteKey) {
|
|
167
|
+
if (!path) return false;
|
|
168
|
+
for (const [cleanupSuiteKey, paths] of pathsBySuite) {
|
|
169
|
+
if (!paths.has(path)) continue;
|
|
170
|
+
if (!cleanupSuiteKey || suiteKey === cleanupSuiteKey) return true;
|
|
171
|
+
if (suiteKey.startsWith(`${cleanupSuiteKey}/`)) return true;
|
|
172
|
+
}
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
beginSetup(kind, suiteKey = currentSuiteKey()) {
|
|
178
|
+
activeSuiteKey = kind === "per-test" ? suiteKey : undefined;
|
|
179
|
+
},
|
|
180
|
+
clearReplaySuite() {
|
|
181
|
+
replaySuiteKey = undefined;
|
|
182
|
+
},
|
|
183
|
+
currentSuiteKey,
|
|
184
|
+
endSetup() {
|
|
185
|
+
activeSuiteKey = undefined;
|
|
186
|
+
},
|
|
187
|
+
enterSuite() {
|
|
188
|
+
suiteStack.push(String(nextSuiteId++));
|
|
189
|
+
},
|
|
190
|
+
exitSuite() {
|
|
191
|
+
suiteStack.pop();
|
|
192
|
+
},
|
|
193
|
+
has,
|
|
194
|
+
remember(path) {
|
|
195
|
+
if (!path || activeSuiteKey === undefined) return;
|
|
196
|
+
const paths = pathsBySuite.get(activeSuiteKey) ?? new Set();
|
|
197
|
+
paths.add(path);
|
|
198
|
+
pathsBySuite.set(activeSuiteKey, paths);
|
|
199
|
+
},
|
|
200
|
+
setReplaySuite(suiteKey) {
|
|
201
|
+
replaySuiteKey = suiteKey;
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
114
206
|
module.exports = {
|
|
115
207
|
childNodes,
|
|
116
208
|
collectPatternNames,
|
|
209
|
+
createCleanupTracker,
|
|
210
|
+
firstNamedCallbackArgument,
|
|
117
211
|
isCalledFunction,
|
|
118
212
|
isFunctionNode,
|
|
213
|
+
isInlineSetupCallback,
|
|
119
214
|
isInlineTestCallback,
|
|
120
215
|
isMutableInitializer,
|
|
216
|
+
calleeName,
|
|
217
|
+
isSetupCall,
|
|
121
218
|
isTestCall,
|
|
122
|
-
|
|
219
|
+
mutatingCallPropertyName,
|
|
220
|
+
mutatingCallTarget,
|
|
221
|
+
mutationPath,
|
|
123
222
|
mutationRootName,
|
|
124
223
|
namedCallbackArgument,
|
|
224
|
+
setupCallbackKind,
|
|
125
225
|
};
|
|
@@ -2,16 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
const { rule } = require("../helpers");
|
|
4
4
|
const {
|
|
5
|
-
|
|
5
|
+
createRegistryReports,
|
|
6
|
+
createViMockTracker,
|
|
7
|
+
isInsideUncalledNestedFunction,
|
|
8
|
+
isModuleMutable,
|
|
9
|
+
isResetAssignment,
|
|
10
|
+
walkSharedMutations,
|
|
11
|
+
} = require("./test-no-shared-state-analysis");
|
|
12
|
+
const {
|
|
13
|
+
calleeName,
|
|
6
14
|
collectPatternNames,
|
|
7
|
-
|
|
15
|
+
createCleanupTracker,
|
|
16
|
+
firstNamedCallbackArgument,
|
|
8
17
|
isFunctionNode,
|
|
9
|
-
isInlineTestCallback,
|
|
10
18
|
isMutableInitializer,
|
|
11
19
|
isTestCall,
|
|
12
|
-
|
|
20
|
+
mutatingCallPropertyName,
|
|
21
|
+
mutatingCallTarget,
|
|
22
|
+
mutationPath,
|
|
13
23
|
mutationRootName,
|
|
14
24
|
namedCallbackArgument,
|
|
25
|
+
setupCallbackKind,
|
|
15
26
|
} = require("./test-no-shared-state-helpers");
|
|
16
27
|
|
|
17
28
|
module.exports = rule(
|
|
@@ -26,28 +37,29 @@ module.exports = rule(
|
|
|
26
37
|
},
|
|
27
38
|
(context) => {
|
|
28
39
|
const mutableTopLevel = new Set();
|
|
29
|
-
const functionDeclarations = new Map();
|
|
30
40
|
const pendingNamedCallbacks = [];
|
|
41
|
+
const pendingNamedSetupCallbacks = [];
|
|
42
|
+
const cleanupTracker = createCleanupTracker();
|
|
43
|
+
const viMockTracker = createViMockTracker(context, mutableTopLevel);
|
|
44
|
+
const registryReports = createRegistryReports(
|
|
45
|
+
context,
|
|
46
|
+
mutableTopLevel,
|
|
47
|
+
cleanupTracker,
|
|
48
|
+
(name) => viMockTracker.isCaptured(name),
|
|
49
|
+
);
|
|
31
50
|
let testDepth = 0;
|
|
32
|
-
|
|
33
|
-
function isModuleMutable(node, name) {
|
|
34
|
-
let scope = context.sourceCode.getScope(node);
|
|
35
|
-
while (scope) {
|
|
36
|
-
const variable = scope.variables.find((candidate) => candidate.name === name);
|
|
37
|
-
if (variable) {
|
|
38
|
-
return (
|
|
39
|
-
mutableTopLevel.has(variable.name) &&
|
|
40
|
-
(variable.scope.type === "module" || variable.scope.block.type === "Program")
|
|
41
|
-
);
|
|
42
|
-
}
|
|
43
|
-
scope = scope.upper;
|
|
44
|
-
}
|
|
45
|
-
return false;
|
|
46
|
-
}
|
|
51
|
+
let setupDepth = 0;
|
|
47
52
|
|
|
48
53
|
function reportIfShared(node, name) {
|
|
49
|
-
if (
|
|
54
|
+
if (
|
|
55
|
+
name &&
|
|
56
|
+
testDepth > 0 &&
|
|
57
|
+
setupDepth === 0 &&
|
|
58
|
+
!viMockTracker.isCaptured(name) &&
|
|
59
|
+
isModuleMutable(context, mutableTopLevel, node, name)
|
|
60
|
+
) {
|
|
50
61
|
context.report({ node, messageId: "shared" });
|
|
62
|
+
}
|
|
51
63
|
}
|
|
52
64
|
|
|
53
65
|
function reportAssignment(node) {
|
|
@@ -56,70 +68,141 @@ module.exports = rule(
|
|
|
56
68
|
reportIfShared(node, mutationRootName(node.left));
|
|
57
69
|
}
|
|
58
70
|
|
|
71
|
+
function rememberSetupCleanup(node, name, path) {
|
|
72
|
+
if (
|
|
73
|
+
name &&
|
|
74
|
+
setupDepth > 0 &&
|
|
75
|
+
mutableTopLevel.has(name) &&
|
|
76
|
+
isModuleMutable(context, mutableTopLevel, node, name)
|
|
77
|
+
) {
|
|
78
|
+
cleanupTracker.remember(path);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function rememberCall(node) {
|
|
83
|
+
const { name, path } = mutatingCallTarget(node);
|
|
84
|
+
if (mutatingCallPropertyName(node) === "clear") {
|
|
85
|
+
rememberSetupCleanup(node, name, path);
|
|
86
|
+
}
|
|
87
|
+
registryReports.remember(node, name, path, testDepth, setupDepth);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function rememberAssignmentCleanup(node) {
|
|
91
|
+
if (setupDepth === 0) return;
|
|
92
|
+
if (!isResetAssignment(node, isMutableInitializer)) return;
|
|
93
|
+
if (node.left.type === "Identifier") {
|
|
94
|
+
rememberSetupCleanup(node, node.left.name, node.left.name);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
rememberSetupCleanup(node, mutationRootName(node.left), mutationPath(node.left.object));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function resolveFunctionCallback(node, callback) {
|
|
101
|
+
let scope = context.sourceCode.getScope(node);
|
|
102
|
+
while (scope) {
|
|
103
|
+
const variable = scope.variables.find((candidate) => candidate.name === callback.name);
|
|
104
|
+
const declaration = variable?.defs[0]?.node;
|
|
105
|
+
if (declaration?.type === "FunctionDeclaration") return declaration;
|
|
106
|
+
if (declaration?.type === "VariableDeclarator" && isFunctionNode(declaration.init)) {
|
|
107
|
+
return declaration.init;
|
|
108
|
+
}
|
|
109
|
+
scope = scope.upper;
|
|
110
|
+
}
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
|
|
59
114
|
return {
|
|
60
115
|
"Program > VariableDeclaration"(node) {
|
|
61
116
|
for (const declaration of node.declarations) {
|
|
62
|
-
if (declaration.id.type === "Identifier" && isFunctionNode(declaration.init)) {
|
|
63
|
-
functionDeclarations.set(declaration.id.name, declaration.init);
|
|
64
|
-
}
|
|
65
117
|
if (node.kind === "const" && !isMutableInitializer(declaration.init)) continue;
|
|
66
|
-
for (const name of collectPatternNames(declaration.id))
|
|
118
|
+
for (const name of collectPatternNames(declaration.id)) {
|
|
119
|
+
mutableTopLevel.add(name);
|
|
120
|
+
viMockTracker.markIfCaptured(name);
|
|
121
|
+
}
|
|
67
122
|
}
|
|
68
123
|
},
|
|
69
|
-
"Program > FunctionDeclaration"(node) {
|
|
70
|
-
if (node.id?.name) functionDeclarations.set(node.id.name, node);
|
|
71
|
-
},
|
|
72
124
|
"Program:exit"() {
|
|
125
|
+
for (const { declaration, suiteKey, kind } of pendingNamedSetupCallbacks) {
|
|
126
|
+
if (!declaration) continue;
|
|
127
|
+
const previousSetupDepth = setupDepth;
|
|
128
|
+
setupDepth = 1;
|
|
129
|
+
cleanupTracker.beginSetup(kind, suiteKey);
|
|
130
|
+
checkSharedMutations(declaration.body);
|
|
131
|
+
setupDepth = previousSetupDepth;
|
|
132
|
+
cleanupTracker.endSetup();
|
|
133
|
+
}
|
|
73
134
|
testDepth = 1;
|
|
74
|
-
for (const
|
|
75
|
-
|
|
76
|
-
|
|
135
|
+
for (const { declaration, suiteKey } of pendingNamedCallbacks) {
|
|
136
|
+
if (!declaration) continue;
|
|
137
|
+
cleanupTracker.setReplaySuite(suiteKey);
|
|
138
|
+
checkSharedMutations(declaration.body);
|
|
139
|
+
cleanupTracker.clearReplaySuite();
|
|
77
140
|
}
|
|
141
|
+
registryReports.flush();
|
|
78
142
|
},
|
|
79
143
|
CallExpression(node) {
|
|
144
|
+
viMockTracker.collectFactoryReferences(node);
|
|
145
|
+
if (calleeName(node.callee) === "describe") cleanupTracker.enterSuite();
|
|
80
146
|
if (isTestCall(node)) {
|
|
81
147
|
testDepth += 1;
|
|
82
148
|
const callback = namedCallbackArgument(node.arguments);
|
|
83
|
-
if (callback)
|
|
149
|
+
if (callback) {
|
|
150
|
+
pendingNamedCallbacks.push({
|
|
151
|
+
declaration: resolveFunctionCallback(node, callback),
|
|
152
|
+
suiteKey: cleanupTracker.currentSuiteKey(),
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
const setupKind = setupCallbackKind(node);
|
|
157
|
+
if (setupKind) {
|
|
158
|
+
const callback = firstNamedCallbackArgument(node.arguments);
|
|
159
|
+
if (callback) {
|
|
160
|
+
pendingNamedSetupCallbacks.push({
|
|
161
|
+
declaration: resolveFunctionCallback(node, callback),
|
|
162
|
+
suiteKey: cleanupTracker.currentSuiteKey(),
|
|
163
|
+
kind: setupKind,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (setupKind) {
|
|
168
|
+
setupDepth += 1;
|
|
169
|
+
cleanupTracker.beginSetup(setupKind);
|
|
84
170
|
}
|
|
85
171
|
},
|
|
86
172
|
AssignmentExpression(node) {
|
|
87
|
-
if (isInsideUncalledNestedFunction(node)) return;
|
|
173
|
+
if (isInsideUncalledNestedFunction(node, testDepth, setupDepth)) return;
|
|
174
|
+
rememberAssignmentCleanup(node);
|
|
88
175
|
reportAssignment(node);
|
|
89
176
|
},
|
|
90
177
|
UpdateExpression(node) {
|
|
91
|
-
if (isInsideUncalledNestedFunction(node)) return;
|
|
178
|
+
if (isInsideUncalledNestedFunction(node, testDepth, setupDepth)) return;
|
|
92
179
|
reportIfShared(node, mutationRootName(node.argument));
|
|
93
180
|
},
|
|
94
181
|
"CallExpression:exit"(node) {
|
|
95
182
|
if (isTestCall(node)) testDepth -= 1;
|
|
96
|
-
if (
|
|
97
|
-
|
|
183
|
+
if (setupCallbackKind(node)) {
|
|
184
|
+
setupDepth -= 1;
|
|
185
|
+
cleanupTracker.endSetup();
|
|
186
|
+
}
|
|
187
|
+
const isInsideNested = isInsideUncalledNestedFunction(node, testDepth, setupDepth);
|
|
188
|
+
if (!isInsideNested) {
|
|
189
|
+
rememberCall(node);
|
|
190
|
+
}
|
|
191
|
+
if (calleeName(node.callee) === "describe") cleanupTracker.exitSuite();
|
|
98
192
|
},
|
|
99
193
|
};
|
|
100
194
|
|
|
101
|
-
function isInsideUncalledNestedFunction(node) {
|
|
102
|
-
if (testDepth === 0) return false;
|
|
103
|
-
let current = node.parent;
|
|
104
|
-
while (current) {
|
|
105
|
-
const isUncalledFunction =
|
|
106
|
-
isFunctionNode(current) && !isInlineTestCallback(current) && !isCalledFunction(current);
|
|
107
|
-
if (isUncalledFunction) return true;
|
|
108
|
-
current = current.parent;
|
|
109
|
-
}
|
|
110
|
-
return false;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
195
|
function checkSharedMutations(node) {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
196
|
+
walkSharedMutations(node, {
|
|
197
|
+
onAssignment: (assignment) => {
|
|
198
|
+
rememberAssignmentCleanup(assignment);
|
|
199
|
+
reportAssignment(assignment);
|
|
200
|
+
},
|
|
201
|
+
onCall: (call) => {
|
|
202
|
+
rememberCall(call);
|
|
203
|
+
},
|
|
204
|
+
onUpdate: (update) => reportIfShared(update, mutationRootName(update.argument)),
|
|
205
|
+
});
|
|
123
206
|
}
|
|
124
207
|
},
|
|
125
208
|
);
|
|
@@ -12,6 +12,35 @@ function isTypeExport(node, specifier) {
|
|
|
12
12
|
return node.exportKind === "type" || specifier.exportKind === "type";
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
function pathPatterns(options) {
|
|
16
|
+
return (options.includePathPatterns || []).flatMap((pattern) => {
|
|
17
|
+
try {
|
|
18
|
+
return [new RegExp(pattern)];
|
|
19
|
+
} catch {
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function normalizedPaths(context) {
|
|
26
|
+
const normalized = context.filename.replace(/\\/g, "/");
|
|
27
|
+
const cwd = context.cwd?.replace(/\\/g, "/");
|
|
28
|
+
if (cwd && normalized.startsWith(`${cwd}/`)) {
|
|
29
|
+
return [normalized.slice(cwd.length + 1), normalized];
|
|
30
|
+
}
|
|
31
|
+
return [normalized];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function shouldCheckFile(context, options, patterns) {
|
|
35
|
+
if ((options.includePathPatterns || []).length > 0 && patterns.length === 0) return false;
|
|
36
|
+
if (patterns.length === 0) return true;
|
|
37
|
+
return normalizedPaths(context).some((path) => patterns.some((pattern) => pattern.test(path)));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function isDefaultReExportAlias(node, specifier) {
|
|
41
|
+
return exportName(specifier.local) === "default" && Boolean(node.source);
|
|
42
|
+
}
|
|
43
|
+
|
|
15
44
|
module.exports = rule(
|
|
16
45
|
{
|
|
17
46
|
type: "problem",
|
|
@@ -19,24 +48,41 @@ module.exports = rule(
|
|
|
19
48
|
description: "disallow value export renaming",
|
|
20
49
|
recommended: true,
|
|
21
50
|
},
|
|
22
|
-
schema: [],
|
|
23
51
|
messages: {
|
|
24
52
|
renamed:
|
|
25
53
|
"Do not rename value exports. Export the original name or rename the declaration itself so agents can trace symbols directly.",
|
|
26
54
|
},
|
|
55
|
+
schema: [
|
|
56
|
+
{
|
|
57
|
+
type: "object",
|
|
58
|
+
properties: {
|
|
59
|
+
allowDefaultReExports: { type: "boolean" },
|
|
60
|
+
includePathPatterns: { type: "array", items: { type: "string" } },
|
|
61
|
+
},
|
|
62
|
+
additionalProperties: false,
|
|
63
|
+
},
|
|
64
|
+
],
|
|
27
65
|
},
|
|
28
|
-
(context) =>
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
66
|
+
(context) => {
|
|
67
|
+
const options = context.options[0] || {};
|
|
68
|
+
const patterns = pathPatterns(options);
|
|
69
|
+
if (!shouldCheckFile(context, options, patterns)) return {};
|
|
70
|
+
return {
|
|
71
|
+
ExportNamedDeclaration(node) {
|
|
72
|
+
for (const specifier of node.specifiers || []) {
|
|
73
|
+
if (specifier.type !== "ExportSpecifier" || isTypeExport(node, specifier)) {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (options.allowDefaultReExports && isDefaultReExportAlias(node, specifier)) {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
const local = exportName(specifier.local);
|
|
80
|
+
const exported = exportName(specifier.exported);
|
|
81
|
+
if (local && exported && local !== exported) {
|
|
82
|
+
context.report({ node: specifier, messageId: "renamed" });
|
|
83
|
+
}
|
|
38
84
|
}
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
}
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
},
|
|
42
88
|
);
|
|
@@ -56,6 +56,8 @@ function isSameArgumentList(params, args) {
|
|
|
56
56
|
|
|
57
57
|
function isSelfCall(node, call) {
|
|
58
58
|
if (call.callee.type !== "Identifier") return false;
|
|
59
|
+
const wrapper = variableWrapperName(node);
|
|
60
|
+
if (wrapper) return wrapper === call.callee.name;
|
|
59
61
|
if (node.id && node.id.name === call.callee.name) return true;
|
|
60
62
|
const parent = node.parent;
|
|
61
63
|
return (
|
|
@@ -66,7 +68,36 @@ function isSelfCall(node, call) {
|
|
|
66
68
|
);
|
|
67
69
|
}
|
|
68
70
|
|
|
71
|
+
function assignmentWrapperName(node) {
|
|
72
|
+
if (node.parent?.type !== "AssignmentExpression" || node.parent.right !== node) return null;
|
|
73
|
+
const left = node.parent.left;
|
|
74
|
+
if (left.type === "Identifier") return left.name;
|
|
75
|
+
if (left.type === "MemberExpression" && !left.computed) return identifierName(left.property);
|
|
76
|
+
if (left.type === "MemberExpression" && left.property.type === "Literal") {
|
|
77
|
+
return String(left.property.value);
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function variableWrapperName(node) {
|
|
83
|
+
return node.parent?.type === "VariableDeclarator" && node.parent.id.type === "Identifier"
|
|
84
|
+
? node.parent.id.name
|
|
85
|
+
: null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function wrapperName(node) {
|
|
89
|
+
return variableWrapperName(node) || assignmentWrapperName(node);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function isNamedWrapper(node) {
|
|
93
|
+
if (node.type === "FunctionDeclaration") {
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
return Boolean(wrapperName(node));
|
|
97
|
+
}
|
|
98
|
+
|
|
69
99
|
function reportIfAlias(node, context) {
|
|
100
|
+
if (!isNamedWrapper(node)) return;
|
|
70
101
|
const call = onlyCallExpression(node.body);
|
|
71
102
|
if (!call || call.type !== "CallExpression") return;
|
|
72
103
|
if (isSelfCall(node, call)) return;
|
|
@@ -5,16 +5,17 @@ const { rule } = require("../helpers");
|
|
|
5
5
|
const TEST_FILE_PATTERN = /\.(?:test|spec)\.[cm]?[jt]sx?$/;
|
|
6
6
|
const MOCK_TEST_FILE_PATTERN = /\.mock\.test\.[cm]?[jt]sx?$/;
|
|
7
7
|
const MOCK_METHODS = new Set([
|
|
8
|
+
"fn",
|
|
8
9
|
"mock",
|
|
9
10
|
"doMock",
|
|
10
11
|
"unmock",
|
|
11
12
|
"doUnmock",
|
|
12
13
|
"spyOn",
|
|
13
|
-
"fn",
|
|
14
14
|
"clearAllMocks",
|
|
15
15
|
"resetAllMocks",
|
|
16
16
|
"restoreAllMocks",
|
|
17
17
|
"stubEnv",
|
|
18
|
+
"stubGlobal",
|
|
18
19
|
"setSystemTime",
|
|
19
20
|
]);
|
|
20
21
|
|