@intentius/chant 0.0.4 → 0.0.8

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.
Files changed (75) hide show
  1. package/README.md +10 -351
  2. package/bin/chant +20 -0
  3. package/package.json +18 -17
  4. package/src/bench.test.ts +3 -54
  5. package/src/cli/commands/__fixtures__/init-lexicon-output/src/plugin.ts +8 -23
  6. package/src/cli/commands/__fixtures__/init-lexicon-output/src/validate.ts +22 -18
  7. package/src/cli/commands/__snapshots__/init-lexicon.test.ts.snap +8 -23
  8. package/src/cli/commands/build.ts +1 -2
  9. package/src/cli/commands/import.test.ts +1 -1
  10. package/src/cli/commands/import.ts +2 -2
  11. package/src/cli/commands/init-lexicon.test.ts +0 -3
  12. package/src/cli/commands/init-lexicon.ts +31 -95
  13. package/src/cli/commands/init.test.ts +10 -14
  14. package/src/cli/commands/init.ts +16 -10
  15. package/src/cli/commands/lint.ts +9 -33
  16. package/src/cli/commands/list.ts +2 -2
  17. package/src/cli/commands/update.ts +5 -3
  18. package/src/cli/conflict-check.test.ts +0 -1
  19. package/src/cli/handlers/dev.ts +1 -9
  20. package/src/cli/main.ts +14 -4
  21. package/src/cli/mcp/server.test.ts +207 -4
  22. package/src/cli/mcp/server.ts +6 -0
  23. package/src/cli/mcp/tools/explain.ts +134 -0
  24. package/src/cli/mcp/tools/scaffold.ts +107 -0
  25. package/src/cli/mcp/tools/search.ts +98 -0
  26. package/src/codegen/docs-interpolation.test.ts +2 -2
  27. package/src/codegen/docs.ts +5 -4
  28. package/src/codegen/generate-registry.test.ts +2 -2
  29. package/src/codegen/generate-registry.ts +5 -6
  30. package/src/codegen/generate-typescript.test.ts +6 -6
  31. package/src/codegen/generate-typescript.ts +2 -6
  32. package/src/codegen/generate.ts +1 -12
  33. package/src/codegen/package.ts +28 -1
  34. package/src/codegen/typecheck.ts +6 -11
  35. package/src/codegen/validate.ts +16 -0
  36. package/src/config.ts +4 -0
  37. package/src/discovery/files.ts +6 -6
  38. package/src/discovery/import.ts +1 -1
  39. package/src/index.ts +1 -2
  40. package/src/lexicon-integrity.ts +5 -4
  41. package/src/lexicon.ts +2 -6
  42. package/src/lint/config.ts +8 -6
  43. package/src/lint/engine.ts +1 -5
  44. package/src/lint/rule.ts +0 -18
  45. package/src/lint/rules/evl009-composite-no-constant.test.ts +24 -8
  46. package/src/lint/rules/evl009-composite-no-constant.ts +50 -29
  47. package/src/lint/rules/index.ts +1 -22
  48. package/src/runtime-adapter.ts +158 -0
  49. package/src/serializer-walker.test.ts +0 -9
  50. package/src/serializer-walker.ts +1 -3
  51. package/src/stack-output.ts +3 -3
  52. package/src/barrel.test.ts +0 -157
  53. package/src/barrel.ts +0 -101
  54. package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/rollback.ts +0 -45
  55. package/src/codegen/case.test.ts +0 -30
  56. package/src/codegen/case.ts +0 -11
  57. package/src/codegen/rollback.test.ts +0 -92
  58. package/src/codegen/rollback.ts +0 -115
  59. package/src/lint/rules/barrel-import-style.test.ts +0 -80
  60. package/src/lint/rules/barrel-import-style.ts +0 -59
  61. package/src/lint/rules/enforce-barrel-import.test.ts +0 -169
  62. package/src/lint/rules/enforce-barrel-import.ts +0 -81
  63. package/src/lint/rules/enforce-barrel-ref.test.ts +0 -114
  64. package/src/lint/rules/enforce-barrel-ref.ts +0 -75
  65. package/src/lint/rules/evl006-barrel-usage.test.ts +0 -63
  66. package/src/lint/rules/evl006-barrel-usage.ts +0 -95
  67. package/src/lint/rules/evl008-unresolvable-barrel-ref.test.ts +0 -118
  68. package/src/lint/rules/evl008-unresolvable-barrel-ref.ts +0 -140
  69. package/src/lint/rules/prefer-namespace-import.test.ts +0 -102
  70. package/src/lint/rules/prefer-namespace-import.ts +0 -63
  71. package/src/lint/rules/stale-barrel-types.ts +0 -60
  72. package/src/project/scan.test.ts +0 -178
  73. package/src/project/scan.ts +0 -182
  74. package/src/project/sync.test.ts +0 -87
  75. package/src/project/sync.ts +0 -46
@@ -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
- };