Skip to content

Commit b44ae49

Browse files
Add rule for disallowing direct barrel imports (#30)
1 parent 727ddfd commit b44ae49

File tree

7 files changed

+201
-5
lines changed

7 files changed

+201
-5
lines changed

.changeset/wise-monkeys-remain.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@effect/eslint-plugin": patch
3+
---
4+
5+
Add rule for disallowing direct barrel imports

eslint.config.mjs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { FlatCompat } from "@eslint/eslintrc"
21
import eslint from "@eslint/js"
32
import * as tsResolver from "eslint-import-resolver-typescript"
43
import importPlugin from "eslint-plugin-import-x"
@@ -7,16 +6,15 @@ import sortDestructureKeys from "eslint-plugin-sort-destructure-keys"
76
import * as Path from "node:path"
87
import * as Url from "node:url"
98
import tseslint from "typescript-eslint"
9+
import eslintPluginPrettier from 'eslint-plugin-prettier/recommended'
1010

1111
const __filename = Url.fileURLToPath(import.meta.url)
1212
const __dirname = Path.dirname(__filename)
1313

14-
const compat = new FlatCompat({
15-
baseDirectory: __dirname,
16-
})
1714

1815
export default tseslint.config(
1916
{
17+
files: ["src/**/*.ts", "test/**/*.ts"],
2018
ignores: ["**/dist", "**/build", "**/docs", "**/*.md"],
2119
},
2220
eslint.configs.recommended,
@@ -111,4 +109,5 @@ export default tseslint.config(
111109
"@typescript-eslint/unified-signatures": "off",
112110
},
113111
},
112+
eslintPluginPrettier
114113
)

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,9 @@
7272
"prettier": "^3.3.2",
7373
"typescript": "^5.7.3",
7474
"typescript-eslint": "^8.21.0",
75-
"vitest": "^3.0.4"
75+
"vitest": "^3.0.4",
76+
"eslint-plugin-prettier":"^5.2.6",
77+
"eslint-config-prettier":"^10.1.2"
7678
},
7779
"imports": {
7880
"#dist/*": {

pnpm-lock.yaml

Lines changed: 55 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/plugin.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import { dprint } from "@effect/eslint-plugin/rules/dprint"
2+
import { noImportFromBarrelPackage } from "@effect/eslint-plugin/rules/no-import-from-barrel-package"
23

34
export const meta = {
45
name: "@effect/eslint-plugin",
56
}
67

78
export const rules = {
89
dprint,
10+
noImportFromBarrelPackage,
911
}
1012

1113
// NOTE: unfortunately plugins needs a self-reference inside configs,
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { createRule } from "@effect/eslint-plugin/utils/eslint"
2+
import { AST_NODE_TYPES } from "@typescript-eslint/utils"
3+
import type { RuleFixer } from "@typescript-eslint/utils/ts-eslint"
4+
5+
export type Options = {
6+
packageNames: Array<string>
7+
}
8+
export type MessageIds = "replaceImport"
9+
10+
export const noImportFromBarrelPackage = createRule<[Options], MessageIds>({
11+
name: "no-import-from-barrel-package",
12+
meta: {
13+
type: "suggestion",
14+
docs: {
15+
description:
16+
"Disallow importing from barrel packages, and encourages importing the specific module instead.",
17+
},
18+
fixable: "code",
19+
messages: {
20+
replaceImport: `Use import * as {{localName}} from "{{packageName}}/{{moduleName}}" instead`,
21+
},
22+
schema: [
23+
{
24+
type: "object",
25+
properties: {
26+
packageNames: {
27+
type: "array",
28+
description: "List of packages to check for barrel imports",
29+
items: [
30+
{
31+
type: "string",
32+
},
33+
],
34+
},
35+
},
36+
additionalProperties: false,
37+
},
38+
],
39+
},
40+
defaultOptions: [{ packageNames: [] }],
41+
create: (context, options) => {
42+
return {
43+
ImportDeclaration: node => {
44+
// destruct options
45+
const [{ packageNames }] = options
46+
// first we check if the import is from one of the configured modules
47+
const packageName = node.source.value
48+
if (packageNames.indexOf(packageName) > -1) {
49+
for (const specifier of node.specifiers) {
50+
// check only imports with style import {A, B} from "foo"
51+
if (specifier.type === AST_NODE_TYPES.ImportSpecifier) {
52+
// we are fine with type imports
53+
if (specifier.importKind === "type") continue
54+
const moduleName =
55+
specifier.imported.type === AST_NODE_TYPES.Identifier
56+
? specifier.imported.name
57+
: specifier.imported.value
58+
const localName = specifier.local.name
59+
// fix only with a single specifier
60+
const fixable =
61+
node.specifiers.length === 1
62+
? {
63+
fix: (fixer: RuleFixer) =>
64+
fixer.replaceTextRange(
65+
node.range,
66+
`import * as ${localName} from "${packageName}/${moduleName}"`,
67+
),
68+
}
69+
: {}
70+
// report the error
71+
context.report({
72+
loc: specifier.loc,
73+
node: specifier,
74+
messageId: "replaceImport",
75+
data: {
76+
packageName,
77+
moduleName,
78+
localName,
79+
},
80+
...fixable,
81+
})
82+
}
83+
}
84+
}
85+
},
86+
}
87+
},
88+
})
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import type { Options } from "@effect/eslint-plugin/rules/no-import-from-barrel-package"
2+
import { noImportFromBarrelPackage as rule } from "@effect/eslint-plugin/rules/no-import-from-barrel-package"
3+
import { ruleTester } from "@effect/eslint-plugin/test/utils/index"
4+
5+
const options: [Options] = [{ packageNames: ["effect"] }]
6+
7+
ruleTester.run("dprint", rule, {
8+
valid: [
9+
{
10+
code: `import * as T from "effect/Effect"`,
11+
options,
12+
},
13+
{
14+
code: `import { Effect as Eff} from "effect/Effect"
15+
`,
16+
options,
17+
},
18+
{
19+
code: `import Effect from "effect/Effect"`,
20+
options,
21+
},
22+
{
23+
code: `import {type Effect } from "effect";`,
24+
options,
25+
},
26+
{
27+
code: `import {test} from "lodash";`,
28+
options,
29+
},
30+
],
31+
invalid: [
32+
{
33+
code: `import { Effect } from "effect"`,
34+
options,
35+
errors: [{ line: 1, messageId: "replaceImport" }],
36+
output: `import * as Effect from "effect/Effect"`,
37+
},
38+
{
39+
code: `import { Effect as Eff } from "effect"`,
40+
options,
41+
errors: [{ line: 1, messageId: "replaceImport" }],
42+
output: `import * as Eff from "effect/Effect"`,
43+
},
44+
],
45+
})

0 commit comments

Comments
 (0)