ttsc 0.27.0 → 0.28.0

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 (73) hide show
  1. package/cmd/graphbench/main.go +26 -16
  2. package/cmd/graphdump/main.go +37 -12
  3. package/cmd/graphdump/run_declares_the_capability_its_output_carries_test.go +137 -0
  4. package/cmd/graphdump/run_reports_an_unknown_flag_without_exiting_the_process_test.go +56 -0
  5. package/cmd/graphdump/run_reports_an_unloadable_project_without_printing_a_dump_test.go +46 -0
  6. package/cmd/ttsc/api_transform.go +17 -6
  7. package/cmd/ttscgraph/dump.go +43 -11
  8. package/cmd/ttscgraph/run_dump_claims_artifacts_only_when_it_asked_test.go +135 -0
  9. package/cmd/ttscgraph/run_dump_declares_the_capability_its_output_carries_test.go +96 -0
  10. package/cmd/ttscgraph/serve.go +253 -25
  11. package/cmd/ttscgraph/serve_adopts_a_republished_artifact_set_test.go +348 -0
  12. package/cmd/ttscgraph/serve_projects_an_artifact_no_source_owns_test.go +90 -0
  13. package/cmd/ttscgraph/serve_shards.go +31 -9
  14. package/driver/config_discovery.go +121 -0
  15. package/driver/emit_plugin.go +57 -8
  16. package/driver/plugins.go +125 -14
  17. package/driver/transform_dependencies.go +283 -0
  18. package/driver/transform_dependencies_declares_every_file_without_a_contributor_test.go +28 -0
  19. package/driver/transform_dependencies_drops_a_self_reported_dependency_test.go +21 -0
  20. package/driver/transform_dependencies_keys_a_reported_path_like_the_envelope_test.go +46 -0
  21. package/driver/transform_dependencies_requires_every_contributor_to_declare_test.go +38 -0
  22. package/driver/transform_dependencies_stays_silent_for_an_undeclared_contributor_test.go +23 -0
  23. package/driver/transform_dependencies_withdraws_completeness_after_an_unusable_dependency_test.go +37 -0
  24. package/internal/graph/artifacts.go +230 -0
  25. package/internal/graph/artifacts_index_what_a_plugin_published_test.go +154 -0
  26. package/internal/graph/artifacts_survive_a_partial_rebuild_test.go +89 -0
  27. package/internal/graph/build.go +35 -0
  28. package/internal/graph/build_releases_the_scratch_it_no_longer_reads_test.go +118 -0
  29. package/internal/graph/doc_refs_are_the_use_the_checker_already_counts_test.go +93 -0
  30. package/internal/graph/doc_refs_resolve_documentation_links_test.go +335 -0
  31. package/internal/graph/doc_tags.go +416 -0
  32. package/internal/graph/doc_tags_are_claimed_before_they_are_read_test.go +131 -0
  33. package/internal/graph/doc_tags_capture_every_unrecognized_tag_test.go +165 -0
  34. package/internal/graph/doc_tags_read_only_what_the_parser_attached_test.go +97 -0
  35. package/internal/graph/doc_tags_stop_at_the_external_boundary_test.go +59 -0
  36. package/internal/graph/doc_tags_survive_line_endings_and_repeated_blocks_test.go +146 -0
  37. package/internal/graph/dump.go +60 -13
  38. package/internal/graph/dump_checkout_paths_are_stable_test.go +2 -2
  39. package/internal/graph/dump_paths.go +2 -2
  40. package/internal/graph/dump_paths_are_portable_and_injective_test.go +1 -1
  41. package/internal/graph/edge_kinds_names_every_declared_kind_test.go +49 -0
  42. package/internal/graph/edges.go +1 -0
  43. package/internal/graph/graph.go +79 -4
  44. package/internal/graph/provenance.go +29 -2
  45. package/lib/compiler/internal/project/createNativeProjectContextArgs.d.ts +9 -0
  46. package/lib/compiler/internal/project/createNativeProjectContextArgs.js +16 -4
  47. package/lib/compiler/internal/project/createNativeProjectContextArgs.js.map +1 -1
  48. package/lib/index.d.ts +1 -0
  49. package/lib/index.js +1 -0
  50. package/lib/index.js.map +1 -1
  51. package/lib/launcher/internal/runtimeHooks.d.ts +10 -2
  52. package/lib/launcher/internal/runtimeHooks.js +93 -2
  53. package/lib/launcher/internal/runtimeHooks.js.map +1 -1
  54. package/lib/plugin/internal/capabilityResolutionCache.d.ts +76 -0
  55. package/lib/plugin/internal/capabilityResolutionCache.js +224 -0
  56. package/lib/plugin/internal/capabilityResolutionCache.js.map +1 -0
  57. package/lib/plugin/resolveCapabilityPlugins.d.ts +55 -0
  58. package/lib/plugin/resolveCapabilityPlugins.js +118 -0
  59. package/lib/plugin/resolveCapabilityPlugins.js.map +1 -0
  60. package/lib/structures/ITtscCompilerTransformation.d.ts +7 -0
  61. package/package.json +8 -8
  62. package/shim/ast/parent.go +16 -3
  63. package/shim/checker/enums_gen.go +11 -0
  64. package/shim/checker/shim.go +3 -0
  65. package/shim/compiler/emithook.go +10 -0
  66. package/src/compiler/internal/project/createNativeProjectContextArgs.ts +19 -4
  67. package/src/index.ts +1 -0
  68. package/src/launcher/internal/runtimeHooks.ts +111 -2
  69. package/src/plugin/internal/capabilityResolutionCache.ts +305 -0
  70. package/src/plugin/resolveCapabilityPlugins.ts +171 -0
  71. package/src/structures/ITtscCompilerTransformation.ts +7 -0
  72. package/tools/shim_audit/baseline.json +0 -1
  73. package/utility/host.go +15 -6
@@ -0,0 +1,118 @@
1
+ package graph
2
+
3
+ import (
4
+ "path/filepath"
5
+ "reflect"
6
+ "testing"
7
+
8
+ "github.com/samchon/ttsc/packages/ttsc/driver"
9
+ )
10
+
11
+ // TestBuildReleasesTheScratchItNoLongerReads verifies that a returned Graph
12
+ // carries only what a consumer reads, and still carries what the shard path
13
+ // reads after the call.
14
+ //
15
+ // Every field asserted nil below is documented build-only and holds pointers
16
+ // into the compiler AST or into the preceding generation's nodes. They used to
17
+ // survive the build, so a consumer retaining the Graph pinned all of it —
18
+ // internal/graphsymbols keeps one for the lifetime of an editor session between
19
+ // invalidations, closing the Program while the maps referencing its AST live on.
20
+ // Measured on this repository's own packages, holding the Graph after closing
21
+ // the Program retained 38.1 MB for @ttsc/lint, 52.5 MB for ttsc, and 58.2 MB for
22
+ // @ttsc/graph; releasing the scratch brings those to 3.7, 5.0, and 4.0 MB.
23
+ //
24
+ // The assertion is structural rather than a heap measurement on purpose: a
25
+ // megabyte threshold is a flaky test, while "the producer stopped holding it" is
26
+ // the invariant and is exact.
27
+ //
28
+ // 1. Build the complete graph for a one-file project.
29
+ // 2. Assert every build-only field is released.
30
+ // 3. Assert the two fields the shard expansion reads after the call, and the
31
+ // graph itself, are not.
32
+ func TestBuildReleasesTheScratchItNoLongerReads(t *testing.T) {
33
+ root := t.TempDir()
34
+ writeFile(t, filepath.Join(root, "tsconfig.json"), `{
35
+ "compilerOptions": { "target": "ES2022", "module": "commonjs", "strict": true },
36
+ "files": ["src/main.ts"]
37
+ }
38
+ `)
39
+ writeFile(t, filepath.Join(root, "src", "main.ts"), `/** @evidence docs/a.md#x Cited. */
40
+ export interface ISale { id: string }
41
+ export class Store implements ISale {
42
+ public id: string = "";
43
+ public save(): void { this.load() }
44
+ public load(): void {}
45
+ }
46
+ export function run(store: Store): void { store.save() }
47
+ `)
48
+
49
+ prog, _, err := driver.LoadProgram(root, "tsconfig.json", driver.LoadProgramOptions{})
50
+ if err != nil || prog == nil {
51
+ t.Fatalf("could not load the probe project: %v", err)
52
+ }
53
+ defer func() { _ = prog.Close() }()
54
+
55
+ g := Build(prog)
56
+ assertScratchReleased(t, g, "complete build")
57
+
58
+ // The partial build is not a second case of the same thing: `baseNodes` and
59
+ // `selectedFiles` are nil on arrival in a complete build, so a complete build
60
+ // alone cannot fail on them. `baseNodes` is the largest of the nine and the
61
+ // one #1243 flagged as the caution, because it is passed in rather than built.
62
+ var mainFile string
63
+ for _, node := range g.Nodes {
64
+ mainFile = node.File
65
+ break
66
+ }
67
+ if mainFile == "" {
68
+ t.Fatal("the probe project produced no node to select a file from")
69
+ }
70
+ partial := BuildFiles(prog, []string{mainFile}, g.Nodes)
71
+ assertScratchReleased(t, partial, "partial build")
72
+
73
+ // The negative twin. Clearing scratch must not clear what the shard expansion
74
+ // in cmd/ttscgraph/serve_shards.go reads after BuildFiles returns.
75
+ if g.ExportedTargets == nil {
76
+ t.Error("ExportedTargets was released, but the shard path reads it after the build")
77
+ }
78
+ if g.ImplementationSources == nil {
79
+ t.Error("ImplementationSources was released, but the shard path reads it after the build")
80
+ }
81
+ if len(g.Nodes) == 0 || len(g.Edges) == 0 || len(g.DocTags) == 0 {
82
+ t.Fatalf("the graph itself is empty: %d nodes, %d edges, %d tags",
83
+ len(g.Nodes), len(g.Edges), len(g.DocTags))
84
+ }
85
+ }
86
+
87
+ // assertScratchReleased requires every unexported reference field of a returned
88
+ // Graph to be nil.
89
+ //
90
+ // It reflects rather than naming the fields, because a hand-written list has to
91
+ // be edited by the same person who forgets to edit releaseBuildState — which is
92
+ // exactly how docHosts arrived beside resolved. Unexported fields are readable
93
+ // this way: reflect.Value.IsNil needs no exported access, only Interface does.
94
+ //
95
+ // Exported fields are skipped: Nodes, Edges, Decorators, and DocTags are the
96
+ // graph, and ExportedTargets and ImplementationSources are read after the build.
97
+ func assertScratchReleased(t *testing.T, g *Graph, label string) {
98
+ t.Helper()
99
+ value := reflect.ValueOf(g).Elem()
100
+ for index := 0; index < value.NumField(); index++ {
101
+ field := value.Type().Field(index)
102
+ if field.IsExported() {
103
+ continue
104
+ }
105
+ switch value.Field(index).Kind() {
106
+ case reflect.Map, reflect.Slice, reflect.Ptr:
107
+ default:
108
+ continue
109
+ }
110
+ if !value.Field(index).IsNil() {
111
+ t.Errorf(
112
+ "%s: %s survived; it pins the AST for as long as a consumer holds the graph",
113
+ label,
114
+ field.Name,
115
+ )
116
+ }
117
+ }
118
+ }
@@ -0,0 +1,93 @@
1
+ package graph
2
+
3
+ import (
4
+ "path/filepath"
5
+ "strings"
6
+ "testing"
7
+
8
+ "github.com/samchon/ttsc/packages/ttsc/driver"
9
+ )
10
+
11
+ // unusedLocalsFixtureTSConfig is the shared fixture config with the one flag
12
+ // this test is about. `noUnusedLocals` is what makes the compiler state, in a
13
+ // diagnostic, whether it counted a documentation link as a use.
14
+ const unusedLocalsFixtureTSConfig = `{
15
+ "compilerOptions": {
16
+ "target": "ES2022",
17
+ "module": "commonjs",
18
+ "strict": true,
19
+ "noUnusedLocals": true,
20
+ "rootDir": "src",
21
+ "outDir": "dist"
22
+ },
23
+ "files": ["src/main.ts"]
24
+ }
25
+ `
26
+
27
+ // TestDocRefsAreTheUseTheCheckerAlreadyCounts verifies the premise this edge
28
+ // rests on: the compiler resolves a documentation link and counts it as a use,
29
+ // and recording the edge does not change what it reports.
30
+ //
31
+ // The whole argument for the edge is that it is a compiler fact rather than a
32
+ // text match, and the compiler says so itself — an import supporting only a link
33
+ // survives `noUnusedLocals` while the same import without one does not. That
34
+ // asymmetry is the evidence the issue was opened on, and it lived only in its
35
+ // prose. Pinning it here matters twice: it fails if a TypeScript upgrade ever
36
+ // stops counting links as uses, which would make the edge a text match dressed
37
+ // as a checker fact, and it fails if this pass ever changes what the compiler
38
+ // reports, which it must never do.
39
+ //
40
+ // 1. Build one file importing two types, using one only through a link and the
41
+ // other not at all.
42
+ // 2. Assert the compiler reports the unlinked import unused and the linked one
43
+ // not.
44
+ // 3. Assert the link produced its edge in the same build.
45
+ func TestDocRefsAreTheUseTheCheckerAlreadyCounts(t *testing.T) {
46
+ root := t.TempDir()
47
+ writeFile(t, filepath.Join(root, "tsconfig.json"), unusedLocalsFixtureTSConfig)
48
+ writeFile(t, filepath.Join(root, "src", "main.ts"), `interface ILinked {
49
+ a: number;
50
+ }
51
+
52
+ interface IUnlinked {
53
+ b: number;
54
+ }
55
+
56
+ /** Names {@link ILinked} and nothing else. */
57
+ export function subject(): void {}
58
+ `)
59
+
60
+ prog, _, err := driver.LoadProgram(root, "tsconfig.json", driver.LoadProgramOptions{})
61
+ if err != nil {
62
+ t.Fatal(err)
63
+ }
64
+ defer func() { _ = prog.Close() }()
65
+
66
+ linked, unlinked := false, false
67
+ for _, diagnostic := range prog.Diagnostics() {
68
+ message := diagnostic.String()
69
+ if !strings.Contains(message, "never used") &&
70
+ !strings.Contains(message, "declared but") {
71
+ continue
72
+ }
73
+ if strings.Contains(message, "ILinked") {
74
+ linked = true
75
+ }
76
+ if strings.Contains(message, "IUnlinked") {
77
+ unlinked = true
78
+ }
79
+ }
80
+ if !unlinked {
81
+ t.Fatal("the unlinked declaration was not reported unused, so this fixture " +
82
+ "cannot demonstrate the asymmetry the edge rests on")
83
+ }
84
+ if linked {
85
+ t.Fatal("the linked declaration was reported unused, so the compiler no " +
86
+ "longer counts a documentation link as a use and this edge is a text " +
87
+ "match rather than a checker fact")
88
+ }
89
+
90
+ // The same build produced the edge, so the fact the compiler counted and the
91
+ // fact the graph records are one fact.
92
+ assertDocRef(t, Build(prog), "#subject:function", "#ILinked:interface")
93
+ }
@@ -0,0 +1,335 @@
1
+ package graph
2
+
3
+ import (
4
+ "path/filepath"
5
+ "strings"
6
+ "testing"
7
+
8
+ "github.com/samchon/ttsc/packages/ttsc/driver"
9
+ )
10
+
11
+ // TestDocRefsResolveDocumentationLinks verifies that an inline link in a
12
+ // declaration's documentation becomes a checker-resolved edge, and that the tag
13
+ // it sits under changes nothing.
14
+ //
15
+ // The checker already resolves such a name and counts it as a use — the
16
+ // companion negative case is a project where `noUnusedLocals` keeps an import
17
+ // that only a link supports — so this was the one class of resolved reference
18
+ // the graph held no edge for, and the citation-only `import type` a citation
19
+ // convention recommends is exactly the form nothing else in the module records.
20
+ //
21
+ // 1. Build a fixture linking one type from a tag, one from ordinary prose, one
22
+ // through `{@linkcode}`, one qualified, and one that resolves to nothing.
23
+ // 2. Assert each resolvable link produced exactly one doc_ref edge to the
24
+ // declaration the checker resolved.
25
+ // 3. Assert the unresolvable link, the self-link, and the untagged declaration
26
+ // produced none.
27
+ // 4. Declare the same name in two more modules that flank it alphabetically,
28
+ // import all three, and assert every link resolves to the one this module
29
+ // imports the name from — the claim the rest of the file now rests on,
30
+ // since three declarations of `ISale` exist here.
31
+ func TestDocRefsResolveDocumentationLinks(t *testing.T) {
32
+ root := t.TempDir()
33
+ writeFile(t, filepath.Join(root, "tsconfig.json"), fixtureTSConfig)
34
+ // Two rivals, one sorting before `sale` and one after, each imported so the
35
+ // program holds it. One rival is not enough: a text matcher has to break the
36
+ // tie somehow, and whichever end it picks — lowest id, highest id, first or
37
+ // last in program order — a single rival leaves half of those choices landing
38
+ // on the right answer by luck. Flanking the real module closes every one.
39
+ writeFile(t, filepath.Join(root, "src", "archive.ts"), `export interface ISale {
40
+ archived: boolean;
41
+ }
42
+
43
+ export interface IArchive {
44
+ flag: boolean;
45
+ }
46
+ `)
47
+ writeFile(t, filepath.Join(root, "src", "vendor.ts"), `export interface ISale {
48
+ vendored: boolean;
49
+ }
50
+
51
+ export interface IVendor {
52
+ flag: boolean;
53
+ }
54
+ `)
55
+ writeFile(t, filepath.Join(root, "src", "sale.ts"), `export interface ISale {
56
+ price: number;
57
+ }
58
+
59
+ export namespace Shopping {
60
+ export interface ICoupon {
61
+ code: string;
62
+ }
63
+ }
64
+ `)
65
+ writeFile(t, filepath.Join(root, "src", "main.ts"), `// The real module is imported between its two rivals, so no end of the import
66
+ // order is the right answer either.
67
+ import type { IArchive } from "./archive";
68
+ import type { ISale, Shopping } from "./sale";
69
+ import type { IVendor } from "./vendor";
70
+
71
+ /** @evidence {@link ISale} Cited from a tag. */
72
+ export function fromTag(): void {}
73
+
74
+ /** Ordinary prose naming {@link ISale} with no tag at all. */
75
+ export function fromProse(): void {}
76
+
77
+ /** @evidence {@linkcode ISale} Code form. */
78
+ export function fromCode(): void {}
79
+
80
+ /** @evidence {@linkplain ISale} Plain form. */
81
+ export function fromPlain(): void {}
82
+
83
+ /** Two links, {@link ISale} and {@link Shopping.ICoupon}, plus {@link ISale} twice. */
84
+ export function severalLinks(): void {}
85
+
86
+ /** @evidence {@link Shopping.ICoupon} Qualified. */
87
+ export function fromQualified(): void {}
88
+
89
+ /** @evidence {@link NoSuchSymbol} Resolves to nothing. */
90
+ export function fromUnresolved(): void {}
91
+
92
+ /** @see {@link ISale} A tag TypeScript does know. */
93
+ export function fromSee(): void {}
94
+
95
+ /**
96
+ * @param value The parameter, whose tag carries no link.
97
+ * @returns Nothing.
98
+ */
99
+ export function withKnownTags(value: string): void {}
100
+
101
+ /** Names {@link selfLinked} itself. */
102
+ export function selfLinked(): void {}
103
+
104
+ /** Carries documentation but no link. */
105
+ export function noLink(): void {}
106
+
107
+ // A link in a line comment: {@link ISale} is not documentation.
108
+ export function lineCommentLink(): void {}
109
+
110
+ export namespace Reached {
111
+ /** A namespace member naming {@link ISale}. */
112
+ export function member(): void {}
113
+ }
114
+
115
+ export function hostsClosure(): void {
116
+ /** A closure naming {@link ISale}. */
117
+ function inner(): void {}
118
+ inner();
119
+ }
120
+
121
+ /** Names {@link ISale}, of which this file imports exactly one. */
122
+ export function ambiguousName(): void {}
123
+
124
+ /** Names {@link IArchive}, which only the first rival declares. */
125
+ export function usesArchive(): void {}
126
+
127
+ /** Names {@link IVendor}, which only the second rival declares. */
128
+ export function usesVendor(): void {}
129
+
130
+ /** Names a library type: {@link Promise}. */
131
+ export function linksExternal(): void {}
132
+
133
+ /** A class documented with {@link ISale} on the class itself. */
134
+ export class Documented {
135
+ /** @evidence {@link Shopping.ICoupon} On the member, not the class. */
136
+ public readonly coupon: string = "";
137
+
138
+ /** Method documentation naming {@link ISale}. */
139
+ public run(): void {}
140
+ }
141
+
142
+ /** An interface documented with {@link ISale}. */
143
+ export interface IDocumented {
144
+ value: number;
145
+ }
146
+
147
+ /** A namespace documented with {@link ISale}. */
148
+ export namespace Documented2 {
149
+ export const value = 1;
150
+ }
151
+ `)
152
+
153
+ prog, diags, err := driver.LoadProgram(root, "tsconfig.json", driver.LoadProgramOptions{})
154
+ if err != nil {
155
+ t.Fatal(err)
156
+ }
157
+ if len(diags) != 0 {
158
+ t.Fatalf("unexpected diagnostics: %v", diags)
159
+ }
160
+ defer func() { _ = prog.Close() }()
161
+
162
+ g := Build(prog)
163
+
164
+ assertDocRef(t, g, "#fromTag:function", "sale.ts#ISale:interface")
165
+ // The tag decides nothing: a link in ordinary prose is the same relation.
166
+ assertDocRef(t, g, "#fromProse:function", "sale.ts#ISale:interface")
167
+ assertDocRef(t, g, "#fromCode:function", "sale.ts#ISale:interface")
168
+ assertDocRef(t, g, "#fromPlain:function", "sale.ts#ISale:interface")
169
+ // Distinct targets each get an edge; a target named twice collapses under the
170
+ // uniqueness rule every edge kind shares, keeping the first span.
171
+ assertDocRef(t, g, "#severalLinks:function", "sale.ts#ISale:interface")
172
+ assertDocRef(t, g, "#severalLinks:function", "#Shopping.ICoupon:interface")
173
+ assertDocRef(t, g, "#fromQualified:function", "#Shopping.ICoupon:interface")
174
+ // A link under a tag TypeScript recognizes is the same relation. Reading a
175
+ // tag's comment through its per-kind struct crashed on the first `@param`
176
+ // instead, so this case and the one below are one fix and one regression.
177
+ assertDocRef(t, g, "#fromSee:function", "sale.ts#ISale:interface")
178
+
179
+ // A name the checker cannot resolve is not a relation, and must not
180
+ // fabricate a node to point at.
181
+ assertNoDocRef(t, g, "#fromUnresolved:function")
182
+ // A declaration naming itself is not an edge, the same rule typeRefEdge keeps.
183
+ assertNoDocRef(t, g, "#selfLinked:function")
184
+ assertNoDocRef(t, g, "#noLink:function")
185
+ assertNoDocRef(t, g, "#withKnownTags:function")
186
+ // A `//` comment is not documentation, so the parser attaches it to nothing
187
+ // and no link can be read out of it.
188
+ assertNoDocRef(t, g, "#lineCommentLink:function")
189
+
190
+ // Both nested forms are nodes the build pass records, so both resolve their
191
+ // own links — the edge pass reads the same host set the node pass filled.
192
+ assertDocRef(t, g, "#Reached.member:function", "sale.ts#ISale:interface")
193
+ assertDocRef(t, g, "#hostsClosure.inner:function", "sale.ts#ISale:interface")
194
+
195
+ // A class, an interface, and a namespace carry documentation of their own,
196
+ // and the container walk the edge pass would naturally reuse never visits any
197
+ // of them as a node: it descends straight into their members. Each of these
198
+ // resolved to nothing while the tag beside it was indexed, so the two halves
199
+ // a reader composes disagreed exactly where a type is documented.
200
+ assertDocRef(t, g, "#Documented:class", "sale.ts#ISale:interface")
201
+ assertDocRef(t, g, "#IDocumented:interface", "sale.ts#ISale:interface")
202
+ // A namespace is a grouping container the graph models as no node at all, so
203
+ // its own documentation has nothing to hang on — and the tag half agrees,
204
+ // because `putDeclaredNode` is never called for one either. The two halves
205
+ // being absent together is the invariant; one of them answering alone is the
206
+ // defect this pairing exists to catch.
207
+ assertNoNamespaceNode(t, g, "#Documented2:")
208
+ assertNoDocRefTo(t, g, "#Documented2.value:variable", "sale.ts#ISale:interface")
209
+
210
+ // A member's link belongs to the member. The same walk hands a property's
211
+ // subtree to its class as well, which is right for dependency edges and wrong
212
+ // here: the class's own documentation names nothing.
213
+ assertDocRef(t, g, "#Documented.coupon:variable", "#Shopping.ICoupon:interface")
214
+ assertNoDocRefTo(t, g, "#Documented:class", "#Shopping.ICoupon:interface")
215
+ assertDocRef(t, g, "#Documented.run:method", "sale.ts#ISale:interface")
216
+
217
+ // Two modules declare `ISale`, and only the checker knows which one a link
218
+ // means: it resolves the name through this module's imports. A text match on
219
+ // the written token could not tell them apart, and would have to guess.
220
+ assertNodeExists(t, g, "archive.ts#ISale:interface")
221
+ assertNodeExists(t, g, "vendor.ts#ISale:interface")
222
+ // Each rival is reached for a name only it declares, which is what keeps its
223
+ // import from being the kind a stricter project would remove.
224
+ assertDocRef(t, g, "#usesArchive:function", "archive.ts#IArchive:interface")
225
+ assertDocRef(t, g, "#usesVendor:function", "vendor.ts#IVendor:interface")
226
+ assertDocRef(t, g, "#ambiguousName:function", "sale.ts#ISale:interface")
227
+ assertNoDocRefTo(t, g, "#ambiguousName:function", "archive.ts#ISale:interface")
228
+ assertNoDocRefTo(t, g, "#ambiguousName:function", "vendor.ts#ISale:interface")
229
+
230
+ // A link to a library type follows the external-boundary policy every other
231
+ // edge kind keeps: the target is a named endpoint, not walked into.
232
+ assertExternalDocRef(t, g, "#linksExternal:function", "Promise")
233
+ }
234
+
235
+ // assertExternalDocRef fails unless exactly one doc_ref edge leaves the node and
236
+ // lands on an external declaration of this name.
237
+ func assertExternalDocRef(t *testing.T, g *Graph, fromSuffix, name string) {
238
+ t.Helper()
239
+ found := 0
240
+ for _, edge := range g.Edges {
241
+ if edge.Kind != EdgeDocRef || !suffixMatch(edge.From, fromSuffix) {
242
+ continue
243
+ }
244
+ target := g.Nodes[edge.To]
245
+ if target == nil {
246
+ t.Fatalf("%s cited %s, which is no node", fromSuffix, edge.To)
247
+ }
248
+ if target.Simple != name {
249
+ continue
250
+ }
251
+ found++
252
+ if !target.External {
253
+ t.Fatalf("%s is a library declaration and must be an external boundary leaf", edge.To)
254
+ }
255
+ }
256
+ if found != 1 {
257
+ t.Fatalf("%s -> external %s edges = %d, want 1", fromSuffix, name, found)
258
+ }
259
+ }
260
+
261
+ // assertNoNamespaceNode fails when the graph holds a node for this namespace,
262
+ // or any doc_ref edge leaving one.
263
+ //
264
+ // A suffix match cannot express this: every node id ends in its kind, so
265
+ // "#Documented2:" is a suffix of nothing and an assertion written that way
266
+ // passes against an implementation that records the namespace and cites from
267
+ // it — the exact defect the pairing exists to catch.
268
+ func assertNoNamespaceNode(t *testing.T, g *Graph, infix string) {
269
+ t.Helper()
270
+ for id := range g.Nodes {
271
+ if strings.Contains(id, infix) {
272
+ t.Fatalf("graph recorded %s; a namespace is modelled as no node, so its documentation has no host", id)
273
+ }
274
+ }
275
+ for _, edge := range g.Edges {
276
+ if edge.Kind == EdgeDocRef && strings.Contains(edge.From, infix) {
277
+ t.Fatalf("a namespace node cited %s", edge.To)
278
+ }
279
+ }
280
+ }
281
+
282
+ // assertNodeExists fails unless the graph holds a node whose id ends this way.
283
+ // It guards the same-name case from passing because the rival never existed.
284
+ func assertNodeExists(t *testing.T, g *Graph, suffix string) {
285
+ t.Helper()
286
+ for id := range g.Nodes {
287
+ if suffixMatch(id, suffix) {
288
+ return
289
+ }
290
+ }
291
+ t.Fatalf("the graph holds no %s, so a resolution asserted against it proves nothing", suffix)
292
+ }
293
+
294
+ // assertNoDocRefTo fails when a doc_ref edge runs between these two nodes.
295
+ func assertNoDocRefTo(t *testing.T, g *Graph, fromSuffix, toSuffix string) {
296
+ t.Helper()
297
+ for _, edge := range g.Edges {
298
+ if edge.Kind == EdgeDocRef &&
299
+ suffixMatch(edge.From, fromSuffix) &&
300
+ suffixMatch(edge.To, toSuffix) {
301
+ t.Fatalf("%s must not name %s", fromSuffix, toSuffix)
302
+ }
303
+ }
304
+ }
305
+
306
+ // assertDocRef fails unless exactly one doc_ref edge runs between the two nodes
307
+ // whose ids end with these suffixes, carrying a span.
308
+ func assertDocRef(t *testing.T, g *Graph, fromSuffix, toSuffix string) {
309
+ t.Helper()
310
+ found := 0
311
+ for _, edge := range g.Edges {
312
+ if edge.Kind != EdgeDocRef {
313
+ continue
314
+ }
315
+ if suffixMatch(edge.From, fromSuffix) && suffixMatch(edge.To, toSuffix) {
316
+ found++
317
+ if edge.Pos < 0 || edge.End <= edge.Pos {
318
+ t.Fatalf("%s -> %s carries no span (%d..%d)", fromSuffix, toSuffix, edge.Pos, edge.End)
319
+ }
320
+ }
321
+ }
322
+ if found != 1 {
323
+ t.Fatalf("%s -> %s doc_ref edges = %d, want 1", fromSuffix, toSuffix, found)
324
+ }
325
+ }
326
+
327
+ // assertNoDocRef fails when any doc_ref edge leaves the named node.
328
+ func assertNoDocRef(t *testing.T, g *Graph, fromSuffix string) {
329
+ t.Helper()
330
+ for _, edge := range g.Edges {
331
+ if edge.Kind == EdgeDocRef && suffixMatch(edge.From, fromSuffix) {
332
+ t.Fatalf("%s produced an unexpected doc_ref edge to %s", fromSuffix, edge.To)
333
+ }
334
+ }
335
+ }