ttsc 0.26.2 → 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 (170) hide show
  1. package/README.md +9 -0
  2. package/cmd/graphbench/main.go +26 -16
  3. package/cmd/graphdump/main.go +37 -12
  4. package/cmd/graphdump/run_declares_the_capability_its_output_carries_test.go +137 -0
  5. package/cmd/graphdump/run_reports_an_unknown_flag_without_exiting_the_process_test.go +56 -0
  6. package/cmd/graphdump/run_reports_an_unloadable_project_without_printing_a_dump_test.go +46 -0
  7. package/cmd/ttsc/api_transform.go +28 -3
  8. package/cmd/ttscgraph/auxiliary_identity_state_tracks_retargets_not_source_contents_test.go +85 -0
  9. package/cmd/ttscgraph/disk_realpath_other.go +16 -0
  10. package/cmd/ttscgraph/disk_realpath_windows.go +56 -0
  11. package/cmd/ttscgraph/dump.go +43 -11
  12. package/cmd/ttscgraph/run_dump_claims_artifacts_only_when_it_asked_test.go +135 -0
  13. package/cmd/ttscgraph/run_dump_declares_the_capability_its_output_carries_test.go +96 -0
  14. package/cmd/ttscgraph/serve.go +357 -60
  15. package/cmd/ttscgraph/serve_adopts_a_republished_artifact_set_test.go +348 -0
  16. package/cmd/ttscgraph/serve_projects_an_artifact_no_source_owns_test.go +90 -0
  17. package/cmd/ttscgraph/serve_shards.go +31 -9
  18. package/driver/config_discovery.go +121 -0
  19. package/driver/emit_plugin.go +77 -11
  20. package/driver/graph.go +64 -11
  21. package/driver/input_observation.go +273 -0
  22. package/driver/input_observation_test.go +149 -0
  23. package/driver/plugins.go +506 -15
  24. package/driver/program.go +54 -1
  25. package/driver/resolution_candidates.go +13 -5
  26. package/driver/rewrite.go +45 -2
  27. package/driver/transform_dependencies.go +283 -0
  28. package/driver/transform_dependencies_declares_every_file_without_a_contributor_test.go +28 -0
  29. package/driver/transform_dependencies_drops_a_self_reported_dependency_test.go +21 -0
  30. package/driver/transform_dependencies_keys_a_reported_path_like_the_envelope_test.go +46 -0
  31. package/driver/transform_dependencies_requires_every_contributor_to_declare_test.go +38 -0
  32. package/driver/transform_dependencies_stays_silent_for_an_undeclared_contributor_test.go +23 -0
  33. package/driver/transform_dependencies_withdraws_completeness_after_an_unusable_dependency_test.go +37 -0
  34. package/internal/graph/artifacts.go +230 -0
  35. package/internal/graph/artifacts_index_what_a_plugin_published_test.go +154 -0
  36. package/internal/graph/artifacts_survive_a_partial_rebuild_test.go +89 -0
  37. package/internal/graph/build.go +35 -0
  38. package/internal/graph/build_releases_the_scratch_it_no_longer_reads_test.go +118 -0
  39. package/internal/graph/doc_refs_are_the_use_the_checker_already_counts_test.go +93 -0
  40. package/internal/graph/doc_refs_resolve_documentation_links_test.go +335 -0
  41. package/internal/graph/doc_tags.go +416 -0
  42. package/internal/graph/doc_tags_are_claimed_before_they_are_read_test.go +131 -0
  43. package/internal/graph/doc_tags_capture_every_unrecognized_tag_test.go +165 -0
  44. package/internal/graph/doc_tags_read_only_what_the_parser_attached_test.go +97 -0
  45. package/internal/graph/doc_tags_stop_at_the_external_boundary_test.go +59 -0
  46. package/internal/graph/doc_tags_survive_line_endings_and_repeated_blocks_test.go +146 -0
  47. package/internal/graph/dump.go +60 -13
  48. package/internal/graph/dump_checkout_paths_are_stable_test.go +2 -2
  49. package/internal/graph/dump_paths.go +2 -2
  50. package/internal/graph/dump_paths_are_portable_and_injective_test.go +1 -1
  51. package/internal/graph/edge_kinds_names_every_declared_kind_test.go +49 -0
  52. package/internal/graph/edges.go +1 -0
  53. package/internal/graph/graph.go +79 -4
  54. package/internal/graph/provenance.go +29 -2
  55. package/lib/TtscCompiler.d.ts +12 -11
  56. package/lib/TtscCompiler.js +44 -29
  57. package/lib/TtscCompiler.js.map +1 -1
  58. package/lib/compiler/internal/captureProcessOutput.js +2 -2
  59. package/lib/compiler/internal/captureProcessOutput.js.map +1 -1
  60. package/lib/compiler/internal/compileProjectInMemory.js +4 -3
  61. package/lib/compiler/internal/compileProjectInMemory.js.map +1 -1
  62. package/lib/compiler/internal/project/createNativeProjectContextArgs.d.ts +9 -0
  63. package/lib/compiler/internal/project/createNativeProjectContextArgs.js +16 -4
  64. package/lib/compiler/internal/project/createNativeProjectContextArgs.js.map +1 -1
  65. package/lib/compiler/internal/resolveEmittedJavaScript.d.ts +2 -0
  66. package/lib/compiler/internal/resolveEmittedJavaScript.js +3 -0
  67. package/lib/compiler/internal/resolveEmittedJavaScript.js.map +1 -1
  68. package/lib/compiler/internal/runBuild.d.ts +1 -1
  69. package/lib/compiler/internal/runBuild.js +80 -34
  70. package/lib/compiler/internal/runBuild.js.map +1 -1
  71. package/lib/compiler/internal/runSingleFileEmit.js +2 -2
  72. package/lib/compiler/internal/runSingleFileEmit.js.map +1 -1
  73. package/lib/compiler/internal/sharedHostHelpers.d.ts +28 -0
  74. package/lib/compiler/internal/sharedHostHelpers.js +39 -0
  75. package/lib/compiler/internal/sharedHostHelpers.js.map +1 -1
  76. package/lib/compiler/internal/startResidentTransform.js +12 -4
  77. package/lib/compiler/internal/startResidentTransform.js.map +1 -1
  78. package/lib/compiler/internal/transformProjectInMemory.d.ts +3 -0
  79. package/lib/compiler/internal/transformProjectInMemory.js +239 -35
  80. package/lib/compiler/internal/transformProjectInMemory.js.map +1 -1
  81. package/lib/flags/schema.js +12 -5
  82. package/lib/flags/schema.js.map +1 -1
  83. package/lib/index.d.ts +1 -0
  84. package/lib/index.js +1 -0
  85. package/lib/index.js.map +1 -1
  86. package/lib/internal/createCanonicalTempDirectory.d.ts +17 -0
  87. package/lib/internal/createCanonicalTempDirectory.js +42 -0
  88. package/lib/internal/createCanonicalTempDirectory.js.map +1 -0
  89. package/lib/internal/projectInputPathIdentity.d.ts +3 -0
  90. package/lib/internal/projectInputPathIdentity.js +11 -5
  91. package/lib/internal/projectInputPathIdentity.js.map +1 -1
  92. package/lib/internal/resolveNodeBinary.d.ts +14 -0
  93. package/lib/internal/resolveNodeBinary.js +150 -0
  94. package/lib/internal/resolveNodeBinary.js.map +1 -0
  95. package/lib/internal/resolveSafeCacheCleanupTargets.d.ts +15 -0
  96. package/lib/internal/resolveSafeCacheCleanupTargets.js +61 -0
  97. package/lib/internal/resolveSafeCacheCleanupTargets.js.map +1 -0
  98. package/lib/launcher/internal/prepareExecution.d.ts +3 -0
  99. package/lib/launcher/internal/prepareExecution.js +42 -14
  100. package/lib/launcher/internal/prepareExecution.js.map +1 -1
  101. package/lib/launcher/internal/runTtsc.js +25 -13
  102. package/lib/launcher/internal/runTtsc.js.map +1 -1
  103. package/lib/launcher/internal/runTtscserver.js +18 -3
  104. package/lib/launcher/internal/runTtscserver.js.map +1 -1
  105. package/lib/launcher/internal/runTtsx.js +2 -0
  106. package/lib/launcher/internal/runTtsx.js.map +1 -1
  107. package/lib/launcher/internal/runtimeHooks.d.ts +47 -3
  108. package/lib/launcher/internal/runtimeHooks.js +746 -64
  109. package/lib/launcher/internal/runtimeHooks.js.map +1 -1
  110. package/lib/plugin/internal/buildSourcePlugin.d.ts +62 -8
  111. package/lib/plugin/internal/buildSourcePlugin.js +754 -72
  112. package/lib/plugin/internal/buildSourcePlugin.js.map +1 -1
  113. package/lib/plugin/internal/capabilityResolutionCache.d.ts +76 -0
  114. package/lib/plugin/internal/capabilityResolutionCache.js +224 -0
  115. package/lib/plugin/internal/capabilityResolutionCache.js.map +1 -0
  116. package/lib/plugin/internal/loadProjectPlugins.d.ts +17 -5
  117. package/lib/plugin/internal/loadProjectPlugins.js +1339 -77
  118. package/lib/plugin/internal/loadProjectPlugins.js.map +1 -1
  119. package/lib/plugin/resolveCapabilityPlugins.d.ts +55 -0
  120. package/lib/plugin/resolveCapabilityPlugins.js +118 -0
  121. package/lib/plugin/resolveCapabilityPlugins.js.map +1 -0
  122. package/lib/register.d.ts +1 -0
  123. package/lib/register.js +46 -0
  124. package/lib/register.js.map +1 -0
  125. package/lib/structures/ITtscCompilerContext.d.ts +6 -3
  126. package/lib/structures/ITtscCompilerTransformation.d.ts +38 -0
  127. package/lib/structures/ITtscPlugin.d.ts +33 -0
  128. package/package.json +16 -9
  129. package/shim/ast/parent.go +16 -3
  130. package/shim/checker/enums_gen.go +11 -0
  131. package/shim/checker/go.mod +9 -0
  132. package/shim/checker/go.sum +22 -0
  133. package/shim/checker/shim.go +78 -0
  134. package/shim/compiler/emithook.go +10 -0
  135. package/shim/compiler/incremental.go +45 -8
  136. package/src/TtscCompiler.ts +55 -33
  137. package/src/compiler/internal/captureProcessOutput.ts +3 -2
  138. package/src/compiler/internal/compileProjectInMemory.ts +4 -3
  139. package/src/compiler/internal/project/createNativeProjectContextArgs.ts +19 -4
  140. package/src/compiler/internal/resolveEmittedJavaScript.ts +6 -0
  141. package/src/compiler/internal/runBuild.ts +102 -39
  142. package/src/compiler/internal/runSingleFileEmit.ts +2 -2
  143. package/src/compiler/internal/sharedHostHelpers.ts +44 -0
  144. package/src/compiler/internal/startResidentTransform.ts +12 -3
  145. package/src/compiler/internal/transformProjectInMemory.ts +410 -39
  146. package/src/flags/schema.ts +12 -5
  147. package/src/index.ts +1 -0
  148. package/src/internal/createCanonicalTempDirectory.ts +53 -0
  149. package/src/internal/projectInputPathIdentity.ts +17 -4
  150. package/src/internal/resolveNodeBinary.ts +173 -0
  151. package/src/internal/resolveSafeCacheCleanupTargets.ts +85 -0
  152. package/src/launcher/internal/prepareExecution.ts +50 -14
  153. package/src/launcher/internal/runTtsc.ts +24 -13
  154. package/src/launcher/internal/runTtscserver.ts +18 -3
  155. package/src/launcher/internal/runTtsx.ts +2 -0
  156. package/src/launcher/internal/runtimeHooks.ts +934 -68
  157. package/src/plugin/internal/buildSourcePlugin.ts +984 -79
  158. package/src/plugin/internal/capabilityResolutionCache.ts +305 -0
  159. package/src/plugin/internal/loadProjectPlugins.ts +1718 -104
  160. package/src/plugin/resolveCapabilityPlugins.ts +171 -0
  161. package/src/register.ts +47 -0
  162. package/src/structures/ITtscCompilerContext.ts +6 -3
  163. package/src/structures/ITtscCompilerTransformation.ts +46 -0
  164. package/src/structures/ITtscPlugin.ts +36 -0
  165. package/tools/shim_audit/baseline.json +0 -2
  166. package/utility/host.go +18 -3
  167. package/lib/internal/assertSafeExplicitCacheDirectory.d.ts +0 -5
  168. package/lib/internal/assertSafeExplicitCacheDirectory.js +0 -26
  169. package/lib/internal/assertSafeExplicitCacheDirectory.js.map +0 -1
  170. package/src/internal/assertSafeExplicitCacheDirectory.ts +0 -29
@@ -0,0 +1,230 @@
1
+ package graph
2
+
3
+ import (
4
+ "encoding/json"
5
+ "os"
6
+ "sort"
7
+ "strings"
8
+ )
9
+
10
+ // Artifact is one thing a declaration's documentation can cite that is not a
11
+ // TypeScript declaration — a document section, a data model field, an API
12
+ // operation.
13
+ //
14
+ // It is the wire shape `@ttsc/lint`'s `graph-nodes` verb prints, restated here
15
+ // rather than imported: `packages/lint` is its own Go module and deliberately
16
+ // carries no requirement on this one, with its reason written in that manifest.
17
+ // What binds the two is the JSON contract, so the keys below are the contract
18
+ // and the Go field names are not — the same rule DumpNode already states.
19
+ //
20
+ // Nothing here is a judgement. The rule that materialized these decides what is
21
+ // covered, what is a legitimate exclusion, and what is missing, and it delivers
22
+ // those as compile errors. The graph reports; the linter judges.
23
+ type Artifact struct {
24
+ Address string `json:"address"`
25
+ Kind string `json:"kind"`
26
+ Readable string `json:"readable,omitempty"`
27
+ Parent string `json:"parent,omitempty"`
28
+ File string `json:"file,omitempty"`
29
+ Line int `json:"line,omitempty"`
30
+ Aliases []string `json:"aliases,omitempty"`
31
+ }
32
+
33
+ // artifactNodeKinds maps a published artifact kind onto this graph's node kind.
34
+ //
35
+ // The vocabulary is closed on purpose. A kind the graph does not model is a node
36
+ // a consumer would rank, contain, and display as something it is not, so it is
37
+ // dropped rather than guessed at — and the capability claim is what tells a
38
+ // consumer the producer looked at all.
39
+ var artifactNodeKinds = map[string]NodeKind{
40
+ "markdown_document": NodeMarkdownDocument,
41
+ "markdown_section": NodeMarkdownSection,
42
+ "prisma_model": NodePrismaModel,
43
+ "prisma_column": NodePrismaColumn,
44
+ "prisma_relation": NodePrismaRelation,
45
+ "swagger_operation": NodeSwaggerOperation,
46
+ }
47
+
48
+ // IsArtifactKind reports whether a node kind names a published artifact rather
49
+ // than a TypeScript declaration.
50
+ //
51
+ // It is what keeps an artifact out of the places that assume a declaration: an
52
+ // id built from the `path#name:kind` grammar, and a tour seed. A document
53
+ // section is neither the shape of a declaration nor an answer to "what is this
54
+ // project and how does it run".
55
+ func IsArtifactKind(kind NodeKind) bool {
56
+ for _, mapped := range artifactNodeKinds {
57
+ if mapped == kind {
58
+ return true
59
+ }
60
+ }
61
+ return false
62
+ }
63
+
64
+ // LoadArtifacts reads the artifact set a plugin published, or nil when no path
65
+ // was given.
66
+ //
67
+ // A missing file is not an error: the caller names a path a plugin may or may
68
+ // not have produced, and a project that publishes nothing is the common case. A
69
+ // file that exists and does not parse is an error, because that is a producer
70
+ // the caller was told to expect.
71
+ func LoadArtifacts(path string) ([]Artifact, error) {
72
+ if strings.TrimSpace(path) == "" {
73
+ return nil, nil
74
+ }
75
+ data, err := os.ReadFile(path)
76
+ if err != nil {
77
+ if os.IsNotExist(err) {
78
+ return nil, nil
79
+ }
80
+ return nil, err
81
+ }
82
+ return ParseArtifacts(data)
83
+ }
84
+
85
+ // ParseArtifacts decodes a published set from the bytes it was read as.
86
+ //
87
+ // Split from LoadArtifacts for a caller that has to hash the same bytes it
88
+ // parses. Reading the file twice — once to state what it holds and once to
89
+ // decode it — lets an overwrite land between the two, leaving a session whose
90
+ // recorded identity describes a set it is not holding.
91
+ func ParseArtifacts(data []byte) ([]Artifact, error) {
92
+ var artifacts []Artifact
93
+ if err := json.Unmarshal(data, &artifacts); err != nil {
94
+ return nil, err
95
+ }
96
+ return artifacts, nil
97
+ }
98
+
99
+ // ApplyArtifacts adds the published artifacts to a built graph as nodes, records
100
+ // each one's parent, and resolves every documentation tag whose target names one
101
+ // into a doc-ref edge.
102
+ //
103
+ // The address is the node id verbatim. That is deliberate and has a real
104
+ // benefit: the token an author wrote in a tag is the handle an agent submits
105
+ // back. It also means these ids do not follow the `path#name:kind` grammar —
106
+ // `prisma:Sale.price` carries no path because a Prisma model name is unique
107
+ // across the schema folder, and `POST:/orders` has no file at all — which is why
108
+ // a consumer parsing an id has to gate on the kind first.
109
+ //
110
+ // An address that collides with an existing node loses: a checker-resolved
111
+ // declaration is a fact of this Program, and a published artifact is not.
112
+ func ApplyArtifacts(g *Graph, artifacts []Artifact) {
113
+ if g == nil || len(artifacts) == 0 {
114
+ return
115
+ }
116
+ // Sorted so the emitted node and edge order is a function of the addresses
117
+ // rather than of the order a plugin happened to walk its documents.
118
+ sorted := make([]Artifact, len(artifacts))
119
+ copy(sorted, artifacts)
120
+ sort.SliceStable(sorted, func(i, j int) bool { return sorted[i].Address < sorted[j].Address })
121
+
122
+ // Every address an artifact answers to, including its aliases, so a citation
123
+ // written against either spelling resolves to the one node.
124
+ byAddress := map[string]string{}
125
+ for _, artifact := range sorted {
126
+ kind, known := artifactNodeKinds[artifact.Kind]
127
+ if !known || artifact.Address == "" {
128
+ continue
129
+ }
130
+ if _, exists := g.Nodes[artifact.Address]; exists {
131
+ continue
132
+ }
133
+ node := &Node{
134
+ ID: artifact.Address,
135
+ Name: artifact.Address,
136
+ Simple: artifact.Readable,
137
+ Kind: kind,
138
+ File: artifact.File,
139
+ }
140
+ if artifact.Line > 0 {
141
+ node.ArtifactLine = artifact.Line
142
+ }
143
+ g.Nodes[artifact.Address] = node
144
+ byAddress[artifact.Address] = artifact.Address
145
+ }
146
+
147
+ // Aliases are registered after every address, and only where nothing claims
148
+ // the spelling. An alias is an additional name for one artifact, never a
149
+ // rename of another: registering it in the same pass let one artifact's alias
150
+ // overwrite an earlier artifact's own address, and a citation of that address
151
+ // then resolved to the wrong node — a confident wrong answer, which is worse
152
+ // than the token it replaced.
153
+ for _, artifact := range sorted {
154
+ if _, published := g.Nodes[artifact.Address]; !published {
155
+ continue
156
+ }
157
+ for _, alias := range artifact.Aliases {
158
+ if alias == "" || alias == artifact.Address {
159
+ continue
160
+ }
161
+ if _, claimed := byAddress[alias]; claimed {
162
+ continue
163
+ }
164
+ byAddress[alias] = artifact.Address
165
+ }
166
+ }
167
+
168
+ // Containment rides the node rather than an edge, because `contains` is
169
+ // synthesized by the TypeScript memory layer for declarations and a second
170
+ // producer of the same relation would put two answers in the graph. A parent
171
+ // naming nothing published is cleared: the child is still a real artifact and
172
+ // sits at the top of its chain, and fabricating the parent is what must not
173
+ // happen.
174
+ for _, artifact := range sorted {
175
+ node, exists := g.Nodes[artifact.Address]
176
+ if !exists || artifact.Parent == "" {
177
+ continue
178
+ }
179
+ if parent, resolved := byAddress[artifact.Parent]; resolved && parent != artifact.Address {
180
+ node.ArtifactParent = parent
181
+ }
182
+ }
183
+
184
+ // A tag's target is its leading token, the same rule the reverse index uses.
185
+ // A tag naming nothing published resolves to nothing and stays what it always
186
+ // was: text on the node, for the linter to judge.
187
+ //
188
+ // The edges are appended with a dedup of their own rather than through
189
+ // addEdge. This runs after the build, and the build releases the scratch it no
190
+ // longer reads — `seen` among it — so the shared helper would write to a map
191
+ // that is deliberately nil. Two declarations citing one address are two edges;
192
+ // one declaration citing it twice, under two tag names, is one.
193
+ seen := map[edgeKey]struct{}{}
194
+ for _, edge := range g.Edges {
195
+ seen[edgeKey{from: edge.From, to: edge.To, kind: wireEdgeKind(edge.Kind, edge.Origin)}] = struct{}{}
196
+ }
197
+ for _, tag := range g.DocTags {
198
+ if tag == nil {
199
+ continue
200
+ }
201
+ target := leadingToken(tag.Text)
202
+ if target == "" {
203
+ continue
204
+ }
205
+ address, resolved := byAddress[target]
206
+ if !resolved {
207
+ continue
208
+ }
209
+ key := edgeKey{from: tag.Target, to: address, kind: wireEdgeKind(EdgeDocRef, "")}
210
+ if _, exists := seen[key]; exists {
211
+ continue
212
+ }
213
+ seen[key] = struct{}{}
214
+ g.Edges = append(g.Edges, &Edge{From: tag.Target, To: address, Kind: EdgeDocRef, Pos: -1, End: -1})
215
+ }
216
+ }
217
+
218
+ // leadingToken is the first whitespace-delimited word of a tag's text, which is
219
+ // the address a citation names. Everything after it is prose the author wrote
220
+ // for a human.
221
+ func leadingToken(text string) string {
222
+ trimmed := strings.TrimSpace(text)
223
+ if trimmed == "" {
224
+ return ""
225
+ }
226
+ if space := strings.IndexAny(trimmed, " \t"); space >= 0 {
227
+ return trimmed[:space]
228
+ }
229
+ return trimmed
230
+ }
@@ -0,0 +1,154 @@
1
+ package graph
2
+
3
+ import (
4
+ "path/filepath"
5
+ "testing"
6
+
7
+ "github.com/samchon/ttsc/packages/ttsc/driver"
8
+ )
9
+
10
+ // TestArtifactsIndexWhatAPluginPublished verifies that a published artifact
11
+ // becomes a node a citation resolves to, and that nothing else does.
12
+ //
13
+ // The reverse question already worked without this: a lookup on an address
14
+ // answered with the declarations citing it, keyed on the token. What it could
15
+ // not answer is what the address names — the heading's own text, what sits under
16
+ // it — and that is what a node adds. The edge is the same `doc-ref` a resolved
17
+ // `{@link}` already produced; the difference is that it now has an other end.
18
+ //
19
+ // Every negative here is a way the index could lie. A fabricated node for an
20
+ // address nobody published would answer a question with an invention; a parent
21
+ // naming nothing would put an artifact under a container that does not exist;
22
+ // and an unknown kind would be ranked and contained as something it is not.
23
+ //
24
+ // 1. Build a graph for a project whose declarations cite four addresses.
25
+ // 2. Apply a published set covering three of them, one by an alias, one under
26
+ // a parent, plus an unpublishable entry.
27
+ // 3. Assert the nodes, their containment, and the resolved edges — and that the
28
+ // uncited address, the unknown kind, and the dangling parent produced none.
29
+ func TestArtifactsIndexWhatAPluginPublished(t *testing.T) {
30
+ root := t.TempDir()
31
+ writeFile(t, filepath.Join(root, "tsconfig.json"), `{
32
+ "compilerOptions": { "target": "ES2022", "module": "commonjs", "strict": true },
33
+ "files": ["src/main.ts"]
34
+ }
35
+ `)
36
+ writeFile(t, filepath.Join(root, "src", "main.ts"), `/** @evidence docs/sale.md#pricing States the rule. */
37
+ export function priced(): void {}
38
+
39
+ /** @evidence prisma:Sale.price The column it reads. */
40
+ export function column(): void {}
41
+
42
+ /** @evidence docs/sale.md#alias Reached by its other name. */
43
+ export function aliased(): void {}
44
+
45
+ /** @evidence docs/sale.md#nobody-published-this Still just text. */
46
+ export function unpublished(): void {}
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
+ before := len(g.Nodes)
57
+ ApplyArtifacts(g, []Artifact{
58
+ {
59
+ Address: "docs/sale.md",
60
+ Kind: "markdown_document",
61
+ Readable: "Sale",
62
+ File: "docs/sale.md",
63
+ Line: 1,
64
+ },
65
+ {
66
+ Address: "docs/sale.md#pricing",
67
+ Kind: "markdown_section",
68
+ Readable: "Pricing",
69
+ Parent: "docs/sale.md",
70
+ File: "docs/sale.md",
71
+ Line: 7,
72
+ Aliases: []string{"docs/sale.md#alias"},
73
+ },
74
+ {
75
+ Address: "prisma:Sale.price",
76
+ Kind: "prisma_column",
77
+ Readable: "Prisma column 'Sale.price'",
78
+ Parent: "prisma:Sale",
79
+ File: "prisma/schema.prisma",
80
+ Line: 12,
81
+ },
82
+ {Address: "docs/sale.md#unknown", Kind: "not_a_kind"},
83
+ // Its alias is another artifact's own address. An alias is an additional
84
+ // name for one artifact, never a rename of another, so this must not move
85
+ // `docs/sale.md#pricing` onto this node — a citation of that address would
86
+ // then resolve to the wrong section, which is a confident wrong answer.
87
+ {
88
+ Address: "docs/sale.md#shadow",
89
+ Kind: "markdown_section",
90
+ Readable: "Shadow",
91
+ Aliases: []string{"docs/sale.md#pricing"},
92
+ File: "docs/sale.md",
93
+ Line: 20,
94
+ },
95
+ })
96
+
97
+ if got := len(g.Nodes) - before; got != 4 {
98
+ t.Fatalf("added %d nodes, want 4; an unknown kind must contribute none", got)
99
+ }
100
+ if _, fabricated := g.Nodes["docs/sale.md#nobody-published-this"]; fabricated {
101
+ t.Fatal("an address nobody published became a node; the tag text carries it and the linter judges it")
102
+ }
103
+ if _, fabricated := g.Nodes["prisma:Sale"]; fabricated {
104
+ t.Fatal("a parent nobody published became a node")
105
+ }
106
+
107
+ section := g.Nodes["docs/sale.md#pricing"]
108
+ if section == nil {
109
+ t.Fatal("the cited section is not a node")
110
+ }
111
+ if section.Simple != "Pricing" {
112
+ t.Fatalf("the section carries %q, not the heading text an index exists to hold", section.Simple)
113
+ }
114
+ if section.ArtifactLine != 7 {
115
+ t.Fatalf("the section starts at line %d, want 7", section.ArtifactLine)
116
+ }
117
+ if section.ArtifactParent != "docs/sale.md" {
118
+ t.Fatalf("the section's parent is %q, want its document", section.ArtifactParent)
119
+ }
120
+ if parent := g.Nodes["prisma:Sale.price"].ArtifactParent; parent != "" {
121
+ t.Fatalf("a parent naming no published node survived as %q; it must be cleared, not fabricated", parent)
122
+ }
123
+
124
+ cited := map[string]bool{}
125
+ for _, edge := range g.Edges {
126
+ if edge.Kind == EdgeDocRef {
127
+ cited[edge.To] = true
128
+ }
129
+ }
130
+ for _, address := range []string{"docs/sale.md#pricing", "prisma:Sale.price"} {
131
+ if !cited[address] {
132
+ t.Fatalf("no doc-ref edge resolved to %q", address)
133
+ }
134
+ }
135
+ if cited["docs/sale.md#alias"] {
136
+ t.Fatal("an alias became its own edge target; it has to resolve to the one node")
137
+ }
138
+ // The alias resolves to the same node, so the declaration citing it and the
139
+ // declaration citing the canonical address point at one artifact.
140
+ aliasEdges := 0
141
+ for _, edge := range g.Edges {
142
+ if edge.Kind == EdgeDocRef && edge.To == "docs/sale.md#pricing" {
143
+ aliasEdges++
144
+ }
145
+ }
146
+ if aliasEdges != 2 {
147
+ t.Fatalf("%d declarations resolved to the section, want 2 (its address and its alias)", aliasEdges)
148
+ }
149
+ for _, edge := range g.Edges {
150
+ if edge.Kind == EdgeDocRef && edge.To == "docs/sale.md#shadow" {
151
+ t.Fatal("an alias claimed another artifact's address; a citation resolved to the wrong node")
152
+ }
153
+ }
154
+ }
@@ -0,0 +1,89 @@
1
+ package graph
2
+
3
+ import (
4
+ "path/filepath"
5
+ "testing"
6
+
7
+ "github.com/samchon/ttsc/packages/ttsc/driver"
8
+ )
9
+
10
+ // TestArtifactsSurviveAPartialRebuild verifies that a citation of an artifact is
11
+ // still a relation after the incremental path rebuilds the file that wrote it.
12
+ //
13
+ // A partial build re-resolves the outgoing facts of the files it selected, and a
14
+ // citation is one of them. The full snapshot applied the published artifacts and
15
+ // the incremental one did not, so editing a declaration that cites a document
16
+ // section rebuilt it with that edge missing — the client's graph silently lost a
17
+ // relation it had a moment earlier, and only a full reload brought it back.
18
+ //
19
+ // The nodes are re-added rather than assumed present because a partial starts
20
+ // with only its selected files' nodes. They are the same nodes under the same
21
+ // ids, which is why the store can replace like with like.
22
+ //
23
+ // 1. Build the complete graph with artifacts applied, and confirm the edge.
24
+ // 2. Rebuild only the citing file, as the shard path does.
25
+ // 3. Assert the edge is there again, and that the artifact came with it.
26
+ func TestArtifactsSurviveAPartialRebuild(t *testing.T) {
27
+ root := t.TempDir()
28
+ writeFile(t, filepath.Join(root, "tsconfig.json"), `{
29
+ "compilerOptions": { "target": "ES2022", "module": "commonjs", "strict": true },
30
+ "files": ["src/main.ts"]
31
+ }
32
+ `)
33
+ source := filepath.Join(root, "src", "main.ts")
34
+ writeFile(t, source, `/** @evidence docs/sale.md#pricing States the rule. */
35
+ export function priced(): void {}
36
+ `)
37
+
38
+ prog, _, err := driver.LoadProgram(root, "tsconfig.json", driver.LoadProgramOptions{})
39
+ if err != nil || prog == nil {
40
+ t.Fatalf("could not load the probe project: %v", err)
41
+ }
42
+ defer func() { _ = prog.Close() }()
43
+
44
+ published := []Artifact{
45
+ {
46
+ Address: "docs/sale.md#pricing",
47
+ Kind: "markdown_section",
48
+ Readable: "Pricing",
49
+ File: "docs/sale.md",
50
+ Line: 7,
51
+ },
52
+ }
53
+
54
+ complete := Build(prog)
55
+ ApplyArtifacts(complete, published)
56
+ if !citesArtifact(complete) {
57
+ t.Fatal("the complete build did not resolve the citation")
58
+ }
59
+
60
+ var selected string
61
+ for _, node := range complete.Nodes {
62
+ if node.File != "" && node.Kind == NodeFunction {
63
+ selected = node.File
64
+ break
65
+ }
66
+ }
67
+ if selected == "" {
68
+ t.Fatal("the probe project produced no declaration to reselect")
69
+ }
70
+
71
+ partial := BuildFiles(prog, []string{selected}, complete.Nodes)
72
+ ApplyArtifacts(partial, published)
73
+ if !citesArtifact(partial) {
74
+ t.Fatal("the partial rebuild dropped the citation edge the full build had")
75
+ }
76
+ if _, published := partial.Nodes["docs/sale.md#pricing"]; !published {
77
+ t.Fatal("the partial rebuild resolved an edge to a node it did not carry")
78
+ }
79
+ }
80
+
81
+ // citesArtifact reports whether any doc-ref edge lands on the probe's artifact.
82
+ func citesArtifact(g *Graph) bool {
83
+ for _, edge := range g.Edges {
84
+ if edge.Kind == EdgeDocRef && edge.To == "docs/sale.md#pricing" {
85
+ return true
86
+ }
87
+ }
88
+ return false
89
+ }
@@ -55,9 +55,39 @@ func BuildFiles(prog *driver.Program, selected []string, baseNodes map[string]*N
55
55
  collectDeclarations(g, file)
56
56
  }
57
57
  g.addEdges(prog, selectedFiles, selected != nil)
58
+ g.releaseBuildState()
58
59
  return g
59
60
  }
60
61
 
62
+ // releaseBuildState drops the scratch the build needed, once the edge pass has
63
+ // finished with it.
64
+ //
65
+ // Every map cleared here is documented build-only, and each holds pointers into
66
+ // the compiler AST or into the preceding generation”'s nodes: resolved is keyed
67
+ // by *shimast.Node, docHosts holds one per documented declaration, and baseNodes
68
+ // is the whole prior endpoint index. A consumer that retains the returned Graph
69
+ // therefore pinned all of it for as long as it held the graph — and
70
+ // internal/graphsymbols retains one for the lifetime of an editor session
71
+ // between invalidations, closing the Program while the maps that reference its
72
+ // AST live on. Clearing them here rather than at that consumer is what keeps the
73
+ // decision with the producer, which is the only side that knows which fields are
74
+ // scratch.
75
+ //
76
+ // ExportedTargets and ImplementationSources are deliberately not cleared: the
77
+ // shard expansion path in cmd/ttscgraph/serve_shards.go reads both after this
78
+ // returns, and Nodes/Edges/Decorators/DocTags are the graph itself.
79
+ func (g *Graph) releaseBuildState() {
80
+ g.docTagPositions = nil
81
+ g.docHosts = nil
82
+ g.docHostPositions = nil
83
+ g.bodyNodes = nil
84
+ g.seen = nil
85
+ g.resolved = nil
86
+ g.edgeEvidenceFiles = nil
87
+ g.baseNodes = nil
88
+ g.selectedFiles = nil
89
+ }
90
+
61
91
  // SourceTexts maps every program source to the resident checker text.
62
92
  // Declaration and virtual bundled files are included because external graph
63
93
  // leaves still carry facts and spans the source manifest must attest to.
@@ -454,6 +484,11 @@ func collectMembers(g *Graph, path string, statement *shimast.Node) {
454
484
 
455
485
  func putDeclaredNode(g *Graph, path, name string, kind NodeKind, declaration *shimast.Node) {
456
486
  id := nodeID(path, name, kind)
487
+ // Tags belong to the identity, not to whichever declaration of it wins the
488
+ // display span below. An overload run documented on its first signature and
489
+ // implemented underneath returns early from the node branch, so collecting
490
+ // there would drop exactly the declaration authors write documentation on.
491
+ collectDocTags(g, id, declaration)
457
492
  hasBody := declarationHasImplementation(declaration, kind)
458
493
  if _, exists := g.Nodes[id]; exists {
459
494
  if !hasBody || g.bodyNodes[id] {
@@ -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
+ }