ttsc 0.18.3 → 0.19.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 (173) hide show
  1. package/README.md +5 -6
  2. package/cmd/graphbench/main.go +1 -1
  3. package/cmd/platform/main.go +1 -48
  4. package/cmd/ttsc/build_branches_test.go +5 -1
  5. package/cmd/ttsc/main.go +0 -50
  6. package/cmd/ttsc/main_alias_branches_test.go +6 -12
  7. package/cmd/ttscgraph/dump.go +13 -3
  8. package/cmd/ttscgraph/main.go +18 -12
  9. package/cmd/ttscgraph/run_help_version_and_bad_flags_exit_codes_test.go +23 -5
  10. package/cmd/ttscgraph/serve.go +863 -0
  11. package/cmd/ttscgraph/serve_protocol_rejects_invalid_request_line_test.go +43 -0
  12. package/cmd/ttscgraph/serve_protocol_retries_session_creation_test.go +83 -0
  13. package/cmd/ttscgraph/serve_protocol_reuses_session_test.go +35 -0
  14. package/cmd/ttscgraph/serve_session_applies_source_edit_incrementally_test.go +55 -0
  15. package/cmd/ttscgraph/serve_session_fails_closed_on_deleted_config_test.go +68 -0
  16. package/cmd/ttscgraph/serve_session_fails_closed_on_invalid_config_test.go +50 -0
  17. package/cmd/ttscgraph/serve_session_keeps_missing_literal_root_unchanged_test.go +46 -0
  18. package/cmd/ttscgraph/serve_session_rebuilds_import_graph_change_test.go +60 -0
  19. package/cmd/ttscgraph/serve_session_reloads_created_literal_root_test.go +44 -0
  20. package/cmd/ttscgraph/serve_session_reloads_extended_config_change_test.go +45 -0
  21. package/cmd/ttscgraph/serve_session_reloads_newly_resolved_conditional_export_test.go +60 -0
  22. package/cmd/ttscgraph/serve_session_reloads_newly_resolved_config_types_test.go +44 -0
  23. package/cmd/ttscgraph/serve_session_reloads_newly_resolved_excluded_import_test.go +62 -0
  24. package/cmd/ttscgraph/serve_session_reloads_newly_resolved_package_export_test.go +46 -0
  25. package/cmd/ttscgraph/serve_session_reloads_newly_resolved_package_import_test.go +46 -0
  26. package/cmd/ttscgraph/serve_session_reloads_newly_resolved_path_alias_test.go +47 -0
  27. package/cmd/ttscgraph/serve_session_reloads_newly_resolved_reference_path_test.go +42 -0
  28. package/cmd/ttscgraph/serve_session_reloads_newly_resolved_root_dirs_target_test.go +46 -0
  29. package/cmd/ttscgraph/serve_session_reloads_newly_resolved_type_reference_test.go +42 -0
  30. package/cmd/ttscgraph/serve_session_reloads_root_file_set_test.go +52 -0
  31. package/cmd/ttscgraph/serve_session_reloads_source_deleted_during_load_test.go +60 -0
  32. package/cmd/ttscgraph/serve_session_reloads_transitive_project_reference_test.go +55 -0
  33. package/cmd/ttscgraph/serve_session_reloads_when_program_plugin_linked_test.go +53 -0
  34. package/cmd/ttscgraph/serve_session_reuses_unchanged_preamble_source_test.go +45 -0
  35. package/cmd/ttscgraph/serve_session_reuses_unchanged_snapshot_test.go +52 -0
  36. package/cmd/ttscgraph/serve_session_revisits_source_edited_during_load_test.go +61 -0
  37. package/cmd/ttscgraph/serve_session_tolerates_os_invalid_resolution_candidate_test.go +56 -0
  38. package/cmd/ttscgraph/serve_session_tracks_all_module_specifier_forms_test.go +41 -0
  39. package/cmd/ttscgraph/serve_session_tracks_diamond_project_references_once_test.go +61 -0
  40. package/cmd/ttscserver/main.go +5 -3
  41. package/driver/lsp.go +13 -1
  42. package/driver/plugins.go +56 -5
  43. package/driver/program.go +25 -10
  44. package/driver/rewrite.go +381 -21
  45. package/driver/session.go +11 -3
  46. package/go.mod +1 -0
  47. package/internal/graph/build.go +297 -3
  48. package/internal/graph/build_records_a_node_per_top_level_declaration_test.go +16 -2
  49. package/internal/graph/diagnostics.go +5 -4
  50. package/internal/graph/dump.go +60 -4
  51. package/internal/graph/dump_is_a_function_of_the_source_test.go +71 -0
  52. package/internal/graph/dump_serializes_the_full_graph_test.go +16 -4
  53. package/internal/graph/edges.go +122 -11
  54. package/internal/graph/exports.go +74 -17
  55. package/internal/graph/graph.go +33 -1
  56. package/internal/graph/node_identity_is_position_invariant_test.go +4 -2
  57. package/internal/graph/resolve.go +24 -0
  58. package/internal/graphsymbols/provider.go +50 -14
  59. package/internal/lspserver/lsp_native_plugin_source.go +77 -23
  60. package/internal/lspserver/lsp_plugin.go +19 -2
  61. package/internal/lspserver/lsp_proxy.go +244 -52
  62. package/internal/lspserver/lsp_server.go +35 -27
  63. package/internal/lspserver/lsp_symbols.go +25 -16
  64. package/lib/TtscCompiler.d.ts +13 -2
  65. package/lib/TtscCompiler.js +30 -20
  66. package/lib/TtscCompiler.js.map +1 -1
  67. package/lib/TtscService.js +9 -7
  68. package/lib/TtscService.js.map +1 -1
  69. package/lib/compiler/internal/compileProjectInMemory.js +15 -1
  70. package/lib/compiler/internal/compileProjectInMemory.js.map +1 -1
  71. package/lib/compiler/internal/project/createNativeProjectContextArgs.d.ts +3 -0
  72. package/lib/compiler/internal/project/createNativeProjectContextArgs.js +14 -0
  73. package/lib/compiler/internal/project/createNativeProjectContextArgs.js.map +1 -0
  74. package/lib/compiler/internal/project/readProjectConfig.js +53 -4
  75. package/lib/compiler/internal/project/readProjectConfig.js.map +1 -1
  76. package/lib/compiler/internal/project/resolveProjectConfig.d.ts +9 -2
  77. package/lib/compiler/internal/project/resolveProjectConfig.js +45 -16
  78. package/lib/compiler/internal/project/resolveProjectConfig.js.map +1 -1
  79. package/lib/compiler/internal/residentTransformProcess.d.ts +16 -5
  80. package/lib/compiler/internal/residentTransformProcess.js +68 -20
  81. package/lib/compiler/internal/residentTransformProcess.js.map +1 -1
  82. package/lib/compiler/internal/runBuild.d.ts +18 -0
  83. package/lib/compiler/internal/runBuild.js +220 -24
  84. package/lib/compiler/internal/runBuild.js.map +1 -1
  85. package/lib/compiler/internal/runSingleFileEmit.js +7 -4
  86. package/lib/compiler/internal/runSingleFileEmit.js.map +1 -1
  87. package/lib/compiler/internal/sharedHostHelpers.d.ts +16 -0
  88. package/lib/compiler/internal/sharedHostHelpers.js +23 -0
  89. package/lib/compiler/internal/sharedHostHelpers.js.map +1 -1
  90. package/lib/compiler/internal/startResidentTransform.js +18 -1
  91. package/lib/compiler/internal/startResidentTransform.js.map +1 -1
  92. package/lib/compiler/internal/transformProjectInMemory.js +34 -8
  93. package/lib/compiler/internal/transformProjectInMemory.js.map +1 -1
  94. package/lib/flags/parser.d.ts +17 -0
  95. package/lib/flags/parser.js +11 -1
  96. package/lib/flags/parser.js.map +1 -1
  97. package/lib/flags/schema.js +7 -0
  98. package/lib/flags/schema.js.map +1 -1
  99. package/lib/launcher/internal/prepareExecution.js +21 -2
  100. package/lib/launcher/internal/prepareExecution.js.map +1 -1
  101. package/lib/launcher/internal/runTtsc.js +13 -53
  102. package/lib/launcher/internal/runTtsc.js.map +1 -1
  103. package/lib/launcher/internal/runTtscserver.js +11 -0
  104. package/lib/launcher/internal/runTtscserver.js.map +1 -1
  105. package/lib/launcher/internal/runTtsx.js +27 -11
  106. package/lib/launcher/internal/runTtsx.js.map +1 -1
  107. package/lib/launcher/internal/runtimeHooks.d.ts +81 -0
  108. package/lib/launcher/internal/runtimeHooks.js +632 -43
  109. package/lib/launcher/internal/runtimeHooks.js.map +1 -1
  110. package/lib/launcher/internal/servedSourceMap.d.ts +25 -0
  111. package/lib/launcher/internal/servedSourceMap.js +164 -0
  112. package/lib/launcher/internal/servedSourceMap.js.map +1 -0
  113. package/lib/plugin/internal/buildSourcePlugin.d.ts +146 -2
  114. package/lib/plugin/internal/buildSourcePlugin.js +651 -113
  115. package/lib/plugin/internal/buildSourcePlugin.js.map +1 -1
  116. package/lib/plugin/internal/loadProjectPlugins.d.ts +7 -0
  117. package/lib/plugin/internal/loadProjectPlugins.js +33 -10
  118. package/lib/plugin/internal/loadProjectPlugins.js.map +1 -1
  119. package/lib/structures/ITtscCompilerContext.d.ts +16 -2
  120. package/lib/structures/ITtscCompilerResult.d.ts +8 -5
  121. package/lib/structures/ITtscPluginCapabilities.d.ts +8 -0
  122. package/lib/structures/ITtscPluginFactoryContext.d.ts +14 -0
  123. package/lib/structures/internal/ITtscParsedProjectConfig.d.ts +3 -0
  124. package/lib/structures/internal/ITtscProjectIdentity.d.ts +17 -0
  125. package/lib/structures/internal/ITtscProjectIdentity.js +3 -0
  126. package/lib/structures/internal/ITtscProjectIdentity.js.map +1 -0
  127. package/lib/structures/internal/TtscCommonOptions.d.ts +8 -0
  128. package/package.json +11 -8
  129. package/shim/ast/lint.go +10 -0
  130. package/shim/ast/shim.go +44 -0
  131. package/shim/checker/enums_gen.go +19 -0
  132. package/shim/checker/shim.go +130 -0
  133. package/shim/diagnosticwriter/lint.go +30 -3
  134. package/shim/printer/shim.go +22 -4
  135. package/shim/scanner/regexp.go +27 -0
  136. package/src/TtscCompiler.ts +29 -22
  137. package/src/TtscService.ts +15 -7
  138. package/src/compiler/internal/compileProjectInMemory.ts +18 -1
  139. package/src/compiler/internal/project/createNativeProjectContextArgs.ts +15 -0
  140. package/src/compiler/internal/project/readProjectConfig.ts +61 -5
  141. package/src/compiler/internal/project/resolveProjectConfig.ts +51 -21
  142. package/src/compiler/internal/residentTransformProcess.ts +95 -21
  143. package/src/compiler/internal/runBuild.ts +313 -32
  144. package/src/compiler/internal/runSingleFileEmit.ts +7 -4
  145. package/src/compiler/internal/sharedHostHelpers.ts +24 -0
  146. package/src/compiler/internal/startResidentTransform.ts +21 -1
  147. package/src/compiler/internal/transformProjectInMemory.ts +49 -6
  148. package/src/flags/parser.ts +28 -1
  149. package/src/flags/schema.ts +8 -0
  150. package/src/launcher/internal/prepareExecution.ts +22 -2
  151. package/src/launcher/internal/runTtsc.ts +13 -61
  152. package/src/launcher/internal/runTtscserver.ts +14 -0
  153. package/src/launcher/internal/runTtsx.ts +28 -13
  154. package/src/launcher/internal/runtimeHooks.ts +771 -53
  155. package/src/launcher/internal/servedSourceMap.ts +183 -0
  156. package/src/plugin/internal/buildSourcePlugin.ts +859 -122
  157. package/src/plugin/internal/loadProjectPlugins.ts +36 -6
  158. package/src/structures/ITtscCompilerContext.ts +17 -2
  159. package/src/structures/ITtscCompilerResult.ts +8 -5
  160. package/src/structures/ITtscPluginCapabilities.ts +9 -0
  161. package/src/structures/ITtscPluginFactoryContext.ts +15 -0
  162. package/src/structures/internal/ITtscParsedProjectConfig.ts +3 -0
  163. package/src/structures/internal/ITtscProjectIdentity.ts +17 -0
  164. package/src/structures/internal/TtscCommonOptions.ts +8 -0
  165. package/tools/shim_audit/baseline.json +16 -7
  166. package/tools/shim_audit/main.go +777 -71
  167. package/tools/shim_audit/main_test.go +28 -1
  168. package/tools/shim_audit/producer_exemptions_require_rationale_test.go +42 -0
  169. package/tools/shim_audit/producer_surface_includes_exposed_methods_test.go +154 -0
  170. package/tools/shim_audit/producer_surface_rejects_unproducible_reference_test.go +142 -0
  171. package/utility/host.go +46 -20
  172. package/utility/serve.go +1 -1
  173. package/shim/checker/signature_api_completeness.go +0 -58
@@ -17,9 +17,11 @@ func Build(prog *driver.Program) *Graph {
17
17
  g := &Graph{
18
18
  Nodes: map[string]*Node{},
19
19
  bodyNodes: map[string]bool{},
20
- seen: map[string]struct{}{},
20
+ seen: map[edgeKey]struct{}{},
21
+ resolved: map[*shimast.Node]*Target{},
21
22
  }
22
23
  for _, file := range prog.SourceFiles() {
24
+ g.putModuleNode(file)
23
25
  collectDeclarations(g, file)
24
26
  }
25
27
  g.addEdges(prog)
@@ -59,6 +61,24 @@ func collectStatements(g *Graph, path string, statements []*shimast.Node) {
59
61
  switch statement.Kind {
60
62
  case shimast.KindFunctionDeclaration:
61
63
  addNode(g, path, statement, NodeFunction)
64
+ // A function declared inside a function is still a name the runtime calls.
65
+ // This was off for a while on the theory that a closure is implementation,
66
+ // and implementation is read from the file — but Vue's renderer chain lives
67
+ // inside `baseCreateRenderer`, so `patch`, `mountElement` and
68
+ // `setupRenderEffect` were not implementation detail the index could skip;
69
+ // they were the flow the index exists to describe, and the graph had a blank
70
+ // where they belong. Asked how a state change reaches the DOM, a model spent
71
+ // three calls hunting for a name the graph did not hold.
72
+ //
73
+ // The cost is small and the answer is not bigger: the graph grows 3% in
74
+ // nodes on Vue and 5% on VS Code, and the tour payload does not change by a
75
+ // byte, because a closure ranks below the surface it hangs under and never
76
+ // takes a seed. It answers when asked for by name. Measured on the
77
+ // specific-flow lane: 59% of baseline tokens saved to 82%, and the calls
78
+ // halve — TypeORM 5 to 1, VS Code 8 to 2, Vue 6 to 2.
79
+ //
80
+ // The bodies stay out. A closure is a node with edges, not source text.
81
+ collectClosures(g, path, statement)
62
82
  case shimast.KindClassDeclaration:
63
83
  addNode(g, path, statement, NodeClass)
64
84
  collectMembers(g, path, statement)
@@ -81,7 +101,8 @@ func collectStatements(g *Graph, path string, statements []*shimast.Node) {
81
101
  }
82
102
 
83
103
  // collectVariables records a variable node for each binding in a top-level
84
- // variable statement (both bindings of `const a = 1, b = 2`).
104
+ // variable statement (both bindings of `const a = 1, b = 2`), then the functions
105
+ // declared inside a binding that holds one.
85
106
  func collectVariables(g *Graph, path string, statement *shimast.Node) {
86
107
  variables := statement.AsVariableStatement()
87
108
  if variables == nil || variables.DeclarationList == nil {
@@ -93,9 +114,248 @@ func collectVariables(g *Graph, path string, statement *shimast.Node) {
93
114
  }
94
115
  for _, binding := range list.Declarations.Nodes {
95
116
  addNode(g, path, binding, NodeVariable)
117
+ collectClosures(g, path, binding)
96
118
  }
97
119
  }
98
120
 
121
+ // collectClosures records a node for each function a declaration's body declares,
122
+ // and for the functions those declare in turn.
123
+ //
124
+ // A factory that closes over its state and returns local functions is how much of
125
+ // the ecosystem writes its engine: Vue's `patch`, `mountElement`, and
126
+ // `setupRenderEffect` are locals of `baseCreateRenderer`; a curried validator's
127
+ // real parse is a local of the function that binds its error class. Recording only
128
+ // what a file declares at its top level left that code out of the graph entirely —
129
+ // a model asking how a state change reaches the DOM found the factory, nothing
130
+ // under it, and went to read the files.
131
+ //
132
+ // Only functions are recorded. A local `const i = 0` is a value, not a place code
133
+ // runs, and the graph would drown in them.
134
+ func collectClosures(g *Graph, path string, declaration *shimast.Node) {
135
+ for _, closure := range ClosuresIn(declaration) {
136
+ name, ok := ClosureName(closure)
137
+ if !ok {
138
+ continue
139
+ }
140
+ kind := NodeFunction
141
+ if closure.Kind == shimast.KindVariableDeclaration {
142
+ kind = NodeVariable
143
+ }
144
+ putDeclaredNode(g, path, name, kind, closure)
145
+ if node, ok := g.Nodes[nodeID(path, name, kind)]; ok {
146
+ node.Closure = true
147
+ }
148
+ collectClosures(g, path, closure)
149
+ }
150
+ }
151
+
152
+ // ClosureName returns the name a closure is recorded under — its own name behind
153
+ // the names of the functions it is nested in, so `baseCreateRenderer.patch` and
154
+ // another file-mate's `patch` are two nodes rather than one merged phantom.
155
+ //
156
+ // It reports false when any enclosing function is anonymous. An `inner` declared
157
+ // inside two different callbacks of one file would otherwise key the same id and
158
+ // merge into a node that is neither, fabricating edges between unrelated scopes.
159
+ // Such a closure stays out of the graph, exactly as every body-scoped declaration
160
+ // did before.
161
+ func ClosureName(closure *shimast.Node) (string, bool) {
162
+ symbol := closure.Symbol()
163
+ if symbol == nil {
164
+ return "", false
165
+ }
166
+ name := qualifiedName(symbol)
167
+ if name == "" {
168
+ return "", false
169
+ }
170
+ for parent := closure.Parent; parent != nil; parent = parent.Parent {
171
+ if parent.Kind == shimast.KindSourceFile {
172
+ break
173
+ }
174
+ if !isFunctionLike(parent) {
175
+ continue
176
+ }
177
+ owner, ok := ownerName(parent)
178
+ if !ok {
179
+ return "", false
180
+ }
181
+ name = owner + "." + name
182
+ }
183
+ return name, true
184
+ }
185
+
186
+ // isFunctionLike reports whether a node is a function whose body is a scope. A
187
+ // variable that binds one is not: the function it holds is a node of this chain
188
+ // already, and counting both would name a closure after its owner twice.
189
+ func isFunctionLike(node *shimast.Node) bool {
190
+ switch node.Kind {
191
+ case shimast.KindFunctionDeclaration,
192
+ shimast.KindFunctionExpression,
193
+ shimast.KindArrowFunction,
194
+ shimast.KindMethodDeclaration,
195
+ shimast.KindConstructor,
196
+ shimast.KindGetAccessor,
197
+ shimast.KindSetAccessor:
198
+ return true
199
+ }
200
+ return false
201
+ }
202
+
203
+ // ownerName returns the name of a function-like declaration a closure sits in: a
204
+ // function or method by its own (class-qualified) name, a function expression or
205
+ // arrow by the variable that binds it. It reports false for an anonymous one.
206
+ func ownerName(declaration *shimast.Node) (string, bool) {
207
+ switch declaration.Kind {
208
+ case shimast.KindFunctionExpression, shimast.KindArrowFunction:
209
+ binding := bindingOf(declaration)
210
+ if binding == nil {
211
+ return "", false
212
+ }
213
+ declaration = binding
214
+ }
215
+ symbol := declaration.Symbol()
216
+ if symbol == nil {
217
+ return "", false
218
+ }
219
+ name := qualifiedName(symbol)
220
+ if name == "" {
221
+ return "", false
222
+ }
223
+ return name, true
224
+ }
225
+
226
+ // bindingOf returns the variable declaration a function expression is bound to,
227
+ // seeing through the wrappers a codebase writes around one, or nil when the
228
+ // function is anonymous.
229
+ func bindingOf(fn *shimast.Node) *shimast.Node {
230
+ for parent := fn.Parent; parent != nil; parent = parent.Parent {
231
+ switch parent.Kind {
232
+ case shimast.KindVariableDeclaration:
233
+ return parent
234
+ case shimast.KindAsExpression,
235
+ shimast.KindSatisfiesExpression,
236
+ shimast.KindParenthesizedExpression:
237
+ continue
238
+ default:
239
+ return nil
240
+ }
241
+ }
242
+ return nil
243
+ }
244
+
245
+ // ClosuresIn returns the functions a declaration's body declares — a nested
246
+ // function declaration, or a binding that holds a function — found however deep
247
+ // in the body's statements they sit (inside an `if`, a `try`, a loop), but not
248
+ // past one: a closure's own closures belong to it, and the caller recurses.
249
+ //
250
+ // A binding that holds no function is not one. A local `const i = 0` is a value,
251
+ // not a place code runs.
252
+ func ClosuresIn(declaration *shimast.Node) []*shimast.Node {
253
+ body := functionBody(declaration)
254
+ if body == nil {
255
+ return nil
256
+ }
257
+ var closures []*shimast.Node
258
+ var walk func(node *shimast.Node)
259
+ walk = func(node *shimast.Node) {
260
+ node.ForEachChild(func(child *shimast.Node) bool {
261
+ if IsClosure(child) {
262
+ closures = append(closures, child)
263
+ return false
264
+ }
265
+ walk(child)
266
+ return false
267
+ })
268
+ }
269
+ walk(body)
270
+ return closures
271
+ }
272
+
273
+ // IsClosure reports whether a node inside a function body is a function the graph
274
+ // records: a nested function declaration, or a binding that holds a function.
275
+ func IsClosure(node *shimast.Node) bool {
276
+ switch node.Kind {
277
+ case shimast.KindFunctionDeclaration:
278
+ return true
279
+ case shimast.KindVariableDeclaration:
280
+ return functionBody(node) != nil
281
+ }
282
+ return false
283
+ }
284
+
285
+ // functionBody returns the block body of any function-like declaration — a
286
+ // function, a method, an accessor, a constructor, or a function/arrow expression
287
+ // a variable binds.
288
+ func functionBody(declaration *shimast.Node) *shimast.Node {
289
+ switch declaration.Kind {
290
+ case shimast.KindFunctionDeclaration:
291
+ if fn := declaration.AsFunctionDeclaration(); fn != nil {
292
+ return fn.Body
293
+ }
294
+ case shimast.KindFunctionExpression:
295
+ if fn := declaration.AsFunctionExpression(); fn != nil {
296
+ return fn.Body
297
+ }
298
+ case shimast.KindArrowFunction:
299
+ if fn := declaration.AsArrowFunction(); fn != nil {
300
+ return fn.Body
301
+ }
302
+ case shimast.KindMethodDeclaration:
303
+ if fn := declaration.AsMethodDeclaration(); fn != nil {
304
+ return fn.Body
305
+ }
306
+ case shimast.KindConstructor:
307
+ if fn := declaration.AsConstructorDeclaration(); fn != nil {
308
+ return fn.Body
309
+ }
310
+ case shimast.KindGetAccessor:
311
+ if fn := declaration.AsGetAccessorDeclaration(); fn != nil {
312
+ return fn.Body
313
+ }
314
+ case shimast.KindSetAccessor:
315
+ if fn := declaration.AsSetAccessorDeclaration(); fn != nil {
316
+ return fn.Body
317
+ }
318
+ case shimast.KindVariableDeclaration:
319
+ if binding := declaration.AsVariableDeclaration(); binding != nil {
320
+ return functionBodyOfInitializer(binding.Initializer)
321
+ }
322
+ }
323
+ return nil
324
+ }
325
+
326
+ // functionBodyOfInitializer unwraps a binding's initializer to the body of the
327
+ // function it holds, seeing through the `as const` / satisfies wrappers a
328
+ // codebase writes around one.
329
+ func functionBodyOfInitializer(initializer *shimast.Node) *shimast.Node {
330
+ for initializer != nil {
331
+ switch initializer.Kind {
332
+ case shimast.KindFunctionExpression, shimast.KindArrowFunction:
333
+ return functionBody(initializer)
334
+ case shimast.KindAsExpression:
335
+ as := initializer.AsAsExpression()
336
+ if as == nil {
337
+ return nil
338
+ }
339
+ initializer = as.Expression
340
+ case shimast.KindSatisfiesExpression:
341
+ satisfies := initializer.AsSatisfiesExpression()
342
+ if satisfies == nil {
343
+ return nil
344
+ }
345
+ initializer = satisfies.Expression
346
+ case shimast.KindParenthesizedExpression:
347
+ parenthesized := initializer.AsParenthesizedExpression()
348
+ if parenthesized == nil {
349
+ return nil
350
+ }
351
+ initializer = parenthesized.Expression
352
+ default:
353
+ return nil
354
+ }
355
+ }
356
+ return nil
357
+ }
358
+
99
359
  // addNode records a node for the symbol declared by node under its
100
360
  // position-invariant id. A declaration the checker did not bind to a single
101
361
  // named symbol (a destructuring pattern) is skipped. Redeclarations keep the
@@ -125,6 +385,7 @@ func collectMembers(g *Graph, path string, statement *shimast.Node) {
125
385
  case isPropertyMember(member.Kind):
126
386
  putDeclaredNode(g, path, name, NodeVariable, member)
127
387
  }
388
+ collectClosures(g, path, member)
128
389
  }
129
390
  }
130
391
 
@@ -253,7 +514,40 @@ func simpleName(symbol *shimast.Symbol) string {
253
514
  if symbol == nil || symbol.Name == "" {
254
515
  return ""
255
516
  }
256
- return strings.ReplaceAll(symbol.Name, "\xFE", "__")
517
+ return stripPrivateMangling(strings.ReplaceAll(symbol.Name, "\xFE", "__"))
518
+ }
519
+
520
+ // stripPrivateMangling removes the checker's per-run counter from the name of a
521
+ // private class member.
522
+ //
523
+ // A `#field` is bound under a mangled name — `__#41@#field` — whose number comes
524
+ // from a counter that advances as the program is bound, so the same field is
525
+ // `__#41@#field` in one run and `__#38@#field` in the next. That counter reached
526
+ // the node id, and with it the wire: on VS Code, 661 nodes and 661 edges changed
527
+ // identity between two dumps of the *same unedited source*. A handle the model
528
+ // was given could name nothing after a restart, and no dump could be compared to
529
+ // another to prove a change had left the facts alone.
530
+ //
531
+ // The number identifies nothing a reader can use — the class already
532
+ // distinguishes the field, and `#field` is what the source calls it. Dropping it
533
+ // makes the id a function of the code again.
534
+ func stripPrivateMangling(name string) string {
535
+ const prefix = "__#"
536
+ start := strings.Index(name, prefix)
537
+ if start == -1 {
538
+ return name
539
+ }
540
+ rest := name[start+len(prefix):]
541
+ at := strings.IndexByte(rest, '@')
542
+ if at <= 0 {
543
+ return name
544
+ }
545
+ for _, char := range rest[:at] {
546
+ if char < '0' || char > '9' {
547
+ return name
548
+ }
549
+ }
550
+ return name[:start] + rest[at+1:]
257
551
  }
258
552
 
259
553
  // qualifiedName is the identity name of a symbol: its own name, prefixed by the
@@ -68,8 +68,10 @@ export const value = 1;
68
68
  "En": NodeEnum,
69
69
  "value": NodeVariable,
70
70
  }
71
- if len(graph.Nodes) != len(want) {
72
- t.Fatalf("expected %d nodes, got %d: %v", len(want), len(graph.Nodes), nodeIDSet(graph))
71
+ // The module node is the file's export surface, not a declaration in it, so
72
+ // the declaration count excludes it.
73
+ if declared := declaredNodeCount(graph); declared != len(want) {
74
+ t.Fatalf("expected %d nodes, got %d: %v", len(want), declared, nodeIDSet(graph))
73
75
  }
74
76
  for name, kind := range want {
75
77
  id := nodeID(path, name, kind)
@@ -86,6 +88,18 @@ export const value = 1;
86
88
  }
87
89
  }
88
90
 
91
+ // declaredNodeCount counts the nodes that stand for a declaration, leaving out
92
+ // the module node a file's export table adds.
93
+ func declaredNodeCount(graph *Graph) int {
94
+ count := 0
95
+ for _, node := range graph.Nodes {
96
+ if node.Kind != NodeModule {
97
+ count++
98
+ }
99
+ }
100
+ return count
101
+ }
102
+
89
103
  // nodeIDSet returns the graph's node ids as a slice for failure messages.
90
104
  func nodeIDSet(graph *Graph) []string {
91
105
  ids := make([]string, 0, len(graph.Nodes))
@@ -8,10 +8,11 @@ import "github.com/samchon/ttsc/packages/ttsc/driver"
8
8
  // over the warm checker, not a second compile.
9
9
  //
10
10
  // This is the compiler-only slice: it carries the TypeScript semantic
11
- // diagnostics, not @ttsc/lint or transform-plugin findings. The MCP server fuses
12
- // those onto the graph separately, through diagnostic providers the launcher
13
- // feeds (see mcp.DiagnosticProvider). This helper and its tests pin the
14
- // compiler-diagnostic conversion (code, column, message) on its own.
11
+ // diagnostics, not @ttsc/lint or transform-plugin findings. Nothing in the
12
+ // graph pipeline calls it yet; the dump and MCP results carry no diagnostics.
13
+ // This helper and its tests pin the compiler-diagnostic conversion (code,
14
+ // column, message) so a future diagnostics integration starts from a proven
15
+ // producer.
15
16
  func FileDiagnostics(prog *driver.Program, path string) []driver.Diagnostic {
16
17
  out := make([]driver.Diagnostic, 0)
17
18
  for _, diagnostic := range prog.Diagnostics() {
@@ -1,7 +1,9 @@
1
1
  package graph
2
2
 
3
3
  import (
4
+ "bufio"
4
5
  "encoding/json"
6
+ "io"
5
7
  "path/filepath"
6
8
  "sort"
7
9
  "strings"
@@ -27,8 +29,15 @@ import (
27
29
 
28
30
  // DumpEvidence is a 1-based source span grounding a node declaration or an edge
29
31
  // expression. It is display/expansion only, never identity.
32
+ //
33
+ // File is omitted when the reader reconstructs it exactly: a node's span is in
34
+ // the node's file, and an edge's span is in the file its `from` id names. The
35
+ // path is long, it rode the wire once per node and once per edge, and on VS Code
36
+ // those two copies are 55 MB of a 323 MB document that then has to be encoded,
37
+ // piped, parsed and validated. An `implementation` span keeps its file — that one
38
+ // can genuinely live in another file from the declaration that owns it.
30
39
  type DumpEvidence struct {
31
- File string `json:"file"`
40
+ File string `json:"file,omitempty"`
32
41
  StartLine int `json:"startLine"`
33
42
  StartCol int `json:"startCol,omitempty"`
34
43
  EndLine int `json:"endLine,omitempty"`
@@ -59,6 +68,7 @@ type DumpNode struct {
59
68
  External bool `json:"external"`
60
69
  Ignored bool `json:"ignored,omitempty"`
61
70
  Exported bool `json:"exported,omitempty"`
71
+ Closure bool `json:"closure,omitempty"`
62
72
  Modifiers []string `json:"modifiers,omitempty"`
63
73
  Evidence *DumpEvidence `json:"evidence,omitempty"`
64
74
  Implementation *DumpEvidence `json:"implementation,omitempty"`
@@ -108,6 +118,11 @@ func NewDump(g *Graph, project, tsconfig string, ignored map[string]bool, source
108
118
  nodes := make([]DumpNode, 0, len(g.Nodes))
109
119
  for _, n := range g.Nodes {
110
120
  name, qualified := nodeNames(n)
121
+ // A module is named by its file, so its name relativizes like a path; every
122
+ // other node is named by a symbol.
123
+ if n.Kind == NodeModule {
124
+ name, qualified = ctx.rel(name), ""
125
+ }
111
126
  nodes = append(nodes, DumpNode{
112
127
  ID: ctx.relID(n.ID),
113
128
  Kind: string(n.Kind),
@@ -117,8 +132,9 @@ func NewDump(g *Graph, project, tsconfig string, ignored map[string]bool, source
117
132
  External: n.External,
118
133
  Ignored: ignored[n.File],
119
134
  Exported: n.Exported,
135
+ Closure: n.Closure,
120
136
  Modifiers: n.Modifiers,
121
- Evidence: ctx.evidence(n.File, n.Pos, n.End),
137
+ Evidence: withoutFile(ctx.evidence(n.File, n.Pos, n.End)),
122
138
  Implementation: ctx.evidence(n.ImplementationFile, n.ImplementationPos, n.ImplementationEnd),
123
139
  Decorators: decByNode[n.ID],
124
140
  })
@@ -131,7 +147,7 @@ func NewDump(g *Graph, project, tsconfig string, ignored map[string]bool, source
131
147
  From: ctx.relID(e.From),
132
148
  To: ctx.relID(e.To),
133
149
  Kind: dumpEdgeKind(e),
134
- Evidence: ctx.edgeEvidence(e),
150
+ Evidence: withoutFile(ctx.edgeEvidence(e)),
135
151
  })
136
152
  }
137
153
  sort.Slice(edges, func(i, j int) bool {
@@ -162,6 +178,26 @@ func MarshalDump(g *Graph, project, tsconfig string, ignored map[string]bool, so
162
178
  return json.Marshal(d)
163
179
  }
164
180
 
181
+ // EncodeDump writes the export JSON straight to w, one buffered pass, ending it
182
+ // with the newline the one-shot protocol expects.
183
+ //
184
+ // The alternative is what the dump command used to do: marshal the whole
185
+ // document into a byte slice, convert that slice into a string, and print the
186
+ // string. On VS Code the document is 323 MB, so the conversion was a second full
187
+ // copy of it held live beside the first — half a gigabyte of peak heap that
188
+ // bought nothing, because the bytes were already exactly what stdout wanted.
189
+ func EncodeDump(w io.Writer, g *Graph, project, tsconfig string, ignored map[string]bool, sources map[string]string, pretty bool) error {
190
+ buffered := bufio.NewWriterSize(w, 1<<20)
191
+ encoder := json.NewEncoder(buffered)
192
+ if pretty {
193
+ encoder.SetIndent("", " ")
194
+ }
195
+ if err := encoder.Encode(NewDump(g, project, tsconfig, ignored, sources)); err != nil {
196
+ return err
197
+ }
198
+ return buffered.Flush()
199
+ }
200
+
165
201
  // dumpEdgeKind maps an internal edge kind, refined by Edge.Origin, onto the
166
202
  // schema's finer relationship kind.
167
203
  func dumpEdgeKind(e *Edge) string {
@@ -256,12 +292,22 @@ func (c *dumpContext) rel(file string) string {
256
292
 
257
293
  // relID relativizes the path portion of a node id ("path#qualifiedName:kind").
258
294
  // An id with no path (a future virtual node) is returned unchanged.
295
+ //
296
+ // A module is named by its own file path, so both halves of its id are paths and
297
+ // both relativize. Every other node is named by a symbol, which passes through
298
+ // untouched.
259
299
  func (c *dumpContext) relID(id string) string {
260
300
  hash := strings.Index(id, "#")
261
301
  if hash < 0 {
262
302
  return id
263
303
  }
264
- return c.rel(id[:hash]) + id[hash:]
304
+ head, tail := c.rel(id[:hash]), id[hash+1:]
305
+ suffix := ":" + string(NodeModule)
306
+ if strings.HasSuffix(tail, suffix) {
307
+ name := strings.TrimSuffix(tail, suffix)
308
+ return head + "#" + c.rel(name) + suffix
309
+ }
310
+ return head + "#" + tail
265
311
  }
266
312
 
267
313
  // evidence builds the line/col span for a byte range in file, or nil when the
@@ -337,6 +383,16 @@ func (c *dumpContext) edgeEvidence(e *Edge) *DumpEvidence {
337
383
  return c.evidence(file, e.Pos, e.End)
338
384
  }
339
385
 
386
+ // withoutFile drops a span file the reader reconstructs from the node or edge
387
+ // that carries the span. It never touches an implementation span.
388
+ func withoutFile(ev *DumpEvidence) *DumpEvidence {
389
+ if ev == nil {
390
+ return nil
391
+ }
392
+ ev.File = ""
393
+ return ev
394
+ }
395
+
340
396
  // nodeFile recovers the source file path embedded in a node id
341
397
  // ("path#qualifiedName:kind"); "" for an id without a path.
342
398
  func nodeFile(id string) string {
@@ -0,0 +1,71 @@
1
+ package graph
2
+
3
+ import (
4
+ "path/filepath"
5
+ "testing"
6
+
7
+ "github.com/samchon/ttsc/packages/ttsc/driver"
8
+ )
9
+
10
+ // TestDumpIsAFunctionOfTheSource pins the graph to the code it describes: two
11
+ // builds of one unedited snapshot must produce one byte-identical document.
12
+ //
13
+ // It did not. A private class member — `#count` — is bound under a mangled name
14
+ // carrying a counter that advances as the program is bound: `__#41@#count` in one
15
+ // run, `__#38@#count` in the next. That counter reached the node id, and with it
16
+ // the wire. On VS Code, 661 nodes and 661 edges changed identity between two
17
+ // dumps of the *same source*: a handle the model was given could name nothing
18
+ // after a restart, and no dump could be compared with another to prove that a
19
+ // change had left the facts alone — the byte test every graph optimisation is
20
+ // supposed to answer to was itself meaningless.
21
+ //
22
+ // Two classes each declare a `#count`, so the mangling counter is exercised more
23
+ // than once and a per-class collision would surface here rather than in a
24
+ // three-million-line repository.
25
+ func TestDumpIsAFunctionOfTheSource(t *testing.T) {
26
+ root := t.TempDir()
27
+ writeFile(t, filepath.Join(root, "tsconfig.json"), fixtureTSConfig)
28
+ writeFile(t, filepath.Join(root, "src", "main.ts"), `export class Counter {
29
+ #count = 0;
30
+ #step: number;
31
+ constructor(step: number) {
32
+ this.#step = step;
33
+ }
34
+ bump(): number {
35
+ this.#count += this.#step;
36
+ return this.#count;
37
+ }
38
+ }
39
+
40
+ export class Other {
41
+ #count = 0;
42
+ read(): number {
43
+ return this.#count;
44
+ }
45
+ }
46
+ `)
47
+
48
+ first := dumpBytes(t, root)
49
+ second := dumpBytes(t, root)
50
+ if string(first) != string(second) {
51
+ t.Fatalf("two dumps of one unedited snapshot differ: the graph is not a function of the source")
52
+ }
53
+ }
54
+
55
+ func dumpBytes(t *testing.T, root string) []byte {
56
+ t.Helper()
57
+ prog, diags, err := driver.LoadProgram(root, "tsconfig.json", driver.LoadProgramOptions{})
58
+ if err != nil {
59
+ t.Fatal(err)
60
+ }
61
+ if len(diags) != 0 {
62
+ t.Fatalf("unexpected diagnostics: %v", diags)
63
+ }
64
+ defer func() { _ = prog.Close() }()
65
+
66
+ data, err := MarshalDump(Build(prog), root, "tsconfig.json", nil, SourceTexts(prog), false)
67
+ if err != nil {
68
+ t.Fatalf("MarshalDump: %v", err)
69
+ }
70
+ return data
71
+ }
@@ -102,8 +102,18 @@ export function main(): void {
102
102
  if call == nil {
103
103
  t.Fatalf("no calls edge main -> helper in dump:\n%s", data)
104
104
  }
105
- if call.Evidence == nil || call.Evidence.StartLine == 0 || call.Evidence.File != "src/main.ts" {
106
- t.Fatalf("call edge missing project-relative line/col evidence: %+v", call.Evidence)
105
+ // The edge's span carries no file: it is the file its `from` id names, which
106
+ // the loader reconstructs (see ITtscGraphDump.IEdge). Sending the path a
107
+ // second time on every edge is 17% of the document for a value the reader
108
+ // already holds.
109
+ if call.Evidence == nil || call.Evidence.StartLine == 0 {
110
+ t.Fatalf("call edge missing line/col evidence: %+v", call.Evidence)
111
+ }
112
+ if call.Evidence.File != "" {
113
+ t.Fatalf("call edge repeats the file its `from` id already names: %+v", call.Evidence)
114
+ }
115
+ if nodeFile(call.From) != "src/main.ts" {
116
+ t.Fatalf("the edge's file is not recoverable from its from id: %q", call.From)
107
117
  }
108
118
 
109
119
  // Every edge endpoint resolves to a dumped node.
@@ -129,8 +139,10 @@ export function main(): void {
129
139
  }
130
140
  }
131
141
  // The dump carries no ceremony keys: it is wholly checker-resolved, so it has
132
- // no per-edge trust flags and no schema version to negotiate.
133
- for _, gone := range []string{`"schemaVersion"`, `"provenance":`, `"confidence":`, `"text":`} {
142
+ // no per-edge trust flags and no schema version to negotiate. It also carries
143
+ // no keys the schema stopped promising: no inlined evidence text and no
144
+ // diagnostics array.
145
+ for _, gone := range []string{`"schemaVersion"`, `"provenance":`, `"confidence":`, `"text":`, `"diagnostics":`} {
134
146
  if strings.Contains(s, gone) {
135
147
  t.Fatalf("dump still emits removed key %s:\n%s", gone, s)
136
148
  }