| 1 | import rule from "@effect/oxc/oxlint/rules/no-import-from-barrel-package" |
| 2 | import { describe, expect, it } from "vitest" |
| 3 | import { runRule } from "./utils.ts" |
| 4 | |
| 5 | describe("no-import-from-barrel-package", () => { |
| 6 | const testOptions = { |
| 7 | filename: "/test/file.ts", |
| 8 | cwd: "/test", |
| 9 | ruleOptions: [{ checkPatterns: ["^effect$"] }] |
| 10 | } |
| 11 | |
| 12 | const createImportDeclaration = ( |
| 13 | source: string, |
| 14 | specifiers: Array<unknown>, |
| 15 | importKind?: "type" | "value" |
| 16 | ) => { |
| 17 | const base = { |
| 18 | type: "ImportDeclaration" as const, |
| 19 | source: { value: source }, |
| 20 | specifiers, |
| 21 | range: [0, 50] as [number, number] |
| 22 | } |
| 23 | return importKind !== undefined ? { ...base, importKind } : base |
| 24 | } |
| 25 | |
| 26 | const createNamedSpecifier = ( |
| 27 | name: string, |
| 28 | local?: string, |
| 29 | importKind?: "type" | "value" |
| 30 | ) => { |
| 31 | const base = { |
| 32 | type: "ImportSpecifier" as const, |
| 33 | imported: { type: "Identifier" as const, name }, |
| 34 | local: { name: local ?? name } |
| 35 | } |
| 36 | return importKind !== undefined ? { ...base, importKind } : base |
| 37 | } |
| 38 | |
| 39 | it("should not report for imports from non-barrel packages", () => { |
| 40 | const node = createImportDeclaration("effect/Effect", [ |
| 41 | createNamedSpecifier("Effect") |
| 42 | ]) |
| 43 | const errors = runRule(rule, "ImportDeclaration", node, testOptions) |
| 44 | expect(errors).toHaveLength(0) |
| 45 | }) |
| 46 | |
| 47 | it("should report for named imports from effect barrel", () => { |
| 48 | const node = createImportDeclaration("effect", [ |
| 49 | createNamedSpecifier("Effect") |
| 50 | ]) |
| 51 | const errors = runRule(rule, "ImportDeclaration", node, testOptions) |
| 52 | expect(errors).toHaveLength(1) |
| 53 | expect(errors[0].message).toBe( |
| 54 | `Use import * as Effect from "effect/Effect" instead` |
| 55 | ) |
| 56 | }) |
| 57 | |
| 58 | it("should report for multiple named imports from effect barrel", () => { |
| 59 | const node = createImportDeclaration("effect", [ |
| 60 | createNamedSpecifier("Effect"), |
| 61 | createNamedSpecifier("Option"), |
| 62 | createNamedSpecifier("Either") |
| 63 | ]) |
| 64 | const errors = runRule(rule, "ImportDeclaration", node, testOptions) |
| 65 | expect(errors).toHaveLength(3) |
| 66 | }) |
| 67 | |
| 68 | it("should not report for type-only import declarations", () => { |
| 69 | const node = createImportDeclaration( |
| 70 | "effect", |
| 71 | [createNamedSpecifier("Effect")], |
| 72 | "type" |
| 73 | ) |
| 74 | const errors = runRule(rule, "ImportDeclaration", node, testOptions) |
| 75 | expect(errors).toHaveLength(0) |
| 76 | }) |
| 77 | |
| 78 | it("should not report for type imports within named specifiers", () => { |
| 79 | const node = createImportDeclaration("effect", [ |
| 80 | createNamedSpecifier("Effect", "Effect", "type") |
| 81 | ]) |
| 82 | const errors = runRule(rule, "ImportDeclaration", node, testOptions) |
| 83 | expect(errors).toHaveLength(0) |
| 84 | }) |
| 85 | |
| 86 | it("should handle aliased imports", () => { |
| 87 | const node = createImportDeclaration("effect", [ |
| 88 | createNamedSpecifier("Effect", "Eff") |
| 89 | ]) |
| 90 | const errors = runRule(rule, "ImportDeclaration", node, testOptions) |
| 91 | expect(errors).toHaveLength(1) |
| 92 | expect(errors[0].message).toBe( |
| 93 | `Use import * as Eff from "effect/Effect" instead` |
| 94 | ) |
| 95 | }) |
| 96 | |
| 97 | it("should report for namespace imports from barrel", () => { |
| 98 | const node = createImportDeclaration("effect", [ |
| 99 | { |
| 100 | type: "ImportNamespaceSpecifier", |
| 101 | local: { name: "Effect" } |
| 102 | } |
| 103 | ]) |
| 104 | const errors = runRule(rule, "ImportDeclaration", node, testOptions) |
| 105 | expect(errors).toHaveLength(1) |
| 106 | expect(errors[0].message).toBe( |
| 107 | `Do not use namespace import from barrel file "effect", import from specific modules instead` |
| 108 | ) |
| 109 | }) |
| 110 | |
| 111 | it("should not report for namespace imports from module paths", () => { |
| 112 | const node = createImportDeclaration("effect/Effect", [ |
| 113 | { |
| 114 | type: "ImportNamespaceSpecifier", |
| 115 | local: { name: "Effect" } |
| 116 | } |
| 117 | ]) |
| 118 | const errors = runRule(rule, "ImportDeclaration", node, testOptions) |
| 119 | expect(errors).toHaveLength(0) |
| 120 | }) |
| 121 | |
| 122 | it("should not report for default imports", () => { |
| 123 | const node = createImportDeclaration("effect", [ |
| 124 | { |
| 125 | type: "ImportDefaultSpecifier", |
| 126 | local: { name: "Effect" } |
| 127 | } |
| 128 | ]) |
| 129 | const errors = runRule(rule, "ImportDeclaration", node, testOptions) |
| 130 | expect(errors).toHaveLength(0) |
| 131 | }) |
| 132 | |
| 133 | describe("configuration", () => { |
| 134 | it("should match regex patterns", () => { |
| 135 | const node = createImportDeclaration("@myorg/utils", [ |
| 136 | createNamedSpecifier("helper") |
| 137 | ]) |
| 138 | // No patterns configured - doesn't match |
| 139 | const defaultErrors = runRule(rule, "ImportDeclaration", node, testOptions) |
| 140 | expect(defaultErrors).toHaveLength(0) |
| 141 | |
| 142 | // Pattern matches @myorg/* |
| 143 | const customErrors = runRule(rule, "ImportDeclaration", node, { |
| 144 | ...testOptions, |
| 145 | ruleOptions: [{ checkPatterns: ["^@myorg/"] }] |
| 146 | }) |
| 147 | expect(customErrors).toHaveLength(1) |
| 148 | }) |
| 149 | |
| 150 | it("should match exact package with pattern", () => { |
| 151 | const node = createImportDeclaration("lodash", [ |
| 152 | createNamedSpecifier("map") |
| 153 | ]) |
| 154 | // No patterns - doesn't match |
| 155 | const defaultErrors = runRule(rule, "ImportDeclaration", node, testOptions) |
| 156 | expect(defaultErrors).toHaveLength(0) |
| 157 | |
| 158 | // Exact match pattern |
| 159 | const customErrors = runRule(rule, "ImportDeclaration", node, { |
| 160 | ...testOptions, |
| 161 | ruleOptions: [{ checkPatterns: ["^lodash$"] }] |
| 162 | }) |
| 163 | expect(customErrors).toHaveLength(1) |
| 164 | }) |
| 165 | |
| 166 | it("should disable relative index checking when configured", () => { |
| 167 | const node = createImportDeclaration("./index.ts", [ |
| 168 | createNamedSpecifier("foo") |
| 169 | ]) |
| 170 | // Default checks relative imports |
| 171 | const defaultErrors = runRule(rule, "ImportDeclaration", node, testOptions) |
| 172 | expect(defaultErrors).toHaveLength(1) |
| 173 | |
| 174 | // Disabled relative checking |
| 175 | const customErrors = runRule(rule, "ImportDeclaration", node, { |
| 176 | ...testOptions, |
| 177 | ruleOptions: [{ checkRelativeIndexImports: false }] |
| 178 | }) |
| 179 | expect(customErrors).toHaveLength(0) |
| 180 | }) |
| 181 | }) |
| 182 | }) |