ttsc 0.27.0 → 0.28.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.
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,165 @@
1
+ package graph
2
+
3
+ import (
4
+ "path/filepath"
5
+ "testing"
6
+
7
+ "github.com/samchon/ttsc/packages/ttsc/driver"
8
+ )
9
+
10
+ // TestDocTagsCaptureEveryUnrecognizedTag verifies that the build pass records a
11
+ // DocTag for each documentation tag TypeScript does not recognize, on every
12
+ // declaration form, and records nothing for the tags it does.
13
+ //
14
+ // The population boundary is the whole design: naming particular tags would make
15
+ // the compiler host know one convention, so the pass collects exactly what the
16
+ // parser could not interpret. That makes the known-tag half of this test
17
+ // load-bearing rather than incidental — if `@param` ever arrived here, the field
18
+ // would silently become a second, worse copy of facts the AST already models.
19
+ //
20
+ // 1. Build a fixture whose declarations carry convention tags, known tags, a
21
+ // tag with no text, and a multi-line reason, across a function, a class, a
22
+ // class member, an interface member, a variable, a namespace member, and a
23
+ // closure.
24
+ // 2. Assert every unrecognized tag is recorded on its own declaration's node.
25
+ // 3. Assert no known tag is recorded, and that an untagged declaration
26
+ // contributes nothing.
27
+ func TestDocTagsCaptureEveryUnrecognizedTag(t *testing.T) {
28
+ root := t.TempDir()
29
+ writeFile(t, filepath.Join(root, "tsconfig.json"), fixtureTSConfig)
30
+ writeFile(t, filepath.Join(root, "src", "main.ts"), `/**
31
+ * Renders the notice.
32
+ *
33
+ * @evidence docs/discount.md#coupon-stacking States the per-issuer limit
34
+ * this section defines, in the buyer's words.
35
+ * @evidenceExclude prisma:Coupon.issuedAt Not surfaced here.
36
+ * @param unused Ignored by the graph.
37
+ * @returns Nothing the graph records.
38
+ */
39
+ export function renderNotice(): string {
40
+ return "notice";
41
+ }
42
+
43
+ /** @reference https://example.com/spec Reference document. */
44
+ export class Notice {
45
+ /** @evidence docs/discount.md#member Member-level citation. */
46
+ public render(): void {}
47
+
48
+ /** @spec invented Any convention is one fact. */
49
+ public flag: boolean = false;
50
+ }
51
+
52
+ export interface INotice {
53
+ /** @evidence docs/discount.md#signature Interface member citation. */
54
+ render(): void;
55
+ }
56
+
57
+ /** @evidence docs/discount.md#variable Variable citation. */
58
+ export const NOTICE_KIND = "notice";
59
+
60
+ export namespace Shopping {
61
+ /** @evidence docs/discount.md#namespace Namespace member citation. */
62
+ export function inner(): void {}
63
+ }
64
+
65
+ export function withClosure(): void {
66
+ /** @evidence docs/discount.md#closure Declared inside a body. */
67
+ function inner(): void {}
68
+ inner();
69
+ }
70
+
71
+ /** Ordinary documentation with no tag at all. */
72
+ export function untagged(): void {}
73
+
74
+ /** @bare */
75
+ export function bareTag(): void {}
76
+ `)
77
+
78
+ prog, diags, err := driver.LoadProgram(root, "tsconfig.json", driver.LoadProgramOptions{})
79
+ if err != nil {
80
+ t.Fatal(err)
81
+ }
82
+ if len(diags) != 0 {
83
+ t.Fatalf("unexpected diagnostics: %v", diags)
84
+ }
85
+ defer func() { _ = prog.Close() }()
86
+
87
+ tags := docTagsByTargetSuffix(Build(prog))
88
+
89
+ assertDocTag(t, tags, "#renderNotice:function", "evidence",
90
+ "docs/discount.md#coupon-stacking States the per-issuer limit this section defines, in the buyer's words.")
91
+ assertDocTag(t, tags, "#renderNotice:function", "evidenceExclude",
92
+ "prisma:Coupon.issuedAt Not surfaced here.")
93
+ assertDocTag(t, tags, "#Notice:class", "reference",
94
+ "https://example.com/spec Reference document.")
95
+ assertDocTag(t, tags, "#Notice.render:method", "evidence",
96
+ "docs/discount.md#member Member-level citation.")
97
+ assertDocTag(t, tags, "#Notice.flag:variable", "spec",
98
+ "invented Any convention is one fact.")
99
+ assertDocTag(t, tags, "#INotice.render:method", "evidence",
100
+ "docs/discount.md#signature Interface member citation.")
101
+ assertDocTag(t, tags, "#NOTICE_KIND:variable", "evidence",
102
+ "docs/discount.md#variable Variable citation.")
103
+ assertDocTag(t, tags, "#Shopping.inner:function", "evidence",
104
+ "docs/discount.md#namespace Namespace member citation.")
105
+ // A function declared inside a body is a node the graph records, so a tag on
106
+ // it is a fact like any other. It is reached through the same single
107
+ // collection point, which is why no declaration form needs its own handling.
108
+ assertDocTag(t, tags, "#withClosure.inner:function", "evidence",
109
+ "docs/discount.md#closure Declared inside a body.")
110
+ // A tag with a name and no text is a written fact; dropping it would report
111
+ // the declaration as carrying nothing, which is a different claim.
112
+ assertDocTag(t, tags, "#bareTag:function", "bare", "")
113
+
114
+ for _, known := range []string{"param", "returns"} {
115
+ for target, list := range tags {
116
+ for _, tag := range list {
117
+ if tag.Name == known {
118
+ t.Fatalf("recorded known tag %q on %s; only tags the parser could not interpret belong here", known, target)
119
+ }
120
+ }
121
+ }
122
+ }
123
+ for target, list := range tags {
124
+ if suffixMatch(target, "#untagged:function") && len(list) > 0 {
125
+ t.Fatalf("untagged declaration carries %d tags", len(list))
126
+ }
127
+ }
128
+ }
129
+
130
+ // docTagsByTargetSuffix groups a built graph's tags by their target node id.
131
+ func docTagsByTargetSuffix(g *Graph) map[string][]*DocTag {
132
+ out := map[string][]*DocTag{}
133
+ for _, tag := range g.DocTags {
134
+ out[tag.Target] = append(out[tag.Target], tag)
135
+ }
136
+ return out
137
+ }
138
+
139
+ // assertDocTag fails unless exactly one tag with this name sits on the node
140
+ // whose id ends with suffix, carrying text.
141
+ func assertDocTag(t *testing.T, tags map[string][]*DocTag, suffix, name, text string) {
142
+ t.Helper()
143
+ found := 0
144
+ for target, list := range tags {
145
+ if !suffixMatch(target, suffix) {
146
+ continue
147
+ }
148
+ for _, tag := range list {
149
+ if tag.Name != name {
150
+ continue
151
+ }
152
+ found++
153
+ if tag.Text != text {
154
+ t.Fatalf("%s @%s text = %q, want %q", suffix, name, tag.Text, text)
155
+ }
156
+ }
157
+ }
158
+ if found != 1 {
159
+ t.Fatalf("%s @%s recorded %d times, want 1", suffix, name, found)
160
+ }
161
+ }
162
+
163
+ func suffixMatch(value, suffix string) bool {
164
+ return len(value) >= len(suffix) && value[len(value)-len(suffix):] == suffix
165
+ }
@@ -0,0 +1,97 @@
1
+ package graph
2
+
3
+ import (
4
+ "path/filepath"
5
+ "testing"
6
+
7
+ "github.com/samchon/ttsc/packages/ttsc/driver"
8
+ )
9
+
10
+ // TestDocTagsReadOnlyWhatTheParserAttached verifies the boundary cases that
11
+ // decide whether a tag-shaped run of text is a fact about a declaration.
12
+ //
13
+ // Every case here is one a text scan would get wrong, and a wrong answer is
14
+ // silent in both directions: a tag adopted from a line comment or from prose
15
+ // attributes a citation to code that never made it, while a tag lost from a
16
+ // merged declaration reports the declaration as citing nothing. The parser
17
+ // already decides which block belongs to which declaration, so this pins that
18
+ // the collection follows it rather than re-deciding.
19
+ //
20
+ // 1. Build a fixture with a tag in a line comment, a tag written mid-sentence
21
+ // inside a documentation block, an overload run documented on its
22
+ // signature, and a link tag whose braced form must survive verbatim.
23
+ // 2. Assert the line comment contributes nothing and the mid-sentence one is
24
+ // the tag the parser says it is.
25
+ // 3. Assert the overload keeps its signature's tag and the link keeps its
26
+ // braces.
27
+ func TestDocTagsReadOnlyWhatTheParserAttached(t *testing.T) {
28
+ root := t.TempDir()
29
+ writeFile(t, filepath.Join(root, "tsconfig.json"), fixtureTSConfig)
30
+ writeFile(t, filepath.Join(root, "src", "main.ts"), `export interface ISale {
31
+ price: number;
32
+ }
33
+
34
+ // @evidence docs/a.md#line A line comment is not documentation.
35
+ export function fromLineComment(): void {}
36
+
37
+ /**
38
+ * Explains that an @evidence tag names a document, without being one.
39
+ */
40
+ export function fromProse(): void {}
41
+
42
+ /** @evidence docs/a.md#overload Documented on the signature. */
43
+ export function overloaded(value: string): string;
44
+ export function overloaded(value: number): string;
45
+ export function overloaded(value: string | number): string {
46
+ return String(value);
47
+ }
48
+
49
+ /** @evidence {@link ISale} The contract is mirrored here. */
50
+ export function linked(): void {}
51
+
52
+ /** @evidence {@linkcode ISale} And in code form. */
53
+ export function linkedCode(): void {}
54
+ `)
55
+
56
+ prog, diags, err := driver.LoadProgram(root, "tsconfig.json", driver.LoadProgramOptions{})
57
+ if err != nil {
58
+ t.Fatal(err)
59
+ }
60
+ if len(diags) != 0 {
61
+ t.Fatalf("unexpected diagnostics: %v", diags)
62
+ }
63
+ defer func() { _ = prog.Close() }()
64
+
65
+ tags := docTagsByTargetSuffix(Build(prog))
66
+
67
+ // A `//` comment is not documentation, so the parser attaches it to nothing
68
+ // and no tag can be read out of it.
69
+ for target, list := range tags {
70
+ if suffixMatch(target, "#fromLineComment:function") && len(list) > 0 {
71
+ t.Fatalf("line comment produced %d tags; only a documentation block attaches one", len(list))
72
+ }
73
+ }
74
+
75
+ // A tag written mid-sentence is still a tag, because TypeScript's own parser
76
+ // opens one at the `@` wherever it sits. This is deliberately not second-
77
+ // guessed: the population is "what the parser could not interpret", and a
78
+ // graph that re-decided where a tag may begin would be reinterpreting the
79
+ // compiler rather than reporting it. A convention that wants the stricter
80
+ // rule — the tag must open its line — enforces that in its own rule, where the
81
+ // author gets a diagnostic; here the fact is simply reported as parsed.
82
+ assertDocTag(t, tags, "#fromProse:function", "evidence",
83
+ "tag names a document, without being one.")
84
+
85
+ // The implementation signature carries no block of its own. Collecting tags
86
+ // outside the node-creation branch is what keeps the overload run's
87
+ // documentation, which is written on the first signature.
88
+ assertDocTag(t, tags, "#overloaded:function", "evidence",
89
+ "docs/a.md#overload Documented on the signature.")
90
+
91
+ // A citation target is matched by the token the author wrote, so the braces
92
+ // and the link keyword have to survive the round trip through the AST.
93
+ assertDocTag(t, tags, "#linked:function", "evidence",
94
+ "{@link ISale} The contract is mirrored here.")
95
+ assertDocTag(t, tags, "#linkedCode:function", "evidence",
96
+ "{@linkcode ISale} And in code form.")
97
+ }
@@ -0,0 +1,59 @@
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
+ // TestDocTagsStopAtTheExternalBoundary verifies that a documentation tag written
12
+ // in a dependency contributes nothing.
13
+ //
14
+ // The graph's boundary is the workspace: a dependency's declaration enters only
15
+ // as a named endpoint, never walked into. A tag read from one would put a
16
+ // citation the consumer did not write into their index — answering "which code
17
+ // implements this specification" with somebody else's code — and it would do so
18
+ // silently, because the address would look like any other. The collector reaches
19
+ // only declarations the build pass records, so this holds by construction; the
20
+ // test is here because "by construction" is what stops being true when the
21
+ // construction changes.
22
+ //
23
+ // 1. Build a fixture whose `node_modules` dependency carries a tag and whose
24
+ // workspace source carries another.
25
+ // 2. Assert the workspace tag is recorded.
26
+ // 3. Assert no tag is recorded from the dependency, under any target.
27
+ func TestDocTagsStopAtTheExternalBoundary(t *testing.T) {
28
+ root := t.TempDir()
29
+ writeFile(t, filepath.Join(root, "tsconfig.json"), nodeModulesGlobalFixtureTSConfig)
30
+ writeFile(t, filepath.Join(root, "node_modules", "dep", "globals.ts"), `/** @evidence docs/vendor.md#theirs Written by the dependency. */
31
+ export function vendored(): void {}
32
+ `)
33
+ writeFile(t, filepath.Join(root, "src", "main.ts"), `/** @evidence docs/ours.md#mine Written here. */
34
+ export function ours(): void {}
35
+ `)
36
+
37
+ prog, diags, err := driver.LoadProgram(root, "tsconfig.json", driver.LoadProgramOptions{})
38
+ if err != nil {
39
+ t.Fatal(err)
40
+ }
41
+ if len(diags) != 0 {
42
+ t.Fatalf("unexpected diagnostics: %v", diags)
43
+ }
44
+ defer func() { _ = prog.Close() }()
45
+
46
+ tags := docTagsByTargetSuffix(Build(prog))
47
+
48
+ assertDocTag(t, tags, "#ours:function", "evidence", "docs/ours.md#mine Written here.")
49
+
50
+ for target, list := range tags {
51
+ for _, tag := range list {
52
+ if strings.Contains(target, "node_modules") ||
53
+ strings.Contains(tag.Text, "docs/vendor.md") {
54
+ t.Fatalf("recorded %s from %s; a dependency's citation is not this project's",
55
+ tag.Text, target)
56
+ }
57
+ }
58
+ }
59
+ }
@@ -0,0 +1,146 @@
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
+ // TestDocTagsSurviveLineEndingsAndRepeatedBlocks verifies the boundary cases
12
+ // where one declaration's tags can be silently lost or doubled.
13
+ //
14
+ // Each is invisible when wrong. A CRLF checkout that joined a reason
15
+ // differently would make the same source produce different index entries on
16
+ // Windows and POSIX, so a citation found on one machine would be missing on the
17
+ // other. A merged identity declared twice keeps whichever declarations the walk
18
+ // reached, and a `var` redeclaration is the one shape where the same identity
19
+ // carries two documentation blocks that both have to survive. A link written
20
+ // with trailing text inside its braces is reassembled from two fields, so the
21
+ // text can be dropped without the target changing.
22
+ //
23
+ // 1. Build a fixture with CRLF sources, a redeclared `var` documented twice, a
24
+ // multi-binding statement, a declaration carrying two documentation blocks,
25
+ // and a link carrying trailing text.
26
+ // 2. Assert the CRLF reason joins exactly as its LF twin does.
27
+ // 3. Assert both blocks of the merged identity are kept, every binding of the
28
+ // statement carries its documentation, and the link keeps its text.
29
+ func TestDocTagsSurviveLineEndingsAndRepeatedBlocks(t *testing.T) {
30
+ root := t.TempDir()
31
+ writeFile(t, filepath.Join(root, "tsconfig.json"), fixtureTSConfig)
32
+
33
+ lf := `export interface ISale {
34
+ price: number;
35
+ }
36
+
37
+ /**
38
+ * Renders it.
39
+ *
40
+ * @evidence docs/a.md#reason States the limit
41
+ * this section defines.
42
+ */
43
+ export function joined(): void {}
44
+
45
+ /** @evidence {@link ISale the sale contract} And prose after it. */
46
+ export function linkText(): void {}
47
+
48
+ /** @evidence docs/a.md#first The first declaration. */
49
+ export var merged: number;
50
+ /** @evidence docs/a.md#second The second declaration. */
51
+ export var merged: number;
52
+
53
+ /** @evidence docs/a.md#shared Documents the whole statement. */
54
+ export var first = 1,
55
+ second = 2;
56
+
57
+ /** @evidence docs/a.md#one First block. */
58
+ /** @evidence docs/a.md#two Second block on the same declaration. */
59
+ export function twoBlocks(): void {}
60
+ `
61
+ // The shared fixture config names one root, and the CRLF twin has to be the
62
+ // same declarations rather than a second set, so the two spellings compile as
63
+ // two projects instead of two files of one.
64
+ writeFile(t, filepath.Join(root, "src", "main.ts"), lf)
65
+ crlfRoot := t.TempDir()
66
+ writeFile(t, filepath.Join(crlfRoot, "tsconfig.json"), fixtureTSConfig)
67
+ writeFile(t, filepath.Join(crlfRoot, "src", "main.ts"),
68
+ strings.ReplaceAll(strings.ReplaceAll(lf, "\r\n", "\n"), "\n", "\r\n"))
69
+
70
+ prog, diags, err := driver.LoadProgram(root, "tsconfig.json", driver.LoadProgramOptions{})
71
+ if err != nil {
72
+ t.Fatal(err)
73
+ }
74
+ if len(diags) != 0 {
75
+ t.Fatalf("unexpected diagnostics: %v", diags)
76
+ }
77
+ defer func() { _ = prog.Close() }()
78
+
79
+ crlfProg, crlfDiags, err := driver.LoadProgram(crlfRoot, "tsconfig.json", driver.LoadProgramOptions{})
80
+ if err != nil {
81
+ t.Fatal(err)
82
+ }
83
+ if len(crlfDiags) != 0 {
84
+ t.Fatalf("unexpected CRLF diagnostics: %v", crlfDiags)
85
+ }
86
+ defer func() { _ = crlfProg.Close() }()
87
+
88
+ byRoot := map[string]map[string][]*DocTag{
89
+ "LF": docTagsByTargetSuffix(Build(prog)),
90
+ "CRLF": docTagsByTargetSuffix(Build(crlfProg)),
91
+ }
92
+
93
+ // A reason written across two comment lines is one string, under both
94
+ // spellings of a line terminator.
95
+ for spelling, tags := range byRoot {
96
+ assertDocTagIn(t, tags, spelling, "#joined:function", "evidence",
97
+ "docs/a.md#reason States the limit this section defines.")
98
+ assertDocTagIn(t, tags, spelling, "#linkText:function", "evidence",
99
+ "{@link ISale the sale contract} And prose after it.")
100
+ // Both declarations of one identity contribute: collecting inside the
101
+ // node-creation branch would have kept whichever won the display span.
102
+ assertDocTagIn(t, tags, spelling, "#merged:variable", "evidence",
103
+ "docs/a.md#first The first declaration.")
104
+ assertDocTagIn(t, tags, spelling, "#merged:variable", "evidence",
105
+ "docs/a.md#second The second declaration.")
106
+ // One statement documents every binding it declares: the text sits above
107
+ // the statement and nothing in the source assigns it to the first name.
108
+ assertDocTagIn(t, tags, spelling, "#first:variable", "evidence",
109
+ "docs/a.md#shared Documents the whole statement.")
110
+ assertDocTagIn(t, tags, spelling, "#second:variable", "evidence",
111
+ "docs/a.md#shared Documents the whole statement.")
112
+ // TypeScript attaches more than one documentation block to a declaration
113
+ // when more than one is written, and each is a place a tag can sit. Reading
114
+ // only the last would silently drop the first.
115
+ assertDocTagIn(t, tags, spelling, "#twoBlocks:function", "evidence",
116
+ "docs/a.md#one First block.")
117
+ assertDocTagIn(t, tags, spelling, "#twoBlocks:function", "evidence",
118
+ "docs/a.md#two Second block on the same declaration.")
119
+ }
120
+ }
121
+
122
+ // assertDocTagIn fails unless a tag with this name and text sits on the node
123
+ // whose id ends with suffix, naming the source spelling in the failure.
124
+ func assertDocTagIn(t *testing.T, tags map[string][]*DocTag, spelling, suffix, name, text string) {
125
+ t.Helper()
126
+ for target, list := range tags {
127
+ if !suffixMatch(target, suffix) {
128
+ continue
129
+ }
130
+ for _, tag := range list {
131
+ if tag.Name == name && tag.Text == text {
132
+ return
133
+ }
134
+ }
135
+ t.Fatalf("%s %s carries %s, want @%s %q", spelling, suffix, renderDocTags(list), name, text)
136
+ }
137
+ t.Fatalf("%s %s recorded no tags at all, want @%s %q", spelling, suffix, name, text)
138
+ }
139
+
140
+ func renderDocTags(list []*DocTag) string {
141
+ out := make([]string, 0, len(list))
142
+ for _, tag := range list {
143
+ out = append(out, "@"+tag.Name+" "+tag.Text)
144
+ }
145
+ return "[" + strings.Join(out, " | ") + "]"
146
+ }
@@ -57,6 +57,15 @@ type DumpDecorator struct {
57
57
  Arguments []DumpDecoratorArgument `json:"arguments"`
58
58
  }
59
59
 
60
+ // DumpDocTag is one documentation tag TypeScript does not recognize, carried on
61
+ // the declaration it was written on. `name` is the tag without its `@` and
62
+ // `text` is everything after it, joined into one line. Neither is interpreted:
63
+ // which part of a text names a thing belongs to whichever convention wrote it.
64
+ type DumpDocTag struct {
65
+ Name string `json:"name"`
66
+ Text string `json:"text,omitempty"`
67
+ }
68
+
60
69
  // DumpEnumMember is one member of an enum on the wire: the name a caller writes
61
70
  // and the value it carries. `value` is omitted for a member the checker could
62
71
  // not fold to a constant — the name still stands, and the name is what a caller
@@ -89,19 +98,25 @@ type DumpNode struct {
89
98
  // and stopped early when the head itself contained a brace — a type-literal
90
99
  // parameter, an object return type, a destructured parameter. Neither is a
91
100
  // guess the consumer can win, so the producer renders it here.
92
- Signature string `json:"signature,omitempty"`
93
- File string `json:"file"`
94
- External bool `json:"external"`
95
- Ignored bool `json:"ignored,omitempty"`
96
- Exported bool `json:"exported,omitempty"`
97
- Closure bool `json:"closure,omitempty"`
98
- Modifiers []string `json:"modifiers,omitempty"`
99
- Literals []string `json:"literals,omitempty"`
100
- EnumMembers []DumpEnumMember `json:"enumMembers,omitempty"`
101
- ObjectMembers []DumpObjectMember `json:"objectMembers,omitempty"`
102
- Evidence *DumpEvidence `json:"evidence,omitempty"`
103
- Implementation *DumpEvidence `json:"implementation,omitempty"`
104
- Decorators []DumpDecorator `json:"decorators,omitempty"`
101
+ Signature string `json:"signature,omitempty"`
102
+ File string `json:"file"`
103
+ External bool `json:"external"`
104
+ Ignored bool `json:"ignored,omitempty"`
105
+ Exported bool `json:"exported,omitempty"`
106
+ Closure bool `json:"closure,omitempty"`
107
+ Modifiers []string `json:"modifiers,omitempty"`
108
+ Literals []string `json:"literals,omitempty"`
109
+ EnumMembers []DumpEnumMember `json:"enumMembers,omitempty"`
110
+ ObjectMembers []DumpObjectMember `json:"objectMembers,omitempty"`
111
+ // Parent is the artifact containing this one, and is empty for every
112
+ // declaration: a declaration's containment is synthesized by the TypeScript
113
+ // memory layer, and two producers of one relation would put two answers in
114
+ // the graph.
115
+ Parent string `json:"parent,omitempty"`
116
+ Evidence *DumpEvidence `json:"evidence,omitempty"`
117
+ Implementation *DumpEvidence `json:"implementation,omitempty"`
118
+ Decorators []DumpDecorator `json:"decorators,omitempty"`
119
+ DocTags []DumpDocTag `json:"docTags,omitempty"`
105
120
  }
106
121
 
107
122
  // DumpEdge is the wire shape of a graph edge. Lowercase json keys are the
@@ -233,6 +248,13 @@ func newDumpFacts(g *Graph, project string, ignored map[string]bool, sources map
233
248
  decByNode[d.Target] = append(decByNode[d.Target], DumpDecorator{Name: d.Name, Arguments: args})
234
249
  }
235
250
 
251
+ // Documentation tags ride on their target node the same way, grouped before
252
+ // relativization for the same reason.
253
+ tagsByNode := make(map[string][]DumpDocTag, len(g.DocTags))
254
+ for _, t := range g.DocTags {
255
+ tagsByNode[t.Target] = append(tagsByNode[t.Target], DumpDocTag{Name: t.Name, Text: t.Text})
256
+ }
257
+
236
258
  nodes := make([]DumpNode, 0, len(g.Nodes))
237
259
  for _, n := range g.Nodes {
238
260
  name, qualified := nodeNames(n)
@@ -241,6 +263,28 @@ func newDumpFacts(g *Graph, project string, ignored map[string]bool, sources map
241
263
  if n.Kind == NodeModule {
242
264
  name, qualified = ctx.rel(name), ""
243
265
  }
266
+ // An artifact's id IS the address a citation names, so it is emitted
267
+ // verbatim: relativizing it would rewrite `prisma:Sale.price`, which carries
268
+ // no path on purpose, and `POST:/orders`, which has no file at all. Its name
269
+ // is the readable heading the plugin reported, and its span is the single
270
+ // line where that heading starts — never the section's content.
271
+ if IsArtifactKind(n.Kind) {
272
+ artifact := DumpNode{
273
+ ID: n.ID,
274
+ Kind: string(n.Kind),
275
+ Name: n.Simple,
276
+ File: ctx.rel(n.File),
277
+ Parent: n.ArtifactParent,
278
+ }
279
+ if artifact.Name == "" {
280
+ artifact.Name = n.Name
281
+ }
282
+ if n.ArtifactLine > 0 {
283
+ artifact.Evidence = &DumpEvidence{StartLine: n.ArtifactLine}
284
+ }
285
+ nodes = append(nodes, artifact)
286
+ continue
287
+ }
244
288
  nodes = append(nodes, DumpNode{
245
289
  ID: ctx.relID(n.ID),
246
290
  Kind: string(n.Kind),
@@ -261,6 +305,7 @@ func newDumpFacts(g *Graph, project string, ignored map[string]bool, sources map
261
305
  Evidence: withoutFile(ctx.evidence(n.File, n.Pos, n.End)),
262
306
  Implementation: ctx.evidence(n.ImplementationFile, n.ImplementationPos, n.ImplementationEnd),
263
307
  Decorators: decByNode[n.ID],
308
+ DocTags: tagsByNode[n.ID],
264
309
  })
265
310
  }
266
311
  sort.Slice(nodes, func(i, j int) bool { return nodes[i].ID < nodes[j].ID })
@@ -398,6 +443,8 @@ func wireEdgeKind(kind EdgeKind, origin string) string {
398
443
  return "accesses"
399
444
  case EdgeTypeRef:
400
445
  return "type_ref"
446
+ case EdgeDocRef:
447
+ return "doc_ref"
401
448
  case EdgeHeritage:
402
449
  if origin == "extends" {
403
450
  return "extends"
@@ -20,7 +20,7 @@ const portableDumpTSConfig = `{
20
20
  }
21
21
  `
22
22
 
23
- // TestDumpCheckoutPathsAreStable verifies the whole schema-v6 consequence
23
+ // TestDumpCheckoutPathsAreStable verifies the whole dump-identity consequence
24
24
  // surface, not only the path helper.
25
25
  //
26
26
  // 1. Build the same project plus sibling .ts/.d.ts under two checkout roots.
@@ -51,7 +51,7 @@ func TestDumpCheckoutPathsAreStable(t *testing.T) {
51
51
  assertPortableFixturePaths(t, first)
52
52
  if strings.Contains(string(firstJSON), filepath.ToSlash(firstCheckout)) ||
53
53
  strings.Contains(string(secondJSON), filepath.ToSlash(secondCheckout)) {
54
- t.Fatal("a producer-local checkout path escaped into schema-v6 identity")
54
+ t.Fatal("a producer-local checkout path escaped into dump identity")
55
55
  }
56
56
  }
57
57
 
@@ -9,7 +9,7 @@ import (
9
9
  shimtspath "github.com/microsoft/typescript-go/shim/tspath"
10
10
  )
11
11
 
12
- // dumpPathMapper owns the schema-v6 path vocabulary for one dump. Every
12
+ // dumpPathMapper owns the dump path vocabulary for one dump. Every
13
13
  // identity-bearing path passes through this one cache, so the producer can
14
14
  // reject both an unportable filesystem root and a non-injective projection
15
15
  // before any JSON is written.
@@ -50,7 +50,7 @@ func WireProject(project string) (string, error) {
50
50
  return mapper.project, mapper.err()
51
51
  }
52
52
 
53
- // WirePath maps one compiler path into the portable schema-v6 vocabulary used
53
+ // WirePath maps one compiler path into the portable dump vocabulary used
54
54
  // by dumps and resident graph shards. Callers that project a complete graph
55
55
  // keep one dumpPathMapper so collision detection spans every path; callers
56
56
  // shaping a single source/config coordinate use this helper and receive the
@@ -5,7 +5,7 @@ import (
5
5
  "testing"
6
6
  )
7
7
 
8
- // TestDumpPathMapperUsesPortableCoordinates verifies the schema-v6 vocabulary
8
+ // TestDumpPathMapperUsesPortableCoordinates verifies the dump path vocabulary
9
9
  // directly, independent of the host OS running the test.
10
10
  //
11
11
  // 1. Map in-project and sibling paths for POSIX, drive, and UNC layouts.