eslint-plugin-no-mistakes 0.6.0 → 0.7.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/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 +51 -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 +127 -0
- package/src/rules/test-no-shared-state-helpers.js +125 -0
- package/src/rules/test-no-shared-state.js +125 -0
- package/src/rules/vitest-mock-test-file-naming.js +100 -0
package/package.json
CHANGED
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,51 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { 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
|
+
module.exports = rule(
|
|
29
|
+
{
|
|
30
|
+
type: "problem",
|
|
31
|
+
docs: { description: "prefer next/script over raw script JSX tags", recommended: false },
|
|
32
|
+
schema: [],
|
|
33
|
+
messages: { script: "Use next/script instead of a raw <script> tag." },
|
|
34
|
+
},
|
|
35
|
+
(context) => {
|
|
36
|
+
let isNextFile = isNextPath(context.filename);
|
|
37
|
+
return {
|
|
38
|
+
ImportDeclaration(node) {
|
|
39
|
+
if (typeof node.source.value === "string" && node.source.value.startsWith("next/")) {
|
|
40
|
+
isNextFile = true;
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
JSXOpeningElement(node) {
|
|
44
|
+
if (!isNextFile) return;
|
|
45
|
+
if (node.name.type !== "JSXIdentifier" || node.name.name !== "script") return;
|
|
46
|
+
if (isJsonLdScript(node)) return;
|
|
47
|
+
context.report({ node, messageId: "script" });
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
},
|
|
51
|
+
);
|
|
@@ -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
|
+
);
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { rule } = require("../helpers");
|
|
4
|
+
|
|
5
|
+
const DEFAULT_MAX_TIMEOUT_MS = 10000;
|
|
6
|
+
const TIMEOUT_MATCHERS = new Set([
|
|
7
|
+
"poll",
|
|
8
|
+
"toBeAttached",
|
|
9
|
+
"toBeChecked",
|
|
10
|
+
"toBeDisabled",
|
|
11
|
+
"toBeEditable",
|
|
12
|
+
"toBeEmpty",
|
|
13
|
+
"toBeEnabled",
|
|
14
|
+
"toBeFocused",
|
|
15
|
+
"toBeHidden",
|
|
16
|
+
"toBeInViewport",
|
|
17
|
+
"toBeOK",
|
|
18
|
+
"toBeVisible",
|
|
19
|
+
"toContainText",
|
|
20
|
+
"toHaveAttribute",
|
|
21
|
+
"toHaveClass",
|
|
22
|
+
"toHaveCount",
|
|
23
|
+
"toHaveCSS",
|
|
24
|
+
"toHaveId",
|
|
25
|
+
"toHaveJSProperty",
|
|
26
|
+
"toHaveRole",
|
|
27
|
+
"toHaveScreenshot",
|
|
28
|
+
"toHaveText",
|
|
29
|
+
"toHaveTitle",
|
|
30
|
+
"toHaveURL",
|
|
31
|
+
"toHaveValue",
|
|
32
|
+
"toHaveValues",
|
|
33
|
+
]);
|
|
34
|
+
const PLAYWRIGHT_PATH_PATTERN =
|
|
35
|
+
/(?:^|[/\\])(?:e2e|playwright)(?:[/\\]|$)|(?:^|[/\\])e2e\.(?:spec|test)\.[cm]?[jt]sx?$|\.pw\.(?:spec|test)\.[cm]?[jt]sx?$/;
|
|
36
|
+
|
|
37
|
+
function isPlaywrightPath(filename) {
|
|
38
|
+
return PLAYWRIGHT_PATH_PATTERN.test(filename.replace(/\\/g, "/"));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function isExpectChain(node) {
|
|
42
|
+
let current = node;
|
|
43
|
+
while (current) {
|
|
44
|
+
if (current.type === "Identifier") return current.name === "expect";
|
|
45
|
+
current = current.type === "CallExpression" ? current.callee : current.object;
|
|
46
|
+
}
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function numericLiteral(node) {
|
|
51
|
+
return node?.type === "Literal" && typeof node.value === "number" ? node.value : null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function propertyName(node) {
|
|
55
|
+
if (!node) return null;
|
|
56
|
+
if (node.type === "Identifier") return node.name;
|
|
57
|
+
return node.type === "Literal" ? String(node.value) : null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
module.exports = rule(
|
|
61
|
+
{
|
|
62
|
+
type: "problem",
|
|
63
|
+
docs: { description: "cap Playwright assertion timeouts", recommended: false },
|
|
64
|
+
schema: [
|
|
65
|
+
{
|
|
66
|
+
type: "object",
|
|
67
|
+
properties: { max: { type: "number" } },
|
|
68
|
+
additionalProperties: false,
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
messages: {
|
|
72
|
+
timeout:
|
|
73
|
+
"Assertion timeout must not exceed {{max}} ms. Increase the test timeout or fix the slow condition instead.",
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
(context) => {
|
|
77
|
+
const max = context.options[0]?.max ?? DEFAULT_MAX_TIMEOUT_MS;
|
|
78
|
+
let isPlaywrightFile = isPlaywrightPath(context.filename);
|
|
79
|
+
return {
|
|
80
|
+
ImportDeclaration(node) {
|
|
81
|
+
if (node.source.value === "@playwright/test") isPlaywrightFile = true;
|
|
82
|
+
},
|
|
83
|
+
CallExpression(node) {
|
|
84
|
+
if (!isPlaywrightFile) return;
|
|
85
|
+
if (node.callee.type !== "MemberExpression" || !isExpectChain(node.callee)) return;
|
|
86
|
+
const method = propertyName(node.callee.property);
|
|
87
|
+
if (!TIMEOUT_MATCHERS.has(method)) return;
|
|
88
|
+
const options = node.arguments.at(-1);
|
|
89
|
+
if (options?.type !== "ObjectExpression") return;
|
|
90
|
+
const timeout = options.properties.find(
|
|
91
|
+
(property) => property.type === "Property" && propertyName(property.key) === "timeout",
|
|
92
|
+
);
|
|
93
|
+
if (numericLiteral(timeout?.value) > max) {
|
|
94
|
+
context.report({ node, messageId: "timeout", data: { max } });
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
},
|
|
99
|
+
);
|