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,416 @@
1
+ package graph
2
+
3
+ import (
4
+ "strings"
5
+
6
+ shimast "github.com/microsoft/typescript-go/shim/ast"
7
+ shimchecker "github.com/microsoft/typescript-go/shim/checker"
8
+ )
9
+
10
+ // DocTag is one documentation tag TypeScript itself does not recognize, written
11
+ // on a workspace declaration and captured verbatim.
12
+ //
13
+ // A convention attaches a declaration to something outside the type system — a
14
+ // specification section, an API operation, a reference document — and writes
15
+ // that attachment as a tag. `@evidence docs/pricing.md#sale`, `@reference
16
+ // https://…`, and a consumer's own `@spec` are the same fact in three
17
+ // spellings, and none of them is expressible as an edge because the thing on
18
+ // the other end is not a TypeScript declaration.
19
+ //
20
+ // The population is defined by the compiler rather than by us: these are the
21
+ // tags the parser had no meaning for, so it kept them as
22
+ // KindJSDocUnknownTag. A known tag — `@param`, `@returns`, `@deprecated` — has
23
+ // its own AST shape and its own meaning and is not this. Naming particular tags
24
+ // here would make the compiler host know one convention, and would have left
25
+ // out the 767 `@reference` tags this repository's own sources carry.
26
+ //
27
+ // Nothing is interpreted. Target is not split out of Text, because which part
28
+ // of a tag's text names a thing is a convention's rule and this pass enforces
29
+ // none; a consumer that ranks on a leading token does so in the layer that
30
+ // declares its selection heuristic. This is the contract Decorator already
31
+ // keeps for `@Controller`/`@Get`.
32
+ type DocTag struct {
33
+ // Target is the id of the graph node the tag was written on.
34
+ Target string
35
+ // Name is the tag name without its `@` (`evidence`, `evidenceExclude`,
36
+ // `reference`).
37
+ Name string
38
+ // Text is everything after the tag name, with the comment's leading asterisks
39
+ // and per-line indentation removed and its lines joined by single spaces.
40
+ // Empty when the tag carries no text.
41
+ Text string
42
+ // Pos and End bound the written tag. They are not published — a tag rides its
43
+ // target node, which carries the span a reader cites — and exist so one
44
+ // declaration presented to the node pass twice contributes its tags once.
45
+ Pos int
46
+ End int
47
+ }
48
+
49
+ // collectDocTags records a DocTag for every unrecognized documentation tag on
50
+ // declaration, attributed to the graph node identified by targetID.
51
+ //
52
+ // It runs from putDeclaredNode rather than from a pass of its own, so every
53
+ // declaration form the build records — a function, a class, an interface member,
54
+ // a variable binding, a closure, a namespace member — carries its tags without
55
+ // each form having to remember to ask. A merged identity therefore accumulates
56
+ // the tags of every declaration that names it: an overload run documented on its
57
+ // signature and implemented below keeps the signature's tags, which putting this
58
+ // inside the node-creation branch would have dropped.
59
+ //
60
+ // Positions deduplicate, because the same declaration node can be presented to
61
+ // putDeclaredNode more than once and a tag is a property of where it is written.
62
+ func collectDocTags(g *Graph, targetID string, declaration *shimast.Node) {
63
+ if g == nil || declaration == nil || targetID == "" {
64
+ return
65
+ }
66
+ file := shimast.GetSourceFileOfNode(declaration)
67
+ if file == nil {
68
+ return
69
+ }
70
+ docs := documentationOf(declaration, file)
71
+ if len(docs) == 0 {
72
+ return
73
+ }
74
+ g.recordDocHost(targetID, declaration)
75
+ if g.docTagPositions == nil {
76
+ g.docTagPositions = map[docTagKey]struct{}{}
77
+ }
78
+ for _, doc := range docs {
79
+ if doc == nil || doc.Kind != shimast.KindJSDoc {
80
+ continue
81
+ }
82
+ comment := doc.AsJSDoc()
83
+ if comment == nil || comment.Tags == nil {
84
+ continue
85
+ }
86
+ for _, tag := range comment.Tags.Nodes {
87
+ fact := docTagFact(tag)
88
+ if fact == nil {
89
+ continue
90
+ }
91
+ fact.Target = targetID
92
+ key := docTagKey{target: targetID, pos: fact.Pos, end: fact.End}
93
+ if _, seen := g.docTagPositions[key]; seen {
94
+ continue
95
+ }
96
+ g.docTagPositions[key] = struct{}{}
97
+ g.DocTags = append(g.DocTags, fact)
98
+ }
99
+ }
100
+ }
101
+
102
+ // documentationOf returns the documentation blocks that describe declaration.
103
+ //
104
+ // A variable's block is the one place the node the graph records and the node
105
+ // the parser attached the documentation to are different. TypeScript attaches
106
+ // one leading block to the variable *statement*, while the graph records a node
107
+ // per binding, so asking the binding directly finds nothing and every citation
108
+ // written on a `const` was lost. The statement is reached through its
109
+ // declaration list, and both hops are checked rather than assumed.
110
+ //
111
+ // One statement can declare several bindings (`export const a = 1, b = 2`), and
112
+ // then its block genuinely documents all of them: the text is above the
113
+ // statement, and nothing in the source says it belongs to the first binding
114
+ // only. Each binding's node therefore carries it, which is the same answer the
115
+ // evidence host model reaches from the other direction.
116
+ //
117
+ // The walk stops there. A binding element inside a destructuring pattern is
118
+ // given no documentation by the parser, so climbing further would invent an
119
+ // association the source does not make.
120
+ func documentationOf(declaration *shimast.Node, file *shimast.SourceFile) []*shimast.Node {
121
+ if docs := declaration.JSDoc(file); len(docs) > 0 {
122
+ return docs
123
+ }
124
+ if declaration.Kind != shimast.KindVariableDeclaration {
125
+ return nil
126
+ }
127
+ list := declaration.Parent
128
+ if list == nil || list.Kind != shimast.KindVariableDeclarationList {
129
+ return nil
130
+ }
131
+ statement := list.Parent
132
+ if statement == nil || statement.Kind != shimast.KindVariableStatement {
133
+ return nil
134
+ }
135
+ return statement.JSDoc(file)
136
+ }
137
+
138
+ // docTagKey identifies one written tag on one target, so a declaration visited
139
+ // twice contributes its tags once.
140
+ type docTagKey struct {
141
+ target string
142
+ pos int
143
+ end int
144
+ }
145
+
146
+ // docTagFact reads one tag node into a DocTag, or nil when the tag is one
147
+ // TypeScript recognizes or carries no usable name.
148
+ func docTagFact(tag *shimast.Node) *DocTag {
149
+ if tag == nil || tag.Kind != shimast.KindJSDocUnknownTag {
150
+ return nil
151
+ }
152
+ unknown := tag.AsJSDocUnknownTag()
153
+ if unknown == nil || unknown.TagName == nil {
154
+ return nil
155
+ }
156
+ name := unknown.TagName.Text()
157
+ if name == "" {
158
+ return nil
159
+ }
160
+ return &DocTag{
161
+ Name: name,
162
+ Text: docTagText(unknown.Comment),
163
+ Pos: tag.Pos(),
164
+ End: tag.End(),
165
+ }
166
+ }
167
+
168
+ // docTagText renders a tag's comment list as one line.
169
+ //
170
+ // A tag's text is a list of comment nodes: plain text runs, and link nodes for
171
+ // `{@link Foo}` and its two variants. The link nodes are rendered back to the
172
+ // braced form they were written in, because a consumer matching a citation
173
+ // target has to see the target as the author wrote it — and because the same
174
+ // link is separately available as a resolved edge, so dropping the text here
175
+ // would leave the two halves unable to be read together.
176
+ //
177
+ // Lines join with single spaces. A documentation comment carries its own
178
+ // leading asterisks and indentation on every line after the first, and neither
179
+ // is content; joining on a space is also what makes a reason written across
180
+ // three lines one string rather than three.
181
+ func docTagText(comment *shimast.NodeList) string {
182
+ if comment == nil {
183
+ return ""
184
+ }
185
+ var out strings.Builder
186
+ for _, node := range comment.Nodes {
187
+ if node == nil {
188
+ continue
189
+ }
190
+ switch node.Kind {
191
+ case shimast.KindJSDocLink:
192
+ writeDocTagLink(&out, "{@link ", node)
193
+ case shimast.KindJSDocLinkCode:
194
+ writeDocTagLink(&out, "{@linkcode ", node)
195
+ case shimast.KindJSDocLinkPlain:
196
+ writeDocTagLink(&out, "{@linkplain ", node)
197
+ default:
198
+ out.WriteString(shimast.NodeText(node))
199
+ }
200
+ }
201
+ return joinDocTagLines(out.String())
202
+ }
203
+
204
+ // writeDocTagLink renders one link node back to its written form. The name and
205
+ // the trailing text are separate fields on the node, so `{@link A.B rest}` is
206
+ // reassembled rather than read off one string.
207
+ func writeDocTagLink(out *strings.Builder, opener string, node *shimast.Node) {
208
+ name := ""
209
+ switch node.Kind {
210
+ case shimast.KindJSDocLink:
211
+ if link := node.AsJSDocLink(); link != nil && link.Name() != nil {
212
+ name = shimast.NodeText(link.Name())
213
+ }
214
+ case shimast.KindJSDocLinkCode:
215
+ if link := node.AsJSDocLinkCode(); link != nil && link.Name() != nil {
216
+ name = shimast.NodeText(link.Name())
217
+ }
218
+ case shimast.KindJSDocLinkPlain:
219
+ if link := node.AsJSDocLinkPlain(); link != nil && link.Name() != nil {
220
+ name = shimast.NodeText(link.Name())
221
+ }
222
+ }
223
+ out.WriteString(opener)
224
+ out.WriteString(name)
225
+ text := shimast.NodeText(node)
226
+ if text != "" {
227
+ if name != "" {
228
+ out.WriteString(" ")
229
+ }
230
+ out.WriteString(text)
231
+ }
232
+ out.WriteString("}")
233
+ }
234
+
235
+ // joinDocTagLines collapses a tag's physical lines into one and normalizes the
236
+ // whitespace runs a documentation comment adds around them.
237
+ //
238
+ // strings.Fields splits on every Unicode space, which covers the four
239
+ // ECMAScript line terminators — LF, CR, U+2028, U+2029 — so a CRLF checkout and
240
+ // an LF one produce the same string, and a reason written across three comment
241
+ // lines becomes one.
242
+ func joinDocTagLines(text string) string {
243
+ return strings.Join(strings.Fields(text), " ")
244
+ }
245
+
246
+ // collectDocRefs records an edge from each declaration to the symbols its own
247
+ // documentation names through an inline link.
248
+ //
249
+ // The checker resolves such a name and counts it as a use — a `noUnusedLocals`
250
+ // project keeps an import that exists only to support a link — so this is a
251
+ // compiler fact, not a text match, and it was the one class of resolved
252
+ // reference the graph held no edge for. The link node carries an EntityName,
253
+ // which is the node class typeRefEdge already resolves, so the resolution and
254
+ // the external-boundary and self-reference rules are the existing ones.
255
+ //
256
+ // Links are read from the whole documentation comment rather than from tags
257
+ // alone: a link under `@evidence`, under `@see`, and in ordinary prose are one
258
+ // relation, and privileging a tag would put one convention's name inside the
259
+ // compiler host.
260
+ func (g *Graph) collectDocRefs(checker *shimchecker.Checker, file *shimast.SourceFile) {
261
+ for _, host := range g.docHosts[file.FileName()] {
262
+ for _, doc := range documentationOf(host.declaration, file) {
263
+ g.docRefsWithin(checker, host.target, doc)
264
+ }
265
+ }
266
+ }
267
+
268
+ // recordDocHost remembers that a declaration carries documentation, so the edge
269
+ // pass resolves its links from exactly the declarations the build pass
270
+ // attributed documentation to.
271
+ //
272
+ // The two passes cannot share a walk — one runs before the checker exists and
273
+ // the other needs it — so they would otherwise share only a convention, and a
274
+ // convention is what drifted. The container walk the edge pass would naturally
275
+ // reuse never visits a class, an interface, or a namespace as a node of its
276
+ // own: it descends straight into their members. So a link written on a class's
277
+ // own documentation resolved to nothing while the tag beside it was indexed,
278
+ // and the two halves a reader is meant to compose disagreed exactly where a
279
+ // type is documented. Recording the host here means the edge pass covers every
280
+ // declaration form the node pass records, including forms nobody has written
281
+ // yet.
282
+ //
283
+ // It also fixes the attribution: the container walk hands a property member's
284
+ // subtree to the property *and* to its class, which is deliberate for the
285
+ // dependency edges but would make a class's documentation appear to name a
286
+ // symbol only its member's documentation mentions.
287
+ func (g *Graph) recordDocHost(target string, declaration *shimast.Node) {
288
+ file := shimast.GetSourceFileOfNode(declaration)
289
+ if file == nil {
290
+ return
291
+ }
292
+ key := docTagKey{target: target, pos: declaration.Pos(), end: declaration.End()}
293
+ if g.docHostPositions == nil {
294
+ g.docHostPositions = map[docTagKey]struct{}{}
295
+ }
296
+ if _, seen := g.docHostPositions[key]; seen {
297
+ return
298
+ }
299
+ g.docHostPositions[key] = struct{}{}
300
+ if g.docHosts == nil {
301
+ g.docHosts = map[string][]docHost{}
302
+ }
303
+ path := file.FileName()
304
+ g.docHosts[path] = append(g.docHosts[path], docHost{
305
+ target: target,
306
+ declaration: declaration,
307
+ })
308
+ }
309
+
310
+ // docHost is one declaration that carries documentation, paired with the graph
311
+ // node the build pass attributed it to. They are grouped by file, because the
312
+ // edge pass asks once per file and a flat slice would make that a scan of every
313
+ // documented declaration in the project for each one.
314
+ type docHost struct {
315
+ target string
316
+ declaration *shimast.Node
317
+ }
318
+
319
+ // docRefsWithin resolves every inline link inside one documentation comment.
320
+ //
321
+ // The walk descends through each tag with ForEachChild rather than reading a
322
+ // tag's comment field, because that field lives on a per-kind struct: a
323
+ // type-asserted read crashes on the first `@param` it meets, and a switch over
324
+ // the kinds that carry one would cover the thirty tag shapes TypeScript models
325
+ // only until it missed one. Every tag node visits its own comment list, so one
326
+ // recursion reaches a link under `@evidence`, under `@see`, and under whatever
327
+ // tag a consumer invents, which is the rule this edge is defined by.
328
+ //
329
+ // The JSDoc node itself reports no children, so its prose comment is walked
330
+ // explicitly beside the tags; that is the same reason the existing edge passes
331
+ // never reached a link.
332
+ func (g *Graph) docRefsWithin(checker *shimchecker.Checker, from string, doc *shimast.Node) {
333
+ // Guarded on the kind before the conversion, the way the tag walk is. A
334
+ // documentation block is what this is handed and an AsJSDoc on anything else
335
+ // panics rather than returning nil — which is exactly how reading a tag's
336
+ // comment through its own struct crashed on the first `@param`.
337
+ if doc == nil || doc.Kind != shimast.KindJSDoc {
338
+ return
339
+ }
340
+ comment := doc.AsJSDoc()
341
+ if comment == nil {
342
+ return
343
+ }
344
+ g.docRefsInComment(checker, from, comment.Comment)
345
+ if comment.Tags == nil {
346
+ return
347
+ }
348
+ for _, tag := range comment.Tags.Nodes {
349
+ g.docRefsInSubtree(checker, from, tag)
350
+ }
351
+ }
352
+
353
+ // docRefsInSubtree resolves the links anywhere under one documentation node.
354
+ func (g *Graph) docRefsInSubtree(checker *shimchecker.Checker, from string, node *shimast.Node) {
355
+ if node == nil {
356
+ return
357
+ }
358
+ if name := docLinkName(node); name != nil {
359
+ g.docRefEdge(checker, from, name)
360
+ // A link's own subtree is the name just resolved; descending would resolve
361
+ // it a second time under a different node.
362
+ return
363
+ }
364
+ node.ForEachChild(func(child *shimast.Node) bool {
365
+ g.docRefsInSubtree(checker, from, child)
366
+ return false
367
+ })
368
+ }
369
+
370
+ // docRefsInComment resolves the link nodes in one comment list.
371
+ func (g *Graph) docRefsInComment(checker *shimchecker.Checker, from string, comment *shimast.NodeList) {
372
+ if comment == nil {
373
+ return
374
+ }
375
+ for _, node := range comment.Nodes {
376
+ g.docRefsInSubtree(checker, from, node)
377
+ }
378
+ }
379
+
380
+ // docLinkName returns the entity name a link node points at, or nil for a
381
+ // comment node that is not a link or a link that names nothing — `{@link}` with
382
+ // only text is a formatting choice, not a reference.
383
+ func docLinkName(node *shimast.Node) *shimast.Node {
384
+ if node == nil {
385
+ return nil
386
+ }
387
+ switch node.Kind {
388
+ case shimast.KindJSDocLink:
389
+ if link := node.AsJSDocLink(); link != nil {
390
+ return link.Name()
391
+ }
392
+ case shimast.KindJSDocLinkCode:
393
+ if link := node.AsJSDocLinkCode(); link != nil {
394
+ return link.Name()
395
+ }
396
+ case shimast.KindJSDocLinkPlain:
397
+ if link := node.AsJSDocLinkPlain(); link != nil {
398
+ return link.Name()
399
+ }
400
+ }
401
+ return nil
402
+ }
403
+
404
+ // docRefEdge resolves one link name to its declaration and records the edge,
405
+ // skipping an unresolved name and a self-reference exactly as typeRefEdge does.
406
+ func (g *Graph) docRefEdge(checker *shimchecker.Checker, from string, name *shimast.Node) {
407
+ target := g.resolve(checker, name)
408
+ if target == nil || target.Symbol == nil {
409
+ return
410
+ }
411
+ to := g.ensureTargetNode(target)
412
+ if to == "" || to == from {
413
+ return
414
+ }
415
+ g.addEdgeAt(from, to, EdgeDocRef, "", name.Pos(), name.End())
416
+ }
@@ -0,0 +1,131 @@
1
+ package graph
2
+
3
+ import (
4
+ "encoding/json"
5
+ "path/filepath"
6
+ "reflect"
7
+ "strings"
8
+ "testing"
9
+
10
+ "github.com/samchon/ttsc/packages/ttsc/driver"
11
+ )
12
+
13
+ // TestDocTagsAreClaimedBeforeTheyAreRead verifies that the projection carries a
14
+ // tag onto the wire and that a project with no tag is unchanged beside it.
15
+ //
16
+ // The capability the shipped commands declare is asserted where it is made, in
17
+ // `cmd/ttscgraph`: a test that hands a capability list to the marshaller and
18
+ // reads it back proves only that the slice was copied, and would leave the
19
+ // production list free to lose the member with nothing failing. What this one
20
+ // owns is the other half — that a tag reaches the document, and that a project
21
+ // using no convention pays nothing for the feature.
22
+ func TestDocTagsAreClaimedBeforeTheyAreRead(t *testing.T) {
23
+ tagged := dumpDocTagFixture(t, `/** @evidence docs/a.md#x Cited. */
24
+ export function subject(): void {}
25
+ `)
26
+ untagged := dumpDocTagFixture(t, `/** Ordinary documentation. */
27
+ export function subject(): void {}
28
+ `)
29
+
30
+ if got := docTagTexts(tagged); len(got) != 1 || got[0] != "docs/a.md#x Cited." {
31
+ t.Fatalf("tagged dump carried %v", got)
32
+ }
33
+ if got := docTagTexts(untagged); len(got) != 0 {
34
+ t.Fatalf("untagged dump carried %v", got)
35
+ }
36
+
37
+ // The rest of the untagged document is what it was: the feature costs a
38
+ // project using no convention exactly nothing. Compared structurally rather
39
+ // than by count, because equal counts survive a changed id, name, or span.
40
+ if !reflect.DeepEqual(strippedDocTags(tagged), untagged) {
41
+ t.Fatalf("the two documents differ beyond the tag: tagged %+v, untagged %+v",
42
+ strippedDocTags(tagged), untagged)
43
+ }
44
+ }
45
+
46
+ // dumpDocTagProbe is the slice of the wire contract this test reads.
47
+ type dumpDocTagProbe struct {
48
+ Provenance struct {
49
+ Capabilities []string `json:"capabilities"`
50
+ } `json:"provenance"`
51
+ Nodes []dumpDocTagNode `json:"nodes"`
52
+ Edges []dumpDocTagEdge `json:"edges"`
53
+ }
54
+
55
+ type dumpDocTagNode struct {
56
+ ID string `json:"id"`
57
+ Kind string `json:"kind"`
58
+ Name string `json:"name"`
59
+ Signature string `json:"signature"`
60
+ Evidence any `json:"evidence"`
61
+ DocTags []DumpDocTag `json:"docTags"`
62
+ }
63
+
64
+ type dumpDocTagEdge struct {
65
+ From string `json:"from"`
66
+ To string `json:"to"`
67
+ Kind string `json:"kind"`
68
+ Evidence any `json:"evidence"`
69
+ }
70
+
71
+ // strippedDocTags is the tagged document with its tags removed, which is what
72
+ // the untagged one must equal.
73
+ func strippedDocTags(parsed dumpDocTagProbe) dumpDocTagProbe {
74
+ out := parsed
75
+ out.Nodes = append([]dumpDocTagNode(nil), parsed.Nodes...)
76
+ for index := range out.Nodes {
77
+ out.Nodes[index].DocTags = nil
78
+ }
79
+ return out
80
+ }
81
+
82
+ func docTagTexts(parsed dumpDocTagProbe) []string {
83
+ out := []string{}
84
+ for _, node := range parsed.Nodes {
85
+ for _, tag := range node.DocTags {
86
+ out = append(out, tag.Text)
87
+ }
88
+ }
89
+ return out
90
+ }
91
+
92
+ // dumpDocTagFixture builds and marshals a one-file project, returning the parsed
93
+ // wire document.
94
+ func dumpDocTagFixture(t *testing.T, source string) dumpDocTagProbe {
95
+ t.Helper()
96
+ root := t.TempDir()
97
+ writeFile(t, filepath.Join(root, "tsconfig.json"), fixtureTSConfig)
98
+ writeFile(t, filepath.Join(root, "src", "main.ts"), source)
99
+
100
+ prog, diags, err := driver.LoadProgram(root, "tsconfig.json", driver.LoadProgramOptions{})
101
+ if err != nil {
102
+ t.Fatal(err)
103
+ }
104
+ if len(diags) != 0 {
105
+ t.Fatalf("unexpected diagnostics: %v", diags)
106
+ }
107
+ defer func() { _ = prog.Close() }()
108
+
109
+ g := Build(prog)
110
+ data, err := MarshalDump(g, root, "tsconfig.json", nil, SourceTexts(prog), DumpOrigin{
111
+ Provenance: NewProvenance(
112
+ Producer{Tool: "test", Typescript: TypescriptVersion()},
113
+ []string{CapabilityDocTags},
114
+ nil,
115
+ nil,
116
+ SourceTexts(prog),
117
+ nil,
118
+ ),
119
+ }, false)
120
+ if err != nil {
121
+ t.Fatal(err)
122
+ }
123
+ var parsed dumpDocTagProbe
124
+ if err := json.Unmarshal(data, &parsed); err != nil {
125
+ t.Fatal(err)
126
+ }
127
+ if !strings.Contains(string(data), "\"nodes\"") {
128
+ t.Fatalf("dump carried no nodes section")
129
+ }
130
+ return parsed
131
+ }