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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-no-mistakes",
3
- "version": "0.7.0",
3
+ "version": "0.9.0",
4
4
  "description": "ESLint and Oxlint rules for deterministic no-mistakes code analysis",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -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\\]]+))\\s*(?:[is])?\\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.type === "Identifier") return node.name;
28
- return node.type === "Literal" ? String(node.value) : null;
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 current?.type === "MemberExpression" && propertyName(current.property) === "message";
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 mutatingCallRootName(node) {
56
- return node.callee.type === "MemberExpression" &&
57
- MUTATING_METHODS.has(propertyName(node.callee.property))
58
- ? mutationRootName(node.callee.object)
59
- : null;
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
- mutatingCallRootName,
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
- childNodes,
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
- isCalledFunction,
15
+ createCleanupTracker,
16
+ firstNamedCallbackArgument,
8
17
  isFunctionNode,
9
- isInlineTestCallback,
10
18
  isMutableInitializer,
11
19
  isTestCall,
12
- mutatingCallRootName,
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 (name && testDepth > 0 && isModuleMutable(node, name))
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)) mutableTopLevel.add(name);
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 name of pendingNamedCallbacks) {
75
- const declaration = functionDeclarations.get(name);
76
- if (declaration) checkSharedMutations(declaration.body);
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) pendingNamedCallbacks.push(callback.name);
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 (isInsideUncalledNestedFunction(node)) return;
97
- reportIfShared(node, mutatingCallRootName(node));
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
- if (isFunctionNode(node) && !isCalledFunction(node)) return;
115
- if (node.type === "AssignmentExpression") {
116
- reportAssignment(node);
117
- } else if (node.type === "UpdateExpression") {
118
- reportIfShared(node, mutationRootName(node.argument));
119
- } else if (node.type === "CallExpression") {
120
- reportIfShared(node, mutatingCallRootName(node));
121
- }
122
- for (const child of childNodes(node)) checkSharedMutations(child);
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
- ExportNamedDeclaration(node) {
30
- for (const specifier of node.specifiers || []) {
31
- if (specifier.type !== "ExportSpecifier" || isTypeExport(node, specifier)) {
32
- continue;
33
- }
34
- const local = exportName(specifier.local);
35
- const exported = exportName(specifier.exported);
36
- if (local && exported && local !== exported) {
37
- context.report({ node: specifier, messageId: "renamed" });
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