ttsc 0.19.2 → 0.20.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 (178) hide show
  1. package/README.md +7 -5
  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 +312 -108
  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_schema_version_matches_the_typescript_client_test.go +1 -1
  33. package/internal/graph/dump_uses_ecma_line_terminators_test.go +87 -0
  34. package/internal/graph/duplicate_variable_object_members_keep_first_declaration_test.go +61 -0
  35. package/internal/graph/edges.go +220 -5
  36. package/internal/graph/enum_members_carry_their_names_on_the_enum_node_test.go +131 -0
  37. package/internal/graph/graph.go +177 -5
  38. package/internal/graph/line_model.go +122 -0
  39. package/internal/graph/literals.go +175 -0
  40. package/internal/graph/literals_are_absent_when_the_type_admits_more_than_it_can_name_test.go +74 -0
  41. package/internal/graph/literals_cover_enum_members_the_graph_has_no_nodes_for_test.go +79 -0
  42. package/internal/graph/literals_do_not_depend_on_declaration_line_wrapping_test.go +84 -0
  43. package/internal/graph/literals_render_every_value_in_typescript_source_form_test.go +77 -0
  44. package/internal/graph/literals_resolve_through_alias_indirection_test.go +64 -0
  45. package/internal/graph/member_relation_edges_follow_checker_validity_test.go +85 -0
  46. package/internal/graph/member_relation_edges_preserve_checker_boundaries_test.go +191 -0
  47. package/internal/graph/member_relation_edges_reject_checker_invalid_pairs_test.go +119 -0
  48. package/internal/graph/node_ids_preserve_hash_bearing_components_test.go +41 -0
  49. package/internal/graph/node_modifiers_emit_union_strings_test.go +4 -1
  50. package/internal/graph/object_literal_members_ride_on_the_snapshot_test.go +192 -0
  51. package/internal/graph/object_member_signatures_preserve_literal_whitespace_test.go +67 -0
  52. package/internal/graph/object_members.go +229 -0
  53. package/internal/graph/provenance.go +88 -30
  54. package/internal/graph/source_texts_cover_every_resident_program_source_test.go +73 -0
  55. package/internal/graphsymbols/position_helpers_follow_ecma_line_terminators_test.go +66 -0
  56. package/internal/graphsymbols/provider.go +26 -91
  57. package/internal/lspserver/lsp_clean_didopen_refreshes_compiler_state_test.go +144 -0
  58. package/internal/lspserver/lsp_completion.go +109 -0
  59. package/internal/lspserver/lsp_completion_gate_never_refuses_a_matching_line_test.go +50 -0
  60. package/internal/lspserver/lsp_completion_hints_longest_trigger_wins_test.go +149 -0
  61. package/internal/lspserver/lsp_completion_hints_nearest_trigger_wins_test.go +37 -0
  62. package/internal/lspserver/lsp_completion_merges_into_upstream_response_test.go +149 -0
  63. package/internal/lspserver/lsp_completion_nearest_hint_replaces_nearest_filter_test.go +43 -0
  64. package/internal/lspserver/lsp_completion_preserves_completion_list_extensions_test.go +99 -0
  65. package/internal/lspserver/lsp_completion_refuses_hints_inside_a_string_literal_test.go +69 -0
  66. package/internal/lspserver/lsp_completion_replaces_full_filter_in_rank_order_test.go +85 -0
  67. package/internal/lspserver/lsp_completion_request.go +455 -0
  68. package/internal/lspserver/lsp_completion_scope.go +530 -0
  69. package/internal/lspserver/lsp_completion_scope_bench_test.go +41 -0
  70. package/internal/lspserver/lsp_completion_scope_classifies_regex_statement_and_operator_boundaries_test.go +39 -0
  71. package/internal/lspserver/lsp_completion_scope_ignores_jsdoc_bytes_in_literals_test.go +108 -0
  72. package/internal/lspserver/lsp_completion_scope_refuses_regex_hints_test.go +52 -0
  73. package/internal/lspserver/lsp_completion_trigger_uses_last_rune_test.go +24 -0
  74. package/internal/lspserver/lsp_deleted_watched_file_withdraws_plugin_diagnostics_test.go +110 -0
  75. package/internal/lspserver/lsp_didsave_keeps_the_incremental_changed_uri_path_test.go +78 -0
  76. package/internal/lspserver/lsp_hints_absence_is_not_an_error_test.go +46 -0
  77. package/internal/lspserver/lsp_hints_refresh_coalesces_concurrent_requests_test.go +82 -0
  78. package/internal/lspserver/lsp_hints_refresh_ignores_a_superseded_generation_test.go +40 -0
  79. package/internal/lspserver/lsp_hints_refresh_keeps_each_producer_isolated_test.go +64 -0
  80. package/internal/lspserver/lsp_hints_refresh_replaces_the_session_snapshot_test.go +66 -0
  81. package/internal/lspserver/lsp_hints_refresh_scheduled_by_editor_events_test.go +90 -0
  82. package/internal/lspserver/lsp_hints_refresh_serves_completion_while_running_test.go +91 -0
  83. package/internal/lspserver/lsp_index_byte_from_bounds_its_search_start_test.go +38 -0
  84. package/internal/lspserver/lsp_initialize_constrains_position_encoding_to_utf16_test.go +205 -0
  85. package/internal/lspserver/lsp_late_completion_trigger_is_reported_once_test.go +105 -0
  86. package/internal/lspserver/lsp_native_completion_hints_decode_flat_lint_wire_test.go +66 -0
  87. package/internal/lspserver/lsp_native_completion_hints_grouped_compatibility_boundary_test.go +60 -0
  88. package/internal/lspserver/lsp_native_plugin_source.go +315 -2
  89. package/internal/lspserver/lsp_native_resident.go +304 -0
  90. package/internal/lspserver/lsp_plugin.go +85 -8
  91. package/internal/lspserver/lsp_plugin_completion_resolve_stays_local_test.go +82 -0
  92. package/internal/lspserver/lsp_position_conversions_span_astral_and_combining_runes_test.go +119 -0
  93. package/internal/lspserver/lsp_proxy.go +601 -9
  94. package/internal/lspserver/lsp_refresh.go +70 -0
  95. package/internal/lspserver/lsp_resident_falls_back_when_serve_unsupported_test.go +43 -0
  96. package/internal/lspserver/lsp_resident_maps_verb_args_test.go +34 -0
  97. package/internal/lspserver/lsp_server.go +13 -1
  98. package/internal/lspserver/lsp_watched_file_changes_reach_compiler_caches_test.go +172 -0
  99. package/lib/TtscCompiler.d.ts +3 -1
  100. package/lib/TtscCompiler.js +5 -1
  101. package/lib/TtscCompiler.js.map +1 -1
  102. package/lib/TtscService.d.ts +13 -3
  103. package/lib/TtscService.js +6 -6
  104. package/lib/TtscService.js.map +1 -1
  105. package/lib/compiler/internal/project/readConfigJson.d.ts +30 -0
  106. package/lib/compiler/internal/project/readConfigJson.js +208 -0
  107. package/lib/compiler/internal/project/readConfigJson.js.map +1 -0
  108. package/lib/compiler/internal/project/readProjectConfig.js +53 -149
  109. package/lib/compiler/internal/project/readProjectConfig.js.map +1 -1
  110. package/lib/compiler/internal/residentTransformProcess.d.ts +15 -2
  111. package/lib/compiler/internal/residentTransformProcess.js +159 -39
  112. package/lib/compiler/internal/residentTransformProcess.js.map +1 -1
  113. package/lib/compiler/internal/runBuild.d.ts +6 -0
  114. package/lib/compiler/internal/runBuild.js +143 -44
  115. package/lib/compiler/internal/runBuild.js.map +1 -1
  116. package/lib/compiler/internal/startResidentTransform.d.ts +5 -1
  117. package/lib/compiler/internal/startResidentTransform.js +3 -1
  118. package/lib/compiler/internal/startResidentTransform.js.map +1 -1
  119. package/lib/compiler/internal/transformProjectInMemory.js +12 -2
  120. package/lib/compiler/internal/transformProjectInMemory.js.map +1 -1
  121. package/lib/flags/parser.d.ts +19 -0
  122. package/lib/flags/parser.js +78 -36
  123. package/lib/flags/parser.js.map +1 -1
  124. package/lib/flags/schema.d.ts +49 -11
  125. package/lib/flags/schema.js +94 -22
  126. package/lib/flags/schema.js.map +1 -1
  127. package/lib/internal/assertSafeExplicitCacheDirectory.d.ts +5 -0
  128. package/lib/internal/assertSafeExplicitCacheDirectory.js +26 -0
  129. package/lib/internal/assertSafeExplicitCacheDirectory.js.map +1 -0
  130. package/lib/launcher/internal/runTtsc.js +107 -69
  131. package/lib/launcher/internal/runTtsc.js.map +1 -1
  132. package/lib/launcher/internal/runTtsx.js +28 -41
  133. package/lib/launcher/internal/runTtsx.js.map +1 -1
  134. package/lib/launcher/internal/runtimeHooks.d.ts +15 -0
  135. package/lib/launcher/internal/runtimeHooks.js +18 -1
  136. package/lib/launcher/internal/runtimeHooks.js.map +1 -1
  137. package/lib/launcher/internal/watchTopology.d.ts +43 -0
  138. package/lib/launcher/internal/watchTopology.js +360 -0
  139. package/lib/launcher/internal/watchTopology.js.map +1 -0
  140. package/lib/plugin/internal/buildSourcePlugin.d.ts +14 -3
  141. package/lib/plugin/internal/buildSourcePlugin.js +247 -61
  142. package/lib/plugin/internal/buildSourcePlugin.js.map +1 -1
  143. package/lib/plugin/internal/loadProjectPlugins.js +63 -2
  144. package/lib/plugin/internal/loadProjectPlugins.js.map +1 -1
  145. package/lib/structures/ITtscCompilerTransformation.d.ts +16 -0
  146. package/lib/structures/internal/ITtscParsedProjectConfig.d.ts +2 -0
  147. package/package.json +8 -8
  148. package/shim/ast/shim.go +48 -3
  149. package/shim/astnav/go.mod +14 -0
  150. package/shim/astnav/go.sum +20 -0
  151. package/shim/astnav/shim.go +15 -0
  152. package/shim/checker/shim.go +113 -0
  153. package/shim/core/shim.go +47 -0
  154. package/shim/diagnosticwriter/lint.go +50 -0
  155. package/src/TtscCompiler.ts +5 -1
  156. package/src/TtscService.ts +37 -10
  157. package/src/compiler/internal/project/readConfigJson.ts +209 -0
  158. package/src/compiler/internal/project/readProjectConfig.ts +62 -158
  159. package/src/compiler/internal/residentTransformProcess.ts +197 -39
  160. package/src/compiler/internal/runBuild.ts +169 -47
  161. package/src/compiler/internal/startResidentTransform.ts +12 -1
  162. package/src/compiler/internal/transformProjectInMemory.ts +12 -1
  163. package/src/flags/parser.ts +113 -42
  164. package/src/flags/schema.ts +113 -31
  165. package/src/internal/assertSafeExplicitCacheDirectory.ts +29 -0
  166. package/src/launcher/internal/runTtsc.ts +117 -68
  167. package/src/launcher/internal/runTtsx.ts +29 -44
  168. package/src/launcher/internal/runtimeHooks.ts +30 -2
  169. package/src/launcher/internal/watchTopology.ts +466 -0
  170. package/src/plugin/internal/buildSourcePlugin.ts +326 -64
  171. package/src/plugin/internal/loadProjectPlugins.ts +80 -2
  172. package/src/structures/ITtscCompilerTransformation.ts +17 -0
  173. package/src/structures/internal/ITtscParsedProjectConfig.ts +2 -0
  174. package/tools/shim_audit/baseline.json +0 -3
  175. package/tools/shim_audit/main.go +42 -14
  176. package/tools/shim_audit/shim_dir_coverage_test.go +25 -0
  177. package/utility/flags_gen.go +3 -3
  178. package/utility/host.go +9 -3
@@ -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
@@ -56,22 +56,51 @@ type DumpDecorator struct {
56
56
  Arguments []DumpDecoratorArgument `json:"arguments"`
57
57
  }
58
58
 
59
+ // DumpEnumMember is one member of an enum on the wire: the name a caller writes
60
+ // and the value it carries. `value` is omitted for a member the checker could
61
+ // not fold to a constant — the name still stands, and the name is what a caller
62
+ // asking about the enum came for.
63
+ type DumpEnumMember struct {
64
+ Name string `json:"name"`
65
+ Value string `json:"value,omitempty"`
66
+ }
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
+
59
78
  // DumpNode is the wire shape of a graph node. Lowercase json keys are the
60
79
  // contract; the Go field names are not.
61
80
  type DumpNode struct {
62
- ID string `json:"id"`
63
- Kind string `json:"kind"`
64
- Name string `json:"name"`
65
- QualifiedName string `json:"qualifiedName,omitempty"`
66
- File string `json:"file"`
67
- External bool `json:"external"`
68
- Ignored bool `json:"ignored,omitempty"`
69
- Exported bool `json:"exported,omitempty"`
70
- Closure bool `json:"closure,omitempty"`
71
- Modifiers []string `json:"modifiers,omitempty"`
72
- Evidence *DumpEvidence `json:"evidence,omitempty"`
73
- Implementation *DumpEvidence `json:"implementation,omitempty"`
74
- 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"`
75
104
  }
76
105
 
77
106
  // DumpEdge is the wire shape of a graph edge. Lowercase json keys are the
@@ -117,12 +146,13 @@ type DumpOrigin struct {
117
146
  }
118
147
 
119
148
  // NewDump projects a built graph onto the export shape. project is the absolute
120
- // project root used to relativize file paths and node ids; ignored is the
121
- // git-ignored source set (nil for a non-git project); sources maps a source
122
- // file's path to its text so byte spans become line/col evidence (nil omits
123
- // evidence); origin is the snapshot evidence that proves where the facts came
124
- // from.
125
- 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) {
126
156
  ctx := newDumpContext(project, sources)
127
157
 
128
158
  // Decorators ride on their target node; group by the internal node id before
@@ -148,16 +178,22 @@ func NewDump(g *Graph, project, tsconfig string, ignored map[string]bool, source
148
178
  name, qualified = ctx.rel(name), ""
149
179
  }
150
180
  nodes = append(nodes, DumpNode{
151
- ID: ctx.relID(n.ID),
152
- Kind: string(n.Kind),
153
- Name: name,
154
- QualifiedName: qualified,
155
- File: ctx.rel(n.File),
156
- External: n.External,
157
- Ignored: ignored[n.File],
158
- Exported: n.Exported,
159
- Closure: n.Closure,
160
- Modifiers: n.Modifiers,
181
+ ID: ctx.relID(n.ID),
182
+ Kind: string(n.Kind),
183
+ Name: name,
184
+ QualifiedName: qualified,
185
+ File: ctx.rel(n.File),
186
+ External: n.External,
187
+ Ignored: ignored[n.File],
188
+ Exported: n.Exported,
189
+ Closure: n.Closure,
190
+ Modifiers: n.Modifiers,
191
+ // Uncapped, like every other fact here: the dump is the whole graph and
192
+ // the MCP layer is what applies a response cap and marks it.
193
+ Literals: n.Literals,
194
+ EnumMembers: dumpEnumMembers(n.EnumMembers),
195
+ ObjectMembers: ctx.objectMembers(n),
196
+ Signature: ctx.declarationSignature(n),
161
197
  Evidence: withoutFile(ctx.evidence(n.File, n.Pos, n.End)),
162
198
  Implementation: ctx.evidence(n.ImplementationFile, n.ImplementationPos, n.ImplementationEnd),
163
199
  Decorators: decByNode[n.ID],
@@ -186,7 +222,7 @@ func NewDump(g *Graph, project, tsconfig string, ignored map[string]bool, source
186
222
 
187
223
  // The schema version describes this code, not the caller's belief about it,
188
224
  // so stamp it here rather than trusting what came in.
189
- provenance := origin.Provenance
225
+ provenance := ctx.provenance(origin.Provenance)
190
226
  provenance.SchemaVersion = DumpSchemaVersion
191
227
 
192
228
  // A nil slice encodes as JSON null, and null is not an empty list: a reader
@@ -205,25 +241,74 @@ func NewDump(g *Graph, project, tsconfig string, ignored map[string]bool, source
205
241
  provenance.Universe.Roots = []RootFile{}
206
242
  }
207
243
 
208
- diagnostics := origin.Diagnostics
244
+ diagnostics := ctx.diagnostics(origin.Diagnostics)
209
245
  if diagnostics == nil {
210
246
  diagnostics = []Diagnostic{}
211
247
  }
212
248
 
213
- return Dump{
249
+ dump := Dump{
214
250
  Project: project,
215
- Tsconfig: tsconfig,
251
+ Tsconfig: ctx.rel(tsconfig),
216
252
  Provenance: provenance,
217
253
  Diagnostics: diagnostics,
218
254
  Nodes: nodes,
219
255
  Edges: edges,
220
256
  }
257
+ if err := ctx.pathError(); err != nil {
258
+ return Dump{}, err
259
+ }
260
+ return dump, nil
261
+ }
262
+
263
+ // dumpEnumMembers projects an enum's members onto the wire shape, nil for a
264
+ // node that declares none so the key stays off every other kind.
265
+ func dumpEnumMembers(members []EnumMember) []DumpEnumMember {
266
+ if len(members) == 0 {
267
+ return nil
268
+ }
269
+ out := make([]DumpEnumMember, 0, len(members))
270
+ for _, member := range members {
271
+ out = append(out, DumpEnumMember{Name: member.Name, Value: member.Value})
272
+ }
273
+ return out
274
+ }
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"
221
303
  }
222
304
 
223
305
  // MarshalDump serializes a built graph to the export JSON, indented when pretty.
224
306
  // See NewDump for the parameters.
225
307
  func MarshalDump(g *Graph, project, tsconfig string, ignored map[string]bool, sources map[string]string, origin DumpOrigin, pretty bool) ([]byte, error) {
226
- 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
+ }
227
312
  if pretty {
228
313
  return json.MarshalIndent(d, "", " ")
229
314
  }
@@ -239,12 +324,16 @@ func MarshalDump(g *Graph, project, tsconfig string, ignored map[string]bool, so
239
324
  // copy of it held live beside the first — half a gigabyte of peak heap that
240
325
  // bought nothing, because the bytes were already exactly what stdout wanted.
241
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
+ }
242
331
  buffered := bufio.NewWriterSize(w, 1<<20)
243
332
  encoder := json.NewEncoder(buffered)
244
333
  if pretty {
245
334
  encoder.SetIndent("", " ")
246
335
  }
247
- if err := encoder.Encode(NewDump(g, project, tsconfig, ignored, sources, origin)); err != nil {
336
+ if err := encoder.Encode(dump); err != nil {
248
337
  return err
249
338
  }
250
339
  return buffered.Flush()
@@ -282,6 +371,8 @@ func wireEdgeKind(kind EdgeKind, origin string) string {
282
371
  return "extends"
283
372
  }
284
373
  return "implements"
374
+ case EdgeMemberRelation:
375
+ return origin
285
376
  default:
286
377
  return string(kind)
287
378
  }
@@ -307,41 +398,30 @@ func nodeNames(n *Node) (simple, qualified string) {
307
398
  return n.Simple, n.Name
308
399
  }
309
400
 
310
- // dumpContext relativizes paths and turns byte spans into line/col evidence,
311
- // caching a per-file line index so a large file's many edges cost O(log n) each
312
- // 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.
313
404
  type dumpContext struct {
314
- project string
405
+ paths *dumpPathMapper
315
406
  sources map[string]string
316
407
  lines map[string]lineStarts
317
408
  }
318
409
 
319
410
  func newDumpContext(project string, sources map[string]string) *dumpContext {
320
411
  return &dumpContext{
321
- project: strings.TrimRight(filepath.ToSlash(project), "/"),
412
+ paths: newDumpPathMapper(project),
322
413
  sources: sources,
323
414
  lines: map[string]lineStarts{},
324
415
  }
325
416
  }
326
417
 
327
- // rel makes a source file path project-relative; an external path keeps its
328
- // node_modules-relative tail so a dependency leaf stays readable.
418
+ // rel delegates every identity-bearing path to the dump's one cached mapper.
329
419
  func (c *dumpContext) rel(file string) string {
330
- f := filepath.ToSlash(file)
331
- if c.project != "" {
332
- if f == c.project {
333
- return ""
334
- }
335
- if strings.HasPrefix(f, c.project+"/") {
336
- return f[len(c.project)+1:]
337
- }
338
- }
339
- if i := strings.LastIndex(f, "/node_modules/"); i >= 0 {
340
- return f[i+1:]
341
- }
342
- return f
420
+ return c.paths.mapPath(file)
343
421
  }
344
422
 
423
+ func (c *dumpContext) pathError() error { return c.paths.err() }
424
+
345
425
  // relID relativizes the path portion of a node id ("path#qualifiedName:kind").
346
426
  // An id with no path (a future virtual node) is returned unchanged.
347
427
  //
@@ -349,17 +429,15 @@ func (c *dumpContext) rel(file string) string {
349
429
  // both relativize. Every other node is named by a symbol, which passes through
350
430
  // untouched.
351
431
  func (c *dumpContext) relID(id string) string {
352
- hash := strings.Index(id, "#")
353
- if hash < 0 {
432
+ parts, ok := parseNodeID(id)
433
+ if !ok {
354
434
  return id
355
435
  }
356
- head, tail := c.rel(id[:hash]), id[hash+1:]
357
- suffix := ":" + string(NodeModule)
358
- if strings.HasSuffix(tail, suffix) {
359
- name := strings.TrimSuffix(tail, suffix)
360
- return head + "#" + c.rel(name) + suffix
436
+ name := parts.name
437
+ if parts.kind == NodeModule {
438
+ name = c.rel(name)
361
439
  }
362
- return head + "#" + tail
440
+ return nodeID(c.rel(parts.path), name, parts.kind)
363
441
  }
364
442
 
365
443
  // evidence builds the line/col span for a byte range in file, or nil when the
@@ -384,7 +462,7 @@ func (c *dumpContext) evidence(file string, pos, end int) *DumpEvidence {
384
462
  // leading whitespace and doc comments before the token. Advance to the first
385
463
  // code character so the line/column point at the declaration, not its banner
386
464
  // or its indentation.
387
- pos = firstCodeOffset(text, pos)
465
+ pos = FirstCodeOffset(text, pos)
388
466
  sl, sc := ls.at(pos)
389
467
  ev := &DumpEvidence{File: c.rel(file), StartLine: sl, StartCol: sc}
390
468
  if end > pos && end <= len(text) {
@@ -393,36 +471,178 @@ func (c *dumpContext) evidence(file string, pos, end int) *DumpEvidence {
393
471
  return ev
394
472
  }
395
473
 
396
- // firstCodeOffset advances past leading trivia: whitespace, // line comments,
397
- // and /* */ block comments from pos to the first code byte, or len(text) if
398
- // the rest is all trivia. It mirrors how a token's real start is found, so an
399
- // evidence span lands on the declaration rather than the comment above it.
400
- func firstCodeOffset(text string, pos int) int {
401
- i := pos
402
- 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++ {
403
626
  switch {
404
- 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:
405
638
  i++
406
- case text[i] == '/' && i+1 < len(text) && text[i+1] == '/':
407
- i += 2
408
- for i < len(text) && text[i] != '\n' {
639
+ for i < len(text) && ((text[i] >= 'a' && text[i] <= 'z') || (text[i] >= 'A' && text[i] <= 'Z')) {
409
640
  i++
410
641
  }
411
- case text[i] == '/' && i+1 < len(text) && text[i+1] == '*':
412
- i += 2
413
- for i+1 < len(text) && !(text[i] == '*' && text[i+1] == '/') {
414
- i++
415
- }
416
- if i+1 < len(text) {
417
- i += 2
418
- } else {
419
- i = len(text)
420
- }
421
- default:
422
642
  return i
423
643
  }
424
644
  }
425
- return i
645
+ return start
426
646
  }
427
647
 
428
648
  // edgeEvidence is the evidence range for an edge's source expression.
@@ -445,28 +665,12 @@ func withoutFile(ev *DumpEvidence) *DumpEvidence {
445
665
  return ev
446
666
  }
447
667
 
448
- // nodeFile recovers the source file path embedded in a node id
449
- // ("path#qualifiedName:kind"); "" for an id without a path.
450
- func nodeFile(id string) string {
451
- if hash := strings.Index(id, "#"); hash >= 0 {
452
- return id[:hash]
453
- }
454
- return ""
455
- }
456
-
457
668
  // lineStarts holds the byte offset of each line's start, so an offset maps to a
458
669
  // 1-based line/column by binary search.
459
670
  type lineStarts []int
460
671
 
461
672
  func newLineStarts(text string) lineStarts {
462
- starts := make(lineStarts, 1, 1+strings.Count(text, "\n"))
463
- starts[0] = 0
464
- for i := 0; i < len(text); i++ {
465
- if text[i] == '\n' {
466
- starts = append(starts, i+1)
467
- }
468
- }
469
- return starts
673
+ return lineStarts(ECMALineStarts(text))
470
674
  }
471
675
 
472
676
  // at returns the 1-based line and column of a byte offset.