@intentius/chant 0.0.3 → 0.0.5
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/README.md +10 -351
- package/package.json +1 -1
- package/src/bench.test.ts +3 -54
- package/src/build.ts +14 -1
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/plugin.ts +12 -2
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/validate.ts +22 -18
- package/src/cli/commands/__snapshots__/init-lexicon.test.ts.snap +12 -2
- package/src/cli/commands/import.test.ts +1 -1
- package/src/cli/commands/init-lexicon.ts +34 -20
- package/src/cli/commands/init.test.ts +10 -14
- package/src/cli/commands/init.ts +2 -7
- package/src/cli/commands/lint.ts +9 -33
- package/src/cli/main.ts +1 -1
- package/src/codegen/docs-interpolation.test.ts +77 -0
- package/src/codegen/docs.ts +80 -5
- package/src/codegen/generate-registry.test.ts +1 -1
- package/src/codegen/generate-registry.ts +3 -3
- package/src/codegen/package.ts +28 -1
- package/src/codegen/validate.ts +16 -0
- package/src/discovery/collect.ts +7 -0
- package/src/discovery/files.ts +6 -6
- package/src/discovery/import.ts +1 -1
- package/src/index.ts +0 -1
- package/src/lint/engine.ts +1 -5
- package/src/lint/rule.ts +0 -18
- package/src/lint/rules/evl009-composite-no-constant.test.ts +24 -8
- package/src/lint/rules/evl009-composite-no-constant.ts +50 -29
- package/src/lint/rules/index.ts +1 -22
- package/src/stack-output.ts +3 -3
- package/src/barrel.test.ts +0 -157
- package/src/barrel.ts +0 -101
- package/src/lint/rules/barrel-import-style.test.ts +0 -80
- package/src/lint/rules/barrel-import-style.ts +0 -59
- package/src/lint/rules/enforce-barrel-import.test.ts +0 -169
- package/src/lint/rules/enforce-barrel-import.ts +0 -81
- package/src/lint/rules/enforce-barrel-ref.test.ts +0 -114
- package/src/lint/rules/enforce-barrel-ref.ts +0 -75
- package/src/lint/rules/evl006-barrel-usage.test.ts +0 -63
- package/src/lint/rules/evl006-barrel-usage.ts +0 -95
- package/src/lint/rules/evl008-unresolvable-barrel-ref.test.ts +0 -118
- package/src/lint/rules/evl008-unresolvable-barrel-ref.ts +0 -140
- package/src/lint/rules/prefer-namespace-import.test.ts +0 -102
- package/src/lint/rules/prefer-namespace-import.ts +0 -63
- package/src/lint/rules/stale-barrel-types.ts +0 -60
- package/src/project/scan.test.ts +0 -178
- package/src/project/scan.ts +0 -182
- package/src/project/sync.test.ts +0 -87
- package/src/project/sync.ts +0 -46
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
import * as ts from "typescript";
|
|
2
|
-
import type { LintRule, LintContext, LintDiagnostic } from "../rule";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* COR002: barrel-import-style
|
|
6
|
-
*
|
|
7
|
-
* Enforce `import * as _` for local `_.ts` barrel imports.
|
|
8
|
-
*
|
|
9
|
-
* Triggers on: import { bucketEncryption } from "./_"
|
|
10
|
-
* OK: import * as _ from "./_"
|
|
11
|
-
* OK: import type { Config } from "./_"
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
const barrelPattern = /^\.\/(_|_\..*)$/;
|
|
15
|
-
|
|
16
|
-
function checkNode(node: ts.Node, context: LintContext, diagnostics: LintDiagnostic[]): void {
|
|
17
|
-
if (ts.isImportDeclaration(node)) {
|
|
18
|
-
const moduleSpecifier = node.moduleSpecifier;
|
|
19
|
-
if (!ts.isStringLiteral(moduleSpecifier)) return;
|
|
20
|
-
|
|
21
|
-
const modulePath = moduleSpecifier.text;
|
|
22
|
-
if (!barrelPattern.test(modulePath)) return;
|
|
23
|
-
|
|
24
|
-
// Skip type-only imports: import type { X } from "..."
|
|
25
|
-
if (node.importClause?.isTypeOnly) return;
|
|
26
|
-
|
|
27
|
-
const importClause = node.importClause;
|
|
28
|
-
if (!importClause?.namedBindings) return;
|
|
29
|
-
|
|
30
|
-
// Flag named imports (not namespace imports)
|
|
31
|
-
if (ts.isNamedImports(importClause.namedBindings)) {
|
|
32
|
-
const { line, character } = context.sourceFile.getLineAndCharacterOfPosition(
|
|
33
|
-
node.getStart(context.sourceFile)
|
|
34
|
-
);
|
|
35
|
-
|
|
36
|
-
diagnostics.push({
|
|
37
|
-
file: context.filePath,
|
|
38
|
-
line: line + 1,
|
|
39
|
-
column: character + 1,
|
|
40
|
-
ruleId: "COR002",
|
|
41
|
-
severity: "error",
|
|
42
|
-
message: `Use namespace import for local barrel — replace with: import * as _ from "./_"`,
|
|
43
|
-
});
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
ts.forEachChild(node, child => checkNode(child, context, diagnostics));
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export const barrelImportStyleRule: LintRule = {
|
|
51
|
-
id: "COR002",
|
|
52
|
-
severity: "error",
|
|
53
|
-
category: "style",
|
|
54
|
-
check(context: LintContext): LintDiagnostic[] {
|
|
55
|
-
const diagnostics: LintDiagnostic[] = [];
|
|
56
|
-
checkNode(context.sourceFile, context, diagnostics);
|
|
57
|
-
return diagnostics;
|
|
58
|
-
},
|
|
59
|
-
};
|
|
@@ -1,169 +0,0 @@
|
|
|
1
|
-
import { describe, test, expect, mock, beforeEach, afterEach } from "bun:test";
|
|
2
|
-
import * as ts from "typescript";
|
|
3
|
-
import { enforceBarrelImportRule } from "./enforce-barrel-import";
|
|
4
|
-
import type { LintContext } from "../rule";
|
|
5
|
-
import { writeFileSync, mkdirSync, rmSync } from "fs";
|
|
6
|
-
import { join } from "path";
|
|
7
|
-
|
|
8
|
-
const TEST_DIR = join(import.meta.dir, "__test_barrel_import__");
|
|
9
|
-
|
|
10
|
-
function createContext(code: string, filePath = "test.ts"): LintContext {
|
|
11
|
-
const sourceFile = ts.createSourceFile(
|
|
12
|
-
filePath,
|
|
13
|
-
code,
|
|
14
|
-
ts.ScriptTarget.Latest,
|
|
15
|
-
true
|
|
16
|
-
);
|
|
17
|
-
|
|
18
|
-
return {
|
|
19
|
-
sourceFile,
|
|
20
|
-
entities: [],
|
|
21
|
-
filePath,
|
|
22
|
-
lexicon: undefined,
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
describe("COR014: enforce-barrel-import", () => {
|
|
27
|
-
beforeEach(() => {
|
|
28
|
-
mkdirSync(TEST_DIR, { recursive: true });
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
afterEach(() => {
|
|
32
|
-
rmSync(TEST_DIR, { recursive: true, force: true });
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
test("rule metadata", () => {
|
|
36
|
-
expect(enforceBarrelImportRule.id).toBe("COR014");
|
|
37
|
-
expect(enforceBarrelImportRule.severity).toBe("warning");
|
|
38
|
-
expect(enforceBarrelImportRule.category).toBe("style");
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
test("flags lexicon import in non-barrel file", () => {
|
|
42
|
-
const code = `import * as td from "@intentius/chant-lexicon-testdom";`;
|
|
43
|
-
const context = createContext(code, join(TEST_DIR, "my-stack.ts"));
|
|
44
|
-
const diagnostics = enforceBarrelImportRule.check(context);
|
|
45
|
-
|
|
46
|
-
expect(diagnostics).toHaveLength(1);
|
|
47
|
-
expect(diagnostics[0].ruleId).toBe("COR014");
|
|
48
|
-
expect(diagnostics[0].severity).toBe("warning");
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
test("skips barrel file named _.ts", () => {
|
|
52
|
-
const code = `import * as td from "@intentius/chant-lexicon-testdom";`;
|
|
53
|
-
const context = createContext(code, join(TEST_DIR, "_.ts"));
|
|
54
|
-
const diagnostics = enforceBarrelImportRule.check(context);
|
|
55
|
-
expect(diagnostics).toHaveLength(0);
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
test("skips barrel file with path prefix", () => {
|
|
59
|
-
const code = `import * as td from "@intentius/chant-lexicon-testdom";`;
|
|
60
|
-
const context = createContext(code, join(TEST_DIR, "infra", "_.ts"));
|
|
61
|
-
const diagnostics = enforceBarrelImportRule.check(context);
|
|
62
|
-
expect(diagnostics).toHaveLength(0);
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
test("allows barrel import", () => {
|
|
66
|
-
const code = `import * as _ from "./_";`;
|
|
67
|
-
const context = createContext(code, join(TEST_DIR, "my-stack.ts"));
|
|
68
|
-
const diagnostics = enforceBarrelImportRule.check(context);
|
|
69
|
-
expect(diagnostics).toHaveLength(0);
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
test("allows non-chant imports", () => {
|
|
73
|
-
const code = `import * as ts from "typescript";`;
|
|
74
|
-
const context = createContext(code, join(TEST_DIR, "my-stack.ts"));
|
|
75
|
-
const diagnostics = enforceBarrelImportRule.check(context);
|
|
76
|
-
expect(diagnostics).toHaveLength(0);
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
test("message includes barrel content when barrel missing", () => {
|
|
80
|
-
const code = `import * as td from "@intentius/chant-lexicon-testdom";`;
|
|
81
|
-
const filePath = join(TEST_DIR, "my-stack.ts");
|
|
82
|
-
const context = createContext(code, filePath);
|
|
83
|
-
const diagnostics = enforceBarrelImportRule.check(context);
|
|
84
|
-
|
|
85
|
-
expect(diagnostics).toHaveLength(1);
|
|
86
|
-
expect(diagnostics[0].message).toContain("Create _.ts");
|
|
87
|
-
expect(diagnostics[0].message).toContain(`export * from "@intentius/chant-lexicon-testdom"`);
|
|
88
|
-
expect(diagnostics[0].message).toContain("barrel");
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
test("message is shorter when barrel exists", () => {
|
|
92
|
-
// Create a barrel file in TEST_DIR
|
|
93
|
-
writeFileSync(join(TEST_DIR, "_.ts"), `export * from "@intentius/chant-lexicon-testdom";`);
|
|
94
|
-
|
|
95
|
-
const code = `import * as td from "@intentius/chant-lexicon-testdom";`;
|
|
96
|
-
const filePath = join(TEST_DIR, "my-stack.ts");
|
|
97
|
-
const context = createContext(code, filePath);
|
|
98
|
-
const diagnostics = enforceBarrelImportRule.check(context);
|
|
99
|
-
|
|
100
|
-
expect(diagnostics).toHaveLength(1);
|
|
101
|
-
expect(diagnostics[0].message).not.toContain("Create _.ts");
|
|
102
|
-
expect(diagnostics[0].message).toContain("use the barrel");
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
test("provides auto-fix when barrel exists", () => {
|
|
106
|
-
writeFileSync(join(TEST_DIR, "_.ts"), `export * from "@intentius/chant-lexicon-testdom";`);
|
|
107
|
-
|
|
108
|
-
const code = `import * as td from "@intentius/chant-lexicon-testdom";`;
|
|
109
|
-
const context = createContext(code, join(TEST_DIR, "my-stack.ts"));
|
|
110
|
-
const diagnostics = enforceBarrelImportRule.check(context);
|
|
111
|
-
|
|
112
|
-
expect(diagnostics).toHaveLength(1);
|
|
113
|
-
expect(diagnostics[0].fix).toBeDefined();
|
|
114
|
-
expect(diagnostics[0].fix!.replacement).toBe(`import * as _ from "./_"`);
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
test("does not provide auto-fix when barrel is missing", () => {
|
|
118
|
-
const code = `import * as td from "@intentius/chant-lexicon-testdom";`;
|
|
119
|
-
const context = createContext(code, join(TEST_DIR, "my-stack.ts"));
|
|
120
|
-
const diagnostics = enforceBarrelImportRule.check(context);
|
|
121
|
-
|
|
122
|
-
expect(diagnostics).toHaveLength(1);
|
|
123
|
-
expect(diagnostics[0].fix).toBeUndefined();
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
test("flags type-only lexicon import", () => {
|
|
127
|
-
const code = `import type { Code } from "@intentius/chant-lexicon-testdom";`;
|
|
128
|
-
const context = createContext(code, join(TEST_DIR, "my-stack.ts"));
|
|
129
|
-
const diagnostics = enforceBarrelImportRule.check(context);
|
|
130
|
-
|
|
131
|
-
expect(diagnostics).toHaveLength(1);
|
|
132
|
-
expect(diagnostics[0].ruleId).toBe("COR014");
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
test("skips subpath imports from core", () => {
|
|
136
|
-
const code = `import type { LintRule } from "@intentius/chant/lint/rule";`;
|
|
137
|
-
const context = createContext(code, join(TEST_DIR, "my-rule.ts"));
|
|
138
|
-
const diagnostics = enforceBarrelImportRule.check(context);
|
|
139
|
-
expect(diagnostics).toHaveLength(0);
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
test("skips deep subpath imports from lexicon", () => {
|
|
143
|
-
const code = `import { helper } from "@intentius/chant-lexicon-aws/internal/util";`;
|
|
144
|
-
const context = createContext(code, join(TEST_DIR, "my-stack.ts"));
|
|
145
|
-
const diagnostics = enforceBarrelImportRule.check(context);
|
|
146
|
-
expect(diagnostics).toHaveLength(0);
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
test("flags multiple lexicon imports", () => {
|
|
150
|
-
const code = `import * as td from "@intentius/chant-lexicon-testdom";
|
|
151
|
-
import * as core from "@intentius/chant";`;
|
|
152
|
-
const context = createContext(code, join(TEST_DIR, "my-stack.ts"));
|
|
153
|
-
const diagnostics = enforceBarrelImportRule.check(context);
|
|
154
|
-
|
|
155
|
-
expect(diagnostics).toHaveLength(2);
|
|
156
|
-
expect(diagnostics[0].message).toContain("@intentius/chant-lexicon-testdom");
|
|
157
|
-
expect(diagnostics[1].message).toContain("@intentius/chant");
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
test("reports correct line and column numbers", () => {
|
|
161
|
-
const code = `import * as td from "@intentius/chant-lexicon-testdom";`;
|
|
162
|
-
const context = createContext(code, join(TEST_DIR, "my-stack.ts"));
|
|
163
|
-
const diagnostics = enforceBarrelImportRule.check(context);
|
|
164
|
-
|
|
165
|
-
expect(diagnostics).toHaveLength(1);
|
|
166
|
-
expect(diagnostics[0].line).toBe(1);
|
|
167
|
-
expect(diagnostics[0].column).toBe(1);
|
|
168
|
-
});
|
|
169
|
-
});
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
import * as ts from "typescript";
|
|
2
|
-
import { existsSync } from "fs";
|
|
3
|
-
import { dirname, join, basename } from "path";
|
|
4
|
-
import type { LintRule, LintContext, LintDiagnostic } from "../rule";
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* COR014: enforce-barrel-import
|
|
8
|
-
*
|
|
9
|
-
* Flags direct lexicon imports (`import * as <name> from "@intentius/chant-lexicon-<name>"`)
|
|
10
|
-
* in non-barrel files. Use the barrel (`import * as _ from "./_"`) instead.
|
|
11
|
-
*
|
|
12
|
-
* Triggers on: import * as <name> from "@intentius/chant-lexicon-<name>" (in non-barrel files)
|
|
13
|
-
* OK: import * as _ from "./_"
|
|
14
|
-
* OK: import * as <name> from "@intentius/chant-lexicon-<name>" (in _.ts barrel files)
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
export const enforceBarrelImportRule: LintRule = {
|
|
18
|
-
id: "COR014",
|
|
19
|
-
severity: "warning",
|
|
20
|
-
category: "style",
|
|
21
|
-
check(context: LintContext): LintDiagnostic[] {
|
|
22
|
-
const diagnostics: LintDiagnostic[] = [];
|
|
23
|
-
const sf = context.sourceFile;
|
|
24
|
-
|
|
25
|
-
// Skip barrel files
|
|
26
|
-
if (basename(context.filePath).startsWith("_")) return diagnostics;
|
|
27
|
-
|
|
28
|
-
// Check if barrel exists
|
|
29
|
-
const dir = dirname(context.filePath);
|
|
30
|
-
const barrelExists = existsSync(join(dir, "_.ts"));
|
|
31
|
-
|
|
32
|
-
for (const stmt of sf.statements) {
|
|
33
|
-
if (!ts.isImportDeclaration(stmt)) continue;
|
|
34
|
-
if (!ts.isStringLiteral(stmt.moduleSpecifier)) continue;
|
|
35
|
-
|
|
36
|
-
const modulePath = stmt.moduleSpecifier.text;
|
|
37
|
-
if (!modulePath.startsWith("@intentius/chant") && !modulePath.startsWith("@intentius/chant-lexicon-")) continue;
|
|
38
|
-
|
|
39
|
-
// Skip subpath imports (e.g., @intentius/chant/lint/rule) — these are
|
|
40
|
-
// framework internals not available through the barrel
|
|
41
|
-
const parts = modulePath.split("/");
|
|
42
|
-
if (parts.length > 2) continue;
|
|
43
|
-
|
|
44
|
-
const { line, character } = sf.getLineAndCharacterOfPosition(stmt.getStart(sf));
|
|
45
|
-
const importText = stmt.getText(sf);
|
|
46
|
-
|
|
47
|
-
let message: string;
|
|
48
|
-
if (barrelExists) {
|
|
49
|
-
message = `Direct lexicon import — use the barrel.\n - ${importText}\n + import * as _ from "./_";`;
|
|
50
|
-
} else {
|
|
51
|
-
const lexiconPkg = modulePath;
|
|
52
|
-
message =
|
|
53
|
-
`Direct lexicon import — use a barrel file.\n\n` +
|
|
54
|
-
` Create _.ts:\n\n` +
|
|
55
|
-
` export * from "${lexiconPkg}";\n` +
|
|
56
|
-
` import { barrel } from "@intentius/chant";\n` +
|
|
57
|
-
` export const $ = barrel(import.meta.dir);\n\n` +
|
|
58
|
-
` Then replace this file's import:\n` +
|
|
59
|
-
` - ${importText}\n` +
|
|
60
|
-
` + import * as _ from "./_";`;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
diagnostics.push({
|
|
64
|
-
file: context.filePath,
|
|
65
|
-
line: line + 1,
|
|
66
|
-
column: character + 1,
|
|
67
|
-
ruleId: "COR014",
|
|
68
|
-
severity: "warning",
|
|
69
|
-
message,
|
|
70
|
-
// Only provide auto-fix when barrel exists — replacing the import
|
|
71
|
-
// when no barrel is present corrupts the file
|
|
72
|
-
fix: barrelExists ? {
|
|
73
|
-
range: [stmt.getStart(sf), stmt.getEnd()],
|
|
74
|
-
replacement: `import * as _ from "./_"`,
|
|
75
|
-
} : undefined,
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
return diagnostics;
|
|
80
|
-
},
|
|
81
|
-
};
|
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
import { describe, test, expect } from "bun:test";
|
|
2
|
-
import * as ts from "typescript";
|
|
3
|
-
import { enforceBarrelRefRule } from "./enforce-barrel-ref";
|
|
4
|
-
import type { LintContext } from "../rule";
|
|
5
|
-
|
|
6
|
-
function createContext(code: string, filePath = "test.ts"): LintContext {
|
|
7
|
-
const sourceFile = ts.createSourceFile(
|
|
8
|
-
filePath,
|
|
9
|
-
code,
|
|
10
|
-
ts.ScriptTarget.Latest,
|
|
11
|
-
true
|
|
12
|
-
);
|
|
13
|
-
|
|
14
|
-
return {
|
|
15
|
-
sourceFile,
|
|
16
|
-
entities: [],
|
|
17
|
-
filePath,
|
|
18
|
-
lexicon: undefined,
|
|
19
|
-
};
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
describe("COR007: enforce-barrel-ref", () => {
|
|
23
|
-
test("rule metadata", () => {
|
|
24
|
-
expect(enforceBarrelRefRule.id).toBe("COR007");
|
|
25
|
-
expect(enforceBarrelRefRule.severity).toBe("warning");
|
|
26
|
-
expect(enforceBarrelRefRule.category).toBe("style");
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
test("flags named import from sibling", () => {
|
|
30
|
-
const code = `import { dataBucket } from "./data-bucket";`;
|
|
31
|
-
const context = createContext(code);
|
|
32
|
-
const diagnostics = enforceBarrelRefRule.check(context);
|
|
33
|
-
|
|
34
|
-
expect(diagnostics).toHaveLength(1);
|
|
35
|
-
expect(diagnostics[0].ruleId).toBe("COR007");
|
|
36
|
-
expect(diagnostics[0].severity).toBe("warning");
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
test("flags namespace import from sibling", () => {
|
|
40
|
-
const code = `import * as db from "./data-bucket";`;
|
|
41
|
-
const context = createContext(code);
|
|
42
|
-
const diagnostics = enforceBarrelRefRule.check(context);
|
|
43
|
-
|
|
44
|
-
expect(diagnostics).toHaveLength(1);
|
|
45
|
-
expect(diagnostics[0].ruleId).toBe("COR007");
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
test("skips barrel import", () => {
|
|
49
|
-
const code = `import * as _ from "./_";`;
|
|
50
|
-
const context = createContext(code);
|
|
51
|
-
const diagnostics = enforceBarrelRefRule.check(context);
|
|
52
|
-
expect(diagnostics).toHaveLength(0);
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
test("skips lexicon imports", () => {
|
|
56
|
-
const code = `import * as td from "@intentius/chant-lexicon-testdom";`;
|
|
57
|
-
const context = createContext(code);
|
|
58
|
-
const diagnostics = enforceBarrelRefRule.check(context);
|
|
59
|
-
expect(diagnostics).toHaveLength(0);
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
test("skips barrel file", () => {
|
|
63
|
-
const code = `import { dataBucket } from "./data-bucket";`;
|
|
64
|
-
const context = createContext(code, "_.ts");
|
|
65
|
-
const diagnostics = enforceBarrelRefRule.check(context);
|
|
66
|
-
expect(diagnostics).toHaveLength(0);
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
test("message includes suggestion with _.$ prefix", () => {
|
|
70
|
-
const code = `import { dataBucket } from "./data-bucket";`;
|
|
71
|
-
const context = createContext(code);
|
|
72
|
-
const diagnostics = enforceBarrelRefRule.check(context);
|
|
73
|
-
|
|
74
|
-
expect(diagnostics).toHaveLength(1);
|
|
75
|
-
expect(diagnostics[0].message).toContain("_.$.dataBucket");
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
test("flags multiple sibling imports", () => {
|
|
79
|
-
const code = `import { dataBucket } from "./data-bucket";
|
|
80
|
-
import { logGroup } from "./log-group";`;
|
|
81
|
-
const context = createContext(code);
|
|
82
|
-
const diagnostics = enforceBarrelRefRule.check(context);
|
|
83
|
-
|
|
84
|
-
expect(diagnostics).toHaveLength(2);
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
test("flags parent relative imports", () => {
|
|
88
|
-
const code = `import { x } from "../other";`;
|
|
89
|
-
const context = createContext(code);
|
|
90
|
-
const diagnostics = enforceBarrelRefRule.check(context);
|
|
91
|
-
|
|
92
|
-
expect(diagnostics).toHaveLength(1);
|
|
93
|
-
expect(diagnostics[0].ruleId).toBe("COR007");
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
test("does not provide auto-fix", () => {
|
|
97
|
-
const code = `import { dataBucket } from "./data-bucket";`;
|
|
98
|
-
const context = createContext(code);
|
|
99
|
-
const diagnostics = enforceBarrelRefRule.check(context);
|
|
100
|
-
|
|
101
|
-
expect(diagnostics).toHaveLength(1);
|
|
102
|
-
expect(diagnostics[0].fix).toBeUndefined();
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
test("reports correct line and column numbers", () => {
|
|
106
|
-
const code = `import { dataBucket } from "./data-bucket";`;
|
|
107
|
-
const context = createContext(code);
|
|
108
|
-
const diagnostics = enforceBarrelRefRule.check(context);
|
|
109
|
-
|
|
110
|
-
expect(diagnostics).toHaveLength(1);
|
|
111
|
-
expect(diagnostics[0].line).toBe(1);
|
|
112
|
-
expect(diagnostics[0].column).toBe(1);
|
|
113
|
-
});
|
|
114
|
-
});
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
import * as ts from "typescript";
|
|
2
|
-
import { basename } from "path";
|
|
3
|
-
import type { LintRule, LintContext, LintDiagnostic } from "../rule";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* COR007: enforce-barrel-ref
|
|
7
|
-
*
|
|
8
|
-
* Flags direct sibling imports (`import { dataBucket } from "./data-bucket"`)
|
|
9
|
-
* in non-barrel files. Use `_.$` instead.
|
|
10
|
-
*
|
|
11
|
-
* Triggers on: import { dataBucket } from "./data-bucket"
|
|
12
|
-
* OK: import * as _ from "./_"
|
|
13
|
-
* OK: import { dataBucket } from "./data-bucket" (in _.ts barrel files)
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
const barrelPattern = /^\.\/(_|_\..*)$/;
|
|
17
|
-
|
|
18
|
-
export const enforceBarrelRefRule: LintRule = {
|
|
19
|
-
id: "COR007",
|
|
20
|
-
severity: "warning",
|
|
21
|
-
category: "style",
|
|
22
|
-
check(context: LintContext): LintDiagnostic[] {
|
|
23
|
-
const diagnostics: LintDiagnostic[] = [];
|
|
24
|
-
const sf = context.sourceFile;
|
|
25
|
-
|
|
26
|
-
// Skip barrel files
|
|
27
|
-
if (basename(context.filePath).startsWith("_")) return diagnostics;
|
|
28
|
-
|
|
29
|
-
for (const stmt of sf.statements) {
|
|
30
|
-
if (!ts.isImportDeclaration(stmt)) continue;
|
|
31
|
-
if (!ts.isStringLiteral(stmt.moduleSpecifier)) continue;
|
|
32
|
-
|
|
33
|
-
const modulePath = stmt.moduleSpecifier.text;
|
|
34
|
-
|
|
35
|
-
// Skip non-relative imports
|
|
36
|
-
if (!modulePath.startsWith("./") && !modulePath.startsWith("../")) continue;
|
|
37
|
-
|
|
38
|
-
// Skip barrel imports
|
|
39
|
-
if (barrelPattern.test(modulePath)) continue;
|
|
40
|
-
|
|
41
|
-
// This is a direct sibling import — flag it
|
|
42
|
-
const { line, character } = sf.getLineAndCharacterOfPosition(stmt.getStart(sf));
|
|
43
|
-
const importText = stmt.getText(sf);
|
|
44
|
-
|
|
45
|
-
// Extract imported names for the suggestion
|
|
46
|
-
const importedNames: string[] = [];
|
|
47
|
-
const clause = stmt.importClause;
|
|
48
|
-
if (clause?.namedBindings) {
|
|
49
|
-
if (ts.isNamedImports(clause.namedBindings)) {
|
|
50
|
-
for (const el of clause.namedBindings.elements) {
|
|
51
|
-
importedNames.push(el.name.text);
|
|
52
|
-
}
|
|
53
|
-
} else if (ts.isNamespaceImport(clause.namedBindings)) {
|
|
54
|
-
importedNames.push(clause.namedBindings.name.text + ".*");
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const refSuggestion =
|
|
59
|
-
importedNames.length > 0
|
|
60
|
-
? `\n Access via: ${importedNames.map((n) => `_.$.${n}`).join(", ")}`
|
|
61
|
-
: "";
|
|
62
|
-
|
|
63
|
-
diagnostics.push({
|
|
64
|
-
file: context.filePath,
|
|
65
|
-
line: line + 1,
|
|
66
|
-
column: character + 1,
|
|
67
|
-
ruleId: "COR007",
|
|
68
|
-
severity: "warning",
|
|
69
|
-
message: `Direct sibling import — use _.$ instead.\n - ${importText}${refSuggestion}`,
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
return diagnostics;
|
|
74
|
-
},
|
|
75
|
-
};
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
import { describe, test, expect } from "bun:test";
|
|
2
|
-
import * as ts from "typescript";
|
|
3
|
-
import { evl006BarrelUsageRule } from "./evl006-barrel-usage";
|
|
4
|
-
import type { LintContext } from "../rule";
|
|
5
|
-
|
|
6
|
-
function createContext(code: string, filePath = "test.ts"): LintContext {
|
|
7
|
-
const sourceFile = ts.createSourceFile(filePath, code, ts.ScriptTarget.Latest, true);
|
|
8
|
-
return { sourceFile, entities: [], filePath, lexicon: undefined };
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
describe("EVL006: barrel-usage", () => {
|
|
12
|
-
test("rule metadata", () => {
|
|
13
|
-
expect(evl006BarrelUsageRule.id).toBe("EVL006");
|
|
14
|
-
expect(evl006BarrelUsageRule.severity).toBe("error");
|
|
15
|
-
expect(evl006BarrelUsageRule.category).toBe("correctness");
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
test("allows correct barrel usage", () => {
|
|
19
|
-
const ctx = createContext(`export const $ = barrel(import.meta.dir);`);
|
|
20
|
-
expect(evl006BarrelUsageRule.check(ctx)).toHaveLength(0);
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
test("flags string literal argument", () => {
|
|
24
|
-
const ctx = createContext(`export const $ = barrel("./src");`);
|
|
25
|
-
const diags = evl006BarrelUsageRule.check(ctx);
|
|
26
|
-
expect(diags.some((d) => d.message.includes("import.meta.dir"))).toBe(true);
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
test("flags variable argument", () => {
|
|
30
|
-
const ctx = createContext(`export const $ = barrel(myDir);`);
|
|
31
|
-
const diags = evl006BarrelUsageRule.check(ctx);
|
|
32
|
-
expect(diags.some((d) => d.message.includes("import.meta.dir"))).toBe(true);
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
test("flags non-exported barrel", () => {
|
|
36
|
-
const ctx = createContext(`const $ = barrel(import.meta.dir);`);
|
|
37
|
-
const diags = evl006BarrelUsageRule.check(ctx);
|
|
38
|
-
expect(diags.some((d) => d.message.includes("export const $"))).toBe(true);
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
test("flags barrel not assigned to $", () => {
|
|
42
|
-
const ctx = createContext(`export const myBarrel = barrel(import.meta.dir);`);
|
|
43
|
-
const diags = evl006BarrelUsageRule.check(ctx);
|
|
44
|
-
expect(diags.some((d) => d.message.includes("export const $"))).toBe(true);
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
test("flags let instead of const", () => {
|
|
48
|
-
const ctx = createContext(`export let $ = barrel(import.meta.dir);`);
|
|
49
|
-
const diags = evl006BarrelUsageRule.check(ctx);
|
|
50
|
-
expect(diags.some((d) => d.message.includes("export const $"))).toBe(true);
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
test("does not flag non-barrel calls", () => {
|
|
54
|
-
const ctx = createContext(`export const x = someFunction(import.meta.dir);`);
|
|
55
|
-
expect(evl006BarrelUsageRule.check(ctx)).toHaveLength(0);
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
test("flags no arguments", () => {
|
|
59
|
-
const ctx = createContext(`export const $ = barrel();`);
|
|
60
|
-
const diags = evl006BarrelUsageRule.check(ctx);
|
|
61
|
-
expect(diags.some((d) => d.message.includes("import.meta.dir"))).toBe(true);
|
|
62
|
-
});
|
|
63
|
-
});
|
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
import * as ts from "typescript";
|
|
2
|
-
import type { LintRule, LintContext, LintDiagnostic } from "../rule";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* EVL006: Incorrect barrel() Usage
|
|
6
|
-
*
|
|
7
|
-
* Two constraints:
|
|
8
|
-
* 1. The argument to barrel() must be import.meta.dir
|
|
9
|
-
* 2. The call must be part of: export const $ = barrel(import.meta.dir)
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
function isImportMetaDir(node: ts.Node): boolean {
|
|
13
|
-
// import.meta.dir is a PropertyAccessExpression: (import.meta).dir
|
|
14
|
-
if (
|
|
15
|
-
ts.isPropertyAccessExpression(node) &&
|
|
16
|
-
node.name.text === "dir" &&
|
|
17
|
-
ts.isMetaProperty(node.expression) &&
|
|
18
|
-
node.expression.keywordToken === ts.SyntaxKind.ImportKeyword &&
|
|
19
|
-
node.expression.name.text === "meta"
|
|
20
|
-
) {
|
|
21
|
-
return true;
|
|
22
|
-
}
|
|
23
|
-
return false;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function checkNode(node: ts.Node, context: LintContext, diagnostics: LintDiagnostic[]): void {
|
|
27
|
-
if (
|
|
28
|
-
ts.isCallExpression(node) &&
|
|
29
|
-
ts.isIdentifier(node.expression) &&
|
|
30
|
-
node.expression.text === "barrel"
|
|
31
|
-
) {
|
|
32
|
-
// Check constraint 1: argument must be import.meta.dir
|
|
33
|
-
const arg = node.arguments[0];
|
|
34
|
-
if (!arg || !isImportMetaDir(arg)) {
|
|
35
|
-
const { line, character } = context.sourceFile.getLineAndCharacterOfPosition(
|
|
36
|
-
node.getStart(context.sourceFile),
|
|
37
|
-
);
|
|
38
|
-
diagnostics.push({
|
|
39
|
-
file: context.filePath,
|
|
40
|
-
line: line + 1,
|
|
41
|
-
column: character + 1,
|
|
42
|
-
ruleId: "EVL006",
|
|
43
|
-
severity: "error",
|
|
44
|
-
message: "barrel() argument must be import.meta.dir",
|
|
45
|
-
});
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// Check constraint 2: must be export const $ = barrel(...)
|
|
49
|
-
let isValidExport = false;
|
|
50
|
-
const parent = node.parent;
|
|
51
|
-
if (parent && ts.isVariableDeclaration(parent)) {
|
|
52
|
-
if (ts.isIdentifier(parent.name) && parent.name.text === "$") {
|
|
53
|
-
const declList = parent.parent;
|
|
54
|
-
if (declList && ts.isVariableDeclarationList(declList)) {
|
|
55
|
-
const stmt = declList.parent;
|
|
56
|
-
if (
|
|
57
|
-
stmt &&
|
|
58
|
-
ts.isVariableStatement(stmt) &&
|
|
59
|
-
stmt.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) &&
|
|
60
|
-
(declList.flags & ts.NodeFlags.Const) !== 0
|
|
61
|
-
) {
|
|
62
|
-
isValidExport = true;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
if (!isValidExport) {
|
|
69
|
-
const { line, character } = context.sourceFile.getLineAndCharacterOfPosition(
|
|
70
|
-
node.getStart(context.sourceFile),
|
|
71
|
-
);
|
|
72
|
-
diagnostics.push({
|
|
73
|
-
file: context.filePath,
|
|
74
|
-
line: line + 1,
|
|
75
|
-
column: character + 1,
|
|
76
|
-
ruleId: "EVL006",
|
|
77
|
-
severity: "error",
|
|
78
|
-
message: "barrel() must be used as: export const $ = barrel(import.meta.dir)",
|
|
79
|
-
});
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
ts.forEachChild(node, (child) => checkNode(child, context, diagnostics));
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
export const evl006BarrelUsageRule: LintRule = {
|
|
87
|
-
id: "EVL006",
|
|
88
|
-
severity: "error",
|
|
89
|
-
category: "correctness",
|
|
90
|
-
check(context: LintContext): LintDiagnostic[] {
|
|
91
|
-
const diagnostics: LintDiagnostic[] = [];
|
|
92
|
-
checkNode(context.sourceFile, context, diagnostics);
|
|
93
|
-
return diagnostics;
|
|
94
|
-
},
|
|
95
|
-
};
|