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.
- package/cmd/graphbench/main.go +26 -16
- package/cmd/graphdump/main.go +37 -12
- package/cmd/graphdump/run_declares_the_capability_its_output_carries_test.go +137 -0
- package/cmd/graphdump/run_reports_an_unknown_flag_without_exiting_the_process_test.go +56 -0
- package/cmd/graphdump/run_reports_an_unloadable_project_without_printing_a_dump_test.go +46 -0
- package/cmd/ttsc/api_transform.go +17 -6
- package/cmd/ttscgraph/dump.go +43 -11
- package/cmd/ttscgraph/run_dump_claims_artifacts_only_when_it_asked_test.go +135 -0
- package/cmd/ttscgraph/run_dump_declares_the_capability_its_output_carries_test.go +96 -0
- package/cmd/ttscgraph/serve.go +253 -25
- package/cmd/ttscgraph/serve_adopts_a_republished_artifact_set_test.go +348 -0
- package/cmd/ttscgraph/serve_projects_an_artifact_no_source_owns_test.go +90 -0
- package/cmd/ttscgraph/serve_shards.go +31 -9
- package/driver/config_discovery.go +121 -0
- package/driver/emit_plugin.go +57 -8
- package/driver/plugins.go +125 -14
- package/driver/transform_dependencies.go +283 -0
- package/driver/transform_dependencies_declares_every_file_without_a_contributor_test.go +28 -0
- package/driver/transform_dependencies_drops_a_self_reported_dependency_test.go +21 -0
- package/driver/transform_dependencies_keys_a_reported_path_like_the_envelope_test.go +46 -0
- package/driver/transform_dependencies_requires_every_contributor_to_declare_test.go +38 -0
- package/driver/transform_dependencies_stays_silent_for_an_undeclared_contributor_test.go +23 -0
- package/driver/transform_dependencies_withdraws_completeness_after_an_unusable_dependency_test.go +37 -0
- package/internal/graph/artifacts.go +230 -0
- package/internal/graph/artifacts_index_what_a_plugin_published_test.go +154 -0
- package/internal/graph/artifacts_survive_a_partial_rebuild_test.go +89 -0
- package/internal/graph/build.go +35 -0
- package/internal/graph/build_releases_the_scratch_it_no_longer_reads_test.go +118 -0
- package/internal/graph/doc_refs_are_the_use_the_checker_already_counts_test.go +93 -0
- package/internal/graph/doc_refs_resolve_documentation_links_test.go +335 -0
- package/internal/graph/doc_tags.go +416 -0
- package/internal/graph/doc_tags_are_claimed_before_they_are_read_test.go +131 -0
- package/internal/graph/doc_tags_capture_every_unrecognized_tag_test.go +165 -0
- package/internal/graph/doc_tags_read_only_what_the_parser_attached_test.go +97 -0
- package/internal/graph/doc_tags_stop_at_the_external_boundary_test.go +59 -0
- package/internal/graph/doc_tags_survive_line_endings_and_repeated_blocks_test.go +146 -0
- package/internal/graph/dump.go +60 -13
- package/internal/graph/dump_checkout_paths_are_stable_test.go +2 -2
- package/internal/graph/dump_paths.go +2 -2
- package/internal/graph/dump_paths_are_portable_and_injective_test.go +1 -1
- package/internal/graph/edge_kinds_names_every_declared_kind_test.go +49 -0
- package/internal/graph/edges.go +1 -0
- package/internal/graph/graph.go +79 -4
- package/internal/graph/provenance.go +29 -2
- package/lib/compiler/internal/project/createNativeProjectContextArgs.d.ts +9 -0
- package/lib/compiler/internal/project/createNativeProjectContextArgs.js +16 -4
- package/lib/compiler/internal/project/createNativeProjectContextArgs.js.map +1 -1
- package/lib/index.d.ts +1 -0
- package/lib/index.js +1 -0
- package/lib/index.js.map +1 -1
- package/lib/launcher/internal/runtimeHooks.d.ts +10 -2
- package/lib/launcher/internal/runtimeHooks.js +93 -2
- package/lib/launcher/internal/runtimeHooks.js.map +1 -1
- package/lib/plugin/internal/capabilityResolutionCache.d.ts +76 -0
- package/lib/plugin/internal/capabilityResolutionCache.js +224 -0
- package/lib/plugin/internal/capabilityResolutionCache.js.map +1 -0
- package/lib/plugin/resolveCapabilityPlugins.d.ts +55 -0
- package/lib/plugin/resolveCapabilityPlugins.js +118 -0
- package/lib/plugin/resolveCapabilityPlugins.js.map +1 -0
- package/lib/structures/ITtscCompilerTransformation.d.ts +7 -0
- package/package.json +8 -8
- package/shim/ast/parent.go +16 -3
- package/shim/checker/enums_gen.go +11 -0
- package/shim/checker/shim.go +3 -0
- package/shim/compiler/emithook.go +10 -0
- package/src/compiler/internal/project/createNativeProjectContextArgs.ts +19 -4
- package/src/index.ts +1 -0
- package/src/launcher/internal/runtimeHooks.ts +111 -2
- package/src/plugin/internal/capabilityResolutionCache.ts +305 -0
- package/src/plugin/resolveCapabilityPlugins.ts +171 -0
- package/src/structures/ITtscCompilerTransformation.ts +7 -0
- package/tools/shim_audit/baseline.json +0 -1
- 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
|
+
}
|