eslint-plugin-no-mistakes 0.7.0 → 0.8.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.8.0",
4
4
  "description": "ESLint and Oxlint rules for deterministic no-mistakes code analysis",
5
5
  "license": "MIT",
6
6
  "repository": {
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);
@@ -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
  };
@@ -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,23 @@ 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
+ );
47
63
  }
48
64
 
49
65
  function isExpectCall(node) {
@@ -83,6 +99,7 @@ module.exports = rule(
83
99
  messages: { message: "Do not assert on err.message; check the error type or code instead." },
84
100
  },
85
101
  (context) => {
102
+ if (isStaticAnalysisPath(context.filename)) return {};
86
103
  let testDepth = 0;
87
104
  return {
88
105
  CallExpression(node) {
@@ -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,10 @@ function isTestCall(node) {
22
23
  return TEST_CALLEES.has(calleeName(node.callee));
23
24
  }
24
25
 
26
+ function isSetupCall(node) {
27
+ return SETUP_CALLEES.has(calleeName(node.callee));
28
+ }
29
+
25
30
  function collectPatternNames(node, names = new Set()) {
26
31
  if (!node) return names;
27
32
  if (node.type === "Identifier") {
@@ -118,6 +123,7 @@ module.exports = {
118
123
  isFunctionNode,
119
124
  isInlineTestCallback,
120
125
  isMutableInitializer,
126
+ isSetupCall,
121
127
  isTestCall,
122
128
  mutatingCallRootName,
123
129
  mutationRootName,
@@ -8,6 +8,7 @@ const {
8
8
  isFunctionNode,
9
9
  isInlineTestCallback,
10
10
  isMutableInitializer,
11
+ isSetupCall,
11
12
  isTestCall,
12
13
  mutatingCallRootName,
13
14
  mutationRootName,
@@ -28,7 +29,20 @@ module.exports = rule(
28
29
  const mutableTopLevel = new Set();
29
30
  const functionDeclarations = new Map();
30
31
  const pendingNamedCallbacks = [];
32
+ const viMockFactoryReferences = new Set();
33
+ const viMockCapturedMutables = new Set();
31
34
  let testDepth = 0;
35
+ let setupDepth = 0;
36
+
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
+ }
32
46
 
33
47
  function isModuleMutable(node, name) {
34
48
  let scope = context.sourceCode.getScope(node);
@@ -46,8 +60,15 @@ module.exports = rule(
46
60
  }
47
61
 
48
62
  function reportIfShared(node, name) {
49
- if (name && testDepth > 0 && isModuleMutable(node, name))
63
+ if (
64
+ name &&
65
+ testDepth > 0 &&
66
+ setupDepth === 0 &&
67
+ !viMockCapturedMutables.has(name) &&
68
+ isModuleMutable(node, name)
69
+ ) {
50
70
  context.report({ node, messageId: "shared" });
71
+ }
51
72
  }
52
73
 
53
74
  function reportAssignment(node) {
@@ -63,7 +84,10 @@ module.exports = rule(
63
84
  functionDeclarations.set(declaration.id.name, declaration.init);
64
85
  }
65
86
  if (node.kind === "const" && !isMutableInitializer(declaration.init)) continue;
66
- for (const name of collectPatternNames(declaration.id)) mutableTopLevel.add(name);
87
+ for (const name of collectPatternNames(declaration.id)) {
88
+ mutableTopLevel.add(name);
89
+ markViMockCapturedMutable(name);
90
+ }
67
91
  }
68
92
  },
69
93
  "Program > FunctionDeclaration"(node) {
@@ -77,11 +101,13 @@ module.exports = rule(
77
101
  }
78
102
  },
79
103
  CallExpression(node) {
104
+ collectViMockFactoryReferences(node);
80
105
  if (isTestCall(node)) {
81
106
  testDepth += 1;
82
107
  const callback = namedCallbackArgument(node.arguments);
83
108
  if (callback) pendingNamedCallbacks.push(callback.name);
84
109
  }
110
+ if (isSetupCall(node)) setupDepth += 1;
85
111
  },
86
112
  AssignmentExpression(node) {
87
113
  if (isInsideUncalledNestedFunction(node)) return;
@@ -93,6 +119,7 @@ module.exports = rule(
93
119
  },
94
120
  "CallExpression:exit"(node) {
95
121
  if (isTestCall(node)) testDepth -= 1;
122
+ if (isSetupCall(node)) setupDepth -= 1;
96
123
  if (isInsideUncalledNestedFunction(node)) return;
97
124
  reportIfShared(node, mutatingCallRootName(node));
98
125
  },
@@ -121,5 +148,24 @@ module.exports = rule(
121
148
  }
122
149
  for (const child of childNodes(node)) checkSharedMutations(child);
123
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
+ }
169
+ }
124
170
  },
125
171
  );
@@ -10,11 +10,11 @@ const MOCK_METHODS = new Set([
10
10
  "unmock",
11
11
  "doUnmock",
12
12
  "spyOn",
13
- "fn",
14
13
  "clearAllMocks",
15
14
  "resetAllMocks",
16
15
  "restoreAllMocks",
17
16
  "stubEnv",
17
+ "stubGlobal",
18
18
  "setSystemTime",
19
19
  ]);
20
20