ttsc 0.19.3 → 0.20.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (170) hide show
  1. package/README.md +36 -34
  2. package/cmd/graphdump/main.go +2 -4
  3. package/cmd/ttsc/filter.go +12 -4
  4. package/cmd/ttsc/filter_resolves_flag_names_case_insensitively_test.go +53 -0
  5. package/cmd/ttsc/flags_gen.go +3 -3
  6. package/cmd/ttscgraph/dump.go +5 -7
  7. package/cmd/ttscgraph/run_dump_emits_only_checker_verified_member_relations_test.go +118 -0
  8. package/cmd/ttscgraph/serve.go +58 -308
  9. package/cmd/ttscgraph/serve_session_keeps_base_url_fallback_unchanged_test.go +49 -0
  10. package/cmd/ttscgraph/serve_session_keeps_esm_directory_candidate_unchanged_test.go +61 -0
  11. package/cmd/ttscgraph/serve_session_keeps_exports_blocked_file_candidates_unchanged_test.go +64 -0
  12. package/cmd/ttscgraph/serve_session_keeps_inactive_conditional_export_unchanged_test.go +66 -0
  13. package/cmd/ttscgraph/serve_session_keeps_lower_priority_module_candidate_unchanged_test.go +67 -0
  14. package/cmd/ttscgraph/serve_session_keeps_lower_priority_paths_pattern_unchanged_test.go +58 -0
  15. package/cmd/ttscgraph/serve_session_keeps_nonlongest_root_dirs_candidate_unchanged_test.go +54 -0
  16. package/cmd/ttscgraph/serve_session_reloads_commonjs_package_entry_for_esm_test.go +50 -0
  17. package/cmd/ttscgraph/serve_session_reloads_module_suffix_candidate_test.go +41 -0
  18. package/cmd/ttscgraph/serve_session_reloads_superseding_module_candidates_test.go +216 -0
  19. package/cmd/ttscgraph/serve_session_retries_failed_dump_test.go +103 -0
  20. package/driver/graph.go +12 -6
  21. package/driver/lsp.go +30 -0
  22. package/driver/program.go +37 -1
  23. package/driver/resolution_candidates.go +819 -0
  24. package/driver/windowsjunction/windowsjunction.go +40 -0
  25. package/go.mod +2 -0
  26. package/internal/graph/build.go +45 -11
  27. package/internal/graph/dump.go +276 -100
  28. package/internal/graph/dump_checkout_paths_are_stable_test.go +210 -0
  29. package/internal/graph/dump_diagnostics_ride_the_generation_that_built_it_test.go +9 -3
  30. package/internal/graph/dump_paths.go +172 -0
  31. package/internal/graph/dump_paths_are_portable_and_injective_test.go +93 -0
  32. package/internal/graph/dump_uses_ecma_line_terminators_test.go +87 -0
  33. package/internal/graph/duplicate_variable_object_members_keep_first_declaration_test.go +61 -0
  34. package/internal/graph/edges.go +208 -0
  35. package/internal/graph/graph.go +143 -5
  36. package/internal/graph/line_model.go +122 -0
  37. package/internal/graph/member_relation_edges_follow_checker_validity_test.go +85 -0
  38. package/internal/graph/member_relation_edges_preserve_checker_boundaries_test.go +191 -0
  39. package/internal/graph/member_relation_edges_reject_checker_invalid_pairs_test.go +119 -0
  40. package/internal/graph/node_ids_preserve_hash_bearing_components_test.go +41 -0
  41. package/internal/graph/node_modifiers_emit_union_strings_test.go +4 -1
  42. package/internal/graph/object_literal_members_ride_on_the_snapshot_test.go +192 -0
  43. package/internal/graph/object_member_signatures_preserve_literal_whitespace_test.go +67 -0
  44. package/internal/graph/object_members.go +229 -0
  45. package/internal/graph/provenance.go +88 -30
  46. package/internal/graph/source_texts_cover_every_resident_program_source_test.go +73 -0
  47. package/internal/graphsymbols/position_helpers_follow_ecma_line_terminators_test.go +66 -0
  48. package/internal/graphsymbols/provider.go +26 -91
  49. package/internal/lspserver/lsp_clean_didopen_refreshes_compiler_state_test.go +144 -0
  50. package/internal/lspserver/lsp_completion.go +109 -0
  51. package/internal/lspserver/lsp_completion_gate_never_refuses_a_matching_line_test.go +50 -0
  52. package/internal/lspserver/lsp_completion_hints_longest_trigger_wins_test.go +149 -0
  53. package/internal/lspserver/lsp_completion_hints_nearest_trigger_wins_test.go +37 -0
  54. package/internal/lspserver/lsp_completion_merges_into_upstream_response_test.go +149 -0
  55. package/internal/lspserver/lsp_completion_nearest_hint_replaces_nearest_filter_test.go +43 -0
  56. package/internal/lspserver/lsp_completion_preserves_completion_list_extensions_test.go +99 -0
  57. package/internal/lspserver/lsp_completion_refuses_hints_inside_a_string_literal_test.go +69 -0
  58. package/internal/lspserver/lsp_completion_replaces_full_filter_in_rank_order_test.go +85 -0
  59. package/internal/lspserver/lsp_completion_request.go +455 -0
  60. package/internal/lspserver/lsp_completion_scope.go +530 -0
  61. package/internal/lspserver/lsp_completion_scope_bench_test.go +41 -0
  62. package/internal/lspserver/lsp_completion_scope_classifies_regex_statement_and_operator_boundaries_test.go +39 -0
  63. package/internal/lspserver/lsp_completion_scope_ignores_jsdoc_bytes_in_literals_test.go +108 -0
  64. package/internal/lspserver/lsp_completion_scope_refuses_regex_hints_test.go +52 -0
  65. package/internal/lspserver/lsp_completion_trigger_uses_last_rune_test.go +24 -0
  66. package/internal/lspserver/lsp_deleted_watched_file_withdraws_plugin_diagnostics_test.go +110 -0
  67. package/internal/lspserver/lsp_didsave_keeps_the_incremental_changed_uri_path_test.go +78 -0
  68. package/internal/lspserver/lsp_hints_absence_is_not_an_error_test.go +46 -0
  69. package/internal/lspserver/lsp_hints_refresh_coalesces_concurrent_requests_test.go +82 -0
  70. package/internal/lspserver/lsp_hints_refresh_ignores_a_superseded_generation_test.go +40 -0
  71. package/internal/lspserver/lsp_hints_refresh_keeps_each_producer_isolated_test.go +64 -0
  72. package/internal/lspserver/lsp_hints_refresh_replaces_the_session_snapshot_test.go +66 -0
  73. package/internal/lspserver/lsp_hints_refresh_scheduled_by_editor_events_test.go +90 -0
  74. package/internal/lspserver/lsp_hints_refresh_serves_completion_while_running_test.go +91 -0
  75. package/internal/lspserver/lsp_index_byte_from_bounds_its_search_start_test.go +38 -0
  76. package/internal/lspserver/lsp_initialize_constrains_position_encoding_to_utf16_test.go +205 -0
  77. package/internal/lspserver/lsp_late_completion_trigger_is_reported_once_test.go +105 -0
  78. package/internal/lspserver/lsp_native_completion_hints_decode_flat_lint_wire_test.go +66 -0
  79. package/internal/lspserver/lsp_native_completion_hints_grouped_compatibility_boundary_test.go +60 -0
  80. package/internal/lspserver/lsp_native_plugin_source.go +315 -2
  81. package/internal/lspserver/lsp_native_resident.go +304 -0
  82. package/internal/lspserver/lsp_plugin.go +85 -8
  83. package/internal/lspserver/lsp_plugin_completion_resolve_stays_local_test.go +82 -0
  84. package/internal/lspserver/lsp_position_conversions_span_astral_and_combining_runes_test.go +119 -0
  85. package/internal/lspserver/lsp_proxy.go +601 -9
  86. package/internal/lspserver/lsp_refresh.go +70 -0
  87. package/internal/lspserver/lsp_resident_falls_back_when_serve_unsupported_test.go +43 -0
  88. package/internal/lspserver/lsp_resident_maps_verb_args_test.go +34 -0
  89. package/internal/lspserver/lsp_server.go +13 -1
  90. package/internal/lspserver/lsp_watched_file_changes_reach_compiler_caches_test.go +172 -0
  91. package/lib/TtscCompiler.d.ts +3 -1
  92. package/lib/TtscCompiler.js +5 -1
  93. package/lib/TtscCompiler.js.map +1 -1
  94. package/lib/TtscService.d.ts +13 -3
  95. package/lib/TtscService.js +6 -6
  96. package/lib/TtscService.js.map +1 -1
  97. package/lib/compiler/internal/project/readConfigJson.d.ts +30 -0
  98. package/lib/compiler/internal/project/readConfigJson.js +208 -0
  99. package/lib/compiler/internal/project/readConfigJson.js.map +1 -0
  100. package/lib/compiler/internal/project/readProjectConfig.js +53 -149
  101. package/lib/compiler/internal/project/readProjectConfig.js.map +1 -1
  102. package/lib/compiler/internal/residentTransformProcess.d.ts +15 -2
  103. package/lib/compiler/internal/residentTransformProcess.js +159 -39
  104. package/lib/compiler/internal/residentTransformProcess.js.map +1 -1
  105. package/lib/compiler/internal/runBuild.d.ts +6 -0
  106. package/lib/compiler/internal/runBuild.js +143 -44
  107. package/lib/compiler/internal/runBuild.js.map +1 -1
  108. package/lib/compiler/internal/startResidentTransform.d.ts +5 -1
  109. package/lib/compiler/internal/startResidentTransform.js +3 -1
  110. package/lib/compiler/internal/startResidentTransform.js.map +1 -1
  111. package/lib/compiler/internal/transformProjectInMemory.js +12 -2
  112. package/lib/compiler/internal/transformProjectInMemory.js.map +1 -1
  113. package/lib/flags/parser.d.ts +19 -0
  114. package/lib/flags/parser.js +78 -36
  115. package/lib/flags/parser.js.map +1 -1
  116. package/lib/flags/schema.d.ts +49 -11
  117. package/lib/flags/schema.js +94 -22
  118. package/lib/flags/schema.js.map +1 -1
  119. package/lib/internal/assertSafeExplicitCacheDirectory.d.ts +5 -0
  120. package/lib/internal/assertSafeExplicitCacheDirectory.js +26 -0
  121. package/lib/internal/assertSafeExplicitCacheDirectory.js.map +1 -0
  122. package/lib/launcher/internal/runTtsc.js +107 -69
  123. package/lib/launcher/internal/runTtsc.js.map +1 -1
  124. package/lib/launcher/internal/runTtsx.js +28 -41
  125. package/lib/launcher/internal/runTtsx.js.map +1 -1
  126. package/lib/launcher/internal/runtimeHooks.d.ts +15 -0
  127. package/lib/launcher/internal/runtimeHooks.js +18 -1
  128. package/lib/launcher/internal/runtimeHooks.js.map +1 -1
  129. package/lib/launcher/internal/watchTopology.d.ts +43 -0
  130. package/lib/launcher/internal/watchTopology.js +360 -0
  131. package/lib/launcher/internal/watchTopology.js.map +1 -0
  132. package/lib/plugin/internal/buildSourcePlugin.d.ts +14 -3
  133. package/lib/plugin/internal/buildSourcePlugin.js +247 -61
  134. package/lib/plugin/internal/buildSourcePlugin.js.map +1 -1
  135. package/lib/plugin/internal/loadProjectPlugins.js +63 -2
  136. package/lib/plugin/internal/loadProjectPlugins.js.map +1 -1
  137. package/lib/structures/ITtscCompilerTransformation.d.ts +16 -0
  138. package/lib/structures/internal/ITtscParsedProjectConfig.d.ts +2 -0
  139. package/package.json +8 -8
  140. package/shim/ast/shim.go +48 -3
  141. package/shim/astnav/go.mod +14 -0
  142. package/shim/astnav/go.sum +20 -0
  143. package/shim/astnav/shim.go +15 -0
  144. package/shim/checker/shim.go +100 -0
  145. package/shim/core/shim.go +47 -0
  146. package/shim/diagnosticwriter/lint.go +50 -0
  147. package/src/TtscCompiler.ts +5 -1
  148. package/src/TtscService.ts +37 -10
  149. package/src/compiler/internal/project/readConfigJson.ts +209 -0
  150. package/src/compiler/internal/project/readProjectConfig.ts +62 -158
  151. package/src/compiler/internal/residentTransformProcess.ts +197 -39
  152. package/src/compiler/internal/runBuild.ts +169 -47
  153. package/src/compiler/internal/startResidentTransform.ts +12 -1
  154. package/src/compiler/internal/transformProjectInMemory.ts +12 -1
  155. package/src/flags/parser.ts +113 -42
  156. package/src/flags/schema.ts +113 -31
  157. package/src/internal/assertSafeExplicitCacheDirectory.ts +29 -0
  158. package/src/launcher/internal/runTtsc.ts +117 -68
  159. package/src/launcher/internal/runTtsx.ts +29 -44
  160. package/src/launcher/internal/runtimeHooks.ts +30 -2
  161. package/src/launcher/internal/watchTopology.ts +466 -0
  162. package/src/plugin/internal/buildSourcePlugin.ts +326 -64
  163. package/src/plugin/internal/loadProjectPlugins.ts +80 -2
  164. package/src/structures/ITtscCompilerTransformation.ts +17 -0
  165. package/src/structures/internal/ITtscParsedProjectConfig.ts +2 -0
  166. package/tools/shim_audit/baseline.json +0 -3
  167. package/tools/shim_audit/main.go +42 -14
  168. package/tools/shim_audit/shim_dir_coverage_test.go +25 -0
  169. package/utility/flags_gen.go +3 -3
  170. package/utility/host.go +9 -3
@@ -0,0 +1,40 @@
1
+ // Package windowsjunction creates a Windows directory junction without
2
+ // interpolating filesystem paths into cmd.exe syntax.
3
+ package windowsjunction
4
+
5
+ import (
6
+ "fmt"
7
+ "os"
8
+ "os/exec"
9
+ "strings"
10
+ )
11
+
12
+ const (
13
+ linkEnvironment = "TTSC_WINDOWS_JUNCTION_LINK"
14
+ targetEnvironment = "TTSC_WINDOWS_JUNCTION_TARGET"
15
+ )
16
+
17
+ // Create makes link a directory junction that points to target.
18
+ func Create(link, target string) error {
19
+ // mklink is a cmd.exe builtin. Feed a constant command over stdin and use
20
+ // delayed environment expansion so Go never has to quote a command string
21
+ // for cmd.exe. Delayed values are substituted after cmd has classified the
22
+ // command's metacharacters, keeping every character in each path as data.
23
+ cmd := exec.Command("cmd.exe", "/d", "/q", "/v:on")
24
+ cmd.Stdin = strings.NewReader(
25
+ "mklink /J \"!TTSC_WINDOWS_JUNCTION_LINK!\" \"!TTSC_WINDOWS_JUNCTION_TARGET!\"\r\nexit /b !errorlevel!\r\n",
26
+ )
27
+ cmd.Env = append(
28
+ os.Environ(),
29
+ linkEnvironment+"="+link,
30
+ targetEnvironment+"="+target,
31
+ )
32
+ if out, err := cmd.CombinedOutput(); err != nil {
33
+ return fmt.Errorf(
34
+ "mklink /J failed: %v: %s",
35
+ err,
36
+ strings.TrimSpace(string(out)),
37
+ )
38
+ }
39
+ return nil
40
+ }
package/go.mod CHANGED
@@ -8,6 +8,7 @@ go 1.26
8
8
  // stabilizes.
9
9
  replace (
10
10
  github.com/microsoft/typescript-go/shim/ast => ./shim/ast
11
+ github.com/microsoft/typescript-go/shim/astnav => ./shim/astnav
11
12
  github.com/microsoft/typescript-go/shim/bundled => ./shim/bundled
12
13
  github.com/microsoft/typescript-go/shim/checker => ./shim/checker
13
14
  github.com/microsoft/typescript-go/shim/compiler => ./shim/compiler
@@ -26,6 +27,7 @@ replace (
26
27
 
27
28
  require (
28
29
  github.com/microsoft/typescript-go/shim/ast v0.0.0
30
+ github.com/microsoft/typescript-go/shim/astnav v0.0.0
29
31
  github.com/microsoft/typescript-go/shim/bundled v0.0.0
30
32
  github.com/microsoft/typescript-go/shim/checker v0.0.0
31
33
  github.com/microsoft/typescript-go/shim/compiler v0.0.0
@@ -28,12 +28,22 @@ func Build(prog *driver.Program) *Graph {
28
28
  return g
29
29
  }
30
30
 
31
- // SourceTexts maps each program source file to its text — the evidence input
32
- // NewDump needs to turn a node or edge byte span into a line/column.
31
+ // SourceTexts maps every program source to the resident checker text.
32
+ // Declaration and virtual bundled files are included because external graph
33
+ // leaves still carry facts and spans the source manifest must attest to.
33
34
  func SourceTexts(prog *driver.Program) map[string]string {
34
- files := prog.SourceFiles()
35
+ if prog == nil || prog.TSProgram == nil {
36
+ return map[string]string{}
37
+ }
38
+ // Discarded on purpose: this returns text, not an error channel. A failed
39
+ // apply reaches the dump through `NewDiagnostics`, which asks `Diagnostics`.
40
+ _ = prog.ApplyLinkedPlugins()
41
+ files := prog.TSProgram.SourceFiles()
35
42
  out := make(map[string]string, len(files))
36
43
  for _, file := range files {
44
+ if file == nil {
45
+ continue
46
+ }
37
47
  out[file.FileName()] = file.Text()
38
48
  }
39
49
  return out
@@ -114,6 +124,7 @@ func collectVariables(g *Graph, path string, statement *shimast.Node) {
114
124
  }
115
125
  for _, binding := range list.Declarations.Nodes {
116
126
  addNode(g, path, binding, NodeVariable)
127
+ g.collectObjectMembers(path, binding)
117
128
  collectClosures(g, path, binding)
118
129
  }
119
130
  }
@@ -398,18 +409,41 @@ func putDeclaredNode(g *Graph, path, name string, kind NodeKind, declaration *sh
398
409
  }
399
410
  }
400
411
  g.Nodes[id] = &Node{
401
- ID: id,
402
- Name: name,
403
- Simple: simpleName(declaration.Symbol()),
404
- Kind: kind,
405
- File: path,
406
- Pos: declaration.Pos(),
407
- End: declaration.End(),
408
- Modifiers: declarationModifiers(declaration),
412
+ ID: id,
413
+ Name: name,
414
+ Simple: simpleName(declaration.Symbol()),
415
+ Kind: kind,
416
+ File: path,
417
+ Pos: declaration.Pos(),
418
+ End: declaration.End(),
419
+ SignatureEnd: declarationSignatureEnd(declaration),
420
+ Modifiers: declarationModifiers(declaration),
409
421
  }
410
422
  g.bodyNodes[id] = hasBody
411
423
  }
412
424
 
425
+ // declarationSignatureEnd is the offset where a declaration's head ends, or 0
426
+ // when this declaration has no body to bound.
427
+ //
428
+ // It reuses the boundary walk the object-member outline already uses, which
429
+ // stops at the construct that opens a body — a block, an object or array
430
+ // literal, a class expression, or an arrow's `=>`. Reusing it keeps one
431
+ // definition of "where the head ends" for both outlines rather than two that
432
+ // can disagree.
433
+ //
434
+ // A boundary at or before the declaration's own start, or past its end, is no
435
+ // boundary; those report 0 and leave the consumer on its existing behavior.
436
+ func declarationSignatureEnd(declaration *shimast.Node) int {
437
+ if declaration == nil {
438
+ return 0
439
+ }
440
+ boundary, found := sourceBodyBoundary(declaration)
441
+ if !found || boundary.end <= declaration.Pos() || boundary.end > declaration.End() {
442
+ return 0
443
+ }
444
+ return boundary.end
445
+ }
446
+
413
447
  // declarationModifiers maps a declaration's combined modifier flags onto the
414
448
  // wire-string subset of the TtscGraphNodeModifier union. Only flags with an
415
449
  // exact union member are emitted, in a stable order; an unknown string would
@@ -4,7 +4,6 @@ import (
4
4
  "bufio"
5
5
  "encoding/json"
6
6
  "io"
7
- "path/filepath"
8
7
  "sort"
9
8
  "strings"
10
9
  )
@@ -16,11 +15,12 @@ import (
16
15
  //
17
16
  // - internal node kinds map straight through (NodeTypeAlias is already "type");
18
17
  // - one EdgeValueCall splits by Origin into "calls" / "instantiates" /
19
- // "renders", and EdgeHeritage into "extends" / "implements";
18
+ // "renders", EdgeHeritage into "extends" / "implements", and
19
+ // EdgeMemberRelation into "implements" / "overrides";
20
20
  // - byte spans become 1-based line/col Evidence ranges;
21
21
  // - decorator facts ride on their target node;
22
- // - file paths are project-relative and the output is sorted, so the dump is
23
- // deterministic and diffable.
22
+ // - file paths use one portable, injective project coordinate and the output
23
+ // is sorted, so the dump is deterministic and diffable.
24
24
  //
25
25
  // Structural derivations the schema also defines (file nodes, contains/exports
26
26
  // edges) are left to the TypeScript loader, which has the node set in hand and
@@ -65,24 +65,42 @@ type DumpEnumMember struct {
65
65
  Value string `json:"value,omitempty"`
66
66
  }
67
67
 
68
+ // DumpObjectMember is one direct object-literal member carried on its variable
69
+ // node. Line and Signature are rendered from the same Program-owned source text
70
+ // as the node evidence, so the outline cannot race a later disk write.
71
+ type DumpObjectMember struct {
72
+ Name string `json:"name"`
73
+ Kind string `json:"kind"`
74
+ Line int `json:"line,omitempty"`
75
+ Signature string `json:"signature,omitempty"`
76
+ }
77
+
68
78
  // DumpNode is the wire shape of a graph node. Lowercase json keys are the
69
79
  // contract; the Go field names are not.
70
80
  type DumpNode struct {
71
- ID string `json:"id"`
72
- Kind string `json:"kind"`
73
- Name string `json:"name"`
74
- QualifiedName string `json:"qualifiedName,omitempty"`
75
- File string `json:"file"`
76
- External bool `json:"external"`
77
- Ignored bool `json:"ignored,omitempty"`
78
- Exported bool `json:"exported,omitempty"`
79
- Closure bool `json:"closure,omitempty"`
80
- Modifiers []string `json:"modifiers,omitempty"`
81
- Literals []string `json:"literals,omitempty"`
82
- EnumMembers []DumpEnumMember `json:"enumMembers,omitempty"`
83
- Evidence *DumpEvidence `json:"evidence,omitempty"`
84
- Implementation *DumpEvidence `json:"implementation,omitempty"`
85
- Decorators []DumpDecorator `json:"decorators,omitempty"`
81
+ ID string `json:"id"`
82
+ Kind string `json:"kind"`
83
+ Name string `json:"name"`
84
+ QualifiedName string `json:"qualifiedName,omitempty"`
85
+ // Signature is the declaration head, cut where the compiler says the body
86
+ // opens. A consumer that reconstructed it by scanning physical lines both
87
+ // leaked implementation text when a declaration shared its line with its body
88
+ // and stopped early when the head itself contained a brace — a type-literal
89
+ // parameter, an object return type, a destructured parameter. Neither is a
90
+ // guess the consumer can win, so the producer renders it here.
91
+ Signature string `json:"signature,omitempty"`
92
+ File string `json:"file"`
93
+ External bool `json:"external"`
94
+ Ignored bool `json:"ignored,omitempty"`
95
+ Exported bool `json:"exported,omitempty"`
96
+ Closure bool `json:"closure,omitempty"`
97
+ Modifiers []string `json:"modifiers,omitempty"`
98
+ Literals []string `json:"literals,omitempty"`
99
+ EnumMembers []DumpEnumMember `json:"enumMembers,omitempty"`
100
+ ObjectMembers []DumpObjectMember `json:"objectMembers,omitempty"`
101
+ Evidence *DumpEvidence `json:"evidence,omitempty"`
102
+ Implementation *DumpEvidence `json:"implementation,omitempty"`
103
+ Decorators []DumpDecorator `json:"decorators,omitempty"`
86
104
  }
87
105
 
88
106
  // DumpEdge is the wire shape of a graph edge. Lowercase json keys are the
@@ -128,12 +146,13 @@ type DumpOrigin struct {
128
146
  }
129
147
 
130
148
  // NewDump projects a built graph onto the export shape. project is the absolute
131
- // project root used to relativize file paths and node ids; ignored is the
132
- // git-ignored source set (nil for a non-git project); sources maps a source
133
- // file's path to its text so byte spans become line/col evidence (nil omits
134
- // evidence); origin is the snapshot evidence that proves where the facts came
135
- // from.
136
- func NewDump(g *Graph, project, tsconfig string, ignored map[string]bool, sources map[string]string, origin DumpOrigin) Dump {
149
+ // root of the portable path coordinate; ignored is the git-ignored source set
150
+ // (nil for a non-git project); sources maps a source file's physical path to its
151
+ // text so byte spans become line/col evidence (nil omits evidence); origin is
152
+ // the snapshot evidence that proves where the facts came from. It returns an
153
+ // error before serialization when a path is on another filesystem root or two
154
+ // physical sources would collide at one wire identity.
155
+ func NewDump(g *Graph, project, tsconfig string, ignored map[string]bool, sources map[string]string, origin DumpOrigin) (Dump, error) {
137
156
  ctx := newDumpContext(project, sources)
138
157
 
139
158
  // Decorators ride on their target node; group by the internal node id before
@@ -173,6 +192,8 @@ func NewDump(g *Graph, project, tsconfig string, ignored map[string]bool, source
173
192
  // the MCP layer is what applies a response cap and marks it.
174
193
  Literals: n.Literals,
175
194
  EnumMembers: dumpEnumMembers(n.EnumMembers),
195
+ ObjectMembers: ctx.objectMembers(n),
196
+ Signature: ctx.declarationSignature(n),
176
197
  Evidence: withoutFile(ctx.evidence(n.File, n.Pos, n.End)),
177
198
  Implementation: ctx.evidence(n.ImplementationFile, n.ImplementationPos, n.ImplementationEnd),
178
199
  Decorators: decByNode[n.ID],
@@ -201,7 +222,7 @@ func NewDump(g *Graph, project, tsconfig string, ignored map[string]bool, source
201
222
 
202
223
  // The schema version describes this code, not the caller's belief about it,
203
224
  // so stamp it here rather than trusting what came in.
204
- provenance := origin.Provenance
225
+ provenance := ctx.provenance(origin.Provenance)
205
226
  provenance.SchemaVersion = DumpSchemaVersion
206
227
 
207
228
  // A nil slice encodes as JSON null, and null is not an empty list: a reader
@@ -220,19 +241,23 @@ func NewDump(g *Graph, project, tsconfig string, ignored map[string]bool, source
220
241
  provenance.Universe.Roots = []RootFile{}
221
242
  }
222
243
 
223
- diagnostics := origin.Diagnostics
244
+ diagnostics := ctx.diagnostics(origin.Diagnostics)
224
245
  if diagnostics == nil {
225
246
  diagnostics = []Diagnostic{}
226
247
  }
227
248
 
228
- return Dump{
249
+ dump := Dump{
229
250
  Project: project,
230
- Tsconfig: tsconfig,
251
+ Tsconfig: ctx.rel(tsconfig),
231
252
  Provenance: provenance,
232
253
  Diagnostics: diagnostics,
233
254
  Nodes: nodes,
234
255
  Edges: edges,
235
256
  }
257
+ if err := ctx.pathError(); err != nil {
258
+ return Dump{}, err
259
+ }
260
+ return dump, nil
236
261
  }
237
262
 
238
263
  // dumpEnumMembers projects an enum's members onto the wire shape, nil for a
@@ -248,10 +273,42 @@ func dumpEnumMembers(members []EnumMember) []DumpEnumMember {
248
273
  return out
249
274
  }
250
275
 
276
+ // objectMembers projects AST-owned object member identity and snapshot-owned
277
+ // display text. A missing source omits only line/signature; identity still came
278
+ // from the AST and therefore remains sound.
279
+ func (c *dumpContext) objectMembers(node *Node) []DumpObjectMember {
280
+ if len(node.ObjectMembers) == 0 {
281
+ return nil
282
+ }
283
+ out := make([]DumpObjectMember, 0, len(node.ObjectMembers))
284
+ for _, member := range node.ObjectMembers {
285
+ dumped := DumpObjectMember{
286
+ Name: member.Name,
287
+ Kind: objectMemberWireKind(member.Kind),
288
+ }
289
+ if evidence := c.evidence(node.File, member.Pos, member.End); evidence != nil {
290
+ dumped.Line = evidence.StartLine
291
+ }
292
+ dumped.Signature = c.objectMemberSignature(node.File, member)
293
+ out = append(out, dumped)
294
+ }
295
+ return out
296
+ }
297
+
298
+ func objectMemberWireKind(kind NodeKind) string {
299
+ if kind == NodeMethod {
300
+ return "method"
301
+ }
302
+ return "property"
303
+ }
304
+
251
305
  // MarshalDump serializes a built graph to the export JSON, indented when pretty.
252
306
  // See NewDump for the parameters.
253
307
  func MarshalDump(g *Graph, project, tsconfig string, ignored map[string]bool, sources map[string]string, origin DumpOrigin, pretty bool) ([]byte, error) {
254
- d := NewDump(g, project, tsconfig, ignored, sources, origin)
308
+ d, err := NewDump(g, project, tsconfig, ignored, sources, origin)
309
+ if err != nil {
310
+ return nil, err
311
+ }
255
312
  if pretty {
256
313
  return json.MarshalIndent(d, "", " ")
257
314
  }
@@ -267,12 +324,16 @@ func MarshalDump(g *Graph, project, tsconfig string, ignored map[string]bool, so
267
324
  // copy of it held live beside the first — half a gigabyte of peak heap that
268
325
  // bought nothing, because the bytes were already exactly what stdout wanted.
269
326
  func EncodeDump(w io.Writer, g *Graph, project, tsconfig string, ignored map[string]bool, sources map[string]string, origin DumpOrigin, pretty bool) error {
327
+ dump, err := NewDump(g, project, tsconfig, ignored, sources, origin)
328
+ if err != nil {
329
+ return err
330
+ }
270
331
  buffered := bufio.NewWriterSize(w, 1<<20)
271
332
  encoder := json.NewEncoder(buffered)
272
333
  if pretty {
273
334
  encoder.SetIndent("", " ")
274
335
  }
275
- if err := encoder.Encode(NewDump(g, project, tsconfig, ignored, sources, origin)); err != nil {
336
+ if err := encoder.Encode(dump); err != nil {
276
337
  return err
277
338
  }
278
339
  return buffered.Flush()
@@ -310,6 +371,8 @@ func wireEdgeKind(kind EdgeKind, origin string) string {
310
371
  return "extends"
311
372
  }
312
373
  return "implements"
374
+ case EdgeMemberRelation:
375
+ return origin
313
376
  default:
314
377
  return string(kind)
315
378
  }
@@ -335,41 +398,30 @@ func nodeNames(n *Node) (simple, qualified string) {
335
398
  return n.Simple, n.Name
336
399
  }
337
400
 
338
- // dumpContext relativizes paths and turns byte spans into line/col evidence,
339
- // caching a per-file line index so a large file's many edges cost O(log n) each
340
- // instead of a re-scan.
401
+ // dumpContext maps paths and turns byte spans into line/col evidence, caching a
402
+ // per-file line index so a large file's many edges cost O(log n) each instead
403
+ // of a re-scan.
341
404
  type dumpContext struct {
342
- project string
405
+ paths *dumpPathMapper
343
406
  sources map[string]string
344
407
  lines map[string]lineStarts
345
408
  }
346
409
 
347
410
  func newDumpContext(project string, sources map[string]string) *dumpContext {
348
411
  return &dumpContext{
349
- project: strings.TrimRight(filepath.ToSlash(project), "/"),
412
+ paths: newDumpPathMapper(project),
350
413
  sources: sources,
351
414
  lines: map[string]lineStarts{},
352
415
  }
353
416
  }
354
417
 
355
- // rel makes a source file path project-relative; an external path keeps its
356
- // node_modules-relative tail so a dependency leaf stays readable.
418
+ // rel delegates every identity-bearing path to the dump's one cached mapper.
357
419
  func (c *dumpContext) rel(file string) string {
358
- f := filepath.ToSlash(file)
359
- if c.project != "" {
360
- if f == c.project {
361
- return ""
362
- }
363
- if strings.HasPrefix(f, c.project+"/") {
364
- return f[len(c.project)+1:]
365
- }
366
- }
367
- if i := strings.LastIndex(f, "/node_modules/"); i >= 0 {
368
- return f[i+1:]
369
- }
370
- return f
420
+ return c.paths.mapPath(file)
371
421
  }
372
422
 
423
+ func (c *dumpContext) pathError() error { return c.paths.err() }
424
+
373
425
  // relID relativizes the path portion of a node id ("path#qualifiedName:kind").
374
426
  // An id with no path (a future virtual node) is returned unchanged.
375
427
  //
@@ -377,17 +429,15 @@ func (c *dumpContext) rel(file string) string {
377
429
  // both relativize. Every other node is named by a symbol, which passes through
378
430
  // untouched.
379
431
  func (c *dumpContext) relID(id string) string {
380
- hash := strings.Index(id, "#")
381
- if hash < 0 {
432
+ parts, ok := parseNodeID(id)
433
+ if !ok {
382
434
  return id
383
435
  }
384
- head, tail := c.rel(id[:hash]), id[hash+1:]
385
- suffix := ":" + string(NodeModule)
386
- if strings.HasSuffix(tail, suffix) {
387
- name := strings.TrimSuffix(tail, suffix)
388
- return head + "#" + c.rel(name) + suffix
436
+ name := parts.name
437
+ if parts.kind == NodeModule {
438
+ name = c.rel(name)
389
439
  }
390
- return head + "#" + tail
440
+ return nodeID(c.rel(parts.path), name, parts.kind)
391
441
  }
392
442
 
393
443
  // evidence builds the line/col span for a byte range in file, or nil when the
@@ -412,7 +462,7 @@ func (c *dumpContext) evidence(file string, pos, end int) *DumpEvidence {
412
462
  // leading whitespace and doc comments before the token. Advance to the first
413
463
  // code character so the line/column point at the declaration, not its banner
414
464
  // or its indentation.
415
- pos = firstCodeOffset(text, pos)
465
+ pos = FirstCodeOffset(text, pos)
416
466
  sl, sc := ls.at(pos)
417
467
  ev := &DumpEvidence{File: c.rel(file), StartLine: sl, StartCol: sc}
418
468
  if end > pos && end <= len(text) {
@@ -421,36 +471,178 @@ func (c *dumpContext) evidence(file string, pos, end int) *DumpEvidence {
421
471
  return ev
422
472
  }
423
473
 
424
- // firstCodeOffset advances past leading trivia: whitespace, // line comments,
425
- // and /* */ block comments from pos to the first code byte, or len(text) if
426
- // the rest is all trivia. It mirrors how a token's real start is found, so an
427
- // evidence span lands on the declaration rather than the comment above it.
428
- func firstCodeOffset(text string, pos int) int {
429
- i := pos
430
- for i < len(text) {
474
+ // declarationSignature renders a declaration's head from the same
475
+ // Program-owned source text as its evidence, so the outline cannot race a later
476
+ // disk write. Empty when the producer could not bound the head, which leaves the
477
+ // consumer on its existing line scan rather than handing it a wrong cut.
478
+ func (c *dumpContext) declarationSignature(n *Node) string {
479
+ if n == nil || n.SignatureEnd <= n.Pos || c.sources == nil {
480
+ return ""
481
+ }
482
+ text, ok := c.sources[n.File]
483
+ if !ok {
484
+ return ""
485
+ }
486
+ pos := FirstCodeOffset(text, n.Pos)
487
+ end := min(n.SignatureEnd, len(text))
488
+ if pos >= end {
489
+ return ""
490
+ }
491
+ return strings.TrimSpace(text[pos:end])
492
+ }
493
+
494
+ // objectMemberSignature reproduces the compact outline details has historically
495
+ // returned, but slices it from Program-owned text while the dump is built
496
+ // instead of reopening the live file later.
497
+ func (c *dumpContext) objectMemberSignature(file string, member ObjectMember) string {
498
+ pos, end := member.Pos, member.SignatureEnd
499
+ if pos < 0 || end <= pos || c.sources == nil {
500
+ return ""
501
+ }
502
+ text, ok := c.sources[file]
503
+ if !ok || pos > len(text) {
504
+ return ""
505
+ }
506
+ pos = FirstCodeOffset(text, pos)
507
+ if end > len(text) {
508
+ end = len(text)
509
+ }
510
+ if member.SignatureTokenLen > 0 {
511
+ end = FirstCodeOffset(text, end)
512
+ end = min(end+member.SignatureTokenLen, len(text))
513
+ }
514
+ if pos >= end {
515
+ return ""
516
+ }
517
+ if pos >= end {
518
+ return ""
519
+ }
520
+ signature := strings.TrimSuffix(compactObjectMemberSignature(text[pos:end]), ",")
521
+ const maxObjectMemberSignatureRunes = 160
522
+ runes := []rune(signature)
523
+ if len(runes) > maxObjectMemberSignatureRunes {
524
+ signature = string(runes[:maxObjectMemberSignatureRunes-3]) + "..."
525
+ }
526
+ return signature
527
+ }
528
+
529
+ // compactObjectMemberSignature collapses trivia outside lexical values while
530
+ // leaving quoted strings, template literals, regular expressions, and comments
531
+ // byte-for-byte intact. A display outline may be compact, but changing literal
532
+ // whitespace would change the declaration it claims to quote.
533
+ func compactObjectMemberSignature(text string) string {
534
+ var out strings.Builder
535
+ pendingSpace := false
536
+ for i := 0; i < len(text); {
537
+ if width := sourceWhitespaceWidth(text, i); width > 0 {
538
+ pendingSpace = out.Len() > 0
539
+ i += width
540
+ continue
541
+ }
542
+ if pendingSpace {
543
+ out.WriteByte(' ')
544
+ pendingSpace = false
545
+ }
546
+
547
+ end := i + 1
548
+ switch text[i] {
549
+ case '\'', '"':
550
+ end = quotedSourceEnd(text, i, text[i])
551
+ case '`':
552
+ end = templateSourceEnd(text, i)
553
+ case '/':
554
+ switch {
555
+ case i+1 < len(text) && text[i+1] == '/':
556
+ end = LineCommentEnd(text, i)
557
+ case i+1 < len(text) && text[i+1] == '*':
558
+ end = blockCommentEnd(text, i)
559
+ default:
560
+ if candidate := regularExpressionEnd(text, i); candidate > i {
561
+ end = candidate
562
+ }
563
+ }
564
+ }
565
+ out.WriteString(text[i:end])
566
+ i = end
567
+ }
568
+ return strings.TrimSpace(out.String())
569
+ }
570
+
571
+ func quotedSourceEnd(text string, start int, quote byte) int {
572
+ escaped := false
573
+ for i := start + 1; i < len(text); i++ {
574
+ if escaped {
575
+ escaped = false
576
+ continue
577
+ }
578
+ if text[i] == '\\' {
579
+ escaped = true
580
+ continue
581
+ }
582
+ if text[i] == quote {
583
+ return i + 1
584
+ }
585
+ }
586
+ return len(text)
587
+ }
588
+
589
+ // templateSourceEnd returns the last unescaped backtick in the member span.
590
+ // This deliberately treats substitutions and nested templates as one protected
591
+ // lexical region: preserving a little extra spacing is safer than compacting
592
+ // whitespace that belongs to either template's value.
593
+ func templateSourceEnd(text string, start int) int {
594
+ end := len(text)
595
+ for i := len(text) - 1; i > start; i-- {
596
+ if text[i] == '`' && !sourceByteEscaped(text, i) {
597
+ return i + 1
598
+ }
599
+ }
600
+ return end
601
+ }
602
+
603
+ func sourceByteEscaped(text string, pos int) bool {
604
+ slashes := 0
605
+ for i := pos - 1; i >= 0 && text[i] == '\\'; i-- {
606
+ slashes++
607
+ }
608
+ return slashes%2 == 1
609
+ }
610
+
611
+ func blockCommentEnd(text string, start int) int {
612
+ if end := strings.Index(text[start+2:], "*/"); end >= 0 {
613
+ return start + 2 + end + 2
614
+ }
615
+ return len(text)
616
+ }
617
+
618
+ // regularExpressionEnd conservatively protects a slash-delimited region when
619
+ // one closes on the same source line. Division can look the same without parser
620
+ // context; preserving its spaces is harmless, while compacting a real regular
621
+ // expression would change its pattern.
622
+ func regularExpressionEnd(text string, start int) int {
623
+ escaped := false
624
+ inClass := false
625
+ for i := start + 1; i < len(text); i++ {
431
626
  switch {
432
- case text[i] == ' ' || text[i] == '\t' || text[i] == '\r' || text[i] == '\n':
627
+ case lineTerminatorWidth(text, i) > 0:
628
+ return start
629
+ case escaped:
630
+ escaped = false
631
+ case text[i] == '\\':
632
+ escaped = true
633
+ case text[i] == '[':
634
+ inClass = true
635
+ case text[i] == ']':
636
+ inClass = false
637
+ case text[i] == '/' && !inClass:
433
638
  i++
434
- case text[i] == '/' && i+1 < len(text) && text[i+1] == '/':
435
- i += 2
436
- for i < len(text) && text[i] != '\n' {
639
+ for i < len(text) && ((text[i] >= 'a' && text[i] <= 'z') || (text[i] >= 'A' && text[i] <= 'Z')) {
437
640
  i++
438
641
  }
439
- case text[i] == '/' && i+1 < len(text) && text[i+1] == '*':
440
- i += 2
441
- for i+1 < len(text) && !(text[i] == '*' && text[i+1] == '/') {
442
- i++
443
- }
444
- if i+1 < len(text) {
445
- i += 2
446
- } else {
447
- i = len(text)
448
- }
449
- default:
450
642
  return i
451
643
  }
452
644
  }
453
- return i
645
+ return start
454
646
  }
455
647
 
456
648
  // edgeEvidence is the evidence range for an edge's source expression.
@@ -473,28 +665,12 @@ func withoutFile(ev *DumpEvidence) *DumpEvidence {
473
665
  return ev
474
666
  }
475
667
 
476
- // nodeFile recovers the source file path embedded in a node id
477
- // ("path#qualifiedName:kind"); "" for an id without a path.
478
- func nodeFile(id string) string {
479
- if hash := strings.Index(id, "#"); hash >= 0 {
480
- return id[:hash]
481
- }
482
- return ""
483
- }
484
-
485
668
  // lineStarts holds the byte offset of each line's start, so an offset maps to a
486
669
  // 1-based line/column by binary search.
487
670
  type lineStarts []int
488
671
 
489
672
  func newLineStarts(text string) lineStarts {
490
- starts := make(lineStarts, 1, 1+strings.Count(text, "\n"))
491
- starts[0] = 0
492
- for i := 0; i < len(text); i++ {
493
- if text[i] == '\n' {
494
- starts = append(starts, i+1)
495
- }
496
- }
497
- return starts
673
+ return lineStarts(ECMALineStarts(text))
498
674
  }
499
675
 
500
676
  // at returns the 1-based line and column of a byte offset.