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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jonathan Ong
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,10 @@
1
+ # eslint-plugin-no-mistakes
2
+
3
+ ESLint and Oxlint rules that keep TS/JS, React, Next.js, and Playwright code
4
+ static enough for `no-mistakes` analyzers and AI agents.
5
+
6
+ ```sh
7
+ npm install --save-dev eslint-plugin-no-mistakes
8
+ ```
9
+
10
+ See [docs/eslint-plugin.md](../../docs/eslint-plugin.md).
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "eslint-plugin-no-mistakes",
3
+ "version": "0.6.0",
4
+ "description": "ESLint and Oxlint rules for deterministic no-mistakes code analysis",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/jonathanong/no-mistakes.git",
9
+ "directory": "packages/eslint-plugin-no-mistakes"
10
+ },
11
+ "files": [
12
+ "src/",
13
+ "README.md",
14
+ "LICENSE"
15
+ ],
16
+ "main": "src/index.js",
17
+ "publishConfig": {
18
+ "access": "public"
19
+ },
20
+ "scripts": {
21
+ "test": "vitest run --coverage"
22
+ },
23
+ "devDependencies": {
24
+ "@typescript-eslint/parser": "^8.48.0",
25
+ "@vitest/coverage-v8": "^4.1.6",
26
+ "eslint": "^10.3.0",
27
+ "oxlint": "^1.64.0",
28
+ "vitest": "^4.1.6"
29
+ },
30
+ "peerDependencies": {
31
+ "eslint": ">=9"
32
+ },
33
+ "engines": {
34
+ "node": "^20.19.0 || ^22.13.0 || >=24"
35
+ }
36
+ }
@@ -0,0 +1,209 @@
1
+ "use strict";
2
+
3
+ const { isStringLiteralNode, literalString, staticTemplate } = require("./helpers");
4
+ const EMPTY_FUNCTION = { params: [], body: null };
5
+
6
+ function isLiteralLike(node, opts, context) {
7
+ const value = literalString(node);
8
+ if (value !== null) {
9
+ return true;
10
+ }
11
+ if (opts.allowStaticTemplates && staticTemplate(node)) {
12
+ return true;
13
+ }
14
+ if (opts.allowDefaultedProps && isDefaultedPropReference(node, context)) {
15
+ return true;
16
+ }
17
+ return false;
18
+ }
19
+
20
+ function collectDefaultedProps(params) {
21
+ const props = new Set();
22
+ for (const param of params) {
23
+ collectPatternDefaults(param, props);
24
+ }
25
+ return props;
26
+ }
27
+
28
+ function collectPatternDefaults(pattern, props) {
29
+ if (pattern.type === "AssignmentPattern") {
30
+ collectDefaultName(pattern.left, props, isStringLiteralNode(pattern.right));
31
+ return;
32
+ }
33
+ if (pattern.type === "ObjectPattern") {
34
+ for (const prop of pattern.properties) {
35
+ collectObjectPropertyDefault(prop, props);
36
+ }
37
+ return;
38
+ }
39
+ if (pattern.type === "ArrayPattern") {
40
+ for (const element of pattern.elements) {
41
+ if (element) {
42
+ collectPatternDefaults(element, props);
43
+ }
44
+ }
45
+ }
46
+ }
47
+
48
+ function collectObjectPropertyDefault(prop, props) {
49
+ if (prop.type === "RestElement") {
50
+ return;
51
+ }
52
+ if (prop.value.type === "AssignmentPattern") {
53
+ collectDefaultName(prop.value.left, props, isStringLiteralNode(prop.value.right));
54
+ return;
55
+ }
56
+ collectPatternDefaults(prop.value, props);
57
+ }
58
+
59
+ function collectDefaultName(node, props, hasLiteralDefault) {
60
+ if (hasLiteralDefault && node.type === "Identifier") {
61
+ props.add(node.name);
62
+ }
63
+ if (node.type === "ObjectPattern") {
64
+ collectPatternDefaults(node, props);
65
+ }
66
+ }
67
+
68
+ function collectConstPatternDefaults(node, props) {
69
+ if (!node) {
70
+ return;
71
+ }
72
+ if (node.type === "VariableDeclaration") {
73
+ if (node.kind === "const") {
74
+ for (const declaration of node.declarations) {
75
+ if (declaration.id.type === "ObjectPattern" || declaration.id.type === "ArrayPattern") {
76
+ collectPatternDefaults(declaration.id, props);
77
+ }
78
+ }
79
+ }
80
+ return;
81
+ }
82
+ if (node.type !== "BlockStatement" && isFunctionNode(node)) {
83
+ return;
84
+ }
85
+ for (const child of constDefaultTraversalChildren(node)) {
86
+ collectConstPatternDefaults(child, props);
87
+ }
88
+ }
89
+
90
+ function patternHasLiteralDefault(pattern, name) {
91
+ if (pattern.type === "AssignmentPattern") {
92
+ return pattern.left.type === "Identifier"
93
+ ? pattern.left.name === name && isStringLiteralNode(pattern.right)
94
+ : patternHasLiteralDefault(pattern.left, name);
95
+ }
96
+ if (pattern.type !== "ObjectPattern") {
97
+ return (
98
+ pattern.type === "ArrayPattern" &&
99
+ pattern.elements.some((element) => element && patternHasLiteralDefault(element, name))
100
+ );
101
+ }
102
+ return pattern.properties.some((prop) => {
103
+ if (prop.type === "RestElement") {
104
+ return false;
105
+ }
106
+ return patternHasLiteralDefault(prop.value, name);
107
+ });
108
+ }
109
+
110
+ function constDefaultTraversalChildren(node) {
111
+ if (node.type === "BlockStatement") {
112
+ return node.body;
113
+ }
114
+ if (node.type === "IfStatement") {
115
+ return [node.consequent, node.alternate].filter(Boolean);
116
+ }
117
+ return [];
118
+ }
119
+
120
+ function isFunctionNode(node) {
121
+ return (
122
+ node.type === "FunctionDeclaration" ||
123
+ node.type === "FunctionExpression" ||
124
+ node.type === "ArrowFunctionExpression"
125
+ );
126
+ }
127
+
128
+ function nearestFunction(node) {
129
+ let current = node.parent;
130
+ while (current) {
131
+ if (isFunctionNode(current)) {
132
+ return current;
133
+ }
134
+ current = current.parent;
135
+ }
136
+ return null;
137
+ }
138
+
139
+ function defaultedPropsForNode(node) {
140
+ const fn = nearestFunction(node) || EMPTY_FUNCTION;
141
+ const props = collectDefaultedProps(fn.params);
142
+ collectConstPatternDefaults(fn.body, props);
143
+ return props;
144
+ }
145
+
146
+ function isDefaultedPropReference(node, context) {
147
+ if (node?.type !== "Identifier" || !defaultedPropsForNode(node).has(node.name)) {
148
+ return false;
149
+ }
150
+ const variable = findVariable(context.sourceCode.getScope(node), node.name);
151
+ return Boolean(
152
+ variable?.defs.some((def) => {
153
+ if (def.type === "Parameter") {
154
+ return hasLiteralParameterDefault(node, node.name);
155
+ }
156
+ return hasLiteralConstPatternDefault(def, node, context);
157
+ }),
158
+ );
159
+ }
160
+
161
+ function hasLiteralParameterDefault(node, name) {
162
+ return Boolean(
163
+ nearestFunction(node)?.params.some((param) => patternHasLiteralDefault(param, name)),
164
+ );
165
+ }
166
+
167
+ function hasLiteralConstPatternDefault(def, node, context) {
168
+ return (
169
+ def.type === "Variable" &&
170
+ def.parent?.kind === "const" &&
171
+ (def.node?.id?.type === "ObjectPattern" || def.node?.id?.type === "ArrayPattern") &&
172
+ patternHasLiteralDefault(def.node.id, node.name) &&
173
+ isAfterDeclaration(node, def.node) &&
174
+ isFunctionParameterSource(def.node.init, context)
175
+ );
176
+ }
177
+
178
+ function isAfterDeclaration(node, declaration) {
179
+ return node.range?.[0] >= declaration.range?.[1];
180
+ }
181
+
182
+ function isFunctionParameterSource(node, context) {
183
+ if (node?.type === "MemberExpression") {
184
+ return isFunctionParameterSource(node.object, context);
185
+ }
186
+ if (node?.type !== "Identifier" || !/props$/i.test(node.name)) {
187
+ return false;
188
+ }
189
+ const variable = findVariable(context.sourceCode.getScope(node), node.name);
190
+ return Boolean(variable?.defs.some((def) => def.type === "Parameter"));
191
+ }
192
+
193
+ function findVariable(scope, name) {
194
+ let variable = null;
195
+ for (let current = scope; current && variable === null; current = current.upper) {
196
+ variable = current.variables.find((item) => item.name === name) || null;
197
+ }
198
+ return variable;
199
+ }
200
+
201
+ module.exports = {
202
+ __test: {
203
+ findVariable,
204
+ patternHasLiteralDefault,
205
+ },
206
+ defaultedPropsForNode,
207
+ isDefaultedPropReference,
208
+ isLiteralLike,
209
+ };
package/src/helpers.js ADDED
@@ -0,0 +1,180 @@
1
+ "use strict";
2
+
3
+ const DEFAULT_SELECTOR_ATTRIBUTES = ["data-testid", "data-pw"];
4
+ const DEFAULT_NAMING_PATTERN = "^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$";
5
+ const INTERACTIVE_ELEMENTS = new Set(["button", "input", "select", "textarea"]);
6
+ const SELECTOR_METHODS = new Set([
7
+ "$",
8
+ "$$",
9
+ "$eval",
10
+ "$$eval",
11
+ "check",
12
+ "click",
13
+ "dblclick",
14
+ "dragAndDrop",
15
+ "fill",
16
+ "focus",
17
+ "frameLocator",
18
+ "getByTestId",
19
+ "hover",
20
+ "locator",
21
+ "press",
22
+ "selectOption",
23
+ "setInputFiles",
24
+ "tap",
25
+ "textContent",
26
+ "type",
27
+ "uncheck",
28
+ "waitForSelector",
29
+ ]);
30
+
31
+ function options(context) {
32
+ return context.options[0] || {};
33
+ }
34
+
35
+ function selectorAttributes(option) {
36
+ return option.selectorAttributes || DEFAULT_SELECTOR_ATTRIBUTES;
37
+ }
38
+
39
+ function canonicalAttribute(option) {
40
+ return option.canonicalAttribute || "data-pw";
41
+ }
42
+
43
+ function isSelectorAttribute(name, attrs) {
44
+ return attrs.includes(name);
45
+ }
46
+
47
+ function attributeName(attribute) {
48
+ if (!attribute || attribute.type !== "JSXAttribute") {
49
+ return null;
50
+ }
51
+ if (attribute.name.type !== "JSXIdentifier") {
52
+ return null;
53
+ }
54
+ return attribute.name.name;
55
+ }
56
+
57
+ function literalString(node) {
58
+ if (node.type === "Literal" && typeof node.value === "string") {
59
+ return node.value;
60
+ }
61
+ if (node.type === "TemplateLiteral" && node.expressions.length === 0) {
62
+ return node.quasis.map((quasi) => quasi.value.raw).join("");
63
+ }
64
+ return null;
65
+ }
66
+
67
+ function staticTemplate(node) {
68
+ if (node && node.type === "TemplateLiteral" && node.expressions.length > 0) {
69
+ return node.quasis.some((quasi) => quasi.value.raw.length > 0);
70
+ }
71
+ return false;
72
+ }
73
+
74
+ function jsxAttributeExpression(attribute) {
75
+ if (!attribute.value) {
76
+ return null;
77
+ }
78
+ if (attribute.value.type === "JSXExpressionContainer") {
79
+ return attribute.value.expression;
80
+ }
81
+ return attribute.value;
82
+ }
83
+
84
+ function selectorLiteral(attribute) {
85
+ const expression = jsxAttributeExpression(attribute);
86
+ if (!expression) {
87
+ return null;
88
+ }
89
+ return literalString(expression);
90
+ }
91
+
92
+ function callMethodName(node) {
93
+ if (node.callee.type === "MemberExpression" && !node.callee.computed) {
94
+ return node.callee.property.name;
95
+ }
96
+ if (node.callee.type === "Identifier") {
97
+ return node.callee.name;
98
+ }
99
+ return null;
100
+ }
101
+
102
+ function isSelectorCall(node) {
103
+ const name = callMethodName(node);
104
+ return Boolean(name && SELECTOR_METHODS.has(name));
105
+ }
106
+
107
+ function cssSelectorValues(source, attrs) {
108
+ const values = [];
109
+ for (const attr of attrs) {
110
+ const escaped = attr.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
111
+ const regex = new RegExp(
112
+ `\\[\\s*${escaped}\\s*([*^$]?=)\\s*(?:"([^"]*)"|'([^']*)'|([^\\s\\]]+))\\s*(?:[is])?\\s*\\]`,
113
+ "g",
114
+ );
115
+ let match = regex.exec(source);
116
+ while (match) {
117
+ values.push({ attribute: attr, operator: match[1], value: match[2] ?? match[3] ?? match[4] });
118
+ match = regex.exec(source);
119
+ }
120
+ }
121
+ return values;
122
+ }
123
+
124
+ function isStringLiteralNode(node) {
125
+ return literalString(node) !== null;
126
+ }
127
+
128
+ function isStaticString(node) {
129
+ return Boolean(node && isStringLiteralNode(node));
130
+ }
131
+
132
+ function selectorValueNode(attribute) {
133
+ const expression = jsxAttributeExpression(attribute);
134
+ return expression && expression.type !== "JSXEmptyExpression" ? expression : null;
135
+ }
136
+
137
+ const LOCAL_BINDING_TYPES = new Set(["Variable", "Parameter", "CatchClause", "FunctionName"]);
138
+
139
+ function isFetchShadowed(scope) {
140
+ while (scope) {
141
+ const variable = scope.variables.find((v) => v.name === "fetch");
142
+ if (variable) {
143
+ return variable.defs.some((def) => LOCAL_BINDING_TYPES.has(def.type));
144
+ }
145
+ scope = scope.upper;
146
+ }
147
+ return false;
148
+ }
149
+
150
+ function isFetchCall(node, context) {
151
+ if (node.callee.type !== "Identifier" || node.callee.name !== "fetch") {
152
+ return false;
153
+ }
154
+ return !isFetchShadowed(context.sourceCode.getScope(node));
155
+ }
156
+
157
+ function rule(meta, create) {
158
+ return { meta, create };
159
+ }
160
+
161
+ module.exports = {
162
+ DEFAULT_NAMING_PATTERN,
163
+ attributeName,
164
+ callMethodName,
165
+ canonicalAttribute,
166
+ cssSelectorValues,
167
+ INTERACTIVE_ELEMENTS,
168
+ isFetchCall,
169
+ isSelectorAttribute,
170
+ isSelectorCall,
171
+ isStringLiteralNode,
172
+ isStaticString,
173
+ literalString,
174
+ options,
175
+ rule,
176
+ selectorAttributes,
177
+ selectorLiteral,
178
+ selectorValueNode,
179
+ staticTemplate,
180
+ };
package/src/index.js ADDED
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+
3
+ const rules = {
4
+ "nextjs-static-fetch-method": require("./rules/nextjs-static-fetch-method"),
5
+ "nextjs-static-fetch-url": require("./rules/nextjs-static-fetch-url"),
6
+ "playwright-consistent-attribute": require("./rules/playwright-consistent-attribute"),
7
+ "playwright-defaults": require("./rules/playwright-defaults"),
8
+ "playwright-literals": require("./rules/playwright-literals"),
9
+ "playwright-naming-convention": require("./rules/playwright-naming-convention"),
10
+ "playwright-no-empty": require("./rules/playwright-no-empty"),
11
+ "playwright-prefer-get-by-test-id": require("./rules/playwright-prefer-get-by-test-id"),
12
+ "playwright-require-interactive-test-id": require("./rules/playwright-require-interactive-test-id"),
13
+ "playwright-unique": require("./rules/playwright-unique"),
14
+ "react-no-nullish-react-node": require("./rules/react-no-nullish-react-node"),
15
+ "ts-no-export-renaming": require("./rules/ts-no-export-renaming"),
16
+ "ts-no-function-aliases": require("./rules/ts-no-function-aliases"),
17
+ };
18
+
19
+ const plugin = {
20
+ meta: {
21
+ name: "eslint-plugin-no-mistakes",
22
+ version: require("../package.json").version,
23
+ },
24
+ rules,
25
+ configs: {},
26
+ };
27
+
28
+ plugin.configs.recommended = {
29
+ plugins: {
30
+ "no-mistakes": plugin,
31
+ },
32
+ rules: {
33
+ "no-mistakes/nextjs-static-fetch-method": "error",
34
+ "no-mistakes/nextjs-static-fetch-url": "error",
35
+ "no-mistakes/playwright-defaults": "error",
36
+ "no-mistakes/playwright-literals": "error",
37
+ "no-mistakes/playwright-no-empty": "error",
38
+ "no-mistakes/playwright-unique": "error",
39
+ "no-mistakes/react-no-nullish-react-node": "error",
40
+ "no-mistakes/ts-no-export-renaming": "error",
41
+ "no-mistakes/ts-no-function-aliases": "error",
42
+ },
43
+ };
44
+
45
+ plugin.configs.strict = {
46
+ plugins: {
47
+ "no-mistakes": plugin,
48
+ },
49
+ rules: {
50
+ ...plugin.configs.recommended.rules,
51
+ "no-mistakes/playwright-consistent-attribute": ["error", { canonicalAttribute: "data-pw" }],
52
+ "no-mistakes/playwright-naming-convention": "error",
53
+ "no-mistakes/playwright-prefer-get-by-test-id": "warn",
54
+ "no-mistakes/playwright-require-interactive-test-id": "warn",
55
+ },
56
+ };
57
+
58
+ module.exports = plugin;
@@ -0,0 +1,146 @@
1
+ "use strict";
2
+
3
+ function keyName(node) {
4
+ if (!node) return null;
5
+ if (node.type === "Identifier") return node.name;
6
+ return node.type === "Literal" ? String(node.value) : null;
7
+ }
8
+
9
+ function unwrapType(node) {
10
+ return node &&
11
+ (node.type === "TSParenthesizedType" ||
12
+ node.type === "TSOptionalType" ||
13
+ node.type === "TSRestType")
14
+ ? unwrapType(node.typeAnnotation)
15
+ : node;
16
+ }
17
+
18
+ function typeName(node) {
19
+ const current = unwrapType(node);
20
+ if (!current) return null;
21
+ if (current.type !== "TSTypeReference") return null;
22
+ const name = current.typeName;
23
+ if (name.type === "Identifier") return name.name;
24
+ return name.type === "TSQualifiedName" &&
25
+ name.left.type === "Identifier" &&
26
+ name.right.type === "Identifier"
27
+ ? `${name.left.name}.${name.right.name}`
28
+ : null;
29
+ }
30
+
31
+ function typeAnnotation(node) {
32
+ return node && node.typeAnnotation ? unwrapType(node.typeAnnotation.typeAnnotation) : null;
33
+ }
34
+
35
+ function createReactNodeFacts(program) {
36
+ const reactNodeNames = new Set(["React.ReactNode"]);
37
+ const aliases = new Map();
38
+ const objectProps = new Map();
39
+
40
+ function topLevelDeclaration(statement) {
41
+ return (statement.type === "ExportNamedDeclaration" ||
42
+ statement.type === "ExportDefaultDeclaration") &&
43
+ statement.declaration
44
+ ? statement.declaration
45
+ : statement;
46
+ }
47
+
48
+ function setChanged(previous, next) {
49
+ if (!previous || previous.size !== next.size) return true;
50
+ for (const value of next) {
51
+ if (!previous.has(value)) return true;
52
+ }
53
+ return false;
54
+ }
55
+
56
+ function heritageName(heritage) {
57
+ const expression = heritage && heritage.expression;
58
+ return expression && expression.type === "Identifier" ? expression.name : null;
59
+ }
60
+
61
+ function isReactNodeType(type) {
62
+ const name = typeName(type);
63
+ return Boolean(name && (reactNodeNames.has(name) || aliases.get(name) === true));
64
+ }
65
+
66
+ function collectMembers(members) {
67
+ const props = new Set();
68
+ for (const member of members || []) {
69
+ if (member.type !== "TSPropertySignature" || !isReactNodeType(typeAnnotation(member))) {
70
+ continue;
71
+ }
72
+ const name = keyName(member.key);
73
+ if (name) props.add(name);
74
+ }
75
+ return props;
76
+ }
77
+
78
+ for (const statement of program.body || []) {
79
+ if (statement.type !== "ImportDeclaration" || statement.source.value !== "react") continue;
80
+ for (const specifier of statement.specifiers || []) {
81
+ if (specifier.type === "ImportSpecifier" && keyName(specifier.imported) === "ReactNode") {
82
+ reactNodeNames.add(specifier.local.name);
83
+ }
84
+ if (
85
+ specifier.type === "ImportNamespaceSpecifier" ||
86
+ specifier.type === "ImportDefaultSpecifier"
87
+ ) {
88
+ reactNodeNames.add(`${specifier.local.name}.ReactNode`);
89
+ }
90
+ }
91
+ }
92
+
93
+ let changed = true;
94
+ while (changed) {
95
+ changed = false;
96
+ for (const statement of program.body || []) {
97
+ const declaration = topLevelDeclaration(statement);
98
+ if (declaration.type !== "TSTypeAliasDeclaration") continue;
99
+ const name = declaration.id.name;
100
+ if (!aliases.has(name) && isReactNodeType(declaration.typeAnnotation)) {
101
+ aliases.set(name, true);
102
+ changed = true;
103
+ }
104
+ }
105
+ }
106
+
107
+ let objectsChanged = true;
108
+ while (objectsChanged) {
109
+ objectsChanged = false;
110
+ for (const statement of program.body || []) {
111
+ const declaration = topLevelDeclaration(statement);
112
+ if (declaration.type === "TSInterfaceDeclaration") {
113
+ const props = collectMembers(declaration.body && declaration.body.body);
114
+ for (const heritage of declaration.extends || []) {
115
+ const inherited = objectProps.get(heritageName(heritage));
116
+ if (inherited) {
117
+ for (const prop of inherited) props.add(prop);
118
+ }
119
+ }
120
+ if (setChanged(objectProps.get(declaration.id.name), props)) {
121
+ objectProps.set(declaration.id.name, props);
122
+ objectsChanged = true;
123
+ }
124
+ }
125
+ if (
126
+ declaration.type === "TSTypeAliasDeclaration" &&
127
+ declaration.typeAnnotation.type === "TSTypeLiteral"
128
+ ) {
129
+ const props = collectMembers(declaration.typeAnnotation.members);
130
+ if (setChanged(objectProps.get(declaration.id.name), props)) {
131
+ objectProps.set(declaration.id.name, props);
132
+ objectsChanged = true;
133
+ }
134
+ }
135
+ }
136
+ }
137
+
138
+ return { aliases, objectProps, reactNodeNames };
139
+ }
140
+
141
+ module.exports = {
142
+ createReactNodeFacts,
143
+ keyName,
144
+ typeAnnotation,
145
+ typeName,
146
+ };
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+
3
+ const { isFetchCall, isStaticString, literalString, rule } = require("../helpers");
4
+
5
+ function isMethodKey(property) {
6
+ if (property.computed) return literalString(property.key) === "method";
7
+ return (
8
+ (property.key.type === "Identifier" && property.key.name === "method") ||
9
+ (property.key.type === "Literal" && property.key.value === "method")
10
+ );
11
+ }
12
+
13
+ module.exports = rule(
14
+ {
15
+ type: "problem",
16
+ docs: {
17
+ description: "require static fetch() method option",
18
+ recommended: true,
19
+ },
20
+ schema: [],
21
+ messages: {
22
+ dynamic:
23
+ "fetch() method option must be a string literal or an expression-free template literal so it can be statically analyzed.",
24
+ },
25
+ },
26
+ (context) => ({
27
+ CallExpression(node) {
28
+ if (!isFetchCall(node, context)) return;
29
+ const opts = node.arguments[1];
30
+ if (!opts || opts.type !== "ObjectExpression") return;
31
+ const methodProp = opts.properties.findLast((p) => p.type === "Property" && isMethodKey(p));
32
+ if (!methodProp) return;
33
+ if (!isStaticString(methodProp.value)) {
34
+ context.report({ node: methodProp.value, messageId: "dynamic" });
35
+ }
36
+ },
37
+ }),
38
+ );