eslint-plugin-no-mistakes 0.6.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/index.js +30 -0
- package/src/rules/await-array-methods.js +84 -0
- package/src/rules/nextjs-metadata-exports-location.js +96 -0
- package/src/rules/nextjs-no-manual-script-tags.js +103 -0
- package/src/rules/no-delete-property.js +36 -0
- package/src/rules/no-import-only-test-files.js +66 -0
- package/src/rules/no-placeholder-never-type-exports.js +51 -0
- package/src/rules/no-vitest-sequential.js +77 -0
- package/src/rules/playwright-assertion-timeout-cap.js +99 -0
- package/src/rules/playwright-no-set-timeout.js +64 -0
- package/src/rules/playwright-selector-priority.js +74 -0
- package/src/rules/react-no-iife-in-jsx.js +43 -0
- package/src/rules/react-no-use-promise-resolve.js +95 -0
- package/src/rules/test-no-error-message-matching.js +144 -0
- package/src/rules/test-no-shared-state-helpers.js +131 -0
- package/src/rules/test-no-shared-state.js +171 -0
- package/src/rules/vitest-mock-test-file-naming.js +100 -0
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);
|
package/src/index.js
CHANGED
|
@@ -1,19 +1,34 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
const rules = {
|
|
4
|
+
"await-array-methods": require("./rules/await-array-methods"),
|
|
4
5
|
"nextjs-static-fetch-method": require("./rules/nextjs-static-fetch-method"),
|
|
5
6
|
"nextjs-static-fetch-url": require("./rules/nextjs-static-fetch-url"),
|
|
7
|
+
"nextjs-metadata-exports-location": require("./rules/nextjs-metadata-exports-location"),
|
|
8
|
+
"nextjs-no-manual-script-tags": require("./rules/nextjs-no-manual-script-tags"),
|
|
9
|
+
"no-delete-property": require("./rules/no-delete-property"),
|
|
10
|
+
"no-import-only-test-files": require("./rules/no-import-only-test-files"),
|
|
11
|
+
"no-placeholder-never-type-exports": require("./rules/no-placeholder-never-type-exports"),
|
|
12
|
+
"no-vitest-sequential": require("./rules/no-vitest-sequential"),
|
|
6
13
|
"playwright-consistent-attribute": require("./rules/playwright-consistent-attribute"),
|
|
7
14
|
"playwright-defaults": require("./rules/playwright-defaults"),
|
|
15
|
+
"playwright-assertion-timeout-cap": require("./rules/playwright-assertion-timeout-cap"),
|
|
8
16
|
"playwright-literals": require("./rules/playwright-literals"),
|
|
9
17
|
"playwright-naming-convention": require("./rules/playwright-naming-convention"),
|
|
10
18
|
"playwright-no-empty": require("./rules/playwright-no-empty"),
|
|
19
|
+
"playwright-no-set-timeout": require("./rules/playwright-no-set-timeout"),
|
|
11
20
|
"playwright-prefer-get-by-test-id": require("./rules/playwright-prefer-get-by-test-id"),
|
|
12
21
|
"playwright-require-interactive-test-id": require("./rules/playwright-require-interactive-test-id"),
|
|
22
|
+
"playwright-selector-priority": require("./rules/playwright-selector-priority"),
|
|
23
|
+
"react-no-iife-in-jsx": require("./rules/react-no-iife-in-jsx"),
|
|
13
24
|
"playwright-unique": require("./rules/playwright-unique"),
|
|
14
25
|
"react-no-nullish-react-node": require("./rules/react-no-nullish-react-node"),
|
|
26
|
+
"react-no-use-promise-resolve": require("./rules/react-no-use-promise-resolve"),
|
|
27
|
+
"test-no-error-message-matching": require("./rules/test-no-error-message-matching"),
|
|
28
|
+
"test-no-shared-state": require("./rules/test-no-shared-state"),
|
|
15
29
|
"ts-no-export-renaming": require("./rules/ts-no-export-renaming"),
|
|
16
30
|
"ts-no-function-aliases": require("./rules/ts-no-function-aliases"),
|
|
31
|
+
"vitest-mock-test-file-naming": require("./rules/vitest-mock-test-file-naming"),
|
|
17
32
|
};
|
|
18
33
|
|
|
19
34
|
const plugin = {
|
|
@@ -32,6 +47,8 @@ plugin.configs.recommended = {
|
|
|
32
47
|
rules: {
|
|
33
48
|
"no-mistakes/nextjs-static-fetch-method": "error",
|
|
34
49
|
"no-mistakes/nextjs-static-fetch-url": "error",
|
|
50
|
+
"no-mistakes/no-delete-property": "error",
|
|
51
|
+
"no-mistakes/no-placeholder-never-type-exports": "error",
|
|
35
52
|
"no-mistakes/playwright-defaults": "error",
|
|
36
53
|
"no-mistakes/playwright-literals": "error",
|
|
37
54
|
"no-mistakes/playwright-no-empty": "error",
|
|
@@ -48,10 +65,23 @@ plugin.configs.strict = {
|
|
|
48
65
|
},
|
|
49
66
|
rules: {
|
|
50
67
|
...plugin.configs.recommended.rules,
|
|
68
|
+
"no-mistakes/await-array-methods": "error",
|
|
69
|
+
"no-mistakes/nextjs-metadata-exports-location": "error",
|
|
70
|
+
"no-mistakes/nextjs-no-manual-script-tags": "error",
|
|
71
|
+
"no-mistakes/no-import-only-test-files": "error",
|
|
72
|
+
"no-mistakes/no-vitest-sequential": "error",
|
|
73
|
+
"no-mistakes/playwright-assertion-timeout-cap": "error",
|
|
51
74
|
"no-mistakes/playwright-consistent-attribute": ["error", { canonicalAttribute: "data-pw" }],
|
|
52
75
|
"no-mistakes/playwright-naming-convention": "error",
|
|
76
|
+
"no-mistakes/playwright-no-set-timeout": "error",
|
|
53
77
|
"no-mistakes/playwright-prefer-get-by-test-id": "warn",
|
|
54
78
|
"no-mistakes/playwright-require-interactive-test-id": "warn",
|
|
79
|
+
"no-mistakes/playwright-selector-priority": "error",
|
|
80
|
+
"no-mistakes/react-no-iife-in-jsx": "error",
|
|
81
|
+
"no-mistakes/react-no-use-promise-resolve": "error",
|
|
82
|
+
"no-mistakes/test-no-error-message-matching": "error",
|
|
83
|
+
"no-mistakes/test-no-shared-state": "error",
|
|
84
|
+
"no-mistakes/vitest-mock-test-file-naming": "error",
|
|
55
85
|
},
|
|
56
86
|
};
|
|
57
87
|
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { callMethodName, rule } = require("../helpers");
|
|
4
|
+
|
|
5
|
+
const BANNED_METHODS = new Set(["sort", "toSorted", "every", "findIndex", "slice", "toSpliced"]);
|
|
6
|
+
|
|
7
|
+
function methodName(node) {
|
|
8
|
+
if (node.callee.type !== "MemberExpression") return callMethodName(node);
|
|
9
|
+
if (!node.callee.computed) return node.callee.property.name;
|
|
10
|
+
return node.callee.property.type === "Literal" ? String(node.callee.property.value) : null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function unwrapChain(node) {
|
|
14
|
+
return node?.type === "ChainExpression" ? node.expression : node;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function findVariable(node, context) {
|
|
18
|
+
let scope = context.sourceCode.getScope(node);
|
|
19
|
+
while (scope) {
|
|
20
|
+
const variable = scope.variables.find((candidate) => candidate.name === node.name);
|
|
21
|
+
if (variable) return variable;
|
|
22
|
+
scope = scope.upper;
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function isReassignedBeforeUse(variable, node) {
|
|
28
|
+
const definitionNames = new Set(variable.defs.map((def) => def.name));
|
|
29
|
+
return variable.references.some(
|
|
30
|
+
(reference) =>
|
|
31
|
+
reference.isWrite() &&
|
|
32
|
+
!definitionNames.has(reference.identifier) &&
|
|
33
|
+
reference.identifier.range[0] < node.range[0],
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function isKnownArrayReceiver(node, context) {
|
|
38
|
+
node = unwrapChain(node);
|
|
39
|
+
if (node.type === "ArrayExpression") return true;
|
|
40
|
+
if (node.type !== "Identifier") return false;
|
|
41
|
+
const variable = findVariable(node, context);
|
|
42
|
+
return Boolean(
|
|
43
|
+
variable &&
|
|
44
|
+
variable.defs.some(
|
|
45
|
+
(def) =>
|
|
46
|
+
def.type === "Variable" &&
|
|
47
|
+
def.node?.id?.type === "Identifier" &&
|
|
48
|
+
def.node.init?.type === "ArrayExpression",
|
|
49
|
+
) &&
|
|
50
|
+
!isReassignedBeforeUse(variable, node),
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
module.exports = rule(
|
|
55
|
+
{
|
|
56
|
+
type: "problem",
|
|
57
|
+
docs: {
|
|
58
|
+
description: "disallow awaiting synchronous array methods",
|
|
59
|
+
recommended: false,
|
|
60
|
+
},
|
|
61
|
+
schema: [],
|
|
62
|
+
messages: {
|
|
63
|
+
awaited:
|
|
64
|
+
"Do not await {{method}}(). This array method returns a synchronous value; remove await or await the async work explicitly.",
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
(context) => {
|
|
68
|
+
return {
|
|
69
|
+
AwaitExpression(node) {
|
|
70
|
+
const argument = unwrapChain(node.argument);
|
|
71
|
+
if (argument.type !== "CallExpression") return;
|
|
72
|
+
const method = methodName(argument);
|
|
73
|
+
if (!BANNED_METHODS.has(method)) return;
|
|
74
|
+
if (
|
|
75
|
+
argument.callee.type !== "MemberExpression" ||
|
|
76
|
+
!isKnownArrayReceiver(argument.callee.object, context)
|
|
77
|
+
) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
context.report({ node, messageId: "awaited", data: { method } });
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
},
|
|
84
|
+
);
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { rule } = require("../helpers");
|
|
4
|
+
|
|
5
|
+
const ALLOWED_SUFFIXES = [
|
|
6
|
+
"/page.tsx",
|
|
7
|
+
"/page.ts",
|
|
8
|
+
"/page.jsx",
|
|
9
|
+
"/page.js",
|
|
10
|
+
"/layout.tsx",
|
|
11
|
+
"/layout.ts",
|
|
12
|
+
"/layout.jsx",
|
|
13
|
+
"/layout.js",
|
|
14
|
+
];
|
|
15
|
+
const METADATA_EXPORTS = new Set(["metadata", "generateMetadata"]);
|
|
16
|
+
const NEXT_FILE_PATTERN = /(?:^|[/\\])(?:app|pages)(?:[/\\]|$)/;
|
|
17
|
+
|
|
18
|
+
function isAllowedFile(filename) {
|
|
19
|
+
const normalized = filename.replace(/\\/g, "/");
|
|
20
|
+
return (
|
|
21
|
+
(normalized.startsWith("app/") || normalized.includes("/app/")) &&
|
|
22
|
+
ALLOWED_SUFFIXES.some((suffix) => normalized.endsWith(suffix))
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function isNextFile(filename) {
|
|
27
|
+
return NEXT_FILE_PATTERN.test(filename.replace(/\\/g, "/"));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function specifierName(specifier) {
|
|
31
|
+
return specifier.exported?.name || specifier.exported?.value || specifier.local?.name;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function declarationName(declaration) {
|
|
35
|
+
return declaration.id?.type === "Identifier" ? declaration.id.name : null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function collectPatternNames(node, names = new Set()) {
|
|
39
|
+
if (!node) return names;
|
|
40
|
+
if (node.type === "Identifier") names.add(node.name);
|
|
41
|
+
if (node.type === "ObjectPattern") {
|
|
42
|
+
for (const property of node.properties) collectPatternNames(property.value, names);
|
|
43
|
+
}
|
|
44
|
+
if (node.type === "ArrayPattern") {
|
|
45
|
+
for (const element of node.elements) collectPatternNames(element, names);
|
|
46
|
+
}
|
|
47
|
+
if (node.type === "RestElement") collectPatternNames(node.argument, names);
|
|
48
|
+
return names;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
module.exports = rule(
|
|
52
|
+
{
|
|
53
|
+
type: "problem",
|
|
54
|
+
docs: {
|
|
55
|
+
description: "restrict Next.js metadata exports to route segment files",
|
|
56
|
+
recommended: false,
|
|
57
|
+
},
|
|
58
|
+
schema: [],
|
|
59
|
+
messages: {
|
|
60
|
+
location:
|
|
61
|
+
"metadata and generateMetadata exports are only allowed in Next.js route segment files.",
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
(context) => ({
|
|
65
|
+
ExportNamedDeclaration(node) {
|
|
66
|
+
if (!isNextFile(context.filename)) return;
|
|
67
|
+
if (isAllowedFile(context.filename)) return;
|
|
68
|
+
if (node.declaration?.type === "VariableDeclaration") {
|
|
69
|
+
if (
|
|
70
|
+
node.declaration.declarations.some((declaration) =>
|
|
71
|
+
[...collectPatternNames(declaration.id), declarationName(declaration)].some((name) =>
|
|
72
|
+
METADATA_EXPORTS.has(name),
|
|
73
|
+
),
|
|
74
|
+
)
|
|
75
|
+
) {
|
|
76
|
+
context.report({ node, messageId: "location" });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (
|
|
80
|
+
node.declaration?.type === "FunctionDeclaration" &&
|
|
81
|
+
METADATA_EXPORTS.has(declarationName(node.declaration))
|
|
82
|
+
) {
|
|
83
|
+
context.report({ node, messageId: "location" });
|
|
84
|
+
}
|
|
85
|
+
if (
|
|
86
|
+
node.exportKind !== "type" &&
|
|
87
|
+
node.specifiers?.some(
|
|
88
|
+
(specifier) =>
|
|
89
|
+
specifier.exportKind !== "type" && METADATA_EXPORTS.has(specifierName(specifier)),
|
|
90
|
+
)
|
|
91
|
+
) {
|
|
92
|
+
context.report({ node, messageId: "location" });
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
}),
|
|
96
|
+
);
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { literalString, rule } = require("../helpers");
|
|
4
|
+
|
|
5
|
+
const NEXT_FILE_PATTERN = /(?:^|[/\\])(?:app|pages)(?:[/\\]|$)/;
|
|
6
|
+
|
|
7
|
+
function isNextPath(filename) {
|
|
8
|
+
return NEXT_FILE_PATTERN.test(filename.replace(/\\/g, "/"));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function isJsonLdScript(node) {
|
|
12
|
+
return node.attributes.some((attribute) => {
|
|
13
|
+
if (
|
|
14
|
+
attribute.type !== "JSXAttribute" ||
|
|
15
|
+
attribute.name.type !== "JSXIdentifier" ||
|
|
16
|
+
attribute.name.name !== "type"
|
|
17
|
+
) {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
const value =
|
|
21
|
+
attribute.value?.type === "JSXExpressionContainer"
|
|
22
|
+
? attribute.value.expression
|
|
23
|
+
: attribute.value;
|
|
24
|
+
return value?.type === "Literal" && value.value === "application/ld+json";
|
|
25
|
+
});
|
|
26
|
+
}
|
|
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
|
+
|
|
68
|
+
module.exports = rule(
|
|
69
|
+
{
|
|
70
|
+
type: "problem",
|
|
71
|
+
docs: { description: "prefer next/script over raw script JSX tags", recommended: false },
|
|
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
|
+
],
|
|
82
|
+
messages: { script: "Use next/script instead of a raw <script> tag." },
|
|
83
|
+
},
|
|
84
|
+
(context) => {
|
|
85
|
+
const options = context.options[0] || {};
|
|
86
|
+
const patterns = allowedIdPatterns(options);
|
|
87
|
+
let isNextFile = isNextPath(context.filename);
|
|
88
|
+
return {
|
|
89
|
+
ImportDeclaration(node) {
|
|
90
|
+
if (typeof node.source.value === "string" && node.source.value.startsWith("next/")) {
|
|
91
|
+
isNextFile = true;
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
JSXOpeningElement(node) {
|
|
95
|
+
if (!isNextFile) return;
|
|
96
|
+
if (node.name.type !== "JSXIdentifier" || node.name.name !== "script") return;
|
|
97
|
+
if (isJsonLdScript(node)) return;
|
|
98
|
+
if (isAllowedInlineScript(node, options, patterns)) return;
|
|
99
|
+
context.report({ node, messageId: "script" });
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
},
|
|
103
|
+
);
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { rule } = require("../helpers");
|
|
4
|
+
|
|
5
|
+
function unwrapExpression(node) {
|
|
6
|
+
let current = node;
|
|
7
|
+
while (
|
|
8
|
+
current &&
|
|
9
|
+
["ChainExpression", "TSAsExpression", "TSNonNullExpression", "TSTypeAssertion"].includes(
|
|
10
|
+
current.type,
|
|
11
|
+
)
|
|
12
|
+
) {
|
|
13
|
+
current = current.expression;
|
|
14
|
+
}
|
|
15
|
+
return current;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
module.exports = rule(
|
|
19
|
+
{
|
|
20
|
+
type: "problem",
|
|
21
|
+
docs: { description: "disallow deleting object properties", recommended: false },
|
|
22
|
+
schema: [],
|
|
23
|
+
messages: {
|
|
24
|
+
delete:
|
|
25
|
+
"Avoid deleting object properties. Use an omitted copy or an explicit nullable value so object shape changes stay traceable.",
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
(context) => ({
|
|
29
|
+
UnaryExpression(node) {
|
|
30
|
+
if (node.operator !== "delete") return;
|
|
31
|
+
const argument = unwrapExpression(node.argument);
|
|
32
|
+
if (argument?.type !== "MemberExpression") return;
|
|
33
|
+
context.report({ node, messageId: "delete" });
|
|
34
|
+
},
|
|
35
|
+
}),
|
|
36
|
+
);
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { rule } = require("../helpers");
|
|
4
|
+
|
|
5
|
+
const TEST_FILE_PATTERN = /\.(?:mock\.)?(?:test|spec)\.[cm]?[jt]sx?$/;
|
|
6
|
+
const TEST_IMPORT_PATTERN = /\.(?:mock\.)?(?:test|spec)(?:\.[cm]?[jt]sx?)?$/;
|
|
7
|
+
|
|
8
|
+
function isTestFile(filename) {
|
|
9
|
+
return TEST_FILE_PATTERN.test(filename.replace(/\\/g, "/"));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function isTestImportSource(source) {
|
|
13
|
+
return typeof source === "string" && TEST_IMPORT_PATTERN.test(source);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function isTestRequire(statement) {
|
|
17
|
+
if (
|
|
18
|
+
statement.type === "ExpressionStatement" &&
|
|
19
|
+
statement.expression.type === "CallExpression" &&
|
|
20
|
+
statement.expression.callee.type === "Identifier" &&
|
|
21
|
+
statement.expression.callee.name === "require" &&
|
|
22
|
+
isTestImportSource(statement.expression.arguments[0]?.value)
|
|
23
|
+
) {
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
return (
|
|
27
|
+
statement.type === "VariableDeclaration" &&
|
|
28
|
+
statement.declarations.length > 0 &&
|
|
29
|
+
statement.declarations.every(
|
|
30
|
+
(declaration) =>
|
|
31
|
+
declaration.init?.type === "CallExpression" &&
|
|
32
|
+
declaration.init.callee.type === "Identifier" &&
|
|
33
|
+
declaration.init.callee.name === "require" &&
|
|
34
|
+
isTestImportSource(declaration.init.arguments[0]?.value),
|
|
35
|
+
)
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function isTestImport(statement) {
|
|
40
|
+
return statement.type === "ImportDeclaration" && isTestImportSource(statement.source?.value);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
module.exports = rule(
|
|
44
|
+
{
|
|
45
|
+
type: "problem",
|
|
46
|
+
docs: {
|
|
47
|
+
description: "disallow aggregate test files that only import tests",
|
|
48
|
+
recommended: false,
|
|
49
|
+
},
|
|
50
|
+
schema: [],
|
|
51
|
+
messages: {
|
|
52
|
+
aggregate:
|
|
53
|
+
"Do not create aggregate test files that only import other test files; let the test runner discover those files directly.",
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
(context) => ({
|
|
57
|
+
Program(node) {
|
|
58
|
+
if (!isTestFile(context.filename) || node.body.length === 0) return;
|
|
59
|
+
const statements = node.body.filter((statement) => statement.directive === undefined);
|
|
60
|
+
if (statements.length === 0) return;
|
|
61
|
+
if (!statements.every((statement) => isTestImport(statement) || isTestRequire(statement)))
|
|
62
|
+
return;
|
|
63
|
+
context.report({ node, messageId: "aggregate" });
|
|
64
|
+
},
|
|
65
|
+
}),
|
|
66
|
+
);
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { rule } = require("../helpers");
|
|
4
|
+
|
|
5
|
+
function specifierName(specifier) {
|
|
6
|
+
return specifier.local?.name || specifier.exported?.name || specifier.exported?.value;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
module.exports = rule(
|
|
10
|
+
{
|
|
11
|
+
type: "problem",
|
|
12
|
+
docs: { description: "disallow exported never placeholder type aliases", recommended: false },
|
|
13
|
+
schema: [],
|
|
14
|
+
messages: {
|
|
15
|
+
placeholder:
|
|
16
|
+
"Do not export placeholder never types. Define the real type or remove the export.",
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
(context) => {
|
|
20
|
+
const neverTypes = new Set();
|
|
21
|
+
const exportSpecifiers = [];
|
|
22
|
+
return {
|
|
23
|
+
"Program > TSTypeAliasDeclaration"(node) {
|
|
24
|
+
if (node.typeAnnotation?.type === "TSNeverKeyword") neverTypes.add(node.id.name);
|
|
25
|
+
},
|
|
26
|
+
"Program:exit"() {
|
|
27
|
+
for (const node of exportSpecifiers) {
|
|
28
|
+
if (node.specifiers.some((specifier) => neverTypes.has(specifierName(specifier)))) {
|
|
29
|
+
context.report({ node, messageId: "placeholder" });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
ExportNamedDeclaration(node) {
|
|
34
|
+
const declaration = node.declaration;
|
|
35
|
+
if (declaration?.type === "TSTypeAliasDeclaration") {
|
|
36
|
+
if (declaration.typeAnnotation?.type !== "TSNeverKeyword") return;
|
|
37
|
+
context.report({ node, messageId: "placeholder" });
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (
|
|
41
|
+
!node.source &&
|
|
42
|
+
node.specifiers?.some(
|
|
43
|
+
(specifier) => node.exportKind === "type" || specifier.exportKind === "type",
|
|
44
|
+
)
|
|
45
|
+
) {
|
|
46
|
+
exportSpecifiers.push(node);
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
},
|
|
51
|
+
);
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { rule } = require("../helpers");
|
|
4
|
+
|
|
5
|
+
const TEST_NAMES = new Set(["test", "it", "describe"]);
|
|
6
|
+
const TEST_FILE_PATTERN = /\.(?:test|spec)\.[cm]?[jt]sx?$/;
|
|
7
|
+
|
|
8
|
+
function isTestFile(filename) {
|
|
9
|
+
return TEST_FILE_PATTERN.test(filename.replace(/\\/g, "/"));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function propertyName(node, computed = false) {
|
|
13
|
+
if (node.type === "Literal") return String(node.value);
|
|
14
|
+
if (computed) return null;
|
|
15
|
+
return node.name;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function rootTestName(node) {
|
|
19
|
+
if (node?.type === "Identifier") return node.name;
|
|
20
|
+
if (node?.type === "MemberExpression") return rootTestName(node.object);
|
|
21
|
+
if (node?.type === "CallExpression") return rootTestName(node.callee);
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function hasSequentialMember(node) {
|
|
26
|
+
if (node?.type === "MemberExpression") {
|
|
27
|
+
return (
|
|
28
|
+
propertyName(node.property, node.computed) === "sequential" ||
|
|
29
|
+
hasSequentialMember(node.object)
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
if (node?.type === "CallExpression") return hasSequentialMember(node.callee);
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function hasSequentialOption(node) {
|
|
37
|
+
return node.arguments
|
|
38
|
+
.slice(1)
|
|
39
|
+
.some(
|
|
40
|
+
(argument) =>
|
|
41
|
+
argument.type === "ObjectExpression" &&
|
|
42
|
+
argument.properties.some(
|
|
43
|
+
(property) =>
|
|
44
|
+
property.type === "Property" &&
|
|
45
|
+
propertyName(property.key) === "sequential" &&
|
|
46
|
+
property.value.type === "Literal" &&
|
|
47
|
+
property.value.value === true,
|
|
48
|
+
),
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
module.exports = rule(
|
|
53
|
+
{
|
|
54
|
+
type: "problem",
|
|
55
|
+
docs: { description: "disallow Vitest sequential test modifiers", recommended: false },
|
|
56
|
+
schema: [],
|
|
57
|
+
messages: { sequential: "Use parallel tests instead of .sequential." },
|
|
58
|
+
},
|
|
59
|
+
(context) => {
|
|
60
|
+
let usesVitestImport = false;
|
|
61
|
+
return {
|
|
62
|
+
ImportDeclaration(node) {
|
|
63
|
+
if (node.source.value === "vitest") usesVitestImport = true;
|
|
64
|
+
},
|
|
65
|
+
CallExpression(node) {
|
|
66
|
+
if (!isTestFile(context.filename) && !usesVitestImport) return;
|
|
67
|
+
if (!TEST_NAMES.has(rootTestName(node.callee))) return;
|
|
68
|
+
if (node.callee.type === "MemberExpression" && hasSequentialMember(node.callee)) {
|
|
69
|
+
context.report({ node: node.callee, messageId: "sequential" });
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (!hasSequentialOption(node)) return;
|
|
73
|
+
context.report({ node: node.callee, messageId: "sequential" });
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
},
|
|
77
|
+
);
|