@qq99/eslint-plugin-solid 0.18.0-no-reactive-read-in-effect-callback.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 +21 -0
- package/README.md +289 -0
- package/dist/chunk-4UZNJ5MS.mjs +32 -0
- package/dist/chunk-4UZNJ5MS.mjs.map +1 -0
- package/dist/chunk-EBU4C5NV.mjs +77 -0
- package/dist/chunk-EBU4C5NV.mjs.map +1 -0
- package/dist/chunk-LE6SW2M3.mjs +6231 -0
- package/dist/chunk-LE6SW2M3.mjs.map +1 -0
- package/dist/chunk-MHDQJMLE.mjs +39 -0
- package/dist/chunk-MHDQJMLE.mjs.map +1 -0
- package/dist/configs/recommended.d.mts +109 -0
- package/dist/configs/recommended.d.ts +109 -0
- package/dist/configs/recommended.js +5877 -0
- package/dist/configs/recommended.js.map +1 -0
- package/dist/configs/recommended.mjs +5 -0
- package/dist/configs/recommended.mjs.map +1 -0
- package/dist/configs/typescript.d.mts +103 -0
- package/dist/configs/typescript.d.ts +103 -0
- package/dist/configs/typescript.js +6246 -0
- package/dist/configs/typescript.js.map +1 -0
- package/dist/configs/typescript.mjs +6 -0
- package/dist/configs/typescript.mjs.map +1 -0
- package/dist/configs/v2-strict.d.mts +130 -0
- package/dist/configs/v2-strict.d.ts +130 -0
- package/dist/configs/v2-strict.js +6317 -0
- package/dist/configs/v2-strict.js.map +1 -0
- package/dist/configs/v2-strict.mjs +7 -0
- package/dist/configs/v2-strict.mjs.map +1 -0
- package/dist/configs/v2.d.mts +127 -0
- package/dist/configs/v2.d.ts +127 -0
- package/dist/configs/v2.js +6289 -0
- package/dist/configs/v2.js.map +1 -0
- package/dist/configs/v2.mjs +6 -0
- package/dist/configs/v2.mjs.map +1 -0
- package/dist/index.d.mts +740 -0
- package/dist/index.d.ts +740 -0
- package/dist/index.js +6365 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +42 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +109 -0
- package/src/compat.ts +53 -0
- package/src/configs/recommended.ts +48 -0
- package/src/configs/typescript.ts +21 -0
- package/src/configs/v2-strict.ts +28 -0
- package/src/configs/v2.ts +68 -0
- package/src/deps.d.ts +1 -0
- package/src/index.ts +29 -0
- package/src/plugin.ts +92 -0
- package/src/rules/components-return-once.ts +209 -0
- package/src/rules/event-handlers.ts +397 -0
- package/src/rules/imports.ts +338 -0
- package/src/rules/jsx-no-duplicate-props.ts +110 -0
- package/src/rules/jsx-no-script-url.ts +61 -0
- package/src/rules/jsx-no-undef.ts +230 -0
- package/src/rules/jsx-uses-vars.ts +66 -0
- package/src/rules/no-accessor-as-prop.ts +147 -0
- package/src/rules/no-array-handlers.ts +57 -0
- package/src/rules/no-boolean-enumerated-attribute.ts +209 -0
- package/src/rules/no-browser-globals-in-server-function.ts +108 -0
- package/src/rules/no-destructure.ts +245 -0
- package/src/rules/no-innerhtml.ts +143 -0
- package/src/rules/no-invalid-server-capture.ts +104 -0
- package/src/rules/no-module-scope-reactive-primitive.ts +69 -0
- package/src/rules/no-proxy-apis.ts +96 -0
- package/src/rules/no-react-deps.ts +68 -0
- package/src/rules/no-react-specific-props.ts +60 -0
- package/src/rules/no-reactive-read-in-effect-callback.ts +570 -0
- package/src/rules/no-restated-default-options.ts +96 -0
- package/src/rules/no-single-arg-create-effect.ts +56 -0
- package/src/rules/no-store-mutation-outside-setter.ts +137 -0
- package/src/rules/no-unknown-namespaces.ts +140 -0
- package/src/rules/no-unused-signal.ts +119 -0
- package/src/rules/no-write-in-pure-computation.ts +231 -0
- package/src/rules/prefer-classlist.ts +93 -0
- package/src/rules/prefer-for.ts +93 -0
- package/src/rules/prefer-onSettled-for-side-effects.ts +101 -0
- package/src/rules/prefer-show.ts +102 -0
- package/src/rules/prefer-structured-class.ts +117 -0
- package/src/rules/reactivity.ts +1730 -0
- package/src/rules/removed-api.ts +212 -0
- package/src/rules/require-async-server-function.ts +122 -0
- package/src/rules/self-closing-comp.ts +150 -0
- package/src/rules/style-prop.ts +140 -0
- package/src/rules/valid-use-server.ts +192 -0
- package/src/rules/validate-jsx-nesting.ts +1 -0
- package/src/utils/reactive-imports.ts +382 -0
- package/src/utils/reactive-types.ts +272 -0
- package/src/utils/reactive-values.ts +162 -0
- package/src/utils.ts +442 -0
package/src/utils.ts
ADDED
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
import { TSESTree as T, TSESLint } from "@typescript-eslint/utils";
|
|
2
|
+
import { CompatContext, findVariable } from "./compat";
|
|
3
|
+
|
|
4
|
+
const domElementRegex = /^[a-z]/;
|
|
5
|
+
export const isDOMElementName = (name: string): boolean => domElementRegex.test(name);
|
|
6
|
+
|
|
7
|
+
interface HasSettings {
|
|
8
|
+
settings?: TSESLint.SharedConfigurationSettings;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Read the targeted Solid version from `settings: { solid: { version: 2 } }`.
|
|
13
|
+
* Returns the major version number, or `null` when unset — meaning rules should
|
|
14
|
+
* use permissive dual-version behavior (accept both 1.x and 2.x patterns).
|
|
15
|
+
*/
|
|
16
|
+
export function getSolidVersion(context: HasSettings): number | null {
|
|
17
|
+
const solidSettings = context.settings?.solid;
|
|
18
|
+
if (solidSettings && typeof solidSettings === "object" && "version" in solidSettings) {
|
|
19
|
+
const version = (solidSettings as { version: unknown }).version;
|
|
20
|
+
if (typeof version === "number" && Number.isInteger(version) && version > 0) {
|
|
21
|
+
return version;
|
|
22
|
+
}
|
|
23
|
+
if (typeof version === "string") {
|
|
24
|
+
const major = parseInt(version, 10);
|
|
25
|
+
if (Number.isInteger(major) && major > 0) return major;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Whether the targeted Solid version is 2.x or later. */
|
|
32
|
+
export const isSolidV2 = (context: HasSettings): boolean => (getSolidVersion(context) ?? 0) >= 2;
|
|
33
|
+
|
|
34
|
+
// Matches "solid-js", its submodules ("solid-js/store", etc.), and the Solid 2.0
|
|
35
|
+
// "@solidjs/signals" package, which re-exports the core reactive primitives.
|
|
36
|
+
const DEFAULT_SOURCE_REGEX = /^(?:solid-js(?:\/?|\b)|@solidjs\/signals(?:\/?|\b))/;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The module sources whose exports are treated as Solid's primitives. Custom
|
|
40
|
+
* renderers and re-exporting wrappers can register their own module names via
|
|
41
|
+
* `settings.solid.moduleSources` (exact names, matched whole) so import-gated
|
|
42
|
+
* rules recognize `import { createSignal } from "my-renderer"` (#183).
|
|
43
|
+
*/
|
|
44
|
+
export const getSolidSourceRegex = (context: HasSettings): RegExp => {
|
|
45
|
+
const solidSettings = context.settings?.solid;
|
|
46
|
+
const raw =
|
|
47
|
+
solidSettings && typeof solidSettings === "object" && "moduleSources" in solidSettings
|
|
48
|
+
? (solidSettings as { moduleSources: unknown }).moduleSources
|
|
49
|
+
: undefined;
|
|
50
|
+
if (!Array.isArray(raw)) return DEFAULT_SOURCE_REGEX;
|
|
51
|
+
const escaped = raw
|
|
52
|
+
.filter((source): source is string => typeof source === "string")
|
|
53
|
+
.map((source) => source.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
|
54
|
+
if (!escaped.length) return DEFAULT_SOURCE_REGEX;
|
|
55
|
+
return new RegExp(`${DEFAULT_SOURCE_REGEX.source}|^(?:${escaped.join("|")})$`);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const propsRegex = /[pP]rops/;
|
|
59
|
+
export const isPropsByName = (name: string): boolean => propsRegex.test(name);
|
|
60
|
+
|
|
61
|
+
export const formatList = (strings: Array<string>): string => {
|
|
62
|
+
if (strings.length === 0) {
|
|
63
|
+
return "";
|
|
64
|
+
} else if (strings.length === 1) {
|
|
65
|
+
return `'${strings[0]}'`;
|
|
66
|
+
} else if (strings.length === 2) {
|
|
67
|
+
return `'${strings[0]}' and '${strings[1]}'`;
|
|
68
|
+
} else {
|
|
69
|
+
const last = strings.length - 1;
|
|
70
|
+
return `${strings
|
|
71
|
+
.slice(0, last)
|
|
72
|
+
.map((s) => `'${s}'`)
|
|
73
|
+
.join(", ")}, and '${strings[last]}'`;
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export const find = (node: T.Node, predicate: (node: T.Node) => boolean): T.Node | null => {
|
|
78
|
+
let n: T.Node | undefined = node;
|
|
79
|
+
while (n) {
|
|
80
|
+
const result = predicate(n);
|
|
81
|
+
if (result) {
|
|
82
|
+
return n;
|
|
83
|
+
}
|
|
84
|
+
n = n.parent;
|
|
85
|
+
}
|
|
86
|
+
return null;
|
|
87
|
+
};
|
|
88
|
+
export function findParent<Guard extends T.Node>(
|
|
89
|
+
node: T.Node,
|
|
90
|
+
predicate: (node: T.Node) => node is Guard
|
|
91
|
+
): Guard | null;
|
|
92
|
+
export function findParent(node: T.Node, predicate: (node: T.Node) => boolean): T.Node | null;
|
|
93
|
+
export function findParent(node: T.Node, predicate: (node: T.Node) => boolean): T.Node | null {
|
|
94
|
+
return node.parent ? find(node.parent, predicate) : null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Try to resolve a variable to its definition
|
|
98
|
+
export function trace(node: T.Node, context: CompatContext): T.Node {
|
|
99
|
+
if (node.type === "Identifier") {
|
|
100
|
+
const variable = findVariable(context, node);
|
|
101
|
+
if (!variable) return node;
|
|
102
|
+
|
|
103
|
+
const def = variable.defs[0];
|
|
104
|
+
|
|
105
|
+
// def is `undefined` for Identifier `undefined`
|
|
106
|
+
switch (def?.type) {
|
|
107
|
+
case "FunctionName":
|
|
108
|
+
case "ClassName":
|
|
109
|
+
case "ImportBinding":
|
|
110
|
+
return def.node;
|
|
111
|
+
case "Variable":
|
|
112
|
+
if (
|
|
113
|
+
((def.node.parent as T.VariableDeclaration).kind === "const" ||
|
|
114
|
+
variable.references.every((ref) => ref.init || ref.isReadOnly())) &&
|
|
115
|
+
def.node.id.type === "Identifier" &&
|
|
116
|
+
def.node.init
|
|
117
|
+
) {
|
|
118
|
+
return trace(def.node.init, context);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return node;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Get the relevant node when wrapped by a node that doesn't change the behavior */
|
|
126
|
+
export function ignoreTransparentWrappers(node: T.Node, up = false): T.Node {
|
|
127
|
+
if (
|
|
128
|
+
node.type === "TSAsExpression" ||
|
|
129
|
+
node.type === "TSNonNullExpression" ||
|
|
130
|
+
node.type === "TSSatisfiesExpression"
|
|
131
|
+
) {
|
|
132
|
+
const next = up ? node.parent : node.expression;
|
|
133
|
+
if (next) {
|
|
134
|
+
return ignoreTransparentWrappers(next, up);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return node;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export type FunctionNode = T.FunctionExpression | T.ArrowFunctionExpression | T.FunctionDeclaration;
|
|
141
|
+
const FUNCTION_TYPES = ["FunctionExpression", "ArrowFunctionExpression", "FunctionDeclaration"];
|
|
142
|
+
export const isFunctionNode = (node: T.Node | null | undefined): node is FunctionNode =>
|
|
143
|
+
!!node && FUNCTION_TYPES.includes(node.type);
|
|
144
|
+
|
|
145
|
+
export type ProgramOrFunctionNode = FunctionNode | T.Program;
|
|
146
|
+
const PROGRAM_OR_FUNCTION_TYPES = ["Program"].concat(FUNCTION_TYPES);
|
|
147
|
+
export const isProgramOrFunctionNode = (
|
|
148
|
+
node: T.Node | null | undefined
|
|
149
|
+
): node is ProgramOrFunctionNode => !!node && PROGRAM_OR_FUNCTION_TYPES.includes(node.type);
|
|
150
|
+
|
|
151
|
+
export const isJSXElementOrFragment = (
|
|
152
|
+
node: T.Node | null | undefined
|
|
153
|
+
): node is T.JSXElement | T.JSXFragment =>
|
|
154
|
+
node?.type === "JSXElement" || node?.type === "JSXFragment";
|
|
155
|
+
|
|
156
|
+
export const getFunctionName = (node: FunctionNode): string | null => {
|
|
157
|
+
if (
|
|
158
|
+
(node.type === "FunctionDeclaration" || node.type === "FunctionExpression") &&
|
|
159
|
+
node.id != null
|
|
160
|
+
) {
|
|
161
|
+
return node.id.name;
|
|
162
|
+
}
|
|
163
|
+
if (node.parent?.type === "VariableDeclarator" && node.parent.id.type === "Identifier") {
|
|
164
|
+
return node.parent.id.name;
|
|
165
|
+
}
|
|
166
|
+
return null;
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
export function findInScope(
|
|
170
|
+
node: T.Node,
|
|
171
|
+
scope: ProgramOrFunctionNode,
|
|
172
|
+
predicate: (node: T.Node) => boolean
|
|
173
|
+
): T.Node | null {
|
|
174
|
+
const found = find(node, (node) => node === scope || predicate(node));
|
|
175
|
+
return found === scope && !predicate(node) ? null : found;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// The next two functions were adapted from "eslint-plugin-import" under the MIT license.
|
|
179
|
+
|
|
180
|
+
// Checks whether `node` has a comment (that ends) on the previous line or on
|
|
181
|
+
// the same line as `node` (starts).
|
|
182
|
+
export const getCommentBefore = (
|
|
183
|
+
node: T.Node,
|
|
184
|
+
sourceCode: TSESLint.SourceCode
|
|
185
|
+
): T.Comment | undefined =>
|
|
186
|
+
sourceCode
|
|
187
|
+
.getCommentsBefore(node)
|
|
188
|
+
.find((comment) => comment.loc!.end.line >= node.loc!.start.line - 1);
|
|
189
|
+
|
|
190
|
+
// Checks whether `node` has a comment (that starts) on the same line as `node`
|
|
191
|
+
// (ends).
|
|
192
|
+
export const getCommentAfter = (
|
|
193
|
+
node: T.Node,
|
|
194
|
+
sourceCode: TSESLint.SourceCode
|
|
195
|
+
): T.Comment | undefined =>
|
|
196
|
+
sourceCode
|
|
197
|
+
.getCommentsAfter(node)
|
|
198
|
+
.find((comment) => comment.loc!.start.line === node.loc!.end.line);
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* The leading string-literal expression statements of a program or function
|
|
202
|
+
* body — the directive prologue. Only statements here are directives; a
|
|
203
|
+
* `"use server"` string anywhere else is an ordinary expression.
|
|
204
|
+
*/
|
|
205
|
+
export const getDirectivePrologue = (body: T.Statement[]): T.ExpressionStatement[] => {
|
|
206
|
+
const prologue: T.ExpressionStatement[] = [];
|
|
207
|
+
for (const statement of body) {
|
|
208
|
+
if (
|
|
209
|
+
statement.type === "ExpressionStatement" &&
|
|
210
|
+
statement.expression.type === "Literal" &&
|
|
211
|
+
typeof statement.expression.value === "string"
|
|
212
|
+
) {
|
|
213
|
+
prologue.push(statement);
|
|
214
|
+
} else {
|
|
215
|
+
break;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return prologue;
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
const prologueHasDirective = (body: T.Statement[], directive: string): boolean =>
|
|
222
|
+
getDirectivePrologue(body).some(
|
|
223
|
+
(statement) => (statement.expression as T.StringLiteral).value === directive
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
/** Whether a function has a `"use server"` directive in its body's prologue. */
|
|
227
|
+
export const hasUseServerDirective = (fn: FunctionNode): boolean =>
|
|
228
|
+
fn.body?.type === "BlockStatement" && prologueHasDirective(fn.body.body, "use server");
|
|
229
|
+
|
|
230
|
+
/** Whether a program has a module-level `"use server"` directive. */
|
|
231
|
+
export const programHasUseServerDirective = (program: T.Program): boolean =>
|
|
232
|
+
prologueHasDirective(program.body, "use server");
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Whether the `"use server"` transform would extract this function. Mirrors
|
|
236
|
+
* the compiler: object-literal methods, getters/setters, and class methods
|
|
237
|
+
* are never extracted, so directives inside them are silently ignored.
|
|
238
|
+
*/
|
|
239
|
+
export type UseServerEligibility = "eligible" | "objectMethod" | "accessor" | "classMethod";
|
|
240
|
+
export const getUseServerEligibility = (fn: FunctionNode): UseServerEligibility => {
|
|
241
|
+
const parent = fn.parent;
|
|
242
|
+
if (parent?.type === "Property" && parent.value === fn) {
|
|
243
|
+
if (parent.kind !== "init") return "accessor";
|
|
244
|
+
if (parent.method) return "objectMethod";
|
|
245
|
+
}
|
|
246
|
+
if (parent?.type === "MethodDefinition") {
|
|
247
|
+
return parent.kind === "get" || parent.kind === "set" ? "accessor" : "classMethod";
|
|
248
|
+
}
|
|
249
|
+
return "eligible";
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Whether `fn` is nested inside another function carrying a `"use server"`
|
|
254
|
+
* directive. The transform extracts the outermost marked function only;
|
|
255
|
+
* directives inside it are ignored (the code already runs on the server).
|
|
256
|
+
*/
|
|
257
|
+
export const isInsideUseServerFunction = (fn: FunctionNode): boolean => {
|
|
258
|
+
let parent = findParent(fn, isFunctionNode);
|
|
259
|
+
while (parent) {
|
|
260
|
+
if (isFunctionNode(parent) && hasUseServerDirective(parent)) return true;
|
|
261
|
+
parent = findParent(parent, isFunctionNode);
|
|
262
|
+
}
|
|
263
|
+
return false;
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Compiles a list of user-provided name patterns into a matcher. Entries may
|
|
268
|
+
* be exact names, glob-ish patterns using `*` wildcards ("watch*"), or
|
|
269
|
+
* regexes written as "/pattern/" strings. Invalid regexes fall back to exact
|
|
270
|
+
* string comparison.
|
|
271
|
+
*/
|
|
272
|
+
export const createNameMatcher = (patterns: string[]): ((name: string) => boolean) => {
|
|
273
|
+
const matchers: (string | RegExp)[] = patterns.map((entry) => {
|
|
274
|
+
if (entry.length > 2 && entry.startsWith("/") && entry.endsWith("/")) {
|
|
275
|
+
try {
|
|
276
|
+
return new RegExp(entry.slice(1, -1));
|
|
277
|
+
} catch {
|
|
278
|
+
return entry;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
if (entry.includes("*")) {
|
|
282
|
+
const escaped = entry
|
|
283
|
+
.split("*")
|
|
284
|
+
.map((part) => part.replace(/[.+?^${}()|[\]\\]/g, "\\$&"))
|
|
285
|
+
.join("[a-zA-Z0-9_$]*");
|
|
286
|
+
return new RegExp(`^${escaped}$`);
|
|
287
|
+
}
|
|
288
|
+
return entry;
|
|
289
|
+
});
|
|
290
|
+
return (name: string): boolean =>
|
|
291
|
+
matchers.some((matcher) =>
|
|
292
|
+
typeof matcher === "string" ? matcher === name : matcher.test(name)
|
|
293
|
+
);
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Tracks imports from Solid's module sources (see DEFAULT_SOURCE_REGEX /
|
|
298
|
+
* getSolidSourceRegex), handling aliases. Pass `getSolidSourceRegex(context)`
|
|
299
|
+
* to honor the `settings.solid.moduleSources` escape.
|
|
300
|
+
*/
|
|
301
|
+
export const trackImports = (fromModule: RegExp = DEFAULT_SOURCE_REGEX) => {
|
|
302
|
+
const importMap = new Map<string, string>();
|
|
303
|
+
const handleImportDeclaration = (node: T.ImportDeclaration) => {
|
|
304
|
+
if (fromModule.test(node.source.value)) {
|
|
305
|
+
for (const specifier of node.specifiers) {
|
|
306
|
+
if (specifier.type === "ImportSpecifier") {
|
|
307
|
+
const importedName =
|
|
308
|
+
specifier.imported.type === "Identifier"
|
|
309
|
+
? specifier.imported.name
|
|
310
|
+
: specifier.imported.value;
|
|
311
|
+
importMap.set(importedName, specifier.local.name);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
const matchImport = (imports: string | Array<string>, str: string): string | undefined => {
|
|
317
|
+
const importArr = Array.isArray(imports) ? imports : [imports];
|
|
318
|
+
return importArr.find((i) => importMap.get(i) === str);
|
|
319
|
+
};
|
|
320
|
+
return { matchImport, handleImportDeclaration };
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
export function appendImports(
|
|
324
|
+
fixer: TSESLint.RuleFixer,
|
|
325
|
+
sourceCode: TSESLint.SourceCode,
|
|
326
|
+
importNode: T.ImportDeclaration,
|
|
327
|
+
identifiers: Array<string>
|
|
328
|
+
): TSESLint.RuleFix | null {
|
|
329
|
+
const identifiersString = identifiers.join(", ");
|
|
330
|
+
const reversedSpecifiers = importNode.specifiers.slice().reverse();
|
|
331
|
+
const lastSpecifier = reversedSpecifiers.find((s) => s.type === "ImportSpecifier");
|
|
332
|
+
if (lastSpecifier) {
|
|
333
|
+
// import A, { B } from 'source' => import A, { B, C, D } from 'source'
|
|
334
|
+
// import { B } from 'source' => import { B, C, D } from 'source'
|
|
335
|
+
return fixer.insertTextAfter(lastSpecifier, `, ${identifiersString}`);
|
|
336
|
+
}
|
|
337
|
+
const otherSpecifier = importNode.specifiers.find(
|
|
338
|
+
(s) => s.type === "ImportDefaultSpecifier" || s.type === "ImportNamespaceSpecifier"
|
|
339
|
+
);
|
|
340
|
+
if (otherSpecifier) {
|
|
341
|
+
// import A from 'source' => import A, { B, C, D } from 'source'
|
|
342
|
+
return fixer.insertTextAfter(otherSpecifier, `, { ${identifiersString} }`);
|
|
343
|
+
}
|
|
344
|
+
if (importNode.specifiers.length === 0) {
|
|
345
|
+
const [importToken, maybeBrace] = sourceCode.getFirstTokens(importNode, { count: 2 });
|
|
346
|
+
if (maybeBrace?.value === "{") {
|
|
347
|
+
// import {} from 'source' => import { B, C, D } from 'source'
|
|
348
|
+
return fixer.insertTextAfter(maybeBrace, ` ${identifiersString} `);
|
|
349
|
+
} else {
|
|
350
|
+
// import 'source' => import { B, C, D } from 'source'
|
|
351
|
+
return importToken
|
|
352
|
+
? fixer.insertTextAfter(importToken, ` { ${identifiersString} } from`)
|
|
353
|
+
: null;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
return null;
|
|
357
|
+
}
|
|
358
|
+
export function insertImports(
|
|
359
|
+
fixer: TSESLint.RuleFixer,
|
|
360
|
+
sourceCode: TSESLint.SourceCode,
|
|
361
|
+
source: string,
|
|
362
|
+
identifiers: Array<string>,
|
|
363
|
+
aboveImport?: T.ImportDeclaration,
|
|
364
|
+
isType = false
|
|
365
|
+
): TSESLint.RuleFix {
|
|
366
|
+
const identifiersString = identifiers.join(", ");
|
|
367
|
+
const programNode: T.Program = sourceCode.ast;
|
|
368
|
+
|
|
369
|
+
// insert `import { missing, identifiers } from "source"` above given node or at top of module
|
|
370
|
+
const firstImport = aboveImport ?? programNode.body.find((n) => n.type === "ImportDeclaration");
|
|
371
|
+
if (firstImport) {
|
|
372
|
+
return fixer.insertTextBeforeRange(
|
|
373
|
+
(getCommentBefore(firstImport, sourceCode) ?? firstImport).range,
|
|
374
|
+
`import ${isType ? "type " : ""}{ ${identifiersString} } from "${source}";\n`
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
return fixer.insertTextBeforeRange(
|
|
378
|
+
[0, 0],
|
|
379
|
+
`import ${isType ? "type " : ""}{ ${identifiersString} } from "${source}";\n`
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
export function removeSpecifier(
|
|
384
|
+
fixer: TSESLint.RuleFixer,
|
|
385
|
+
sourceCode: TSESLint.SourceCode,
|
|
386
|
+
specifier: T.ImportSpecifier,
|
|
387
|
+
pure = true
|
|
388
|
+
) {
|
|
389
|
+
const declaration = specifier.parent as T.ImportDeclaration;
|
|
390
|
+
if (declaration.specifiers.length === 1 && pure) {
|
|
391
|
+
return fixer.remove(declaration);
|
|
392
|
+
}
|
|
393
|
+
const maybeComma = sourceCode.getTokenAfter(specifier);
|
|
394
|
+
if (maybeComma?.value === ",") {
|
|
395
|
+
return fixer.removeRange([specifier.range[0], maybeComma.range[1]]);
|
|
396
|
+
}
|
|
397
|
+
return fixer.remove(specifier);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
export function jsxPropName(prop: T.JSXAttribute) {
|
|
401
|
+
if (prop.name.type === "JSXNamespacedName") {
|
|
402
|
+
return `${prop.name.namespace.name}:${prop.name.name.name}`;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
return prop.name.name;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
type Props = T.JSXOpeningElement["attributes"];
|
|
409
|
+
|
|
410
|
+
/** Iterate through both attributes and spread object props, yielding the name and the node. */
|
|
411
|
+
export function* jsxGetAllProps(props: Props): Generator<[string, T.Node]> {
|
|
412
|
+
for (const attr of props) {
|
|
413
|
+
if (attr.type === "JSXSpreadAttribute" && attr.argument.type === "ObjectExpression") {
|
|
414
|
+
for (const property of attr.argument.properties) {
|
|
415
|
+
if (property.type === "Property") {
|
|
416
|
+
if (property.key.type === "Identifier") {
|
|
417
|
+
yield [property.key.name, property.key];
|
|
418
|
+
} else if (property.key.type === "Literal") {
|
|
419
|
+
yield [String(property.key.value), property.key];
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
} else if (attr.type === "JSXAttribute") {
|
|
424
|
+
yield [jsxPropName(attr), attr.name];
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/** Returns whether an element has a prop, checking spread object props. */
|
|
430
|
+
export function jsxHasProp(props: Props, prop: string) {
|
|
431
|
+
for (const [p] of jsxGetAllProps(props)) {
|
|
432
|
+
if (p === prop) return true;
|
|
433
|
+
}
|
|
434
|
+
return false;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/** Get a JSXAttribute, excluding spread props. */
|
|
438
|
+
export function jsxGetProp(props: Props, prop: string) {
|
|
439
|
+
return props.find(
|
|
440
|
+
(attribute) => attribute.type !== "JSXSpreadAttribute" && prop === jsxPropName(attribute)
|
|
441
|
+
) as T.JSXAttribute | undefined;
|
|
442
|
+
}
|