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 +1 -1
- package/src/helpers.js +1 -1
- package/src/rules/nextjs-no-manual-script-tags.js +54 -2
- package/src/rules/test-no-error-message-matching.js +20 -3
- package/src/rules/test-no-shared-state-helpers.js +6 -0
- package/src/rules/test-no-shared-state.js +48 -2
- package/src/rules/vitest-mock-test-file-naming.js +1 -1
package/package.json
CHANGED
package/src/helpers.js
CHANGED
|
@@ -109,7 +109,7 @@ function cssSelectorValues(source, attrs) {
|
|
|
109
109
|
for (const attr of attrs) {
|
|
110
110
|
const escaped = attr.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
111
111
|
const regex = new RegExp(
|
|
112
|
-
`\\[\\s*${escaped}\\s*([*^$]?=)\\s*(?:"([^"]*)"|'([^']*)'|([^\\s\\]]+))
|
|
112
|
+
`\\[\\s*${escaped}\\s*([*^$]?=)\\s*(?:"([^"]*)"|'([^']*)'|([^\\s\\]]+))(?:[is]|\\s+[is])?\\s*\\]`,
|
|
113
113
|
"g",
|
|
114
114
|
);
|
|
115
115
|
let match = regex.exec(source);
|
|
@@ -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
|
|
28
|
-
return node
|
|
27
|
+
if (node?.type === "Identifier") return node.name;
|
|
28
|
+
return node?.type === "Literal" ? String(node.value) : null;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
function unwrap(node) {
|
|
@@ -43,7 +43,23 @@ function unwrap(node) {
|
|
|
43
43
|
|
|
44
44
|
function isMessageMember(node) {
|
|
45
45
|
const current = unwrap(node);
|
|
46
|
-
return
|
|
46
|
+
return (
|
|
47
|
+
current?.type === "MemberExpression" &&
|
|
48
|
+
propertyName(current.property) === "message" &&
|
|
49
|
+
!isBodyMember(current.object)
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function isBodyMember(node) {
|
|
54
|
+
const current = unwrap(node);
|
|
55
|
+
return current?.type === "MemberExpression" && propertyName(current.property) === "body";
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function isStaticAnalysisPath(filename) {
|
|
59
|
+
const normalized = filename.replace(/\\/g, "/");
|
|
60
|
+
return (
|
|
61
|
+
normalized.startsWith("static-code-analysis/") || normalized.includes("/static-code-analysis/")
|
|
62
|
+
);
|
|
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 (
|
|
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))
|
|
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
|
);
|