eslint-plugin-no-mistakes 0.6.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.
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+
3
+ const { isFetchCall, isStaticString, rule } = require("../helpers");
4
+
5
+ module.exports = rule(
6
+ {
7
+ type: "problem",
8
+ docs: {
9
+ description: "require static fetch() URL arguments",
10
+ recommended: true,
11
+ },
12
+ schema: [],
13
+ messages: {
14
+ dynamic:
15
+ "fetch() URL must be a string literal or an expression-free template literal so it can be statically analyzed.",
16
+ },
17
+ },
18
+ (context) => ({
19
+ CallExpression(node) {
20
+ if (!isFetchCall(node, context)) return;
21
+ const url = node.arguments[0];
22
+ if (!isStaticString(url)) {
23
+ context.report({ node: url ?? node, messageId: "dynamic" });
24
+ }
25
+ },
26
+ }),
27
+ );
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+
3
+ const { canonicalAttribute, options, rule } = require("../helpers");
4
+ const { selectorAttributeVisitors } = require("../selector-visitor");
5
+
6
+ module.exports = rule(
7
+ {
8
+ type: "suggestion",
9
+ docs: { description: "require a canonical test ID attribute", recommended: false },
10
+ schema: [
11
+ {
12
+ type: "object",
13
+ properties: {
14
+ selectorAttributes: { type: "array", items: { type: "string" } },
15
+ canonicalAttribute: { type: "string" },
16
+ },
17
+ additionalProperties: false,
18
+ },
19
+ ],
20
+ messages: { attribute: "Use '{{expected}}' instead of '{{actual}}' for test IDs." },
21
+ },
22
+ (context) =>
23
+ selectorAttributeVisitors(context, (node, name) => {
24
+ const expected = canonicalAttribute(options(context));
25
+ if (name !== expected) {
26
+ context.report({
27
+ node: node.name,
28
+ messageId: "attribute",
29
+ data: { actual: name, expected },
30
+ });
31
+ }
32
+ }),
33
+ );
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+
3
+ const { rule, selectorValueNode } = require("../helpers");
4
+ const { isDefaultedPropReference } = require("../defaulted-props");
5
+ const { selectorAttributeVisitors } = require("../selector-visitor");
6
+
7
+ module.exports = rule(
8
+ {
9
+ type: "problem",
10
+ docs: {
11
+ description: "require literal defaults for prop-passed test IDs",
12
+ recommended: true,
13
+ },
14
+ schema: [
15
+ {
16
+ type: "object",
17
+ properties: { selectorAttributes: { type: "array", items: { type: "string" } } },
18
+ additionalProperties: false,
19
+ },
20
+ ],
21
+ messages: {
22
+ default: "Test ID prop passthrough must have a literal default.",
23
+ },
24
+ },
25
+ (context) =>
26
+ selectorAttributeVisitors(context, (node) => {
27
+ const valueNode = selectorValueNode(node);
28
+ if (valueNode?.type === "Identifier" && !isDefaultedPropReference(valueNode, context)) {
29
+ context.report({ node, messageId: "default" });
30
+ }
31
+ }),
32
+ );
@@ -0,0 +1,64 @@
1
+ "use strict";
2
+
3
+ const {
4
+ attributeName,
5
+ callMethodName,
6
+ isSelectorAttribute,
7
+ options,
8
+ rule,
9
+ selectorAttributes,
10
+ selectorValueNode,
11
+ } = require("../helpers");
12
+ const { isLiteralLike } = require("../defaulted-props");
13
+
14
+ module.exports = rule(
15
+ {
16
+ type: "problem",
17
+ docs: {
18
+ description: "require literal Playwright test IDs",
19
+ recommended: true,
20
+ },
21
+ schema: [
22
+ {
23
+ type: "object",
24
+ properties: {
25
+ selectorAttributes: { type: "array", items: { type: "string" } },
26
+ allowDefaultedProps: { type: "boolean" },
27
+ allowStaticTemplates: { type: "boolean" },
28
+ },
29
+ additionalProperties: false,
30
+ },
31
+ ],
32
+ messages: {
33
+ literal: "Use a literal test ID, a static template, or a prop with a literal default.",
34
+ },
35
+ },
36
+ (context) => {
37
+ const opts = {
38
+ allowDefaultedProps: options(context).allowDefaultedProps !== false,
39
+ allowStaticTemplates: options(context).allowStaticTemplates === true,
40
+ };
41
+ const attrs = selectorAttributes(options(context));
42
+ return {
43
+ JSXAttribute(node) {
44
+ const name = attributeName(node);
45
+ if (!name || !isSelectorAttribute(name, attrs)) {
46
+ return;
47
+ }
48
+ const valueNode = selectorValueNode(node);
49
+ if (!valueNode || !isLiteralLike(valueNode, opts, context)) {
50
+ context.report({ node, messageId: "literal" });
51
+ }
52
+ },
53
+ CallExpression(node) {
54
+ if (callMethodName(node) !== "getByTestId") {
55
+ return;
56
+ }
57
+ const arg = node.arguments[0];
58
+ if (!arg || !isLiteralLike(arg, opts, context)) {
59
+ context.report({ node: arg || node, messageId: "literal" });
60
+ }
61
+ },
62
+ };
63
+ },
64
+ );
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+
3
+ const { DEFAULT_NAMING_PATTERN, options, rule, selectorLiteral } = require("../helpers");
4
+ const { selectorAttributeVisitors } = require("../selector-visitor");
5
+
6
+ module.exports = rule(
7
+ {
8
+ type: "suggestion",
9
+ docs: { description: "require a naming convention for literal test IDs", recommended: false },
10
+ schema: [
11
+ {
12
+ type: "object",
13
+ properties: {
14
+ selectorAttributes: { type: "array", items: { type: "string" } },
15
+ pattern: { type: "string" },
16
+ },
17
+ additionalProperties: false,
18
+ },
19
+ ],
20
+ messages: { naming: "Test ID '{{value}}' does not match {{pattern}}." },
21
+ },
22
+ (context) => {
23
+ const pattern = options(context).pattern || DEFAULT_NAMING_PATTERN;
24
+ const regex = new RegExp(pattern);
25
+ return selectorAttributeVisitors(context, (node) => {
26
+ const value = selectorLiteral(node);
27
+ if (value !== null && !regex.test(value)) {
28
+ context.report({ node, messageId: "naming", data: { value, pattern } });
29
+ }
30
+ });
31
+ },
32
+ );
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+
3
+ const { rule, selectorLiteral } = require("../helpers");
4
+ const { selectorAttributeVisitors } = require("../selector-visitor");
5
+
6
+ module.exports = rule(
7
+ {
8
+ type: "problem",
9
+ docs: { description: "disallow empty test IDs", recommended: true },
10
+ schema: [
11
+ {
12
+ type: "object",
13
+ properties: { selectorAttributes: { type: "array", items: { type: "string" } } },
14
+ additionalProperties: false,
15
+ },
16
+ ],
17
+ messages: { empty: "Test ID must not be empty." },
18
+ },
19
+ (context) =>
20
+ selectorAttributeVisitors(context, (node) => {
21
+ if (selectorLiteral(node) === "") {
22
+ context.report({ node, messageId: "empty" });
23
+ }
24
+ }),
25
+ );
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+
3
+ const {
4
+ callMethodName,
5
+ cssSelectorValues,
6
+ isSelectorCall,
7
+ literalString,
8
+ options,
9
+ rule,
10
+ selectorAttributes,
11
+ } = require("../helpers");
12
+
13
+ module.exports = rule(
14
+ {
15
+ type: "suggestion",
16
+ docs: { description: "prefer getByTestId over CSS test-id selectors", recommended: false },
17
+ schema: [
18
+ {
19
+ type: "object",
20
+ properties: { selectorAttributes: { type: "array", items: { type: "string" } } },
21
+ additionalProperties: false,
22
+ },
23
+ ],
24
+ messages: { prefer: "Prefer getByTestId('{{value}}') for exact test ID selectors." },
25
+ },
26
+ (context) => {
27
+ const attrs = selectorAttributes(options(context));
28
+ return {
29
+ CallExpression(node) {
30
+ const methodName = callMethodName(node);
31
+ if (!isSelectorCall(node) || methodName === "getByTestId") {
32
+ return;
33
+ }
34
+ for (const arg of node.arguments.slice(0, methodName === "dragAndDrop" ? 2 : 1)) {
35
+ const source = literalString(arg);
36
+ if (typeof source !== "string" || source === "") {
37
+ continue;
38
+ }
39
+ for (const selector of cssSelectorValues(source, attrs)) {
40
+ if (selector.operator === "=") {
41
+ context.report({ node: arg, messageId: "prefer", data: { value: selector.value } });
42
+ }
43
+ }
44
+ }
45
+ },
46
+ };
47
+ },
48
+ );
@@ -0,0 +1,69 @@
1
+ "use strict";
2
+
3
+ const {
4
+ attributeName,
5
+ INTERACTIVE_ELEMENTS,
6
+ isSelectorAttribute,
7
+ options,
8
+ rule,
9
+ selectorAttributes,
10
+ selectorLiteral,
11
+ } = require("../helpers");
12
+
13
+ module.exports = rule(
14
+ {
15
+ type: "suggestion",
16
+ docs: { description: "require test IDs on interactive JSX elements", recommended: false },
17
+ schema: [
18
+ {
19
+ type: "object",
20
+ properties: { selectorAttributes: { type: "array", items: { type: "string" } } },
21
+ additionalProperties: false,
22
+ },
23
+ ],
24
+ messages: { missing: "Interactive elements should have a test ID." },
25
+ },
26
+ (context) => {
27
+ const attrs = selectorAttributes(options(context));
28
+ return {
29
+ JSXOpeningElement(node) {
30
+ const elementName = node.name.type === "JSXIdentifier" ? node.name.name : null;
31
+ const hasSelector = node.attributes.some((attr) =>
32
+ isSelectorAttribute(attributeName(attr), attrs),
33
+ );
34
+ if (hasSelector || !isInteractiveElement(elementName, node.attributes)) {
35
+ return;
36
+ }
37
+ context.report({ node: node.name, messageId: "missing" });
38
+ },
39
+ };
40
+ },
41
+ );
42
+
43
+ function isInteractiveElement(elementName, attributes) {
44
+ if (INTERACTIVE_ELEMENTS.has(elementName)) {
45
+ return true;
46
+ }
47
+ if (elementName === "a" && attributes.some((attr) => attributeName(attr) === "href")) {
48
+ return true;
49
+ }
50
+ return attributes.some((attr) => {
51
+ if (attributeName(attr) === "onClick") {
52
+ return true;
53
+ }
54
+ if (attributeName(attr) !== "role") {
55
+ return false;
56
+ }
57
+ return [
58
+ "button",
59
+ "checkbox",
60
+ "link",
61
+ "menuitem",
62
+ "option",
63
+ "radio",
64
+ "switch",
65
+ "tab",
66
+ "textbox",
67
+ ].includes(selectorLiteral(attr));
68
+ });
69
+ }
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+
3
+ const { rule, selectorLiteral } = require("../helpers");
4
+ const { selectorAttributeVisitors } = require("../selector-visitor");
5
+
6
+ module.exports = rule(
7
+ {
8
+ type: "problem",
9
+ docs: {
10
+ description: "require unique literal test IDs within a file",
11
+ recommended: true,
12
+ },
13
+ schema: [
14
+ {
15
+ type: "object",
16
+ properties: { selectorAttributes: { type: "array", items: { type: "string" } } },
17
+ additionalProperties: false,
18
+ },
19
+ ],
20
+ messages: {
21
+ duplicate: "Test ID '{{value}}' is already used in this file.",
22
+ },
23
+ },
24
+ (context) => {
25
+ const seen = new Map();
26
+ return selectorAttributeVisitors(context, (node) => {
27
+ const value = selectorLiteral(node);
28
+ if (value === null) {
29
+ return;
30
+ }
31
+ if (seen.has(value)) {
32
+ context.report({ node, messageId: "duplicate", data: { value } });
33
+ } else {
34
+ seen.set(value, node);
35
+ }
36
+ });
37
+ },
38
+ );
@@ -0,0 +1,193 @@
1
+ "use strict";
2
+
3
+ const { rule } = require("../helpers");
4
+ const { createReactNodeFacts, keyName, typeAnnotation, typeName } = require("../react-node-types");
5
+
6
+ function isObjectPattern(node) {
7
+ return node && node.type === "ObjectPattern";
8
+ }
9
+
10
+ function isIdentifier(node) {
11
+ return node && node.type === "Identifier";
12
+ }
13
+
14
+ function definePattern(pattern, props, defineBinding, defineReactNode) {
15
+ if (!isObjectPattern(pattern)) return;
16
+ for (const property of pattern.properties || []) {
17
+ if (property.type !== "Property") continue;
18
+ const name = keyName(property.key);
19
+ if (isIdentifier(property.value)) {
20
+ defineBinding(property.value.name);
21
+ if (name && props && props.has(name)) defineReactNode(property.value.name);
22
+ } else if (property.value.type === "AssignmentPattern" && isIdentifier(property.value.left)) {
23
+ defineBinding(property.value.left.name);
24
+ if (name && props && props.has(name)) defineReactNode(property.value.left.name);
25
+ }
26
+ }
27
+ }
28
+
29
+ module.exports = rule(
30
+ {
31
+ type: "problem",
32
+ docs: {
33
+ description: "disallow nullish coalescing on ReactNode-like values",
34
+ recommended: true,
35
+ },
36
+ schema: [],
37
+ messages: {
38
+ nullish:
39
+ "Do not use ?? with ReactNode values. React renders null, false, and empty values differently; use an explicit undefined check for fallbacks.",
40
+ },
41
+ },
42
+ (context) => {
43
+ const scopes = [];
44
+ let facts = { aliases: new Map(), objectProps: new Map(), reactNodeNames: new Set() };
45
+
46
+ function currentScope() {
47
+ return scopes[scopes.length - 1];
48
+ }
49
+
50
+ function pushScope(kind) {
51
+ scopes.push({ bindings: new Set(), kind, objectTypes: new Map(), reactNodes: new Set() });
52
+ }
53
+
54
+ function popScope() {
55
+ scopes.pop();
56
+ }
57
+
58
+ function isReactNodeType(type) {
59
+ const name = typeName(type);
60
+ return Boolean(name && (facts.reactNodeNames.has(name) || facts.aliases.get(name) === true));
61
+ }
62
+
63
+ function propsForType(type) {
64
+ if (type && type.type === "TSTypeLiteral") {
65
+ const props = new Set();
66
+ for (const member of type.members || []) {
67
+ if (member.type !== "TSPropertySignature" || !isReactNodeType(typeAnnotation(member))) {
68
+ continue;
69
+ }
70
+ const name = keyName(member.key);
71
+ if (name) props.add(name);
72
+ }
73
+ return props;
74
+ }
75
+ const name = typeName(type);
76
+ return name ? facts.objectProps.get(name) : null;
77
+ }
78
+
79
+ function defineReactNode(name, scope = currentScope()) {
80
+ scope.bindings.add(name);
81
+ scope.reactNodes.add(name);
82
+ }
83
+
84
+ function defineBinding(name, scope = currentScope()) {
85
+ scope.bindings.add(name);
86
+ }
87
+
88
+ function defineObjectType(name, type, scope = currentScope()) {
89
+ scope.bindings.add(name);
90
+ const props = propsForType(type);
91
+ if (props && props.size > 0) {
92
+ scope.objectTypes.set(name, props);
93
+ }
94
+ }
95
+
96
+ function variableScope(node) {
97
+ if (!node.parent || node.parent.kind !== "var") return currentScope();
98
+ return (
99
+ scopes.findLast((scope) => scope.kind === "function" || scope.kind === "program") ||
100
+ currentScope()
101
+ );
102
+ }
103
+
104
+ function variableIsReactNode(name) {
105
+ for (let index = scopes.length - 1; index >= 0; index -= 1) {
106
+ if (scopes[index].bindings.has(name)) {
107
+ return scopes[index].reactNodes.has(name);
108
+ }
109
+ }
110
+ return false;
111
+ }
112
+
113
+ function objectProps(name) {
114
+ for (let index = scopes.length - 1; index >= 0; index -= 1) {
115
+ if (scopes[index].bindings.has(name) && !scopes[index].objectTypes.has(name)) return null;
116
+ const props = scopes[index].objectTypes.get(name);
117
+ if (props) return props;
118
+ }
119
+ return null;
120
+ }
121
+
122
+ function defineParam(param) {
123
+ const target = param.type === "AssignmentPattern" ? param.left : param;
124
+ const type = typeAnnotation(param) || typeAnnotation(target);
125
+ if (isIdentifier(target)) {
126
+ currentScope().bindings.add(target.name);
127
+ if (isReactNodeType(type)) defineReactNode(target.name);
128
+ defineObjectType(target.name, type);
129
+ } else if (isObjectPattern(target)) {
130
+ definePattern(target, propsForType(type), defineBinding, defineReactNode);
131
+ }
132
+ }
133
+
134
+ function defineVariable(node) {
135
+ const scope = variableScope(node);
136
+ const type = typeAnnotation(node.id);
137
+ if (isIdentifier(node.id)) {
138
+ scope.bindings.add(node.id.name);
139
+ if (isReactNodeType(type)) defineReactNode(node.id.name, scope);
140
+ defineObjectType(node.id.name, type, scope);
141
+ } else if (isObjectPattern(node.id)) {
142
+ const initType = isIdentifier(node.init) ? objectProps(node.init.name) : null;
143
+ definePattern(
144
+ node.id,
145
+ propsForType(type) || initType,
146
+ (name) => defineBinding(name, scope),
147
+ (name) => defineReactNode(name, scope),
148
+ );
149
+ }
150
+ }
151
+
152
+ function expressionIsReactNode(node) {
153
+ if (node && node.type === "ChainExpression") return expressionIsReactNode(node.expression);
154
+ if (isIdentifier(node)) return variableIsReactNode(node.name);
155
+ if (node && node.type === "MemberExpression" && !node.computed && isIdentifier(node.object)) {
156
+ const props = objectProps(node.object.name);
157
+ return Boolean(props && props.has(keyName(node.property)));
158
+ }
159
+ return false;
160
+ }
161
+
162
+ function enterFunction(node) {
163
+ pushScope("function");
164
+ for (const param of node.params || []) {
165
+ defineParam(param);
166
+ }
167
+ }
168
+
169
+ return {
170
+ Program(node) {
171
+ facts = createReactNodeFacts(node);
172
+ pushScope("program");
173
+ },
174
+ "Program:exit": popScope,
175
+ FunctionDeclaration: enterFunction,
176
+ "FunctionDeclaration:exit": popScope,
177
+ FunctionExpression: enterFunction,
178
+ "FunctionExpression:exit": popScope,
179
+ ArrowFunctionExpression: enterFunction,
180
+ "ArrowFunctionExpression:exit": popScope,
181
+ BlockStatement() {
182
+ pushScope("block");
183
+ },
184
+ "BlockStatement:exit": popScope,
185
+ VariableDeclarator: defineVariable,
186
+ LogicalExpression(node) {
187
+ if (node.operator === "??" && expressionIsReactNode(node.left)) {
188
+ context.report({ node, messageId: "nullish" });
189
+ }
190
+ },
191
+ };
192
+ },
193
+ );
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+
3
+ const { rule } = require("../helpers");
4
+
5
+ function exportName(node) {
6
+ if (!node) return null;
7
+ if (node.type === "Identifier") return node.name;
8
+ return node.type === "Literal" ? String(node.value) : null;
9
+ }
10
+
11
+ function isTypeExport(node, specifier) {
12
+ return node.exportKind === "type" || specifier.exportKind === "type";
13
+ }
14
+
15
+ module.exports = rule(
16
+ {
17
+ type: "problem",
18
+ docs: {
19
+ description: "disallow value export renaming",
20
+ recommended: true,
21
+ },
22
+ schema: [],
23
+ messages: {
24
+ renamed:
25
+ "Do not rename value exports. Export the original name or rename the declaration itself so agents can trace symbols directly.",
26
+ },
27
+ },
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" });
38
+ }
39
+ }
40
+ },
41
+ }),
42
+ );