Skip to content

Commit 1c14642

Browse files
authoredSep 9, 2024
refactor(rules/style): use Program:exit to improve rule logic (#2)
1 parent 4479c1a commit 1c14642

File tree

6 files changed

+132
-114
lines changed

6 files changed

+132
-114
lines changed
 

‎package.json

+3
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,16 @@
55
"description": "ESLint plugin to ensure consistent react imports and relative usage",
66
"main": "index.js",
77
"scripts": {
8+
"build": "tsc -p tsconfig.build.json",
89
"test": "vitest"
910
},
1011
"keywords": [],
1112
"author": "Marco Pasqualetti @marcalexiei",
1213
"license": "MIT",
1314
"packageManager": "pnpm@9.9.0+sha512.60c18acd138bff695d339be6ad13f7e936eea6745660d4cc4a776d5247c540d0edee1a563695c183a66eb917ef88f2b4feb1fc25f32a7adcadc7aaf3438e99c1",
1415
"devDependencies": {
16+
"@types/estree": "1.0.5",
17+
"@types/json-schema": "7.0.15",
1518
"@typescript-eslint/parser": "8.4.0",
1619
"eslint": "9.10.0",
1720
"typescript": "5.5.4",

‎pnpm-lock.yaml

+11
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
File renamed without changes.

‎src/rules/style.ts

+116
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import type { Rule } from "eslint";
2+
import type * as ESTree from "estree";
3+
4+
interface StyleRuleOptions {
5+
syntax: "default" | "namespace";
6+
}
7+
8+
const styleRule: Rule.RuleModule = {
9+
meta: {
10+
type: "suggestion",
11+
fixable: "code",
12+
docs: {
13+
description: [
14+
"Enforces React import style across your code.",
15+
"Can be customized to use default or namespace import.",
16+
"By default converts exports using namespace import",
17+
].join(" "),
18+
},
19+
messages: {
20+
wrongImport: "You should import React using {{syntax}} import syntax",
21+
multipleImport:
22+
"React was already imported. This import should be removed when using {{syntax}} import",
23+
},
24+
schema: [
25+
{
26+
type: "object",
27+
properties: {
28+
syntax: { enum: ["default", "namespace"] },
29+
},
30+
additionalProperties: false,
31+
},
32+
],
33+
},
34+
create(context) {
35+
const [{ syntax = "namespace" } = {}] = context.options as [
36+
StyleRuleOptions?
37+
];
38+
39+
const invalidImportTypes: Array<string> = [
40+
"ImportSpecifier",
41+
/**
42+
* Based on {@link StyleRuleOptions.syntax} we integrate the array with the unwanted import syntax:
43+
* If syntax is `default` we should exclude `namespace` imports and vice-versa.
44+
*/
45+
syntax === "default"
46+
? "ImportNamespaceSpecifier"
47+
: "ImportDefaultSpecifier",
48+
];
49+
50+
/**
51+
* Variable used to store all react imports not following the required syntax
52+
*/
53+
const reactInvalidImports: Array<ESTree.ImportDeclaration> = [];
54+
55+
return {
56+
ImportDeclaration: (node) => {
57+
const { source, specifiers } = node;
58+
59+
/** @todo might change selector to something like ImportDeclaration[source.value="react"] */
60+
if (source.value !== "react") return;
61+
62+
if (specifiers.some((it) => invalidImportTypes.includes(it.type))) {
63+
reactInvalidImports.push(node);
64+
}
65+
},
66+
67+
"Program:exit": () => {
68+
/** Check if there is at least one invalid import */
69+
if (!reactInvalidImports.length) return;
70+
71+
const [firstImport, ...otherReactImports] = reactInvalidImports;
72+
73+
/** Replace the first import with the right import based on options */
74+
context.report({
75+
messageId: "wrongImport",
76+
data: { syntax },
77+
loc: { ...firstImport.loc! },
78+
fix(fixer) {
79+
/** Cycle all imports to understand if it should become a import type */
80+
const importType = reactInvalidImports.every(
81+
(importNode) =>
82+
"importKind" in importNode && importNode.importKind === "type"
83+
)
84+
? "type"
85+
: "";
86+
87+
const correctImportSyntax =
88+
syntax === "default" ? "React" : "* as React";
89+
90+
/** @todo maybe there is a more smart method working directly on AST node to do this */
91+
const newImport = `import ${importType} ${correctImportSyntax} from 'react';`;
92+
93+
return fixer.replaceText(
94+
firstImport,
95+
newImport.replace(/\s+/g, " ")
96+
);
97+
},
98+
});
99+
100+
/** All additional imports can be removed */
101+
for (const reactImportNode of otherReactImports) {
102+
context.report({
103+
messageId: "multipleImport",
104+
data: { syntax },
105+
loc: { ...reactImportNode.loc! },
106+
fix(fixer) {
107+
return fixer.remove(reactImportNode);
108+
},
109+
});
110+
}
111+
},
112+
};
113+
},
114+
};
115+
116+
export default styleRule;

‎src/style.ts

-113
This file was deleted.

‎tsconfig.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{
22
"compilerOptions": {
33
"module": "NodeNext",
4-
"moduleResolution": "NodeNext"
4+
"moduleResolution": "NodeNext",
5+
"strict": true
56
}
67
}

0 commit comments

Comments
 (0)