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
@@ -0,0 +1,210 @@
1
+ package graph
2
+
3
+ import (
4
+ "encoding/json"
5
+ "path/filepath"
6
+ "strings"
7
+ "testing"
8
+
9
+ "github.com/samchon/ttsc/packages/ttsc/driver"
10
+ )
11
+
12
+ const portableDumpTSConfig = `{
13
+ "compilerOptions": {
14
+ "target": "ES2022",
15
+ "module": "commonjs",
16
+ "strict": true,
17
+ "noEmit": true
18
+ },
19
+ "files": ["src/main.ts", "../shared/value.ts", "../shared/types.d.ts"]
20
+ }
21
+ `
22
+
23
+ // TestDumpCheckoutPathsAreStable verifies the whole schema-v6 consequence
24
+ // surface, not only the path helper.
25
+ //
26
+ // 1. Build the same project plus sibling .ts/.d.ts under two checkout roots.
27
+ // 2. Remove only the producer-local Project locator and require byte identity.
28
+ // 3. Assert nodes, module names, endpoints, implementation evidence,
29
+ // diagnostics, manifests, and universe inputs all use one `../shared`
30
+ // coordinate, with the .ts internal and the .d.ts an external leaf.
31
+ func TestDumpCheckoutPathsAreStable(t *testing.T) {
32
+ firstCheckout := filepath.Join(t.TempDir(), "checkout-one")
33
+ secondCheckout := filepath.Join(t.TempDir(), "checkout-two")
34
+ first := portableFixtureDump(t, firstCheckout)
35
+ second := portableFixtureDump(t, secondCheckout)
36
+
37
+ first.Project = ""
38
+ second.Project = ""
39
+ firstJSON, err := json.Marshal(first)
40
+ if err != nil {
41
+ t.Fatal(err)
42
+ }
43
+ secondJSON, err := json.Marshal(second)
44
+ if err != nil {
45
+ t.Fatal(err)
46
+ }
47
+ if string(firstJSON) != string(secondJSON) {
48
+ t.Fatalf("checkout relocation changed the dump:\nfirst %s\nsecond %s", firstJSON, secondJSON)
49
+ }
50
+
51
+ assertPortableFixturePaths(t, first)
52
+ if strings.Contains(string(firstJSON), filepath.ToSlash(firstCheckout)) ||
53
+ strings.Contains(string(secondJSON), filepath.ToSlash(secondCheckout)) {
54
+ t.Fatal("a producer-local checkout path escaped into schema-v6 identity")
55
+ }
56
+ }
57
+
58
+ func portableFixtureDump(t *testing.T, checkout string) Dump {
59
+ t.Helper()
60
+ project := filepath.Join(checkout, "app")
61
+ config := filepath.Join(project, "tsconfig.json")
62
+ mainFile := filepath.Join(project, "src", "main.ts")
63
+ siblingFile := filepath.Join(checkout, "shared", "value.ts")
64
+ declarationFile := filepath.Join(checkout, "shared", "types.d.ts")
65
+
66
+ writeFile(t, config, portableDumpTSConfig)
67
+ writeFile(t, mainFile, `import { sibling } from "../../shared/value";
68
+ import type { ExternalShape } from "../../shared/types";
69
+ export function main(input: ExternalShape): number {
70
+ return sibling(input.value);
71
+ }
72
+ `)
73
+ writeFile(t, siblingFile, `export function sibling(value: number): number {
74
+ return value;
75
+ }
76
+ export const broken: number = "nope";
77
+ `)
78
+ writeFile(t, declarationFile, `export interface ExternalShape {
79
+ value: number;
80
+ }
81
+ `)
82
+
83
+ prog, _, err := driver.LoadProgram(project, "tsconfig.json", driver.LoadProgramOptions{})
84
+ if err != nil {
85
+ t.Fatal(err)
86
+ }
87
+ defer func() { _ = prog.Close() }()
88
+
89
+ built := Build(prog)
90
+ // An implementation span is the one evidence path that cannot be recovered
91
+ // from its owner node. Point one real node at the sibling source so this
92
+ // end-to-end projection proves that optional path uses the same mapper too.
93
+ implementationFile := ""
94
+ for _, node := range built.Nodes {
95
+ if node.Kind == NodeFunction && node.Name == "sibling" {
96
+ implementationFile = node.File
97
+ break
98
+ }
99
+ }
100
+ if implementationFile == "" {
101
+ t.Fatal("fixture graph omitted sibling function")
102
+ }
103
+ for _, node := range built.Nodes {
104
+ if node.Kind == NodeFunction && node.Name == "main" {
105
+ // Use the compiler's canonical spelling rather than filepath.Join's host
106
+ // spelling. On Windows SourceTexts is keyed by tsgo-normalized slashes;
107
+ // injecting the fixture's backslash path would test a missing source-map
108
+ // lookup instead of the wire mapper.
109
+ node.ImplementationFile = implementationFile
110
+ node.ImplementationPos = 0
111
+ node.ImplementationEnd = len("export function sibling")
112
+ break
113
+ }
114
+ }
115
+
116
+ texts := SourceTexts(prog)
117
+ dump, err := NewDump(
118
+ built,
119
+ project,
120
+ "tsconfig.json",
121
+ nil,
122
+ texts,
123
+ DumpOrigin{
124
+ Provenance: NewProvenance(
125
+ Producer{Tool: "portable-fixture", Version: "test", Typescript: TypescriptVersion()},
126
+ []string{CapabilityUniverse, CapabilitySourceDigests, CapabilityDiagnostics},
127
+ []FileDigest{{File: config, Digest: "config-digest"}},
128
+ []RootFile{
129
+ {Config: config, File: mainFile},
130
+ {Config: config, File: siblingFile},
131
+ {Config: config, File: declarationFile},
132
+ },
133
+ texts,
134
+ nil,
135
+ ),
136
+ Diagnostics: NewDiagnostics(prog),
137
+ },
138
+ )
139
+ if err != nil {
140
+ t.Fatal(err)
141
+ }
142
+ return dump
143
+ }
144
+
145
+ func assertPortableFixturePaths(t *testing.T, dump Dump) {
146
+ t.Helper()
147
+ if dump.Tsconfig != "tsconfig.json" {
148
+ t.Fatalf("tsconfig coordinate = %q", dump.Tsconfig)
149
+ }
150
+
151
+ var internal, external, module, implemented bool
152
+ ids := map[string]bool{}
153
+ for _, node := range dump.Nodes {
154
+ ids[node.ID] = true
155
+ switch {
156
+ case node.Kind == string(NodeFunction) && node.Name == "sibling":
157
+ internal = node.File == "../shared/value.ts" && !node.External &&
158
+ strings.HasPrefix(node.ID, "../shared/value.ts#")
159
+ case node.Name == "ExternalShape":
160
+ external = node.File == "../shared/types.d.ts" && node.External &&
161
+ strings.HasPrefix(node.ID, "../shared/types.d.ts#")
162
+ case node.Kind == string(NodeModule) && node.File == "../shared/value.ts":
163
+ module = node.Name == "../shared/value.ts" &&
164
+ strings.Contains(node.ID, "#../shared/value.ts:module")
165
+ case node.Kind == string(NodeFunction) && node.Name == "main":
166
+ implemented = node.Implementation != nil && node.Implementation.File == "../shared/value.ts"
167
+ }
168
+ }
169
+ if !internal || !external || !module || !implemented {
170
+ t.Fatalf("portable node surfaces missing: internal=%t external=%t module=%t implementation=%t", internal, external, module, implemented)
171
+ }
172
+ for _, edge := range dump.Edges {
173
+ if !ids[edge.From] || !ids[edge.To] {
174
+ t.Fatalf("mapped edge endpoint is not a mapped node: %+v", edge)
175
+ }
176
+ }
177
+
178
+ diagnostic := false
179
+ for _, finding := range dump.Diagnostics {
180
+ if finding.Code == 2322 && finding.File == "../shared/value.ts" {
181
+ diagnostic = true
182
+ }
183
+ }
184
+ if !diagnostic {
185
+ t.Fatalf("sibling diagnostic did not use the shared coordinate: %+v", dump.Diagnostics)
186
+ }
187
+
188
+ source := false
189
+ for _, entry := range dump.Provenance.Sources {
190
+ if entry.File == "../shared/value.ts" {
191
+ source = true
192
+ }
193
+ }
194
+ if !source || len(dump.Provenance.Universe.Configs) != 1 ||
195
+ dump.Provenance.Universe.Configs[0].File != "tsconfig.json" {
196
+ t.Fatalf("manifest/config coordinates drifted: %+v", dump.Provenance)
197
+ }
198
+ roots := map[string]bool{}
199
+ for _, root := range dump.Provenance.Universe.Roots {
200
+ if root.Config != "tsconfig.json" {
201
+ t.Fatalf("root config coordinate = %q", root.Config)
202
+ }
203
+ roots[root.File] = true
204
+ }
205
+ for _, want := range []string{"src/main.ts", "../shared/value.ts", "../shared/types.d.ts"} {
206
+ if !roots[want] {
207
+ t.Fatalf("universe roots omit %q: %+v", want, dump.Provenance.Universe.Roots)
208
+ }
209
+ }
210
+ }
@@ -36,9 +36,12 @@ func TestDumpDiagnosticsRideTheGenerationThatBuiltIt(t *testing.T) {
36
36
  }
37
37
  defer func() { _ = prog.Close() }()
38
38
 
39
- dump := NewDump(Build(prog), root, "tsconfig.json", nil, SourceTexts(prog), DumpOrigin{
40
- Diagnostics: NewDiagnostics(prog, root),
39
+ dump, err := NewDump(Build(prog), root, "tsconfig.json", nil, SourceTexts(prog), DumpOrigin{
40
+ Diagnostics: NewDiagnostics(prog),
41
41
  })
42
+ if err != nil {
43
+ t.Fatal(err)
44
+ }
42
45
 
43
46
  var match *Diagnostic
44
47
  for i := range dump.Diagnostics {
@@ -69,7 +72,10 @@ func TestDumpDiagnosticsRideTheGenerationThatBuiltIt(t *testing.T) {
69
72
 
70
73
  // The negative twin: a caller that does not collect diagnostics publishes an
71
74
  // empty list, never a nil that would encode as JSON null.
72
- quiet := NewDump(Build(prog), root, "tsconfig.json", nil, SourceTexts(prog), DumpOrigin{})
75
+ quiet, err := NewDump(Build(prog), root, "tsconfig.json", nil, SourceTexts(prog), DumpOrigin{})
76
+ if err != nil {
77
+ t.Fatal(err)
78
+ }
73
79
  if quiet.Diagnostics == nil {
74
80
  t.Fatal("an uncollected diagnostics list is nil, so it serializes as null rather than []")
75
81
  }
@@ -0,0 +1,172 @@
1
+ package graph
2
+
3
+ import (
4
+ "fmt"
5
+ "strings"
6
+
7
+ shimtspath "github.com/microsoft/typescript-go/shim/tspath"
8
+ )
9
+
10
+ // dumpPathMapper owns the schema-v6 path vocabulary for one dump. Every
11
+ // identity-bearing path passes through this one cache, so the producer can
12
+ // reject both an unportable filesystem root and a non-injective projection
13
+ // before any JSON is written.
14
+ type dumpPathMapper struct {
15
+ project string
16
+ caseSensitive bool
17
+
18
+ physicalToWire map[string]string
19
+ wireToPhysical map[string]string
20
+ mappingErr error
21
+ }
22
+
23
+ func newDumpPathMapper(project string) *dumpPathMapper {
24
+ normalized := shimtspath.NormalizePath(shimtspath.NormalizeSlashes(project))
25
+ mapper := &dumpPathMapper{
26
+ project: normalized,
27
+ caseSensitive: dumpPathRootIsCaseSensitive(normalized),
28
+ physicalToWire: map[string]string{},
29
+ wireToPhysical: map[string]string{},
30
+ }
31
+ if normalized == "" || shimtspath.GetRootLength(normalized) == 0 {
32
+ mapper.mappingErr = fmt.Errorf("ttscgraph: project root %q is not absolute", project)
33
+ }
34
+ return mapper
35
+ }
36
+
37
+ // mapPath returns one portable, slash-normalized coordinate:
38
+ //
39
+ // - project files stay project-relative;
40
+ // - same-root siblings use `../` segments, preserving workspace structure;
41
+ // - package paths keep their full resolution context instead of collapsing
42
+ // to the last node_modules tail;
43
+ // - compiler virtual paths keep their bundled identity.
44
+ //
45
+ // A source on another drive or UNC share has no portable coordinate relative
46
+ // to the project. It records a precise error; NewDump returns that error before
47
+ // a caller can serialize the partial projection.
48
+ func (m *dumpPathMapper) mapPath(file string) string {
49
+ if file == "" {
50
+ return ""
51
+ }
52
+ normalized := shimtspath.NormalizeSlashes(file)
53
+ if strings.HasPrefix(normalized, "bundled:///") {
54
+ return m.claim(normalized, normalized)
55
+ }
56
+ normalized = shimtspath.NormalizePath(normalized)
57
+ if m.project == "" || shimtspath.GetRootLength(m.project) == 0 {
58
+ return normalized
59
+ }
60
+
61
+ physical := normalized
62
+ if shimtspath.GetRootLength(physical) == 0 {
63
+ physical = shimtspath.GetNormalizedAbsolutePath(physical, m.project)
64
+ }
65
+ if !dumpPathRootsEqual(m.project, physical, m.caseSensitive) {
66
+ m.fail(fmt.Errorf(
67
+ "ttscgraph: source path %q cannot be represented relative to project %q because they are on different filesystem roots",
68
+ normalized,
69
+ m.project,
70
+ ))
71
+ return normalized
72
+ }
73
+ options := shimtspath.ComparePathsOptions{
74
+ CurrentDirectory: m.project,
75
+ UseCaseSensitiveFileNames: m.caseSensitive,
76
+ }
77
+ wire := shimtspath.GetRelativePathFromDirectory(m.project, physical, options)
78
+ if shimtspath.GetRootLength(wire) != 0 {
79
+ m.fail(fmt.Errorf(
80
+ "ttscgraph: source path %q cannot be represented relative to project %q because they are on different filesystem roots",
81
+ normalized,
82
+ m.project,
83
+ ))
84
+ return normalized
85
+ }
86
+ return m.claim(physical, wire)
87
+ }
88
+
89
+ // claim records both directions of the projection. The reverse map is the
90
+ // injectivity gate: two distinct compiler sources may never acquire one wire
91
+ // identity, even if a future coordinate rule is added incorrectly.
92
+ func (m *dumpPathMapper) claim(physical, wire string) string {
93
+ key := physical
94
+ if !m.caseSensitive && !strings.HasPrefix(physical, "bundled:///") {
95
+ key = strings.ToLower(key)
96
+ }
97
+ if previous, ok := m.physicalToWire[key]; ok {
98
+ if previous != wire {
99
+ m.fail(fmt.Errorf(
100
+ "ttscgraph: source path %q mapped inconsistently to %q and %q",
101
+ physical,
102
+ previous,
103
+ wire,
104
+ ))
105
+ }
106
+ return previous
107
+ }
108
+ if previous, ok := m.wireToPhysical[wire]; ok && previous != key {
109
+ m.fail(fmt.Errorf(
110
+ "ttscgraph: source paths %q and %q collide at wire identity %q",
111
+ previous,
112
+ physical,
113
+ wire,
114
+ ))
115
+ return wire
116
+ }
117
+ m.physicalToWire[key] = wire
118
+ m.wireToPhysical[wire] = key
119
+ return wire
120
+ }
121
+
122
+ func (m *dumpPathMapper) fail(err error) {
123
+ if m.mappingErr == nil {
124
+ m.mappingErr = err
125
+ }
126
+ }
127
+
128
+ func (m *dumpPathMapper) err() error { return m.mappingErr }
129
+
130
+ // Windows drive and UNC roots use case-insensitive path comparison. POSIX
131
+ // roots remain case-sensitive. This decision follows the path's own grammar so
132
+ // synthetic Windows/UNC fixtures behave the same on every CI host.
133
+ func dumpPathRootIsCaseSensitive(path string) bool {
134
+ rootLength := shimtspath.GetRootLength(path)
135
+ if rootLength == 0 {
136
+ return true
137
+ }
138
+ root := path[:rootLength]
139
+ return !(strings.HasPrefix(root, "//") || (len(root) >= 2 && root[1] == ':'))
140
+ }
141
+
142
+ // dumpPathRootsEqual compares filesystem roots before asking tspath for a
143
+ // relative coordinate. tspath models a UNC root as `//server/`, which is useful
144
+ // for URL-like path operations but too broad for a filesystem identity: on
145
+ // Windows, `//server/share-a` and `//server/share-b` are different volumes and
146
+ // no `../share-b` coordinate can cross between them. Include the share component
147
+ // for that one grammar and keep tspath's roots for drive and POSIX paths.
148
+ func dumpPathRootsEqual(left, right string, caseSensitive bool) bool {
149
+ leftRoot := dumpFilesystemRoot(left)
150
+ rightRoot := dumpFilesystemRoot(right)
151
+ if caseSensitive {
152
+ return leftRoot == rightRoot
153
+ }
154
+ return strings.EqualFold(leftRoot, rightRoot)
155
+ }
156
+
157
+ func dumpFilesystemRoot(path string) string {
158
+ normalized := shimtspath.NormalizeSlashes(path)
159
+ rootLength := shimtspath.GetRootLength(normalized)
160
+ if rootLength == 0 {
161
+ return ""
162
+ }
163
+ root := normalized[:rootLength]
164
+ if !strings.HasPrefix(root, "//") {
165
+ return root
166
+ }
167
+ remainder := normalized[rootLength:]
168
+ if slash := strings.IndexByte(remainder, '/'); slash >= 0 {
169
+ return root + remainder[:slash]
170
+ }
171
+ return root + remainder
172
+ }
@@ -0,0 +1,93 @@
1
+ package graph
2
+
3
+ import (
4
+ "strings"
5
+ "testing"
6
+ )
7
+
8
+ // TestDumpPathMapperUsesPortableCoordinates verifies the schema-v6 vocabulary
9
+ // directly, independent of the host OS running the test.
10
+ //
11
+ // 1. Map in-project and sibling paths for POSIX, drive, and UNC layouts.
12
+ // 2. Keep two pnpm version/peer contexts with the same package subpath apart.
13
+ // 3. Preserve compiler virtual identities exactly.
14
+ func TestDumpPathMapperUsesPortableCoordinates(t *testing.T) {
15
+ tests := []struct {
16
+ name string
17
+ project string
18
+ file string
19
+ want string
20
+ }{
21
+ {"posix-project", "/checkout/app", "/checkout/app/src/main.ts", "src/main.ts"},
22
+ {"posix-sibling", "/checkout/app", "/checkout/shared/src/value.ts", "../shared/src/value.ts"},
23
+ {"windows-project", `C:\checkout\app`, `C:\checkout\app\src\main.ts`, "src/main.ts"},
24
+ {"windows-sibling", `C:\checkout\app`, `C:\checkout\shared\src\value.ts`, "../shared/src/value.ts"},
25
+ {"unc-project", `\\server\share\checkout\app`, `\\server\share\checkout\app\src\main.ts`, "src/main.ts"},
26
+ {"unc-sibling", `\\server\share\checkout\app`, `\\server\share\checkout\shared\src\value.ts`, "../shared/src/value.ts"},
27
+ {
28
+ "pnpm-peer-a",
29
+ "/checkout/app",
30
+ "/checkout/app/node_modules/.pnpm/pkg@1.0.0_peer-a/node_modules/pkg/index.d.ts",
31
+ "node_modules/.pnpm/pkg@1.0.0_peer-a/node_modules/pkg/index.d.ts",
32
+ },
33
+ {
34
+ "pnpm-peer-b",
35
+ "/checkout/app",
36
+ "/checkout/app/node_modules/.pnpm/pkg@2.0.0_peer-b/node_modules/pkg/index.d.ts",
37
+ "node_modules/.pnpm/pkg@2.0.0_peer-b/node_modules/pkg/index.d.ts",
38
+ },
39
+ {"bundled", "/checkout/app", "bundled:///lib.es2024.d.ts", "bundled:///lib.es2024.d.ts"},
40
+ }
41
+ for _, test := range tests {
42
+ t.Run(test.name, func(t *testing.T) {
43
+ mapper := newDumpPathMapper(test.project)
44
+ if got := mapper.mapPath(test.file); got != test.want {
45
+ t.Fatalf("mapPath(%q) = %q, want %q", test.file, got, test.want)
46
+ }
47
+ if err := mapper.err(); err != nil {
48
+ t.Fatalf("mapPath(%q): %v", test.file, err)
49
+ }
50
+ })
51
+ }
52
+ }
53
+
54
+ // TestDumpPathMapperRejectsUnportableRootsAndCollisions pins both fail-closed
55
+ // boundaries of the mapping contract.
56
+ //
57
+ // 1. Reject a different Windows drive and a different UNC share precisely.
58
+ // 2. Force two physical sources through one coordinate and require a collision.
59
+ // 3. Require NewDump to surface the root error before JSON serialization.
60
+ func TestDumpPathMapperRejectsUnportableRootsAndCollisions(t *testing.T) {
61
+ for _, test := range []struct {
62
+ name string
63
+ project string
64
+ file string
65
+ }{
66
+ {"windows-drive", "C:/checkout/app", "D:/shared/value.ts"},
67
+ {"unc-share", "//server/share-a/app", "//server/share-b/value.ts"},
68
+ } {
69
+ t.Run(test.name, func(t *testing.T) {
70
+ mapper := newDumpPathMapper(test.project)
71
+ mapper.mapPath(test.file)
72
+ if err := mapper.err(); err == nil || !strings.Contains(err.Error(), "different filesystem roots") {
73
+ t.Fatalf("cross-root error = %v, want a precise filesystem-root rejection", err)
74
+ }
75
+ })
76
+ }
77
+
78
+ collision := newDumpPathMapper("/checkout/app")
79
+ collision.claim("/physical/one.ts", "shared.ts")
80
+ collision.claim("/physical/two.ts", "shared.ts")
81
+ if err := collision.err(); err == nil || !strings.Contains(err.Error(), "collide at wire identity") {
82
+ t.Fatalf("collision error = %v, want an injectivity rejection", err)
83
+ }
84
+
85
+ file := "D:/shared/value.ts"
86
+ id := nodeID(file, "value", NodeVariable)
87
+ _, err := NewDump(&Graph{Nodes: map[string]*Node{
88
+ id: &Node{ID: id, Name: "value", Simple: "value", Kind: NodeVariable, File: file},
89
+ }}, "C:/checkout/app", "tsconfig.json", nil, nil, DumpOrigin{})
90
+ if err == nil || !strings.Contains(err.Error(), "different filesystem roots") {
91
+ t.Fatalf("NewDump cross-root error = %v, want rejection before serialization", err)
92
+ }
93
+ }
@@ -29,7 +29,7 @@ func TestDumpSchemaVersionMatchesTheTypescriptClient(t *testing.T) {
29
29
  // The checkout is CRLF on Windows, and a line-anchored match would never
30
30
  // see the end of a line that ends in a carriage return.
31
31
  source = bytes.ReplaceAll(source, []byte("\r\n"), []byte("\n"))
32
- match := regexp.MustCompile(`(?m)^const DUMP_SCHEMA_VERSION = (\d+);$`).FindSubmatch(source)
32
+ match := regexp.MustCompile(`(?m)^(?:export )?const DUMP_SCHEMA_VERSION = (\d+);$`).FindSubmatch(source)
33
33
  if match == nil {
34
34
  t.Fatalf("no `const DUMP_SCHEMA_VERSION = <n>;` in %s; if it moved, this gate must follow it", reader)
35
35
  }
@@ -0,0 +1,87 @@
1
+ package graph
2
+
3
+ import (
4
+ "encoding/json"
5
+ "path/filepath"
6
+ "testing"
7
+ )
8
+
9
+ // TestDumpUsesECMALineTerminators verifies graph dump evidence and compact
10
+ // source display use the compiler's complete ECMAScript line model.
11
+ //
12
+ // The graph used to split only on LF, so a CR, LS, or PS file placed every
13
+ // declaration on line one and let a leading // comment consume the rest of the
14
+ // file. Each fixture drives a real compiler program and checks the independent
15
+ // dump coordinates, comment compaction, and regular-expression boundary.
16
+ //
17
+ // 1. Compile the same two-function project with each ECMAScript terminator.
18
+ // 2. Marshal its graph and find both declarations and the call edge.
19
+ // 3. Assert their evidence and source helpers agree on the three logical lines.
20
+ func TestDumpUsesECMALineTerminators(t *testing.T) {
21
+ cases := []struct {
22
+ name string
23
+ terminator string
24
+ }{
25
+ {name: "LF", terminator: "\n"},
26
+ {name: "CRLF", terminator: "\r\n"},
27
+ {name: "CR", terminator: "\r"},
28
+ {name: "LS", terminator: "\u2028"},
29
+ {name: "PS", terminator: "\u2029"},
30
+ }
31
+ for _, tc := range cases {
32
+ t.Run(tc.name, func(t *testing.T) {
33
+ source := "// lead comment" + tc.terminator +
34
+ "export function alpha(): number { return beta(); }" + tc.terminator +
35
+ "export function beta(): number { return 1; }"
36
+ root := t.TempDir()
37
+ writeFile(t, filepath.Join(root, "tsconfig.json"), fixtureTSConfig)
38
+ writeFile(t, filepath.Join(root, "src", "main.ts"), source)
39
+
40
+ var dump Dump
41
+ if err := json.Unmarshal(dumpBytes(t, root), &dump); err != nil {
42
+ t.Fatalf("dump is not valid JSON: %v", err)
43
+ }
44
+ nodes := map[string]*DumpNode{}
45
+ for i := range dump.Nodes {
46
+ node := &dump.Nodes[i]
47
+ if node.Kind == "function" {
48
+ nodes[node.Name] = node
49
+ }
50
+ }
51
+ for _, name := range []string{"alpha", "beta"} {
52
+ node := nodes[name]
53
+ if node == nil || node.Evidence == nil {
54
+ t.Fatalf("missing %s evidence: %+v", name, dump.Nodes)
55
+ }
56
+ }
57
+ if got := nodes["alpha"].Evidence; got.StartLine != 2 || got.StartCol != 1 {
58
+ t.Fatalf("alpha evidence = line %d col %d, want line 2 col 1", got.StartLine, got.StartCol)
59
+ }
60
+ if got := nodes["beta"].Evidence; got.StartLine != 3 || got.StartCol != 1 {
61
+ t.Fatalf("beta evidence = line %d col %d, want line 3 col 1", got.StartLine, got.StartCol)
62
+ }
63
+
64
+ var call *DumpEdge
65
+ for i := range dump.Edges {
66
+ edge := &dump.Edges[i]
67
+ if edge.Kind == "calls" && edge.From == "src/main.ts#alpha:function" {
68
+ call = edge
69
+ break
70
+ }
71
+ }
72
+ if call == nil || call.Evidence == nil || call.Evidence.StartLine != 2 {
73
+ t.Fatalf("alpha call evidence = %+v, want line 2", call)
74
+ }
75
+
76
+ if lines := newLineStarts(source); len(lines) != 3 {
77
+ t.Fatalf("line starts = %v, want three logical lines", lines)
78
+ }
79
+ if got := compactObjectMemberSignature("// lead" + tc.terminator + "value: 1"); got != "// lead"+tc.terminator+"value: 1" {
80
+ t.Fatalf("compact signature = %q, want line comment to end before value", got)
81
+ }
82
+ if got := regularExpressionEnd("/line"+tc.terminator+"next/", 0); got != 0 {
83
+ t.Fatalf("regular-expression end = %d, want reject a cross-line literal", got)
84
+ }
85
+ })
86
+ }
87
+ }
@@ -0,0 +1,61 @@
1
+ package graph
2
+
3
+ import (
4
+ "path/filepath"
5
+ "testing"
6
+
7
+ "github.com/samchon/ttsc/packages/ttsc/driver"
8
+ )
9
+
10
+ // TestDuplicateVariableObjectMembersKeepFirstDeclaration verifies that a legal
11
+ // repeated var declaration cannot attach its initializer to the first node.
12
+ //
13
+ // Variable symbols merge across declarations, while graph node identity keeps
14
+ // the first declaration. Collecting object members after every add used to
15
+ // combine the first declaration's span with the second declaration's members,
16
+ // producing a node that never existed in the compiler snapshot.
17
+ //
18
+ // 1. Compile two var declarations with the same symbol and different objects.
19
+ // 2. Assert the graph keeps the first declaration and its direct member.
20
+ // 3. Dump the graph and assert the same member remains on the wire.
21
+ func TestDuplicateVariableObjectMembersKeepFirstDeclaration(t *testing.T) {
22
+ root := t.TempDir()
23
+ writeFile(t, filepath.Join(root, "tsconfig.json"), fixtureTSConfig)
24
+ writeFile(t, filepath.Join(root, "src", "main.ts"), `export var duplicate = { first: 1 };
25
+ export var duplicate = { second: 2 };
26
+ `)
27
+
28
+ prog, diags, err := driver.LoadProgram(root, "tsconfig.json", driver.LoadProgramOptions{})
29
+ if err != nil {
30
+ t.Fatal(err)
31
+ }
32
+ if len(diags) != 0 {
33
+ t.Fatalf("unexpected diagnostics: %v", diags)
34
+ }
35
+ defer func() { _ = prog.Close() }()
36
+
37
+ graph := Build(prog)
38
+ path := sourceFile(t, prog, "main.ts").FileName()
39
+ node := graph.Nodes[nodeID(path, "duplicate", NodeVariable)]
40
+ if node == nil {
41
+ t.Fatalf("missing duplicate variable; nodes: %v", nodeIDSet(graph))
42
+ }
43
+ if len(node.ObjectMembers) != 1 || node.ObjectMembers[0].Name != "first" {
44
+ t.Fatalf("first declaration members were overwritten: %+v", node.ObjectMembers)
45
+ }
46
+
47
+ dump, err := NewDump(graph, root, "tsconfig.json", nil, SourceTexts(prog), DumpOrigin{})
48
+ if err != nil {
49
+ t.Fatal(err)
50
+ }
51
+ for _, dumped := range dump.Nodes {
52
+ if dumped.ID != "src/main.ts#duplicate:variable" {
53
+ continue
54
+ }
55
+ if len(dumped.ObjectMembers) != 1 || dumped.ObjectMembers[0].Name != "first" || dumped.ObjectMembers[0].Signature != "first: 1" {
56
+ t.Fatalf("dump mixed declarations: %+v", dumped.ObjectMembers)
57
+ }
58
+ return
59
+ }
60
+ t.Fatalf("dump omitted duplicate variable: %+v", dump.Nodes)
61
+ }