eslint-plugin-no-mistakes 0.8.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/index.js +1 -0
- package/src/returned-jsx.js +119 -0
- package/src/rules/playwright-require-exported-component-attribute.js +77 -0
- package/src/rules/test-no-error-message-matching.js +5 -1
- package/src/rules/test-no-shared-state-analysis.js +130 -0
- package/src/rules/test-no-shared-state-helpers.js +100 -6
- package/src/rules/test-no-shared-state.js +130 -93
- 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 +1 -0
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/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
|
+
};
|
|
@@ -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
|
+
}
|
|
@@ -62,6 +62,10 @@ function isStaticAnalysisPath(filename) {
|
|
|
62
62
|
);
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
function isGeneratedTestPath(filename) {
|
|
66
|
+
return filename.replace(/\\/g, "/").includes(".generated.");
|
|
67
|
+
}
|
|
68
|
+
|
|
65
69
|
function isExpectCall(node) {
|
|
66
70
|
const current = unwrap(node);
|
|
67
71
|
if (current?.type !== "CallExpression") return false;
|
|
@@ -99,7 +103,7 @@ module.exports = rule(
|
|
|
99
103
|
messages: { message: "Do not assert on err.message; check the error type or code instead." },
|
|
100
104
|
},
|
|
101
105
|
(context) => {
|
|
102
|
-
if (isStaticAnalysisPath(context.filename)) return {};
|
|
106
|
+
if (isStaticAnalysisPath(context.filename) || isGeneratedTestPath(context.filename)) return {};
|
|
103
107
|
let testDepth = 0;
|
|
104
108
|
return {
|
|
105
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
|
+
};
|
|
@@ -23,6 +23,15 @@ function isTestCall(node) {
|
|
|
23
23
|
return TEST_CALLEES.has(calleeName(node.callee));
|
|
24
24
|
}
|
|
25
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
|
+
|
|
26
35
|
function isSetupCall(node) {
|
|
27
36
|
return SETUP_CALLEES.has(calleeName(node.callee));
|
|
28
37
|
}
|
|
@@ -57,11 +66,30 @@ function propertyName(node) {
|
|
|
57
66
|
return node.type === "Literal" ? String(node.value) : node.name;
|
|
58
67
|
}
|
|
59
68
|
|
|
60
|
-
function
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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;
|
|
65
93
|
}
|
|
66
94
|
|
|
67
95
|
function isFunctionNode(node) {
|
|
@@ -72,6 +100,10 @@ function isInlineTestCallback(node) {
|
|
|
72
100
|
return node.parent?.type === "CallExpression" && isTestCall(node.parent);
|
|
73
101
|
}
|
|
74
102
|
|
|
103
|
+
function isInlineSetupCallback(node) {
|
|
104
|
+
return node.parent?.type === "CallExpression" && isSetupCall(node.parent);
|
|
105
|
+
}
|
|
106
|
+
|
|
75
107
|
function isCalledFunction(node) {
|
|
76
108
|
if (node.parent?.type === "CallExpression" && node.parent.callee === node) return true;
|
|
77
109
|
const declarator = node.parent?.type === "VariableDeclarator" ? node.parent : null;
|
|
@@ -116,16 +148,78 @@ function namedCallbackArgument(args) {
|
|
|
116
148
|
}
|
|
117
149
|
}
|
|
118
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
|
+
|
|
119
206
|
module.exports = {
|
|
120
207
|
childNodes,
|
|
121
208
|
collectPatternNames,
|
|
209
|
+
createCleanupTracker,
|
|
210
|
+
firstNamedCallbackArgument,
|
|
122
211
|
isCalledFunction,
|
|
123
212
|
isFunctionNode,
|
|
213
|
+
isInlineSetupCallback,
|
|
124
214
|
isInlineTestCallback,
|
|
125
215
|
isMutableInitializer,
|
|
216
|
+
calleeName,
|
|
126
217
|
isSetupCall,
|
|
127
218
|
isTestCall,
|
|
128
|
-
|
|
219
|
+
mutatingCallPropertyName,
|
|
220
|
+
mutatingCallTarget,
|
|
221
|
+
mutationPath,
|
|
129
222
|
mutationRootName,
|
|
130
223
|
namedCallbackArgument,
|
|
224
|
+
setupCallbackKind,
|
|
131
225
|
};
|
|
@@ -2,17 +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
|
-
isSetupCall,
|
|
12
19
|
isTestCall,
|
|
13
|
-
|
|
20
|
+
mutatingCallPropertyName,
|
|
21
|
+
mutatingCallTarget,
|
|
22
|
+
mutationPath,
|
|
14
23
|
mutationRootName,
|
|
15
24
|
namedCallbackArgument,
|
|
25
|
+
setupCallbackKind,
|
|
16
26
|
} = require("./test-no-shared-state-helpers");
|
|
17
27
|
|
|
18
28
|
module.exports = rule(
|
|
@@ -27,45 +37,26 @@ module.exports = rule(
|
|
|
27
37
|
},
|
|
28
38
|
(context) => {
|
|
29
39
|
const mutableTopLevel = new Set();
|
|
30
|
-
const functionDeclarations = new Map();
|
|
31
40
|
const pendingNamedCallbacks = [];
|
|
32
|
-
const
|
|
33
|
-
const
|
|
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
|
+
);
|
|
34
50
|
let testDepth = 0;
|
|
35
51
|
let setupDepth = 0;
|
|
36
52
|
|
|
37
|
-
function markViMockCapturedMutable(name) {
|
|
38
|
-
if (
|
|
39
|
-
name.startsWith("mock") &&
|
|
40
|
-
mutableTopLevel.has(name) &&
|
|
41
|
-
viMockFactoryReferences.has(name)
|
|
42
|
-
) {
|
|
43
|
-
viMockCapturedMutables.add(name);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function isModuleMutable(node, name) {
|
|
48
|
-
let scope = context.sourceCode.getScope(node);
|
|
49
|
-
while (scope) {
|
|
50
|
-
const variable = scope.variables.find((candidate) => candidate.name === name);
|
|
51
|
-
if (variable) {
|
|
52
|
-
return (
|
|
53
|
-
mutableTopLevel.has(variable.name) &&
|
|
54
|
-
(variable.scope.type === "module" || variable.scope.block.type === "Program")
|
|
55
|
-
);
|
|
56
|
-
}
|
|
57
|
-
scope = scope.upper;
|
|
58
|
-
}
|
|
59
|
-
return false;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
53
|
function reportIfShared(node, name) {
|
|
63
54
|
if (
|
|
64
55
|
name &&
|
|
65
56
|
testDepth > 0 &&
|
|
66
57
|
setupDepth === 0 &&
|
|
67
|
-
!
|
|
68
|
-
isModuleMutable(node, name)
|
|
58
|
+
!viMockTracker.isCaptured(name) &&
|
|
59
|
+
isModuleMutable(context, mutableTopLevel, node, name)
|
|
69
60
|
) {
|
|
70
61
|
context.report({ node, messageId: "shared" });
|
|
71
62
|
}
|
|
@@ -77,95 +68,141 @@ module.exports = rule(
|
|
|
77
68
|
reportIfShared(node, mutationRootName(node.left));
|
|
78
69
|
}
|
|
79
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
|
+
|
|
80
114
|
return {
|
|
81
115
|
"Program > VariableDeclaration"(node) {
|
|
82
116
|
for (const declaration of node.declarations) {
|
|
83
|
-
if (declaration.id.type === "Identifier" && isFunctionNode(declaration.init)) {
|
|
84
|
-
functionDeclarations.set(declaration.id.name, declaration.init);
|
|
85
|
-
}
|
|
86
117
|
if (node.kind === "const" && !isMutableInitializer(declaration.init)) continue;
|
|
87
118
|
for (const name of collectPatternNames(declaration.id)) {
|
|
88
119
|
mutableTopLevel.add(name);
|
|
89
|
-
|
|
120
|
+
viMockTracker.markIfCaptured(name);
|
|
90
121
|
}
|
|
91
122
|
}
|
|
92
123
|
},
|
|
93
|
-
"Program > FunctionDeclaration"(node) {
|
|
94
|
-
if (node.id?.name) functionDeclarations.set(node.id.name, node);
|
|
95
|
-
},
|
|
96
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
|
+
}
|
|
97
134
|
testDepth = 1;
|
|
98
|
-
for (const
|
|
99
|
-
|
|
100
|
-
|
|
135
|
+
for (const { declaration, suiteKey } of pendingNamedCallbacks) {
|
|
136
|
+
if (!declaration) continue;
|
|
137
|
+
cleanupTracker.setReplaySuite(suiteKey);
|
|
138
|
+
checkSharedMutations(declaration.body);
|
|
139
|
+
cleanupTracker.clearReplaySuite();
|
|
101
140
|
}
|
|
141
|
+
registryReports.flush();
|
|
102
142
|
},
|
|
103
143
|
CallExpression(node) {
|
|
104
|
-
|
|
144
|
+
viMockTracker.collectFactoryReferences(node);
|
|
145
|
+
if (calleeName(node.callee) === "describe") cleanupTracker.enterSuite();
|
|
105
146
|
if (isTestCall(node)) {
|
|
106
147
|
testDepth += 1;
|
|
107
148
|
const callback = namedCallbackArgument(node.arguments);
|
|
108
|
-
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);
|
|
109
170
|
}
|
|
110
|
-
if (isSetupCall(node)) setupDepth += 1;
|
|
111
171
|
},
|
|
112
172
|
AssignmentExpression(node) {
|
|
113
|
-
if (isInsideUncalledNestedFunction(node)) return;
|
|
173
|
+
if (isInsideUncalledNestedFunction(node, testDepth, setupDepth)) return;
|
|
174
|
+
rememberAssignmentCleanup(node);
|
|
114
175
|
reportAssignment(node);
|
|
115
176
|
},
|
|
116
177
|
UpdateExpression(node) {
|
|
117
|
-
if (isInsideUncalledNestedFunction(node)) return;
|
|
178
|
+
if (isInsideUncalledNestedFunction(node, testDepth, setupDepth)) return;
|
|
118
179
|
reportIfShared(node, mutationRootName(node.argument));
|
|
119
180
|
},
|
|
120
181
|
"CallExpression:exit"(node) {
|
|
121
182
|
if (isTestCall(node)) testDepth -= 1;
|
|
122
|
-
if (
|
|
123
|
-
|
|
124
|
-
|
|
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();
|
|
125
192
|
},
|
|
126
193
|
};
|
|
127
194
|
|
|
128
|
-
function isInsideUncalledNestedFunction(node) {
|
|
129
|
-
if (testDepth === 0) return false;
|
|
130
|
-
let current = node.parent;
|
|
131
|
-
while (current) {
|
|
132
|
-
const isUncalledFunction =
|
|
133
|
-
isFunctionNode(current) && !isInlineTestCallback(current) && !isCalledFunction(current);
|
|
134
|
-
if (isUncalledFunction) return true;
|
|
135
|
-
current = current.parent;
|
|
136
|
-
}
|
|
137
|
-
return false;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
195
|
function checkSharedMutations(node) {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
function collectViMockFactoryReferences(node) {
|
|
153
|
-
if (
|
|
154
|
-
node.callee.type !== "MemberExpression" ||
|
|
155
|
-
node.callee.object.type !== "Identifier" ||
|
|
156
|
-
node.callee.object.name !== "vi" ||
|
|
157
|
-
node.callee.property.type !== "Identifier" ||
|
|
158
|
-
node.callee.property.name !== "mock"
|
|
159
|
-
) {
|
|
160
|
-
return;
|
|
161
|
-
}
|
|
162
|
-
const factory = node.arguments[1];
|
|
163
|
-
if (!isFunctionNode(factory)) return;
|
|
164
|
-
for (const { identifier } of context.sourceCode.getScope(factory).through) {
|
|
165
|
-
const name = identifier.name;
|
|
166
|
-
viMockFactoryReferences.add(name);
|
|
167
|
-
markViMockCapturedMutable(name);
|
|
168
|
-
}
|
|
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
|
+
});
|
|
169
206
|
}
|
|
170
207
|
},
|
|
171
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;
|