Skip to content

Commit 7be4a1b

Browse files
committed
Add and test imports rule
1 parent 99735f2 commit 7be4a1b

File tree

8 files changed

+450
-39
lines changed

8 files changed

+450
-39
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ would like to use.
9292
| :---: | :---: | :--- | :--- |
9393
|| 🔧 | [solid/components-return-once](docs/components-return-once.md) | Disallow early returns in components. Solid components only run once, and so conditionals should be inside JSX. |
9494
|| 🔧 | [solid/event-handlers](docs/event-handlers.md) | Enforce naming DOM element event handlers consistently and prevent Solid's analysis from misunderstanding whether a prop should be an event handler. |
95+
|| 🔧 | [solid/imports](docs/imports.md) | Enforce consistent imports from "solid-js", "solid-js/web", and "solid-js/store". |
9596
|| | [solid/jsx-no-duplicate-props](docs/jsx-no-duplicate-props.md) | Disallow passing the same prop twice in JSX. |
9697
|| | [solid/jsx-no-script-url](docs/jsx-no-script-url.md) | Disallow javascript: URLs. |
9798
|| 🔧 | [solid/jsx-no-undef](docs/jsx-no-undef.md) | Disallow references to undefined variables in JSX. Handles custom directives. |

docs/imports.md

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
<!-- AUTO-GENERATED-CONTENT:START (HEADER) -->
2+
# solid/imports
3+
Enforce consistent imports from "solid-js", "solid-js/web", and "solid-js/store".
4+
This rule is **a warning** by default.
5+
6+
[View source](../src/rules/imports.ts) · [View tests](../test/rules/imports.test.ts)
7+
8+
<!-- AUTO-GENERATED-CONTENT:END -->
9+
10+
<!-- AUTO-GENERATED-CONTENT:START (OPTIONS) -->
11+
12+
<!-- AUTO-GENERATED-CONTENT:END -->
13+
14+
<!-- AUTO-GENERATED-CONTENT:START (CASES) -->
15+
## Tests
16+
17+
### Invalid Examples
18+
19+
These snippets cause lint errors, and some can be auto-fixed.
20+
21+
```js
22+
import { createEffect } from "solid-js/web";
23+
// after eslint --fix:
24+
import { createEffect } from "solid-js";
25+
26+
import { createEffect } from "solid-js/web";
27+
import { createSignal } from "solid-js";
28+
// after eslint --fix:
29+
import { createSignal, createEffect } from "solid-js";
30+
31+
import type { Component } from "solid-js/store";
32+
import { createSignal } from "solid-js";
33+
console.log("hi");
34+
// after eslint --fix:
35+
import { createSignal, Component } from "solid-js";
36+
console.log("hi");
37+
38+
import { createEffect } from "solid-js/web";
39+
import { render } from "solid-js";
40+
// after eslint --fix:
41+
import { render, createEffect } from "solid-js";
42+
43+
import { render, createEffect } from "solid-js";
44+
// after eslint --fix:
45+
import { render } from "solid-js/web";
46+
import { createEffect } from "solid-js";
47+
48+
```
49+
50+
### Valid Examples
51+
52+
These snippets don't cause lint errors.
53+
54+
```js
55+
import { createSignal, mergeProps as merge } from "solid-js";
56+
57+
import { createSignal, mergeProps as merge } from "solid-js";
58+
59+
import { render, hydrate } from "solid-js/web";
60+
61+
import { createStore, produce } from "solid-js/store";
62+
63+
import { createSignal } from "solid-js";
64+
import { render } from "solid-js/web";
65+
import { something } from "somewhere/else";
66+
import { createStore } from "solid-js/store";
67+
68+
import * as Solid from "solid-js";
69+
Solid.render();
70+
71+
import type { Component, JSX } from "solid-js";
72+
import type { Store } from "solid-js/store";
73+
74+
```
75+
<!-- AUTO-GENERATED-CONTENT:END -->

src/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import componentsReturnOnce from "./rules/components-return-once";
22
import eventHandlers from "./rules/event-handlers";
3+
import imports from "./rules/imports";
34
import jsxNoDuplicateProps from "./rules/jsx-no-duplicate-props";
45
import jsxNoScriptUrl from "./rules/jsx-no-script-url";
56
import jsxNoUndef from "./rules/jsx-no-undef";
@@ -19,6 +20,7 @@ import styleProp from "./rules/style-prop";
1920
const allRules = {
2021
"components-return-once": componentsReturnOnce,
2122
"event-handlers": eventHandlers,
23+
imports,
2224
"jsx-no-duplicate-props": jsxNoDuplicateProps,
2325
"jsx-no-undef": jsxNoUndef,
2426
"jsx-no-script-url": jsxNoScriptUrl,
@@ -70,6 +72,7 @@ const plugin = {
7072
"solid/reactivity": 1,
7173
"solid/event-handlers": 1,
7274
// these rules are mostly style suggestions
75+
"solid/imports": 1,
7376
"solid/style-prop": 1,
7477
"solid/no-react-specific-props": 1,
7578
"solid/prefer-classlist": 1,
@@ -99,6 +102,7 @@ const plugin = {
99102
"solid/reactivity": 1,
100103
"solid/event-handlers": 1,
101104
// these rules are mostly style suggestions
105+
"solid/imports": 1,
102106
"solid/style-prop": 1,
103107
"solid/no-react-specific-props": 1,
104108
"solid/prefer-classlist": 1,

src/rules/imports.ts

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
import { TSESTree as T, TSESLint } from "@typescript-eslint/utils";
2+
import { appendImports, insertImports, removeSpecifier } from "../utils";
3+
4+
// Below: create maps of imports and types to designated import source.
5+
// We could mess with `Object.keys(require("solid-js"))` to generate this, but requiring it from
6+
// node activates the "node" export condition, which doesn't necessarily match what users will
7+
// receive i.e. through bundlers. Instead, we're manually listing all of the public exports that
8+
// should be imported from "solid-js", etc.
9+
// ==============
10+
11+
type Source = "solid-js" | "solid-js/web" | "solid-js/store";
12+
13+
// Set up map of imports to module
14+
const primitiveMap = new Map<string, Source>();
15+
for (const primitive of [
16+
"createSignal",
17+
"createEffect",
18+
"createMemo",
19+
"createResource",
20+
"onMount",
21+
"onCleanup",
22+
"onError",
23+
"untrack",
24+
"batch",
25+
"on",
26+
"createRoot",
27+
"getOwner",
28+
"runWithOwner",
29+
"mergeProps",
30+
"splitProps",
31+
"useTransition",
32+
"observable",
33+
"from",
34+
"mapArray",
35+
"indexArray",
36+
"createContext",
37+
"useContext",
38+
"children",
39+
"lazy",
40+
"createUniqueId",
41+
"createDeferred",
42+
"createRenderEffect",
43+
"createComputed",
44+
"createReaction",
45+
"createSelector",
46+
"DEV",
47+
"For",
48+
"Show",
49+
"Switch",
50+
"Match",
51+
"Index",
52+
"ErrorBoundary",
53+
"Suspense",
54+
"SuspenseList",
55+
]) {
56+
primitiveMap.set(primitive, "solid-js");
57+
}
58+
for (const primitive of [
59+
"Portal",
60+
"render",
61+
"hydrate",
62+
"renderToString",
63+
"renderToStream",
64+
"isServer",
65+
"renderToStringAsync",
66+
"generateHydrationScript",
67+
"HydrationScript",
68+
"Dynamic",
69+
]) {
70+
primitiveMap.set(primitive, "solid-js/web");
71+
}
72+
for (const primitive of [
73+
"createStore",
74+
"produce",
75+
"reconcile",
76+
"unwrap",
77+
"createMutable",
78+
"modifyMutable",
79+
]) {
80+
primitiveMap.set(primitive, "solid-js/store");
81+
}
82+
83+
// Set up map of type imports to module
84+
const typeMap = new Map<string, Source>();
85+
for (const type of [
86+
"Signal",
87+
"Accessor",
88+
"Setter",
89+
"Resource",
90+
"ResourceActions",
91+
"ResourceOptions",
92+
"ResourceReturn",
93+
"ResourceFetcher",
94+
"InitializedResourceReturn",
95+
"Component",
96+
"VoidProps",
97+
"VoidComponent",
98+
"ParentProps",
99+
"ParentComponent",
100+
"FlowProps",
101+
"FlowComponent",
102+
"ValidComponent",
103+
"ComponentProps",
104+
"Ref",
105+
"MergeProps",
106+
"SplitPrips",
107+
"Context",
108+
"JSX",
109+
"ResolvedChildren",
110+
"MatchProps",
111+
]) {
112+
typeMap.set(type, "solid-js");
113+
}
114+
for (const type of [/* "JSX", */ "MountableElement"]) {
115+
typeMap.set(type, "solid-js/web");
116+
}
117+
for (const type of ["StoreNode", "Store", "SetStoreFunction"]) {
118+
typeMap.set(type, "solid-js/store");
119+
}
120+
121+
const isSource = (source: string): source is Source => /^solid-js(?:\/web|\/store)?$/.test(source);
122+
123+
const rule: TSESLint.RuleModule<"prefer-source", []> = {
124+
meta: {
125+
type: "suggestion",
126+
docs: {
127+
recommended: "warn",
128+
description:
129+
'Enforce consistent imports from "solid-js", "solid-js/web", and "solid-js/store".',
130+
url: "https://github.com/joshwilsonvu/eslint-plugin-solid/blob/main/docs/imports.md",
131+
},
132+
fixable: "code",
133+
schema: [],
134+
messages: {
135+
"prefer-source": 'Prefer importing {{name}} from "{{source}}".',
136+
},
137+
},
138+
create(context) {
139+
return {
140+
ImportDeclaration(node) {
141+
const source = node.source.value;
142+
if (!isSource(source)) return;
143+
144+
for (const specifier of node.specifiers) {
145+
if (specifier.type === "ImportSpecifier") {
146+
const isType = specifier.importKind === "type" || node.importKind === "type";
147+
const map = isType ? typeMap : primitiveMap;
148+
const correctSource = map.get(specifier.imported.name);
149+
if (correctSource != null && correctSource !== source) {
150+
context.report({
151+
node: specifier,
152+
messageId: "prefer-source",
153+
data: {
154+
name: specifier.imported.name,
155+
source: correctSource,
156+
},
157+
fix(fixer) {
158+
const sourceCode = context.getSourceCode();
159+
const program: T.Program = sourceCode.ast;
160+
const correctDeclaration = program.body.find(
161+
(node) =>
162+
node.type === "ImportDeclaration" && node.source.value === correctSource
163+
) as T.ImportDeclaration | undefined;
164+
165+
if (correctDeclaration) {
166+
return [
167+
removeSpecifier(fixer, sourceCode, specifier),
168+
appendImports(fixer, sourceCode, correctDeclaration, [
169+
sourceCode.getText(specifier),
170+
]),
171+
].filter(Boolean) as Array<TSESLint.RuleFix>;
172+
}
173+
174+
const firstSolidDeclaration = program.body.find(
175+
(node) => node.type === "ImportDeclaration" && isSource(node.source.value)
176+
) as T.ImportDeclaration | undefined;
177+
return [
178+
removeSpecifier(fixer, sourceCode, specifier),
179+
insertImports(
180+
fixer,
181+
sourceCode,
182+
correctSource,
183+
[sourceCode.getText(specifier)],
184+
firstSolidDeclaration,
185+
isType
186+
),
187+
];
188+
},
189+
});
190+
}
191+
}
192+
}
193+
},
194+
};
195+
},
196+
};
197+
198+
export default rule;

src/rules/jsx-no-undef.ts

Lines changed: 5 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { TSESTree as T, TSESLint } from "@typescript-eslint/utils";
2-
import { isDOMElementName, formatList, getCommentBefore } from "../utils";
2+
import { isDOMElementName, formatList, appendImports, insertImports } from "../utils";
33

44
// Currently all of the control flow components are from 'solid-js'.
55
const AUTO_COMPONENTS = ["Show", "For", "Index", "Switch", "Match"];
@@ -167,7 +167,6 @@ const rule: TSESLint.RuleModule<
167167
// add in any auto import components used in the program
168168
const missingComponents = Array.from(missingComponentsSet.values());
169169
if (autoImport && missingComponents.length) {
170-
const identifiersString = missingComponents.join(", "); // "Show, For, Switch"
171170
const importNode = programNode.body.find(
172171
(n) =>
173172
n.type === "ImportDeclaration" &&
@@ -180,33 +179,11 @@ const rule: TSESLint.RuleModule<
180179
node: importNode,
181180
messageId: "autoImport",
182181
data: {
183-
imports: formatList(missingComponents),
182+
imports: formatList(missingComponents), // "Show, For, and Switch"
184183
source: SOURCE_MODULE,
185184
},
186185
fix: (fixer) => {
187-
const reversedSpecifiers = importNode.specifiers.slice().reverse();
188-
const lastSpecifier = reversedSpecifiers.find((s) => s.type === "ImportSpecifier");
189-
if (lastSpecifier) {
190-
// import A, { B } from 'source' => import A, { B, C, D } from 'source'
191-
// import { B } from 'source' => import { B, C, D } from 'source'
192-
return fixer.insertTextAfter(lastSpecifier, `, ${identifiersString}`);
193-
}
194-
const otherSpecifier = importNode.specifiers.find(
195-
(s) =>
196-
s.type === "ImportDefaultSpecifier" || s.type === "ImportNamespaceSpecifier"
197-
);
198-
if (otherSpecifier) {
199-
// import A from 'source' => import A, { B, C, D } from 'source'
200-
return fixer.insertTextAfter(otherSpecifier, `, { ${identifiersString} }`);
201-
}
202-
if (importNode.specifiers.length === 0) {
203-
// import 'source' => import { B, C, D } from 'source'
204-
const importToken = context.getSourceCode().getFirstToken(importNode);
205-
return importToken
206-
? fixer.insertTextAfter(importToken, ` { ${identifiersString} } from`)
207-
: null;
208-
}
209-
return null;
186+
return appendImports(fixer, context.getSourceCode(), importNode, missingComponents);
210187
},
211188
});
212189
} else {
@@ -218,18 +195,8 @@ const rule: TSESLint.RuleModule<
218195
source: SOURCE_MODULE,
219196
},
220197
fix: (fixer) => {
221-
// insert `import { missing, identifiers } from "source-module"` at top of module
222-
const firstImport = programNode.body.find((n) => n.type === "ImportDeclaration");
223-
if (firstImport) {
224-
return fixer.insertTextBeforeRange(
225-
(getCommentBefore(firstImport, context.getSourceCode()) ?? firstImport).range,
226-
`import { ${identifiersString} } from "${SOURCE_MODULE}";\n`
227-
);
228-
}
229-
return fixer.insertTextBeforeRange(
230-
[0, 0],
231-
`import { ${identifiersString} } from "${SOURCE_MODULE}";\n`
232-
);
198+
// insert `import { missing, identifiers } from "solid-js"` at top of module
199+
return insertImports(fixer, context.getSourceCode(), "solid-js", missingComponents);
233200
},
234201
});
235202
}

0 commit comments

Comments
 (0)