@intentius/chant 0.0.1
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 +365 -0
- package/package.json +22 -0
- package/src/attrref.test.ts +148 -0
- package/src/attrref.ts +50 -0
- package/src/barrel.test.ts +157 -0
- package/src/barrel.ts +101 -0
- package/src/bench.test.ts +227 -0
- package/src/build.test.ts +437 -0
- package/src/build.ts +425 -0
- package/src/builder.test.ts +312 -0
- package/src/builder.ts +56 -0
- package/src/child-project.ts +44 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/README.md +26 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/docs/astro.config.mjs +14 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/docs/package.json +16 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/docs/src/content/docs/index.mdx +8 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/docs/src/content.config.ts +7 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/docs/tsconfig.json +10 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/examples/getting-started/.gitkeep +0 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/justfile +26 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/package.json +29 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/docs.ts +25 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/generate-cli.ts +8 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/generate.ts +74 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/naming.ts +33 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/package.ts +25 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/rollback.ts +45 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/coverage.ts +11 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/generated/.gitkeep +0 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/import/generator.ts +10 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/import/parser.ts +10 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/index.ts +9 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/lint/rules/index.ts +1 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/lint/rules/sample.ts +18 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/lsp/completions.ts +14 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/lsp/hover.ts +14 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/plugin.ts +110 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/serializer.ts +24 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/spec/fetch.ts +21 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/spec/parse.ts +25 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/validate-cli.ts +4 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/validate.ts +24 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/tsconfig.json +10 -0
- package/src/cli/commands/__fixtures__/sample-rule.ts +11 -0
- package/src/cli/commands/__snapshots__/init-lexicon.test.ts.snap +222 -0
- package/src/cli/commands/build.test.ts +149 -0
- package/src/cli/commands/build.ts +344 -0
- package/src/cli/commands/diff.test.ts +148 -0
- package/src/cli/commands/diff.ts +221 -0
- package/src/cli/commands/doctor.test.ts +239 -0
- package/src/cli/commands/doctor.ts +224 -0
- package/src/cli/commands/import.test.ts +379 -0
- package/src/cli/commands/import.ts +335 -0
- package/src/cli/commands/init-lexicon.test.ts +297 -0
- package/src/cli/commands/init-lexicon.ts +993 -0
- package/src/cli/commands/init.test.ts +317 -0
- package/src/cli/commands/init.ts +505 -0
- package/src/cli/commands/licenses.ts +165 -0
- package/src/cli/commands/lint.test.ts +332 -0
- package/src/cli/commands/lint.ts +408 -0
- package/src/cli/commands/list.test.ts +100 -0
- package/src/cli/commands/list.ts +108 -0
- package/src/cli/commands/update.test.ts +38 -0
- package/src/cli/commands/update.ts +207 -0
- package/src/cli/conflict-check.test.ts +255 -0
- package/src/cli/conflict-check.ts +89 -0
- package/src/cli/debug.ts +8 -0
- package/src/cli/format.test.ts +140 -0
- package/src/cli/format.ts +133 -0
- package/src/cli/handlers/build.ts +58 -0
- package/src/cli/handlers/dev.ts +38 -0
- package/src/cli/handlers/init.ts +46 -0
- package/src/cli/handlers/lint.ts +36 -0
- package/src/cli/handlers/misc.ts +57 -0
- package/src/cli/handlers/serve.ts +26 -0
- package/src/cli/index.ts +3 -0
- package/src/cli/lsp/capabilities.ts +46 -0
- package/src/cli/lsp/diagnostics.ts +52 -0
- package/src/cli/lsp/server.test.ts +618 -0
- package/src/cli/lsp/server.ts +393 -0
- package/src/cli/main.test.ts +257 -0
- package/src/cli/main.ts +224 -0
- package/src/cli/mcp/resources/context.ts +59 -0
- package/src/cli/mcp/server.test.ts +747 -0
- package/src/cli/mcp/server.ts +402 -0
- package/src/cli/mcp/tools/build.ts +117 -0
- package/src/cli/mcp/tools/import.ts +48 -0
- package/src/cli/mcp/tools/lint.ts +45 -0
- package/src/cli/plugins.test.ts +31 -0
- package/src/cli/plugins.ts +94 -0
- package/src/cli/registry.ts +73 -0
- package/src/cli/reporters/stylish.test.ts +282 -0
- package/src/cli/reporters/stylish.ts +186 -0
- package/src/cli/watch.test.ts +81 -0
- package/src/cli/watch.ts +101 -0
- package/src/codegen/case.test.ts +30 -0
- package/src/codegen/case.ts +11 -0
- package/src/codegen/coverage.ts +167 -0
- package/src/codegen/docs.ts +634 -0
- package/src/codegen/fetch.test.ts +119 -0
- package/src/codegen/fetch.ts +261 -0
- package/src/codegen/generate-registry.test.ts +118 -0
- package/src/codegen/generate-registry.ts +107 -0
- package/src/codegen/generate-runtime-index.test.ts +81 -0
- package/src/codegen/generate-runtime-index.ts +99 -0
- package/src/codegen/generate-typescript.test.ts +146 -0
- package/src/codegen/generate-typescript.ts +161 -0
- package/src/codegen/generate.ts +206 -0
- package/src/codegen/json-patch.test.ts +113 -0
- package/src/codegen/json-patch.ts +151 -0
- package/src/codegen/json-schema.test.ts +196 -0
- package/src/codegen/json-schema.ts +209 -0
- package/src/codegen/naming.ts +201 -0
- package/src/codegen/package.ts +161 -0
- package/src/codegen/rollback.test.ts +92 -0
- package/src/codegen/rollback.ts +115 -0
- package/src/codegen/topo-sort.test.ts +69 -0
- package/src/codegen/topo-sort.ts +46 -0
- package/src/codegen/typecheck.test.ts +37 -0
- package/src/codegen/typecheck.ts +74 -0
- package/src/codegen/validate.test.ts +86 -0
- package/src/codegen/validate.ts +143 -0
- package/src/composite.test.ts +426 -0
- package/src/composite.ts +243 -0
- package/src/config.test.ts +91 -0
- package/src/config.ts +87 -0
- package/src/declarable.test.ts +160 -0
- package/src/declarable.ts +47 -0
- package/src/detectLexicon.test.ts +236 -0
- package/src/detectLexicon.ts +37 -0
- package/src/discovery/cache.test.ts +78 -0
- package/src/discovery/cache.ts +86 -0
- package/src/discovery/collect.test.ts +269 -0
- package/src/discovery/collect.ts +51 -0
- package/src/discovery/cycles.test.ts +238 -0
- package/src/discovery/cycles.ts +107 -0
- package/src/discovery/files.test.ts +154 -0
- package/src/discovery/files.ts +61 -0
- package/src/discovery/graph.test.ts +476 -0
- package/src/discovery/graph.ts +150 -0
- package/src/discovery/import.test.ts +199 -0
- package/src/discovery/import.ts +20 -0
- package/src/discovery/index.test.ts +272 -0
- package/src/discovery/index.ts +132 -0
- package/src/discovery/resolve.test.ts +267 -0
- package/src/discovery/resolve.ts +54 -0
- package/src/errors.test.ts +138 -0
- package/src/errors.ts +86 -0
- package/src/import/base-parser.test.ts +67 -0
- package/src/import/base-parser.ts +48 -0
- package/src/import/generator.ts +21 -0
- package/src/import/ir-utils.test.ts +103 -0
- package/src/import/ir-utils.ts +87 -0
- package/src/import/parser.ts +41 -0
- package/src/index.ts +60 -0
- package/src/intrinsic-interpolation.test.ts +91 -0
- package/src/intrinsic-interpolation.ts +89 -0
- package/src/intrinsic.test.ts +69 -0
- package/src/intrinsic.ts +43 -0
- package/src/lexicon-integrity.test.ts +94 -0
- package/src/lexicon-integrity.ts +69 -0
- package/src/lexicon-manifest.test.ts +101 -0
- package/src/lexicon-manifest.ts +71 -0
- package/src/lexicon-output.test.ts +182 -0
- package/src/lexicon-output.ts +82 -0
- package/src/lexicon-schema.test.ts +239 -0
- package/src/lexicon-schema.ts +144 -0
- package/src/lexicon.ts +212 -0
- package/src/lint/config-overrides.test.ts +254 -0
- package/src/lint/config.test.ts +644 -0
- package/src/lint/config.ts +375 -0
- package/src/lint/declarative.test.ts +256 -0
- package/src/lint/declarative.ts +187 -0
- package/src/lint/engine.test.ts +465 -0
- package/src/lint/engine.ts +172 -0
- package/src/lint/named-checks.test.ts +37 -0
- package/src/lint/named-checks.ts +33 -0
- package/src/lint/parser.test.ts +129 -0
- package/src/lint/parser.ts +42 -0
- package/src/lint/post-synth.test.ts +113 -0
- package/src/lint/post-synth.ts +76 -0
- package/src/lint/presets/relaxed.json +19 -0
- package/src/lint/presets/strict.json +19 -0
- package/src/lint/rule-loader.test.ts +67 -0
- package/src/lint/rule-loader.ts +67 -0
- package/src/lint/rule-options.test.ts +141 -0
- package/src/lint/rule.test.ts +196 -0
- package/src/lint/rule.ts +98 -0
- package/src/lint/rules/barrel-import-style.test.ts +80 -0
- package/src/lint/rules/barrel-import-style.ts +59 -0
- package/src/lint/rules/composite-scope.ts +55 -0
- package/src/lint/rules/cor017-composite-name-match.test.ts +107 -0
- package/src/lint/rules/cor017-composite-name-match.ts +108 -0
- package/src/lint/rules/cor018-composite-prefer-lexicon-type.test.ts +172 -0
- package/src/lint/rules/cor018-composite-prefer-lexicon-type.ts +167 -0
- package/src/lint/rules/declarable-naming-convention.test.ts +69 -0
- package/src/lint/rules/declarable-naming-convention.ts +70 -0
- package/src/lint/rules/enforce-barrel-import.test.ts +169 -0
- package/src/lint/rules/enforce-barrel-import.ts +81 -0
- package/src/lint/rules/enforce-barrel-ref.test.ts +114 -0
- package/src/lint/rules/enforce-barrel-ref.ts +75 -0
- package/src/lint/rules/evl001-non-literal-expression.test.ts +158 -0
- package/src/lint/rules/evl001-non-literal-expression.ts +149 -0
- package/src/lint/rules/evl002-control-flow-resource.test.ts +110 -0
- package/src/lint/rules/evl002-control-flow-resource.ts +61 -0
- package/src/lint/rules/evl003-dynamic-property-access.test.ts +63 -0
- package/src/lint/rules/evl003-dynamic-property-access.ts +41 -0
- package/src/lint/rules/evl004-spread-non-const.test.ts +130 -0
- package/src/lint/rules/evl004-spread-non-const.ts +111 -0
- package/src/lint/rules/evl005-resource-block-body.test.ts +59 -0
- package/src/lint/rules/evl005-resource-block-body.ts +49 -0
- package/src/lint/rules/evl006-barrel-usage.test.ts +63 -0
- package/src/lint/rules/evl006-barrel-usage.ts +95 -0
- package/src/lint/rules/evl007-invalid-siblings.test.ts +87 -0
- package/src/lint/rules/evl007-invalid-siblings.ts +139 -0
- package/src/lint/rules/evl008-unresolvable-barrel-ref.test.ts +118 -0
- package/src/lint/rules/evl008-unresolvable-barrel-ref.ts +140 -0
- package/src/lint/rules/evl009-composite-no-constant.test.ts +162 -0
- package/src/lint/rules/evl009-composite-no-constant.ts +171 -0
- package/src/lint/rules/evl010-composite-no-transform.test.ts +121 -0
- package/src/lint/rules/evl010-composite-no-transform.ts +69 -0
- package/src/lint/rules/export-required.test.ts +213 -0
- package/src/lint/rules/export-required.ts +158 -0
- package/src/lint/rules/file-declarable-limit.test.ts +148 -0
- package/src/lint/rules/file-declarable-limit.ts +96 -0
- package/src/lint/rules/flat-declarations.test.ts +210 -0
- package/src/lint/rules/flat-declarations.ts +70 -0
- package/src/lint/rules/index.ts +99 -0
- package/src/lint/rules/no-cyclic-declarable-ref.test.ts +135 -0
- package/src/lint/rules/no-cyclic-declarable-ref.ts +178 -0
- package/src/lint/rules/no-redundant-type-import.test.ts +129 -0
- package/src/lint/rules/no-redundant-type-import.ts +85 -0
- package/src/lint/rules/no-redundant-value-cast.test.ts +51 -0
- package/src/lint/rules/no-redundant-value-cast.ts +46 -0
- package/src/lint/rules/no-string-ref.test.ts +100 -0
- package/src/lint/rules/no-string-ref.ts +66 -0
- package/src/lint/rules/no-unused-declarable-import.test.ts +74 -0
- package/src/lint/rules/no-unused-declarable-import.ts +103 -0
- package/src/lint/rules/no-unused-declarable.test.ts +134 -0
- package/src/lint/rules/no-unused-declarable.ts +118 -0
- package/src/lint/rules/prefer-namespace-import.test.ts +102 -0
- package/src/lint/rules/prefer-namespace-import.ts +63 -0
- package/src/lint/rules/single-concern-file.test.ts +156 -0
- package/src/lint/rules/single-concern-file.ts +98 -0
- package/src/lint/rules/stale-barrel-types.ts +60 -0
- package/src/lint/selectors.test.ts +113 -0
- package/src/lint/selectors.ts +188 -0
- package/src/lsp/lexicon-providers.ts +191 -0
- package/src/lsp/types.ts +79 -0
- package/src/mcp/types.ts +22 -0
- package/src/project/scan.test.ts +178 -0
- package/src/project/scan.ts +182 -0
- package/src/project/sync.test.ts +87 -0
- package/src/project/sync.ts +46 -0
- package/src/project-validation.test.ts +64 -0
- package/src/project-validation.ts +79 -0
- package/src/pseudo-parameter.test.ts +39 -0
- package/src/pseudo-parameter.ts +47 -0
- package/src/runtime.ts +68 -0
- package/src/serializer-walker.test.ts +124 -0
- package/src/serializer-walker.ts +83 -0
- package/src/serializer.ts +42 -0
- package/src/sort.test.ts +290 -0
- package/src/sort.ts +58 -0
- package/src/stack-output.ts +82 -0
- package/src/types.test.ts +307 -0
- package/src/types.ts +46 -0
- package/src/utils.test.ts +195 -0
- package/src/utils.ts +46 -0
- package/src/validation.test.ts +308 -0
- package/src/validation.ts +50 -0
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import { detectCycles, normalizeCycleKey } from "./cycles";
|
|
3
|
+
|
|
4
|
+
describe("detectCycles", () => {
|
|
5
|
+
test("returns empty array for empty graph", () => {
|
|
6
|
+
const graph = {};
|
|
7
|
+
const cycles = detectCycles(graph);
|
|
8
|
+
expect(cycles).toEqual([]);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test("returns empty array for single node with no edges", () => {
|
|
12
|
+
const graph = {
|
|
13
|
+
A: [],
|
|
14
|
+
};
|
|
15
|
+
const cycles = detectCycles(graph);
|
|
16
|
+
expect(cycles).toEqual([]);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("returns empty array for acyclic graph with multiple nodes", () => {
|
|
20
|
+
const graph = {
|
|
21
|
+
A: ["B", "C"],
|
|
22
|
+
B: ["D"],
|
|
23
|
+
C: ["D"],
|
|
24
|
+
D: [],
|
|
25
|
+
};
|
|
26
|
+
const cycles = detectCycles(graph);
|
|
27
|
+
expect(cycles).toEqual([]);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("returns empty array for linear chain", () => {
|
|
31
|
+
const graph = {
|
|
32
|
+
A: ["B"],
|
|
33
|
+
B: ["C"],
|
|
34
|
+
C: ["D"],
|
|
35
|
+
D: [],
|
|
36
|
+
};
|
|
37
|
+
const cycles = detectCycles(graph);
|
|
38
|
+
expect(cycles).toEqual([]);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("detects simple self-loop", () => {
|
|
42
|
+
const graph = {
|
|
43
|
+
A: ["A"],
|
|
44
|
+
};
|
|
45
|
+
const cycles = detectCycles(graph);
|
|
46
|
+
expect(cycles.length).toBe(1);
|
|
47
|
+
expect(cycles[0]).toEqual(["A"]);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("detects simple two-node cycle", () => {
|
|
51
|
+
const graph = {
|
|
52
|
+
A: ["B"],
|
|
53
|
+
B: ["A"],
|
|
54
|
+
};
|
|
55
|
+
const cycles = detectCycles(graph);
|
|
56
|
+
expect(cycles.length).toBe(1);
|
|
57
|
+
expect(cycles[0]).toEqual(["A", "B"]);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("detects three-node cycle", () => {
|
|
61
|
+
const graph = {
|
|
62
|
+
A: ["B"],
|
|
63
|
+
B: ["C"],
|
|
64
|
+
C: ["A"],
|
|
65
|
+
};
|
|
66
|
+
const cycles = detectCycles(graph);
|
|
67
|
+
expect(cycles.length).toBe(1);
|
|
68
|
+
expect(cycles[0]).toEqual(["A", "B", "C"]);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("detects longer cycle", () => {
|
|
72
|
+
const graph = {
|
|
73
|
+
A: ["B"],
|
|
74
|
+
B: ["C"],
|
|
75
|
+
C: ["D"],
|
|
76
|
+
D: ["E"],
|
|
77
|
+
E: ["A"],
|
|
78
|
+
};
|
|
79
|
+
const cycles = detectCycles(graph);
|
|
80
|
+
expect(cycles.length).toBe(1);
|
|
81
|
+
expect(cycles[0]).toEqual(["A", "B", "C", "D", "E"]);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("detects cycle with branching paths", () => {
|
|
85
|
+
const graph = {
|
|
86
|
+
A: ["B", "C"],
|
|
87
|
+
B: ["D"],
|
|
88
|
+
C: ["D"],
|
|
89
|
+
D: ["A"],
|
|
90
|
+
};
|
|
91
|
+
const cycles = detectCycles(graph);
|
|
92
|
+
expect(cycles.length).toBe(1);
|
|
93
|
+
// The cycle should be detected from one of the paths
|
|
94
|
+
expect(cycles[0]).toContain("A");
|
|
95
|
+
expect(cycles[0]).toContain("D");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("detects multiple independent cycles", () => {
|
|
99
|
+
const graph = {
|
|
100
|
+
A: ["B"],
|
|
101
|
+
B: ["A"],
|
|
102
|
+
C: ["D"],
|
|
103
|
+
D: ["C"],
|
|
104
|
+
};
|
|
105
|
+
const cycles = detectCycles(graph);
|
|
106
|
+
expect(cycles.length).toBe(2);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("detects cycle in complex graph with acyclic parts", () => {
|
|
110
|
+
const graph = {
|
|
111
|
+
A: ["B"],
|
|
112
|
+
B: ["C"],
|
|
113
|
+
C: ["D"],
|
|
114
|
+
D: ["B", "E"],
|
|
115
|
+
E: [],
|
|
116
|
+
};
|
|
117
|
+
const cycles = detectCycles(graph);
|
|
118
|
+
expect(cycles.length).toBe(1);
|
|
119
|
+
expect(cycles[0]).toEqual(["B", "C", "D"]);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("handles graph with nodes that have no outgoing edges", () => {
|
|
123
|
+
const graph = {
|
|
124
|
+
A: ["B"],
|
|
125
|
+
B: ["C"],
|
|
126
|
+
C: ["A"],
|
|
127
|
+
D: [],
|
|
128
|
+
E: ["D"],
|
|
129
|
+
};
|
|
130
|
+
const cycles = detectCycles(graph);
|
|
131
|
+
expect(cycles.length).toBe(1);
|
|
132
|
+
expect(cycles[0]).toEqual(["A", "B", "C"]);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("handles disconnected components with one containing cycle", () => {
|
|
136
|
+
const graph = {
|
|
137
|
+
A: ["B"],
|
|
138
|
+
B: ["C"],
|
|
139
|
+
C: [],
|
|
140
|
+
D: ["E"],
|
|
141
|
+
E: ["D"],
|
|
142
|
+
};
|
|
143
|
+
const cycles = detectCycles(graph);
|
|
144
|
+
expect(cycles.length).toBe(1);
|
|
145
|
+
expect(cycles[0]).toEqual(["D", "E"]);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("detects nested cycles", () => {
|
|
149
|
+
const graph = {
|
|
150
|
+
A: ["B"],
|
|
151
|
+
B: ["C"],
|
|
152
|
+
C: ["A", "D"],
|
|
153
|
+
D: ["E"],
|
|
154
|
+
E: ["D"],
|
|
155
|
+
};
|
|
156
|
+
const cycles = detectCycles(graph);
|
|
157
|
+
// Should detect at least one cycle
|
|
158
|
+
expect(cycles.length).toBeGreaterThanOrEqual(1);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test("handles graph with multiple edges from same node", () => {
|
|
162
|
+
const graph = {
|
|
163
|
+
A: ["B", "C", "D"],
|
|
164
|
+
B: ["E"],
|
|
165
|
+
C: ["E"],
|
|
166
|
+
D: ["E"],
|
|
167
|
+
E: ["A"],
|
|
168
|
+
};
|
|
169
|
+
const cycles = detectCycles(graph);
|
|
170
|
+
expect(cycles.length).toBe(1);
|
|
171
|
+
expect(cycles[0][0]).toBe("A");
|
|
172
|
+
expect(cycles[0][cycles[0].length - 1]).toBe("E");
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test("handles nodes referenced as neighbors but not defined as keys", () => {
|
|
176
|
+
const graph = {
|
|
177
|
+
A: ["B"],
|
|
178
|
+
B: ["C"],
|
|
179
|
+
// C is referenced but not defined
|
|
180
|
+
};
|
|
181
|
+
const cycles = detectCycles(graph);
|
|
182
|
+
expect(cycles).toEqual([]);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test("preserves node IDs correctly in cycle path", () => {
|
|
186
|
+
const graph = {
|
|
187
|
+
node1: ["node2"],
|
|
188
|
+
node2: ["node3"],
|
|
189
|
+
node3: ["node1"],
|
|
190
|
+
};
|
|
191
|
+
const cycles = detectCycles(graph);
|
|
192
|
+
expect(cycles.length).toBe(1);
|
|
193
|
+
expect(cycles[0]).toEqual(["node1", "node2", "node3"]);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test("handles graph with numeric-like string node IDs", () => {
|
|
197
|
+
const graph = {
|
|
198
|
+
"1": ["2"],
|
|
199
|
+
"2": ["3"],
|
|
200
|
+
"3": ["1"],
|
|
201
|
+
};
|
|
202
|
+
const cycles = detectCycles(graph);
|
|
203
|
+
expect(cycles.length).toBe(1);
|
|
204
|
+
expect(cycles[0]).toEqual(["1", "2", "3"]);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test("accepts Map<string, Set<string>> input", () => {
|
|
208
|
+
const graph = new Map<string, Set<string>>([
|
|
209
|
+
["A", new Set(["B"])],
|
|
210
|
+
["B", new Set(["C"])],
|
|
211
|
+
["C", new Set(["A"])],
|
|
212
|
+
]);
|
|
213
|
+
const cycles = detectCycles(graph);
|
|
214
|
+
expect(cycles.length).toBe(1);
|
|
215
|
+
expect(cycles[0]).toEqual(["A", "B", "C"]);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test("deduplicates equivalent cycles from different starting nodes", () => {
|
|
219
|
+
// Both A->B->C->A and B->C->A->B are the same cycle
|
|
220
|
+
const graph = new Map<string, Set<string>>([
|
|
221
|
+
["A", new Set(["B"])],
|
|
222
|
+
["B", new Set(["C"])],
|
|
223
|
+
["C", new Set(["A"])],
|
|
224
|
+
]);
|
|
225
|
+
const cycles = detectCycles(graph);
|
|
226
|
+
expect(cycles.length).toBe(1);
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
describe("normalizeCycleKey", () => {
|
|
231
|
+
test("rotates to lexicographically smallest node", () => {
|
|
232
|
+
expect(normalizeCycleKey(["C", "A", "B", "C"])).toBe("A,B,C");
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
test("handles single-node cycle", () => {
|
|
236
|
+
expect(normalizeCycleKey(["A", "A"])).toBe("A");
|
|
237
|
+
});
|
|
238
|
+
});
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cycle detection using DFS with three-color marking (white/gray/black).
|
|
3
|
+
*
|
|
4
|
+
* Accepts either `Map<string, Set<string>>` or `Record<string, string[]>` as input.
|
|
5
|
+
* Returns deduplicated cycles, each represented as an array of node IDs.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
type GraphInput = Map<string, Set<string>> | Record<string, string[]>;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Normalize graph input to Map<string, Set<string>>.
|
|
12
|
+
*/
|
|
13
|
+
function normalizeGraph(graph: GraphInput): Map<string, Set<string>> {
|
|
14
|
+
if (graph instanceof Map) return graph;
|
|
15
|
+
const result = new Map<string, Set<string>>();
|
|
16
|
+
for (const [node, neighbors] of Object.entries(graph)) {
|
|
17
|
+
result.set(node, new Set(neighbors));
|
|
18
|
+
}
|
|
19
|
+
return result;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Detects cycles in a directed graph using DFS with three-color marking.
|
|
24
|
+
*
|
|
25
|
+
* @param graph - Adjacency list: node → neighbors. Accepts Map<string, Set<string>>
|
|
26
|
+
* or Record<string, string[]>.
|
|
27
|
+
* @returns Array of deduplicated cycles. Each cycle is a node ID array
|
|
28
|
+
* (e.g. ["A", "B", "C"]). Self-loops return ["A"].
|
|
29
|
+
*/
|
|
30
|
+
export function detectCycles(graph: GraphInput): string[][] {
|
|
31
|
+
const g = normalizeGraph(graph);
|
|
32
|
+
|
|
33
|
+
const WHITE = 0; // Not visited
|
|
34
|
+
const GRAY = 1; // In current DFS path
|
|
35
|
+
const BLACK = 2; // Fully explored
|
|
36
|
+
|
|
37
|
+
const color = new Map<string, number>();
|
|
38
|
+
const parent = new Map<string, string>();
|
|
39
|
+
const cycles: string[][] = [];
|
|
40
|
+
const reportedCycles = new Set<string>();
|
|
41
|
+
|
|
42
|
+
for (const node of g.keys()) {
|
|
43
|
+
color.set(node, WHITE);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function dfs(node: string): void {
|
|
47
|
+
color.set(node, GRAY);
|
|
48
|
+
|
|
49
|
+
const neighbors = g.get(node) ?? new Set();
|
|
50
|
+
for (const neighbor of neighbors) {
|
|
51
|
+
const c = color.get(neighbor);
|
|
52
|
+
if (c === undefined || c === WHITE) {
|
|
53
|
+
if (c === undefined) {
|
|
54
|
+
// Node referenced but not defined as a key — treat as fully explored
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
parent.set(neighbor, node);
|
|
58
|
+
dfs(neighbor);
|
|
59
|
+
} else if (c === GRAY) {
|
|
60
|
+
// Found a cycle — reconstruct it
|
|
61
|
+
const cycle: string[] = [neighbor];
|
|
62
|
+
let current = node;
|
|
63
|
+
while (current !== neighbor) {
|
|
64
|
+
cycle.push(current);
|
|
65
|
+
current = parent.get(current)!;
|
|
66
|
+
}
|
|
67
|
+
cycle.push(neighbor);
|
|
68
|
+
cycle.reverse();
|
|
69
|
+
|
|
70
|
+
// Normalize to avoid duplicate cycle reports
|
|
71
|
+
const key = normalizeCycleKey(cycle);
|
|
72
|
+
if (!reportedCycles.has(key)) {
|
|
73
|
+
reportedCycles.add(key);
|
|
74
|
+
// Strip the trailing repeated node for a clean cycle representation
|
|
75
|
+
cycles.push(cycle.slice(0, -1));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
color.set(node, BLACK);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
for (const node of g.keys()) {
|
|
84
|
+
if (color.get(node) === WHITE) {
|
|
85
|
+
dfs(node);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return cycles;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Normalize a cycle for deduplication.
|
|
94
|
+
* Rotate to start with the lexicographically smallest node.
|
|
95
|
+
*/
|
|
96
|
+
export function normalizeCycleKey(cycle: string[]): string {
|
|
97
|
+
// Remove the repeated last element for rotation
|
|
98
|
+
const nodes = cycle.slice(0, -1);
|
|
99
|
+
let minIdx = 0;
|
|
100
|
+
for (let i = 1; i < nodes.length; i++) {
|
|
101
|
+
if (nodes[i] < nodes[minIdx]) {
|
|
102
|
+
minIdx = i;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const rotated = [...nodes.slice(minIdx), ...nodes.slice(0, minIdx)];
|
|
106
|
+
return rotated.join(",");
|
|
107
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import { findInfraFiles } from "./files";
|
|
3
|
+
import { withTestDir } from "@intentius/chant-test-utils";
|
|
4
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
|
|
7
|
+
describe("findInfraFiles", () => {
|
|
8
|
+
test("returns empty array for empty directory", async () => {
|
|
9
|
+
await withTestDir(async (testDir) => {
|
|
10
|
+
const files = await findInfraFiles(testDir);
|
|
11
|
+
expect(files).toEqual([]);
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test("finds .ts files in root directory", async () => {
|
|
16
|
+
await withTestDir(async (testDir) => {
|
|
17
|
+
await writeFile(join(testDir, "app.ts"), "export const app = {};");
|
|
18
|
+
await writeFile(join(testDir, "config.ts"), "export const config = {};");
|
|
19
|
+
|
|
20
|
+
const files = await findInfraFiles(testDir);
|
|
21
|
+
expect(files).toHaveLength(2);
|
|
22
|
+
expect(files.some((f) => f.endsWith("app.ts"))).toBe(true);
|
|
23
|
+
expect(files.some((f) => f.endsWith("config.ts"))).toBe(true);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test.each([
|
|
28
|
+
{ extension: ".test.ts", pattern: /app\.test\.ts$/ },
|
|
29
|
+
{ extension: ".spec.ts", pattern: /app\.spec\.ts$/ },
|
|
30
|
+
])("excludes $extension files", async ({ extension, pattern }) => {
|
|
31
|
+
await withTestDir(async (testDir) => {
|
|
32
|
+
await writeFile(join(testDir, "app.ts"), "export const app = {};");
|
|
33
|
+
await writeFile(join(testDir, `app${extension}`), "test();");
|
|
34
|
+
|
|
35
|
+
const files = await findInfraFiles(testDir);
|
|
36
|
+
expect(files).toHaveLength(1);
|
|
37
|
+
expect(files[0]).toMatch(/app\.ts$/);
|
|
38
|
+
expect(files[0]).not.toMatch(pattern);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("finds .ts files recursively", async () => {
|
|
43
|
+
await withTestDir(async (testDir) => {
|
|
44
|
+
const subDir = join(testDir, "src", "lib");
|
|
45
|
+
await mkdir(subDir, { recursive: true });
|
|
46
|
+
await writeFile(join(testDir, "root.ts"), "export const root = {};");
|
|
47
|
+
await writeFile(join(testDir, "src", "app.ts"), "export const app = {};");
|
|
48
|
+
await writeFile(
|
|
49
|
+
join(subDir, "utils.ts"),
|
|
50
|
+
"export const utils = {};"
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
const files = await findInfraFiles(testDir);
|
|
54
|
+
expect(files).toHaveLength(3);
|
|
55
|
+
expect(files.some((f) => f.endsWith("root.ts"))).toBe(true);
|
|
56
|
+
expect(files.some((f) => f.endsWith("app.ts"))).toBe(true);
|
|
57
|
+
expect(files.some((f) => f.endsWith("utils.ts"))).toBe(true);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("excludes node_modules directory", async () => {
|
|
62
|
+
await withTestDir(async (testDir) => {
|
|
63
|
+
const nodeModulesDir = join(testDir, "node_modules", "some-package");
|
|
64
|
+
await mkdir(nodeModulesDir, { recursive: true });
|
|
65
|
+
await writeFile(join(testDir, "app.ts"), "export const app = {};");
|
|
66
|
+
await writeFile(
|
|
67
|
+
join(nodeModulesDir, "index.ts"),
|
|
68
|
+
"export const lib = {};"
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
const files = await findInfraFiles(testDir);
|
|
72
|
+
expect(files).toHaveLength(1);
|
|
73
|
+
expect(files[0]).toMatch(/app\.ts$/);
|
|
74
|
+
expect(files.some((f) => f.includes("node_modules"))).toBe(false);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("excludes nested node_modules directories", async () => {
|
|
79
|
+
await withTestDir(async (testDir) => {
|
|
80
|
+
const srcDir = join(testDir, "src");
|
|
81
|
+
const nodeModulesDir = join(srcDir, "node_modules");
|
|
82
|
+
await mkdir(nodeModulesDir, { recursive: true });
|
|
83
|
+
await writeFile(join(testDir, "app.ts"), "export const app = {};");
|
|
84
|
+
await writeFile(join(srcDir, "lib.ts"), "export const lib = {};");
|
|
85
|
+
await writeFile(
|
|
86
|
+
join(nodeModulesDir, "package.ts"),
|
|
87
|
+
"export const pkg = {};"
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
const files = await findInfraFiles(testDir);
|
|
91
|
+
expect(files).toHaveLength(2);
|
|
92
|
+
expect(files.some((f) => f.endsWith("app.ts"))).toBe(true);
|
|
93
|
+
expect(files.some((f) => f.endsWith("lib.ts"))).toBe(true);
|
|
94
|
+
expect(files.some((f) => f.includes("node_modules"))).toBe(false);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("ignores non-.ts files", async () => {
|
|
99
|
+
await withTestDir(async (testDir) => {
|
|
100
|
+
await writeFile(join(testDir, "app.ts"), "export const app = {};");
|
|
101
|
+
await writeFile(join(testDir, "readme.md"), "# README");
|
|
102
|
+
await writeFile(join(testDir, "config.json"), "{}");
|
|
103
|
+
await writeFile(join(testDir, "script.js"), "console.log();");
|
|
104
|
+
|
|
105
|
+
const files = await findInfraFiles(testDir);
|
|
106
|
+
expect(files).toHaveLength(1);
|
|
107
|
+
expect(files[0]).toMatch(/app\.ts$/);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("handles mixed file types and test files", async () => {
|
|
112
|
+
await withTestDir(async (testDir) => {
|
|
113
|
+
await writeFile(join(testDir, "app.ts"), "export const app = {};");
|
|
114
|
+
await writeFile(join(testDir, "app.test.ts"), "test();");
|
|
115
|
+
await writeFile(join(testDir, "app.spec.ts"), "test();");
|
|
116
|
+
await writeFile(join(testDir, "config.ts"), "export const config = {};");
|
|
117
|
+
await writeFile(join(testDir, "readme.md"), "# README");
|
|
118
|
+
|
|
119
|
+
const files = await findInfraFiles(testDir);
|
|
120
|
+
expect(files).toHaveLength(2);
|
|
121
|
+
expect(files.some((f) => f.endsWith("app.ts"))).toBe(true);
|
|
122
|
+
expect(files.some((f) => f.endsWith("config.ts"))).toBe(true);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("returns full paths to files", async () => {
|
|
127
|
+
await withTestDir(async (testDir) => {
|
|
128
|
+
await writeFile(join(testDir, "app.ts"), "export const app = {};");
|
|
129
|
+
|
|
130
|
+
const files = await findInfraFiles(testDir);
|
|
131
|
+
expect(files).toHaveLength(1);
|
|
132
|
+
expect(files[0]).toBe(join(testDir, "app.ts"));
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("handles directories with no .ts files", async () => {
|
|
137
|
+
await withTestDir(async (testDir) => {
|
|
138
|
+
await mkdir(join(testDir, "docs"), { recursive: true });
|
|
139
|
+
await writeFile(join(testDir, "docs", "readme.md"), "# README");
|
|
140
|
+
await writeFile(join(testDir, "package.json"), "{}");
|
|
141
|
+
|
|
142
|
+
const files = await findInfraFiles(testDir);
|
|
143
|
+
expect(files).toEqual([]);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("handles non-existent directory gracefully", async () => {
|
|
148
|
+
await withTestDir(async (testDir) => {
|
|
149
|
+
const nonExistentPath = join(testDir, "does-not-exist");
|
|
150
|
+
const files = await findInfraFiles(nonExistentPath);
|
|
151
|
+
expect(files).toEqual([]);
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { readdir, stat } from "node:fs/promises";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { join, relative } from "node:path";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Recursively find all TypeScript infrastructure files in a directory
|
|
7
|
+
* @param path - The directory path to search
|
|
8
|
+
* @returns Array of file paths to .ts files (excluding test files)
|
|
9
|
+
*/
|
|
10
|
+
export async function findInfraFiles(path: string): Promise<string[]> {
|
|
11
|
+
const files: string[] = [];
|
|
12
|
+
let sourceRoot: string | null = null;
|
|
13
|
+
|
|
14
|
+
async function scanDirectory(dir: string): Promise<void> {
|
|
15
|
+
let entries;
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
19
|
+
} catch (error) {
|
|
20
|
+
// Skip directories we can't read
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
for (const entry of entries) {
|
|
25
|
+
const fullPath = join(dir, entry.name);
|
|
26
|
+
|
|
27
|
+
// Skip node_modules
|
|
28
|
+
if (entry.isDirectory() && entry.name === "node_modules") {
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (entry.isDirectory()) {
|
|
33
|
+
// Child project boundary — a directory with its own barrel file is a
|
|
34
|
+
// separate scope, but only if we've already found the project's own
|
|
35
|
+
// source root (the first barrel directory). The project's own src/
|
|
36
|
+
// with _.ts is the source root, not a child project.
|
|
37
|
+
const barrelPath = join(fullPath, "_.ts");
|
|
38
|
+
if (existsSync(barrelPath)) {
|
|
39
|
+
if (sourceRoot === null) {
|
|
40
|
+
sourceRoot = fullPath;
|
|
41
|
+
} else {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
await scanDirectory(fullPath);
|
|
46
|
+
} else if (entry.isFile()) {
|
|
47
|
+
// Include only .ts files, exclude test files
|
|
48
|
+
if (
|
|
49
|
+
entry.name.endsWith(".ts") &&
|
|
50
|
+
!entry.name.endsWith(".test.ts") &&
|
|
51
|
+
!entry.name.endsWith(".spec.ts")
|
|
52
|
+
) {
|
|
53
|
+
files.push(fullPath);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
await scanDirectory(path);
|
|
60
|
+
return files;
|
|
61
|
+
}
|