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
@@ -37,7 +37,12 @@ func (g *Graph) addEdgeAt(from, to string, kind EdgeKind, origin string, pos, en
37
37
  // target that surface as different relationships (a call and a `new`, an
38
38
  // `extends` and an `implements` of the same base) are both kept, while
39
39
  // repeated uses of the same form collapse to one edge.
40
- key := from + "\x00" + to + "\x00" + wireEdgeKind(kind, origin)
40
+ //
41
+ // The key is a struct, not a concatenation. Concatenating allocated a fresh
42
+ // string per *candidate* edge — including the duplicates thrown away on the
43
+ // next line, which on a large program are most of them — and a comparable
44
+ // struct of the three fields hashes just as well for nothing.
45
+ key := edgeKey{from: from, to: to, kind: wireEdgeKind(kind, origin)}
41
46
  if _, exists := g.seen[key]; exists {
42
47
  return
43
48
  }
@@ -101,7 +106,7 @@ func (g *Graph) heritageEdges(checker *shimchecker.Checker, path string, node *s
101
106
  if base == nil || base.Expression == nil {
102
107
  continue
103
108
  }
104
- target := Resolve(checker, base.Expression)
109
+ target := g.resolve(checker, base.Expression)
105
110
  if target == nil || target.Symbol == nil {
106
111
  continue
107
112
  }
@@ -147,6 +152,10 @@ func forEachContainerIn(path string, statements []*shimast.Node, fn func(string,
147
152
  if id := topLevelID(path, statement, NodeFunction); id != "" {
148
153
  fn(id, statement)
149
154
  }
155
+ // Closure nodes are not indexed (build.go): what a function body runs is
156
+ // implementation, and implementation is read from the file. The walker
157
+ // stays for the day the caller asks for them.
158
+ forEachClosureIn(path, statement, fn)
150
159
  case shimast.KindTypeAliasDeclaration:
151
160
  if id := topLevelID(path, statement, NodeTypeAlias); id != "" {
152
161
  fn(id, statement)
@@ -166,10 +175,24 @@ func forEachContainerIn(path string, statements []*shimast.Node, fn func(string,
166
175
  forEachVariable(path, statement, fn)
167
176
  case shimast.KindModuleDeclaration:
168
177
  forEachContainerIn(path, moduleStatements(statement), fn)
178
+ default:
179
+ // A statement that declares nothing still runs: a test suite's
180
+ // `describe(() => it(() => schema.parse(x)))`, an entry file's
181
+ // `bootstrap()`, a registration side effect. No declaration owns those
182
+ // calls, so attributing edges to owners alone dropped every one of them,
183
+ // and with them every test the graph could have pointed at. The module
184
+ // runs them, so the module owns them.
185
+ fn(moduleID(path), statement)
169
186
  }
170
187
  }
171
188
  }
172
189
 
190
+ // moduleID is the node id of the file's module — the owner of what its top-level
191
+ // statements do.
192
+ func moduleID(path string) string {
193
+ return nodeID(path, path, NodeModule)
194
+ }
195
+
173
196
  // topLevelID returns the node id for a named declaration, or "". The name is
174
197
  // namespace-qualified, so a namespaced declaration lands on the node the build
175
198
  // pass recorded.
@@ -189,6 +212,7 @@ func topLevelID(path string, statement *shimast.Node, kind NodeKind) string {
189
212
  func forEachMember(path string, statement *shimast.Node, kind NodeKind, fn func(string, *shimast.Node)) {
190
213
  containerID := topLevelID(path, statement, kind)
191
214
  for _, member := range classMembers(statement) {
215
+ forEachClosureIn(path, member, fn)
192
216
  if isMethodMember(member.Kind) {
193
217
  if name := methodName(member.Symbol()); name != "" {
194
218
  fn(nodeID(path, name, NodeMethod), member)
@@ -259,14 +283,44 @@ func forEachVariable(path string, statement *shimast.Node, fn func(string, *shim
259
283
  continue
260
284
  }
261
285
  fn(nodeID(path, qualifiedName(symbol), NodeVariable), binding)
286
+ forEachClosureIn(path, binding, fn)
287
+ }
288
+ }
289
+
290
+ // forEachClosureIn pairs each function a declaration's body declares with its
291
+ // own node, and recurses: the renderer's `patch` owns the calls it makes, not the
292
+ // factory it closes over. `callsWithin` stops at the same boundary, so no call is
293
+ // attributed twice.
294
+ func forEachClosureIn(path string, declaration *shimast.Node, fn func(string, *shimast.Node)) {
295
+ for _, closure := range ClosuresIn(declaration) {
296
+ if id := closureID(path, closure); id != "" {
297
+ fn(id, closure)
298
+ }
299
+ forEachClosureIn(path, closure, fn)
262
300
  }
263
301
  }
264
302
 
303
+ // closureID is the node id build.go recorded for a closure.
304
+ func closureID(path string, closure *shimast.Node) string {
305
+ name, ok := ClosureName(closure)
306
+ if !ok {
307
+ return ""
308
+ }
309
+ kind := NodeFunction
310
+ if closure.Kind == shimast.KindVariableDeclaration {
311
+ kind = NodeVariable
312
+ }
313
+ return nodeID(path, name, kind)
314
+ }
315
+
265
316
  // callsWithin walks node's subtree and records runtime value-use edges from
266
317
  // `from` to the resolved target: calls/new/tagged templates/JSX components as
267
318
  // value-call edges, and property or element access as value-access edges.
268
319
  func (g *Graph) callsWithin(checker *shimchecker.Checker, from string, node *shimast.Node) {
269
320
  node.ForEachChild(func(child *shimast.Node) bool {
321
+ // With closures unindexed, a body's calls belong to the declaration that
322
+ // owns the body — the walk does not stop at one. (When they are indexed, it
323
+ // must: a closure that is its own node owns the calls it makes.)
270
324
  switch child.Kind {
271
325
  case shimast.KindCallExpression:
272
326
  // A decorator's own factory call (`@Column()`, `@Entity()`) is metadata,
@@ -280,6 +334,7 @@ func (g *Graph) callsWithin(checker *shimchecker.Checker, from string, node *shi
280
334
  }
281
335
  if call := child.AsCallExpression(); call != nil && call.Expression != nil {
282
336
  g.callEdge(checker, from, call.Expression, "call")
337
+ g.handedOffValues(checker, from, call)
283
338
  }
284
339
  case shimast.KindNewExpression:
285
340
  if newExpr := child.AsNewExpression(); newExpr != nil && newExpr.Expression != nil {
@@ -334,7 +389,7 @@ func (g *Graph) assignedFunctionTarget(checker *shimchecker.Checker, from string
334
389
  !shimast.IsFunctionLike(binary.Right) {
335
390
  return ""
336
391
  }
337
- target := Resolve(checker, binary.Left)
392
+ target := g.resolve(checker, binary.Left)
338
393
  if target == nil || target.Symbol == nil {
339
394
  return ""
340
395
  }
@@ -387,6 +442,34 @@ func (g *Graph) callEdge(checker *shimchecker.Checker, from string, callee *shim
387
442
  g.valueUseEdge(checker, from, callee, EdgeValueCall, origin)
388
443
  }
389
444
 
445
+ // handedOffValues records the workspace functions a call is handed, as uses of
446
+ // them.
447
+ //
448
+ // `React.memo(ExcalidrawBase)`, `forwardRef(Impl)`, `app.use(handler)`,
449
+ // `pipe(map(project))` — the argument is not called here and is not a property
450
+ // read, so the collector saw neither a call nor an access and recorded nothing.
451
+ // The wrapper became a dead end: Excalidraw's public export, `export const
452
+ // Excalidraw = React.memo(ExcalidrawBase)`, reached zero files in the graph while
453
+ // the component behind it reached forty-four, so a tour of the public API opened
454
+ // on an app-level export dialog instead, and the model went looking for the real
455
+ // component by hand.
456
+ //
457
+ // A function handed to another function is a use of it, and it is the one the
458
+ // runtime will run. Bounded to argument position: this is the callback, the
459
+ // wrapped component, the registered handler — not every identifier in every
460
+ // expression.
461
+ func (g *Graph) handedOffValues(checker *shimchecker.Checker, from string, call *shimast.CallExpression) {
462
+ if call.Arguments == nil {
463
+ return
464
+ }
465
+ for _, argument := range call.Arguments.Nodes {
466
+ switch argument.Kind {
467
+ case shimast.KindIdentifier, shimast.KindPropertyAccessExpression:
468
+ g.valueUseEdge(checker, from, argument, EdgeValueAccess, "")
469
+ }
470
+ }
471
+ }
472
+
390
473
  // accessEdge resolves a property or element access to its declaration and
391
474
  // records a value-access edge, skipping unresolved/external/self targets.
392
475
  func (g *Graph) accessEdge(checker *shimchecker.Checker, from string, access *shimast.Node) {
@@ -397,7 +480,7 @@ func (g *Graph) valueUseEdge(checker *shimchecker.Checker, from string, targetEx
397
480
  if targetExpr == nil {
398
481
  return
399
482
  }
400
- target := Resolve(checker, targetExpr)
483
+ target := g.resolve(checker, targetExpr)
401
484
  if target == nil || target.Symbol == nil {
402
485
  return
403
486
  }
@@ -461,7 +544,7 @@ func (g *Graph) typeRefEdge(checker *shimchecker.Checker, from string, typeName
461
544
  if typeName == nil {
462
545
  return
463
546
  }
464
- target := Resolve(checker, typeName)
547
+ target := g.resolve(checker, typeName)
465
548
  if target == nil || target.Symbol == nil {
466
549
  return
467
550
  }
@@ -511,12 +594,17 @@ func (g *Graph) ensureTargetNode(target *Target) string {
511
594
  return id
512
595
  }
513
596
  if !target.External {
514
- // A workspace target Build did not record is a function-local or otherwise
515
- // body-scoped declaration (Build records top-level declarations, namespace
516
- // members, and class/interface members only). Its name is unqualified and
517
- // position-free, so two same-named locals in different scopes would key the
518
- // same id and merge into one node, fabricating false edges. Drop it — the
519
- // same workspace-only discipline the NodeMethod branch already applies.
597
+ // A closure Build recorded is keyed by its scoped name
598
+ // (`baseCreateRenderer.patch`), which the symbol alone does not spell, so
599
+ // resolve it from the declaration the checker pointed at.
600
+ if id := g.closureTargetID(target); id != "" {
601
+ return id
602
+ }
603
+ // Any other workspace target Build did not record is body-scoped and
604
+ // unnameable without collisions (a local of an anonymous callback), so its
605
+ // name would key the same id as a same-named local of another scope and
606
+ // merge two nodes into one that is neither. Drop it — the same
607
+ // workspace-only discipline the NodeMethod branch already applies.
520
608
  return ""
521
609
  }
522
610
  g.Nodes[id] = &Node{
@@ -532,6 +620,29 @@ func (g *Graph) ensureTargetNode(target *Target) string {
532
620
  return id
533
621
  }
534
622
 
623
+ // closureTargetID returns the node id of a closure the checker resolved to, or ""
624
+ // when the target is not one Build recorded.
625
+ func (g *Graph) closureTargetID(target *Target) string {
626
+ for _, declaration := range target.Symbol.Declarations {
627
+ if !IsClosure(declaration) {
628
+ continue
629
+ }
630
+ name, ok := ClosureName(declaration)
631
+ if !ok {
632
+ continue
633
+ }
634
+ kind := NodeFunction
635
+ if declaration.Kind == shimast.KindVariableDeclaration {
636
+ kind = NodeVariable
637
+ }
638
+ id := nodeID(target.File, name, kind)
639
+ if _, exists := g.Nodes[id]; exists {
640
+ return id
641
+ }
642
+ }
643
+ return ""
644
+ }
645
+
535
646
  // symbolNodeKind maps a resolved symbol's declarations/flags to a NodeKind, or
536
647
  // "" when the symbol is not a kind the graph records as a node. Declaration kind
537
648
  // wins over flags because property-like accessor symbols can otherwise be
@@ -5,18 +5,39 @@ import (
5
5
  shimchecker "github.com/microsoft/typescript-go/shim/checker"
6
6
  )
7
7
 
8
- // markExports flags every node reachable through a source file's export table as
9
- // Exported. It runs on the checker, so it sees the real public surface: an
10
- // inline `export class`, a separate `export { Foo }`, a re-export
8
+ // markExports records a module's export surface: a node for the module itself
9
+ // and an `exports` edge to every declaration its export table resolves to. It
10
+ // runs on the checker, so it sees the real public surface — an inline
11
+ // `export class`, a separate `export { Foo }`, a re-export
11
12
  // `export { Foo } from "./foo"`, and a barrel `export *` all land here, where a
12
13
  // purely syntactic `export`-modifier scan would miss the re-export forms that
13
- // carry most of a monorepo's public API.
14
+ // carry most of a monorepo's public API. Each resolved node is also flagged
15
+ // Exported.
16
+ //
17
+ // The edge is what the flag cannot say. A package's front door and its legacy
18
+ // subpath both re-export, so both surfaces end up Exported and a ranker that
19
+ // reads the flag alone cannot tell zod's `.` entry from its `./v3` one: it picks
20
+ // whichever name matches the query best, which is how a tour of the current API
21
+ // came back centered on the legacy implementation. The edge keeps the module the
22
+ // symbol came out of, so a caller holding a package.json — which names its entry
23
+ // file and nothing else — can ask the graph what that file, and only that file,
24
+ // puts on the wire.
25
+ //
26
+ // A barrel declares nothing of its own, so without a module node it is absent
27
+ // from the graph entirely: the file a package.json points at, the one thing that
28
+ // answers "what is this package's public API", is the one file the index never
29
+ // had. The module node is that anchor, and the export edges hang from it.
14
30
  func (g *Graph) markExports(checker *shimchecker.Checker, file *shimast.SourceFile) {
15
31
  moduleSymbol := checker.GetSymbolAtLocation(file.AsNode())
16
32
  if moduleSymbol == nil {
17
33
  return
18
34
  }
19
- for _, export := range shimchecker.Checker_getExportsOfModule(checker, moduleSymbol) {
35
+ exports := shimchecker.Checker_getExportsOfModule(checker, moduleSymbol)
36
+ if len(exports) == 0 {
37
+ return
38
+ }
39
+ module := g.putModuleNode(file)
40
+ for _, export := range exports {
20
41
  symbol := export
21
42
  // A re-export is an alias; unwrap it to the declaration it points at so the
22
43
  // exported flag lands on the real node, not a re-exporting index file's
@@ -26,35 +47,71 @@ func (g *Graph) markExports(checker *shimchecker.Checker, file *shimast.SourceFi
26
47
  symbol = aliased
27
48
  }
28
49
  }
29
- g.markExportedSymbol(symbol)
50
+ if id, ok := g.markExportedSymbol(symbol); ok {
51
+ g.addEdge(module.ID, id, EdgeExports)
52
+ }
30
53
  }
31
54
  }
32
55
 
33
- // markExportedSymbol sets Exported on the node a resolved export symbol declares,
34
- // when the graph recorded one. A symbol whose declaration the graph does not
35
- // model as a node (a re-exported value from a dependency, a kind it does not
36
- // track) is skipped rather than fabricated.
37
- func (g *Graph) markExportedSymbol(symbol *shimast.Symbol) {
56
+ // putModuleNode records the node for a source file. Its name is the file path,
57
+ // the only name a module has that a caller can hold.
58
+ //
59
+ // Every source file gets one, exporting or not, because a module is also what
60
+ // runs a file's top-level statements — and those statements are where a test
61
+ // file keeps everything it does. `describe(() => it(() => schema.parse(x)))` is
62
+ // a call the checker resolves and the graph used to drop on the floor: it sits
63
+ // inside a callback, no declaration owns it, and edges are attributed to owners.
64
+ // A whole test suite left no trace, which is why a tour that promised the tests
65
+ // to read next could name two of zod's and none of NestJS's, and why the model
66
+ // went off to glob for them.
67
+ func (g *Graph) putModuleNode(file *shimast.SourceFile) *Node {
68
+ path := file.FileName()
69
+ id := nodeID(path, path, NodeModule)
70
+ if existing, ok := g.Nodes[id]; ok {
71
+ return existing
72
+ }
73
+ node := &Node{
74
+ ID: id,
75
+ Name: path,
76
+ Simple: path,
77
+ Kind: NodeModule,
78
+ File: path,
79
+ // A module that exports is by definition part of some surface; whether it is
80
+ // the package's front door is the package.json's to say, not the graph's.
81
+ Exported: true,
82
+ }
83
+ g.Nodes[id] = node
84
+ return node
85
+ }
86
+
87
+ // markExportedSymbol sets Exported on the node a resolved export symbol declares
88
+ // and returns that node's id. A symbol whose declaration the graph does not model
89
+ // as a node (a re-exported value from a dependency, a kind it does not track) is
90
+ // skipped rather than fabricated.
91
+ func (g *Graph) markExportedSymbol(symbol *shimast.Symbol) (string, bool) {
38
92
  declaration := declarationNode(symbol)
39
93
  if declaration == nil {
40
- return
94
+ return "", false
41
95
  }
42
96
  file := shimast.GetSourceFileOfNode(declaration)
43
97
  if file == nil {
44
- return
98
+ return "", false
45
99
  }
46
100
  kind := symbolNodeKind(symbol)
47
101
  if kind == "" {
48
- return
102
+ return "", false
49
103
  }
50
104
  name := qualifiedName(symbol)
51
105
  if kind == NodeMethod {
52
106
  name = methodName(symbol)
53
107
  }
54
108
  if name == "" {
55
- return
109
+ return "", false
56
110
  }
57
- if node, ok := g.Nodes[nodeID(file.FileName(), name, kind)]; ok {
58
- node.Exported = true
111
+ node, ok := g.Nodes[nodeID(file.FileName(), name, kind)]
112
+ if !ok {
113
+ return "", false
59
114
  }
115
+ node.Exported = true
116
+ return node.ID, true
60
117
  }
@@ -1,5 +1,9 @@
1
1
  package graph
2
2
 
3
+ import (
4
+ shimast "github.com/microsoft/typescript-go/shim/ast"
5
+ )
6
+
3
7
  // NodeKind classifies a graph node by what its symbol declares.
4
8
  type NodeKind string
5
9
 
@@ -14,6 +18,10 @@ const (
14
18
  // accessor). Its id is class-qualified ("path#Class.method:method") so a
15
19
  // resolved method call lands on the same node the build pass recorded.
16
20
  NodeMethod NodeKind = "method"
21
+ // NodeModule is a source file with an export table — the surface a consumer
22
+ // imports from. A barrel declares nothing, so this is the only node it has,
23
+ // and it is the node a package.json entry path resolves to.
24
+ NodeModule NodeKind = "module"
17
25
  )
18
26
 
19
27
  // Node is one declared symbol. Its ID is position-invariant, built from the file
@@ -38,6 +46,12 @@ type Node struct {
38
46
  // a barrel (`export *`) counts, not only an inline `export` modifier. It is
39
47
  // the signal a public-API projection filters on.
40
48
  Exported bool
49
+ // Closure marks a node declared inside another declaration's body — Vue's
50
+ // `baseCreateRenderer.patch`, a callback bound to a const inside a method.
51
+ // It is a name the runtime calls, and a model that asks for it by name gets
52
+ // it; but an orientation tour ranks and walks the surface, so the surface is
53
+ // what it sees. The flag is how a projection tells the two apart.
54
+ Closure bool
41
55
  // Modifiers holds the declaration's syntactic modifiers as wire strings (a
42
56
  // subset of the TtscGraphNodeModifier union: export/default/declare/abstract/
43
57
  // static/readonly/async/const/public/private/protected). It is recorded from
@@ -74,6 +88,11 @@ const (
74
88
  // type it mentions (a parameter, return, property, or alias type). It is not a
75
89
  // runtime call, so an impact query can filter value edges from type edges.
76
90
  EdgeTypeRef EdgeKind = "type-ref"
91
+ // EdgeExports runs from a module to a declaration its export table resolves
92
+ // to, through re-exports and barrels. It records which surface a symbol is
93
+ // public on, which the Exported flag cannot: a package's front door and its
94
+ // legacy subpath both export, and only the edge says which one did.
95
+ EdgeExports EdgeKind = "exports"
77
96
  )
78
97
 
79
98
  // Edge is a directed, checker-resolved relationship from one node to another,
@@ -112,7 +131,20 @@ type Graph struct {
112
131
  bodyNodes map[string]bool
113
132
  // seen deduplicates edges in O(1) during construction, so building a graph
114
133
  // with N edges is O(N), not O(N²). Keyed by from\x00to\x00kind.
115
- seen map[string]struct{}
134
+ seen map[edgeKey]struct{}
135
+
136
+ // resolved memoizes the checker resolution of an AST node for the length of a
137
+ // build: the edge pass visits the same node several times, and a resolution
138
+ // cannot change while the program is fixed.
139
+ resolved map[*shimast.Node]*Target
140
+ }
141
+
142
+ // edgeKey identifies an edge by its two ends and the wire kind it will surface
143
+ // as: a comparable struct, so the dedup set costs no allocation per candidate.
144
+ type edgeKey struct {
145
+ from string
146
+ to string
147
+ kind string
116
148
  }
117
149
 
118
150
  // nodeID builds the position-invariant identity for a symbol named name,
@@ -41,8 +41,10 @@ export const value = 1;
41
41
  if firstText == secondText {
42
42
  t.Fatal("the reload returned identical source text; the probe is reading a stale cache, not the shifted file")
43
43
  }
44
- if len(firstIDs) != 2 {
45
- t.Fatalf("expected 2 nodes, got %d: %v", len(firstIDs), firstIDs)
44
+ // fn, value, and the module node for main.ts, whose id is the file path and so
45
+ // is position-invariant like the rest.
46
+ if len(firstIDs) != 3 {
47
+ t.Fatalf("expected 3 nodes, got %d: %v", len(firstIDs), firstIDs)
46
48
  }
47
49
  if !equalStringSets(firstIDs, secondIDs) {
48
50
  t.Fatalf("node identity changed when positions shifted:\n first=%v\n second=%v", firstIDs, secondIDs)
@@ -35,6 +35,30 @@ type Target struct {
35
35
  End int
36
36
  }
37
37
 
38
+ // resolve is Resolve with the graph's memo in front of it.
39
+ //
40
+ // The edge pass walks the same AST more than once by construction: `collectCalls`
41
+ // and `collectTypeRefs` each descend the whole container tree, and a closure's
42
+ // body is walked again for every container that encloses it. Every one of those
43
+ // visits asked the checker again, and for an identifier that is not a cheap
44
+ // lookup — typescript-go caches a resolved property access on the node, but a
45
+ // plain identifier goes back through `resolveEntityName` and a full scope walk
46
+ // each time.
47
+ //
48
+ // A node's resolution cannot change while the program is fixed, which it is for
49
+ // the length of a build, so the second answer is always the first one.
50
+ func (g *Graph) resolve(checker *shimchecker.Checker, ref *shimast.Node) *Target {
51
+ if ref == nil {
52
+ return nil
53
+ }
54
+ if cached, hit := g.resolved[ref]; hit {
55
+ return cached
56
+ }
57
+ target := Resolve(checker, ref)
58
+ g.resolved[ref] = target
59
+ return target
60
+ }
61
+
38
62
  // Resolve follows ref to the real declaration the checker binds it to. It
39
63
  // unwraps import/export alias chains so a reference through a barrel re-export
40
64
  // lands on the sibling source that declares the symbol, not the re-exporting
@@ -1,9 +1,13 @@
1
1
  // Package graphsymbols answers the two graph-oriented LSP methods
2
2
  // (textDocument/documentSymbol and textDocument/references) from ttsc's
3
3
  // compiler-backed code graph. It is the lspserver.SymbolProvider implementation
4
- // ttscserver wires in so a raw-LSP graph consumer such as @samchon/graph gets
5
- // compiler-complete declarations (graph nodes) and usages (graph edges) instead
6
- // of the empty answers tsgo's native LSP returns for these methods.
4
+ // ttscserver wires in so a raw-LSP graph consumer such as @samchon/graph can get
5
+ // declarations (graph nodes) and usages (graph edges) computed from that graph.
6
+ //
7
+ // tsgo's own LSP implements both methods with its compiler-exact language
8
+ // service, so the proxy forwards to tsgo by default and falls back to this
9
+ // provider only when tsgo does not advertise the capability, or when a consumer
10
+ // explicitly opts into graph-derived answers (see lspserver.Proxy).
7
11
  //
8
12
  // It lives in its own package rather than in internal/lspserver because it
9
13
  // imports internal/graph and driver, and driver already imports lspserver;
@@ -31,9 +35,9 @@ import (
31
35
 
32
36
  // Provider computes documentSymbol/references from a code graph built for one
33
37
  // project (tsconfig). The graph is loaded lazily on the first request and
34
- // cached for the process lifetime: the graph-indexing use case walks a project
35
- // once, so a single compiler load keeps indexing fast without re-checking on
36
- // every request. It is safe for concurrent use.
38
+ // cached until Invalidate is called; the proxy invalidates on didChange/didSave
39
+ // so a long-lived editor session reflects source edits instead of freezing at
40
+ // the first request's snapshot. It is safe for concurrent use.
37
41
  type Provider struct {
38
42
  cwd string
39
43
  tsconfig string
@@ -88,6 +92,19 @@ func (pr *Provider) load() (*graph.Graph, map[string]string, error) {
88
92
  return pr.graph, pr.sources, nil
89
93
  }
90
94
 
95
+ // Invalidate discards the cached graph so the next DocumentSymbols/References
96
+ // call rebuilds it against the current on-disk sources. The proxy calls this on
97
+ // didChange/didSave; without it the first request's snapshot would answer every
98
+ // later request for the process lifetime.
99
+ func (pr *Provider) Invalidate() {
100
+ pr.mu.Lock()
101
+ defer pr.mu.Unlock()
102
+ pr.loaded = false
103
+ pr.graph = nil
104
+ pr.sources = nil
105
+ pr.loadErr = nil
106
+ }
107
+
91
108
  // DocumentSymbols returns the declarations in uri as a hierarchy: top-level
92
109
  // declarations at the root, class/interface members nested under their owner.
93
110
  func (pr *Provider) DocumentSymbols(uri string) ([]lspserver.LSPDocumentSymbol, error) {
@@ -103,7 +120,7 @@ func (pr *Provider) DocumentSymbols(uri string) ([]lspserver.LSPDocumentSymbol,
103
120
 
104
121
  var fileNodes []*graph.Node
105
122
  for _, n := range g.Nodes {
106
- if n.External {
123
+ if !surfaceableNode(n) {
107
124
  continue
108
125
  }
109
126
  if pathsEqual(n.File, file) {
@@ -162,8 +179,13 @@ func (pr *Provider) References(uri string, pos lspserver.LSPPosition, includeDec
162
179
  // Edges are directed From a using declaration To the referenced symbol, so an
163
180
  // edge whose To is the target node is one usage site; its span lives in the
164
181
  // From node's file (graph's edge-evidence contract).
182
+ //
183
+ // A structural edge is not a usage. A module's `exports` edge says the symbol
184
+ // stands on that module's public surface — a fact about the declaration, not a
185
+ // place it is used — and it carries no span, so counting it as a usage puts a
186
+ // phantom reference at the top of the file.
165
187
  for _, e := range g.Edges {
166
- if e.To != target.ID {
188
+ if e.To != target.ID || isStructuralEdge(e.Kind) {
167
189
  continue
168
190
  }
169
191
  f := nodeFile(e.From)
@@ -228,12 +250,10 @@ func nodeToSymbol(n *graph.Node, text string) lspserver.LSPDocumentSymbol {
228
250
  Start: offsetToPosition(text, start),
229
251
  End: offsetToPosition(text, end),
230
252
  }
231
- name := n.Simple
232
- if name == "" {
233
- name = n.Name
234
- }
253
+ // Callers pass only surfaceableNode candidates, so Simple is the declared
254
+ // identifier and never the file path a module node carries as its name.
235
255
  return lspserver.LSPDocumentSymbol{
236
- Name: name,
256
+ Name: n.Simple,
237
257
  Kind: symbolKind(n.Kind),
238
258
  Range: rng,
239
259
  // The graph does not record the identifier's own span separately, so the
@@ -289,7 +309,7 @@ func symbolKind(k graph.NodeKind) lspserver.LSPSymbolKind {
289
309
  func targetNodeAt(g *graph.Graph, file string, offset int) *graph.Node {
290
310
  var best *graph.Node
291
311
  for _, n := range g.Nodes {
292
- if n.External || !pathsEqual(n.File, file) {
312
+ if !surfaceableNode(n) || !pathsEqual(n.File, file) {
293
313
  continue
294
314
  }
295
315
  if offset >= n.Pos && offset < n.End {
@@ -314,6 +334,22 @@ func targetNodeAt(g *graph.Graph, file string, offset int) *graph.Node {
314
334
  return nil
315
335
  }
316
336
 
337
+ // isStructuralEdge reports an edge that records where a symbol stands rather than
338
+ // a place it is used.
339
+ func isStructuralEdge(kind graph.EdgeKind) bool {
340
+ return kind == graph.EdgeExports
341
+ }
342
+
343
+ // surfaceableNode reports whether a graph node should appear as a user-facing
344
+ // LSP symbol or reference target. A per-file module node (NodeModule) carries
345
+ // the source file path as its name and spans the whole file, so surfacing it
346
+ // would put the absolute path in the outline and swallow whole-file reference
347
+ // queries. External boundary leaves and nameless nodes are likewise not
348
+ // addressable declarations.
349
+ func surfaceableNode(n *graph.Node) bool {
350
+ return !n.External && n.Kind != graph.NodeModule && n.Simple != ""
351
+ }
352
+
317
353
  func sortNodes(nodes []*graph.Node) {
318
354
  sort.Slice(nodes, func(i, j int) bool {
319
355
  if nodes[i].Pos != nodes[j].Pos {