ttsc 0.22.0 → 0.24.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 (91) hide show
  1. package/cmd/ttscgraph/serve.go +264 -48
  2. package/cmd/ttscgraph/serve_early_error_encode_failure_is_observable_test.go +40 -0
  3. package/cmd/ttscgraph/serve_graph_snapshot_version_matches_typescript_client_test.go +32 -0
  4. package/cmd/ttscgraph/serve_phase_trace_accounts_for_load_failure_test.go +45 -0
  5. package/cmd/ttscgraph/serve_phase_trace_is_opt_in_and_payload_free_test.go +52 -0
  6. package/cmd/ttscgraph/serve_protocol_negotiates_incremental_shards_test.go +44 -0
  7. package/cmd/ttscgraph/serve_session_reloads_commonjs_package_entry_for_esm_test.go +2 -2
  8. package/cmd/ttscgraph/serve_session_reloads_superseding_module_candidates_test.go +46 -20
  9. package/cmd/ttscgraph/serve_session_retries_failed_dump_test.go +9 -9
  10. package/cmd/ttscgraph/serve_shards.go +1390 -0
  11. package/cmd/ttscgraph/serve_shards_abandoned_generation_converges_on_retry_test.go +92 -0
  12. package/cmd/ttscgraph/serve_shards_delete_removed_source_test.go +57 -0
  13. package/cmd/ttscgraph/serve_shards_external_endpoints_test.go +53 -0
  14. package/cmd/ttscgraph/serve_shards_keep_source_distributed_dependencies_at_the_boundary_test.go +108 -0
  15. package/cmd/ttscgraph/serve_shards_match_full_dump_oracle_test.go +88 -0
  16. package/cmd/ttscgraph/serve_shards_merge_pending_and_new_edits_test.go +50 -0
  17. package/cmd/ttscgraph/serve_shards_prune_unreferenced_external_endpoints_test.go +65 -0
  18. package/cmd/ttscgraph/serve_shards_publish_created_source_test.go +92 -0
  19. package/cmd/ttscgraph/serve_shards_rebuild_declaration_edit_test.go +43 -0
  20. package/cmd/ttscgraph/serve_shards_refresh_cross_file_implementation_test.go +63 -0
  21. package/cmd/ttscgraph/serve_shards_refresh_dependent_diagnostics_test.go +80 -0
  22. package/cmd/ttscgraph/serve_shards_rename_moves_source_identity_test.go +95 -0
  23. package/cmd/ttscgraph/serve_shards_retry_preserves_committed_generation_test.go +48 -0
  24. package/cmd/ttscgraph/serve_shards_reuse_unaffected_sources_test.go +121 -0
  25. package/driver/declaration_shape.go +85 -0
  26. package/driver/program.go +39 -10
  27. package/go.mod +1 -1
  28. package/internal/graph/build.go +62 -10
  29. package/internal/graph/build_files_reuses_committed_cross_file_endpoints_test.go +87 -0
  30. package/internal/graph/build_keeps_source_distributed_dependencies_at_the_external_boundary_test.go +74 -0
  31. package/internal/graph/dump.go +84 -43
  32. package/internal/graph/dump_path_mapper_caches_alias_resolution_test.go +29 -0
  33. package/internal/graph/dump_paths.go +134 -11
  34. package/internal/graph/dump_paths_canonicalize_windows_short_root_test.go +39 -0
  35. package/internal/graph/dump_rejects_provenance_only_path_errors_test.go +32 -0
  36. package/internal/graph/dump_serializes_the_full_graph_test.go +2 -2
  37. package/internal/graph/dump_symlink_project_publishes_matching_base_test.go +82 -0
  38. package/internal/graph/edges.go +58 -16
  39. package/internal/graph/exports.go +9 -2
  40. package/internal/graph/graph.go +47 -7
  41. package/internal/graph/graph_kind_contracts_match_their_producers_test.go +121 -0
  42. package/internal/graph/node_modifiers_emit_union_strings_test.go +1 -1
  43. package/internal/graph/provenance.go +11 -1
  44. package/internal/graph/resolve.go +1 -4
  45. package/internal/graph/source.go +23 -0
  46. package/internal/graph/wire_node_ids_share_alias_resolution_test.go +29 -0
  47. package/internal/lspserver/lsp_launcher_reload_directory_fingerprint_parity_windows_test.go +94 -0
  48. package/internal/lspserver/lsp_project_input_glob_path_other.go +16 -0
  49. package/internal/lspserver/lsp_project_input_glob_path_windows.go +36 -0
  50. package/internal/lspserver/lsp_project_input_path_key_other.go +17 -0
  51. package/internal/lspserver/lsp_project_input_path_key_windows.go +228 -0
  52. package/internal/lspserver/lsp_project_input_path_key_windows_test.go +460 -0
  53. package/internal/lspserver/lsp_project_inputs.go +117 -49
  54. package/internal/lspserver/lsp_reload_directories_separate_glob_territory_test.go +181 -0
  55. package/lib/flags/schema.js +1 -1
  56. package/lib/flags/schema.js.map +1 -1
  57. package/lib/internal/projectInputPathIdentity.d.ts +13 -5
  58. package/lib/internal/projectInputPathIdentity.js +95 -41
  59. package/lib/internal/projectInputPathIdentity.js.map +1 -1
  60. package/lib/internal/windowsDirectoryCaseSensitivity.d.ts +10 -0
  61. package/lib/internal/windowsDirectoryCaseSensitivity.js +34 -0
  62. package/lib/internal/windowsDirectoryCaseSensitivity.js.map +1 -0
  63. package/lib/launcher/internal/runTtsc.js +5 -0
  64. package/lib/launcher/internal/runTtsc.js.map +1 -1
  65. package/lib/launcher/internal/runTtscserver.js +59 -8
  66. package/lib/launcher/internal/runTtscserver.js.map +1 -1
  67. package/lib/launcher/internal/runTtsx.js +61 -7
  68. package/lib/launcher/internal/runTtsx.js.map +1 -1
  69. package/lib/launcher/internal/runtimeHooks.d.ts +6 -5
  70. package/lib/launcher/internal/runtimeHooks.js +7 -15
  71. package/lib/launcher/internal/runtimeHooks.js.map +1 -1
  72. package/lib/launcher/internal/watchTopology.d.ts +82 -8
  73. package/lib/launcher/internal/watchTopology.js +535 -187
  74. package/lib/launcher/internal/watchTopology.js.map +1 -1
  75. package/lib/plugin/internal/descriptorProcessFailure.d.ts +32 -0
  76. package/lib/plugin/internal/descriptorProcessFailure.js +73 -0
  77. package/lib/plugin/internal/descriptorProcessFailure.js.map +1 -0
  78. package/lib/plugin/internal/loadProjectPlugins.js +29 -8
  79. package/lib/plugin/internal/loadProjectPlugins.js.map +1 -1
  80. package/package.json +12 -8
  81. package/src/flags/schema.ts +2 -1
  82. package/src/internal/projectInputPathIdentity.ts +150 -46
  83. package/src/internal/windowsDirectoryCaseSensitivity.ts +33 -0
  84. package/src/launcher/internal/runTtsc.ts +7 -0
  85. package/src/launcher/internal/runTtscserver.ts +81 -11
  86. package/src/launcher/internal/runTtsx.ts +87 -10
  87. package/src/launcher/internal/runtimeHooks.ts +16 -16
  88. package/src/launcher/internal/watchTopology.ts +679 -208
  89. package/src/plugin/internal/descriptorProcessFailure.ts +100 -0
  90. package/src/plugin/internal/loadProjectPlugins.ts +41 -8
  91. package/tools/shim_audit/baseline.json +0 -9
@@ -0,0 +1,82 @@
1
+ package graph
2
+
3
+ import (
4
+ "os"
5
+ "path/filepath"
6
+ "testing"
7
+ )
8
+
9
+ // TestDumpSymlinkProjectPublishesMatchingBase verifies relative wire paths and
10
+ // the published project coordinate share one canonical filesystem base.
11
+ //
12
+ // A raw symlink cwd combined with canonical source paths makes `../` siblings,
13
+ // missing literal roots and a symlinked relative tsconfig resolve incorrectly
14
+ // unless project publication and path mapping use the same physical root.
15
+ //
16
+ // 1. Select a real project directory through a symlink and add sibling inputs.
17
+ // 2. Map an existing sibling, a missing child and a relative config symlink.
18
+ // 3. Require stable coordinates and a canonical published project base.
19
+ func TestDumpSymlinkProjectPublishesMatchingBase(t *testing.T) {
20
+ root := t.TempDir()
21
+ realRoot := filepath.Join(root, "real")
22
+ project := filepath.Join(realRoot, "pkg")
23
+ configDir := filepath.Join(realRoot, "config")
24
+ if err := os.MkdirAll(project, 0o755); err != nil {
25
+ t.Fatal(err)
26
+ }
27
+ if err := os.MkdirAll(configDir, 0o755); err != nil {
28
+ t.Fatal(err)
29
+ }
30
+ shared := filepath.Join(realRoot, "shared.ts")
31
+ config := filepath.Join(configDir, "tsconfig.json")
32
+ if err := os.WriteFile(shared, []byte("export const shared = true;\n"), 0o644); err != nil {
33
+ t.Fatal(err)
34
+ }
35
+ if err := os.WriteFile(config, []byte("{}\n"), 0o644); err != nil {
36
+ t.Fatal(err)
37
+ }
38
+ alias := filepath.Join(root, "project-link")
39
+ if err := os.Symlink(project, alias); err != nil {
40
+ t.Skipf("host cannot create directory symlink: %v", err)
41
+ }
42
+ if err := os.Symlink(config, filepath.Join(project, "tsconfig.json")); err != nil {
43
+ t.Skipf("host cannot create file symlink: %v", err)
44
+ }
45
+
46
+ mapper := newDumpPathMapper(alias)
47
+ if wire := mapper.mapPath(shared); wire != "../shared.ts" {
48
+ t.Fatalf("sibling wire path = %q, want ../shared.ts", wire)
49
+ }
50
+ if wire := mapper.mapPath(filepath.Join(alias, "src", "new.ts")); wire != "src/new.ts" {
51
+ t.Fatalf("missing-root wire path = %q, want src/new.ts", wire)
52
+ }
53
+ if wire := mapper.mapPath("tsconfig.json"); wire != "../config/tsconfig.json" {
54
+ t.Fatalf("relative config wire path = %q, want ../config/tsconfig.json", wire)
55
+ }
56
+ dump, err := NewDump(&Graph{Nodes: map[string]*Node{}}, alias, "tsconfig.json", nil, nil, DumpOrigin{})
57
+ if err != nil {
58
+ t.Fatal(err)
59
+ }
60
+ physicalProject, err := filepath.EvalSymlinks(alias)
61
+ if err != nil {
62
+ t.Fatal(err)
63
+ }
64
+ if filepath.Clean(filepath.FromSlash(dump.Project)) != filepath.Clean(physicalProject) {
65
+ t.Fatalf("published project = %q, want %q", dump.Project, physicalProject)
66
+ }
67
+ resolvedSibling := filepath.Clean(filepath.Join(filepath.FromSlash(dump.Project), filepath.FromSlash("../shared.ts")))
68
+ resolvedSiblingInfo, err := os.Stat(resolvedSibling)
69
+ if err != nil {
70
+ t.Fatal(err)
71
+ }
72
+ sharedInfo, err := os.Stat(shared)
73
+ if err != nil {
74
+ t.Fatal(err)
75
+ }
76
+ if !os.SameFile(resolvedSiblingInfo, sharedInfo) {
77
+ t.Fatalf("published base resolves sibling to %q, want %q", resolvedSibling, shared)
78
+ }
79
+ if dump.Tsconfig != "../config/tsconfig.json" {
80
+ t.Fatalf("published tsconfig = %q, want ../config/tsconfig.json", dump.Tsconfig)
81
+ }
82
+ }
@@ -18,9 +18,15 @@ import (
18
18
  // it annotates, a literal value set on the type that admits it. Both need the
19
19
  // same checker and the same walk, so they are resolved here and written to the
20
20
  // node the build pass keyed.
21
- func (g *Graph) addEdges(prog *driver.Program) {
21
+ func (g *Graph) addEdges(prog *driver.Program, selected map[string]bool, partial bool) {
22
22
  checker := prog.Checker
23
23
  for _, file := range prog.SourceFiles() {
24
+ if !IsWorkspaceSourceFile(file) {
25
+ continue
26
+ }
27
+ if partial && !selected[file.FileName()] {
28
+ continue
29
+ }
24
30
  g.markExports(checker, file)
25
31
  g.collectHeritage(checker, file)
26
32
  g.collectCalls(checker, file)
@@ -54,7 +60,8 @@ func (g *Graph) addEdgeAt(from, to string, kind EdgeKind, origin string, pos, en
54
60
  return
55
61
  }
56
62
  g.seen[key] = struct{}{}
57
- g.Edges = append(g.Edges, &Edge{From: from, To: to, Kind: kind, Origin: origin, Pos: pos, End: end})
63
+ file := g.edgeEvidenceFiles[from]
64
+ g.Edges = append(g.Edges, &Edge{From: from, To: to, Kind: kind, Origin: origin, File: file, Pos: pos, End: end})
58
65
  }
59
66
 
60
67
  // collectHeritage adds a heritage edge for every base of every class and
@@ -320,7 +327,7 @@ func graphMemberNodeID(g *Graph, member *shimast.Node) string {
320
327
  return ""
321
328
  }
322
329
  id := nodeID(file.FileName(), name, kind)
323
- stored, ok := g.Nodes[id]
330
+ stored, ok := g.lookupNode(id)
324
331
  if !ok {
325
332
  return ""
326
333
  }
@@ -576,8 +583,10 @@ func (g *Graph) callsWithin(checker *shimchecker.Checker, from string, node *shi
576
583
  }
577
584
  case shimast.KindBinaryExpression:
578
585
  if target := g.assignedFunctionTarget(checker, from, child); target != "" {
579
- g.recordImplementation(target, child)
580
- g.callsWithin(checker, target, child.AsBinaryExpression().Right)
586
+ file := g.recordImplementation(target, child)
587
+ g.withEdgeEvidenceFile(target, file, func() {
588
+ g.callsWithin(checker, target, child.AsBinaryExpression().Right)
589
+ })
581
590
  }
582
591
  case shimast.KindJsxSelfClosingElement:
583
592
  // `<Component />` is a use of the component; an intrinsic tag (`<div />`)
@@ -615,18 +624,45 @@ func (g *Graph) assignedFunctionTarget(checker *shimchecker.Checker, from string
615
624
  return to
616
625
  }
617
626
 
618
- func (g *Graph) recordImplementation(id string, assignment *shimast.Node) {
619
- node := g.Nodes[id]
620
- if node == nil || node.External || node.ImplementationFile != "" {
621
- return
627
+ func (g *Graph) recordImplementation(id string, assignment *shimast.Node) string {
628
+ node, ok := g.lookupNode(id)
629
+ if !ok || node.External {
630
+ return ""
622
631
  }
623
632
  file := shimast.GetSourceFileOfNode(assignment)
624
633
  if file == nil {
634
+ return ""
635
+ }
636
+ sources := g.ImplementationSources[id]
637
+ if sources == nil {
638
+ sources = map[string]bool{}
639
+ g.ImplementationSources[id] = sources
640
+ }
641
+ sources[file.FileName()] = true
642
+ current := g.Nodes[id]
643
+ if current != nil && current.ImplementationFile == "" {
644
+ current.ImplementationFile = file.FileName()
645
+ current.ImplementationPos = assignment.Pos()
646
+ current.ImplementationEnd = assignment.End()
647
+ }
648
+ return file.FileName()
649
+ }
650
+
651
+ func (g *Graph) withEdgeEvidenceFile(from, file string, visit func()) {
652
+ if file == "" {
653
+ visit()
625
654
  return
626
655
  }
627
- node.ImplementationFile = file.FileName()
628
- node.ImplementationPos = assignment.Pos()
629
- node.ImplementationEnd = assignment.End()
656
+ previous, existed := g.edgeEvidenceFiles[from]
657
+ g.edgeEvidenceFiles[from] = file
658
+ defer func() {
659
+ if existed {
660
+ g.edgeEvidenceFiles[from] = previous
661
+ } else {
662
+ delete(g.edgeEvidenceFiles, from)
663
+ }
664
+ }()
665
+ visit()
630
666
  }
631
667
 
632
668
  func isInvokedAccess(access *shimast.Node) bool {
@@ -745,7 +781,13 @@ func (g *Graph) typeRefsWithin(checker *shimchecker.Checker, from string, node *
745
781
  }
746
782
  case shimast.KindBinaryExpression:
747
783
  if target := g.assignedFunctionTarget(checker, from, child); target != "" {
748
- g.typeRefsWithin(checker, target, child.AsBinaryExpression().Right)
784
+ file := ""
785
+ if source := shimast.GetSourceFileOfNode(child); source != nil {
786
+ file = source.FileName()
787
+ }
788
+ g.withEdgeEvidenceFile(target, file, func() {
789
+ g.typeRefsWithin(checker, target, child.AsBinaryExpression().Right)
790
+ })
749
791
  }
750
792
  }
751
793
  g.typeRefsWithin(checker, from, child)
@@ -795,7 +837,7 @@ func (g *Graph) ensureTargetNode(target *Target) string {
795
837
  return ""
796
838
  }
797
839
  id := nodeID(target.File, name, NodeMethod)
798
- if _, exists := g.Nodes[id]; exists {
840
+ if _, exists := g.lookupNode(id); exists {
799
841
  return id
800
842
  }
801
843
  return ""
@@ -805,7 +847,7 @@ func (g *Graph) ensureTargetNode(target *Target) string {
805
847
  }
806
848
  name := qualifiedName(target.Symbol)
807
849
  id := nodeID(target.File, name, kind)
808
- if _, exists := g.Nodes[id]; exists {
850
+ if _, exists := g.lookupNode(id); exists {
809
851
  return id
810
852
  }
811
853
  if !target.External {
@@ -851,7 +893,7 @@ func (g *Graph) closureTargetID(target *Target) string {
851
893
  kind = NodeVariable
852
894
  }
853
895
  id := nodeID(target.File, name, kind)
854
- if _, exists := g.Nodes[id]; exists {
896
+ if _, exists := g.lookupNode(id); exists {
855
897
  return id
856
898
  }
857
899
  }
@@ -108,10 +108,17 @@ func (g *Graph) markExportedSymbol(symbol *shimast.Symbol) (string, bool) {
108
108
  if name == "" {
109
109
  return "", false
110
110
  }
111
- node, ok := g.Nodes[nodeID(file.FileName(), name, kind)]
111
+ node, ok := g.lookupNode(nodeID(file.FileName(), name, kind))
112
112
  if !ok {
113
113
  return "", false
114
114
  }
115
- node.Exported = true
115
+ g.ExportedTargets[node.ID] = true
116
+ // A complete build and an invalidation transaction that includes the
117
+ // declaration owner both hold a fresh node and may stamp it directly. Never
118
+ // mutate a base node: it belongs to the last committed generation until the
119
+ // caller has assembled and validated every replacement shard.
120
+ if current, exists := g.Nodes[node.ID]; exists {
121
+ current.Exported = true
122
+ }
116
123
  return node.ID, true
117
124
  }
@@ -171,9 +171,11 @@ const (
171
171
  )
172
172
 
173
173
  // Edge is a directed, checker-resolved relationship from one node to another,
174
- // both referenced by Node.ID. Pos and End bound the source expression in the
175
- // From node's file that produced the edge. They are evidence, not identity; a
176
- // duplicate relationship keeps the first source-order span.
174
+ // both referenced by Node.ID. File, Pos, and End bound the source expression
175
+ // that produced the edge. File is empty when the From node names it and is set
176
+ // only when an assigned implementation attributes an expression in another
177
+ // file to the declaration node it implements. They are evidence, not identity;
178
+ // a duplicate relationship keeps the first source-order span.
177
179
  type Edge struct {
178
180
  From string
179
181
  To string
@@ -186,6 +188,7 @@ type Edge struct {
186
188
  // "jsx", or "tagged"; for EdgeHeritage it is "extends" or "implements";
187
189
  // for EdgeMemberRelation it is "implements" or "overrides".
188
190
  Origin string
191
+ File string
189
192
  Pos int
190
193
  End int
191
194
  }
@@ -196,10 +199,10 @@ type Graph struct {
196
199
  Nodes map[string]*Node
197
200
  Edges []*Edge
198
201
  // Decorators holds the decorators written on the workspace's declarations,
199
- // captured syntactically so the JSON dump can emit `decorates` edges and a
200
- // consumer can interpret `@Controller`/`@Get` conventions without re-parsing
201
- // source. It is dump-only metadata, separate from Edges so the existing
202
- // checker-resolved relationships are untouched.
202
+ // captured syntactically so the JSON dump can attach raw facts to each target
203
+ // node and a consumer can interpret `@Controller`/`@Get` conventions without
204
+ // re-parsing source. It is dump-only metadata, separate from Edges so the
205
+ // existing checker-resolved relationships are untouched.
203
206
  Decorators []*Decorator
204
207
  // bodyNodes tracks whether a callable node's display span is the overload
205
208
  // implementation rather than an overload signature. It is build-only metadata
@@ -213,6 +216,43 @@ type Graph struct {
213
216
  // build: the edge pass visits the same node several times, and a resolution
214
217
  // cannot change while the program is fixed.
215
218
  resolved map[*shimast.Node]*Target
219
+
220
+ // edgeEvidenceFiles temporarily overrides a declaration node's source while
221
+ // an assigned implementation in another file is walked. Builds are
222
+ // single-threaded, and the helper that installs an override restores it
223
+ // before the traversal returns.
224
+ edgeEvidenceFiles map[string]string
225
+
226
+ // baseNodes is the immutable endpoint index of the preceding committed
227
+ // generation during a partial build. Nodes owned by selectedFiles are never
228
+ // read through it: a changed file may delete or re-key a declaration, so its
229
+ // old nodes must disappear before the new checker walk begins.
230
+ baseNodes map[string]*Node
231
+ selectedFiles map[string]bool
232
+
233
+ // ExportedTargets records every declaration reached through a selected
234
+ // module's checker export table. A partial caller uses it to add re-exported
235
+ // owner files to the same invalidation transaction before publishing.
236
+ ExportedTargets map[string]bool
237
+
238
+ // ImplementationSources records every source that assigns a function body
239
+ // to a modeled declaration, including candidates after the first assignment
240
+ // that supplied the node's display span. A partial caller uses the complete
241
+ // set to rebuild a declaration owner and all competing assignments together.
242
+ ImplementationSources map[string]map[string]bool
243
+ }
244
+
245
+ // lookupNode returns a node from this build, then from the preceding committed
246
+ // endpoint index when that node's file is not being replaced.
247
+ func (g *Graph) lookupNode(id string) (*Node, bool) {
248
+ if node, ok := g.Nodes[id]; ok {
249
+ return node, true
250
+ }
251
+ node, ok := g.baseNodes[id]
252
+ if !ok || g.selectedFiles[node.File] {
253
+ return nil, false
254
+ }
255
+ return node, true
216
256
  }
217
257
 
218
258
  // edgeKey identifies an edge by its two ends and the wire kind it will surface
@@ -0,0 +1,121 @@
1
+ package graph
2
+
3
+ import (
4
+ "os"
5
+ "path/filepath"
6
+ "regexp"
7
+ "sort"
8
+ "testing"
9
+ )
10
+
11
+ // TestGraphKindContractsMatchTheirProducers keeps the hand-authored TypeScript
12
+ // unions exact: raw dump kinds equal the Go producer, general kinds add only the
13
+ // TypeScript memory/trace synthesis, and modifiers equal the emitted flag table.
14
+ //
15
+ // 1. Read every published graph-kind and modifier union.
16
+ // 2. Derive raw dump values from the native constants and wire mapping.
17
+ // 3. Apply the memory layer's module fold and synthetic kinds.
18
+ // 4. Assert every published union equals its producer set exactly.
19
+ func TestGraphKindContractsMatchTheirProducers(t *testing.T) {
20
+ structures := filepath.Join("..", "..", "..", "graph", "src", "structures")
21
+ rawNodes := stringUnion(t, filepath.Join(structures, "TtscGraphDumpNodeKind.ts"), "TtscGraphDumpNodeKind")
22
+ rawEdges := stringUnion(t, filepath.Join(structures, "TtscGraphDumpEdgeKind.ts"), "TtscGraphDumpEdgeKind")
23
+ nodes := stringUnion(t, filepath.Join(structures, "TtscGraphNodeKind.ts"), "TtscGraphNodeKind")
24
+ edges := stringUnion(t, filepath.Join(structures, "TtscGraphEdgeKind.ts"), "TtscGraphEdgeKind")
25
+ modifiers := stringUnion(t, filepath.Join(structures, "TtscGraphNodeModifier.ts"), "TtscGraphNodeModifier")
26
+
27
+ source, err := os.ReadFile("graph.go")
28
+ if err != nil {
29
+ t.Fatalf("read graph.go: %v", err)
30
+ }
31
+ nodeConstants := sourceConstants(t, source, `(?m)^\s*Node[A-Za-z0-9_]*\s+NodeKind\s*=\s*"([^"]+)"`)
32
+ edgeConstants := sourceConstants(t, source, `(?m)^\s*Edge[A-Za-z0-9_]*\s+EdgeKind\s*=\s*"([^"]+)"`)
33
+
34
+ assertStringSet(t, "raw dump node kinds", rawNodes, nodeConstants)
35
+ assertStringSet(t, "raw dump edge kinds", rawEdges, producedWireEdgeKinds(edgeConstants))
36
+ memoryNodeKinds := withoutString(nodeConstants, "module")
37
+ assertStringSet(t, "general graph node kinds", nodes, append(memoryNodeKinds, "file", "property"))
38
+ assertStringSet(t, "general graph edge kinds", edges, append(append([]string{}, producedWireEdgeKinds(edgeConstants)...), "contains", "dispatches"))
39
+
40
+ emittedModifiers := make([]string, 0, len(modifierFlagStrings))
41
+ for _, modifier := range modifierFlagStrings {
42
+ emittedModifiers = append(emittedModifiers, modifier.text)
43
+ }
44
+ assertStringSet(t, "graph node modifiers", modifiers, emittedModifiers)
45
+ }
46
+
47
+ func withoutString(values []string, excluded string) []string {
48
+ out := make([]string, 0, len(values))
49
+ for _, value := range values {
50
+ if value != excluded {
51
+ out = append(out, value)
52
+ }
53
+ }
54
+ return out
55
+ }
56
+
57
+ func stringUnion(t *testing.T, path, name string) []string {
58
+ t.Helper()
59
+ source, err := os.ReadFile(path)
60
+ if err != nil {
61
+ t.Fatalf("read %s: %v", path, err)
62
+ }
63
+ declaration := regexp.MustCompile(`(?s)export type ` + regexp.QuoteMeta(name) + `\s*=\s*(.*?);`).FindSubmatch(source)
64
+ if declaration == nil {
65
+ t.Fatalf("find exported union %s in %s", name, path)
66
+ }
67
+ return sourceConstants(t, declaration[1], `"([^"]+)"`)
68
+ }
69
+
70
+ func sourceConstants(t *testing.T, source []byte, pattern string) []string {
71
+ t.Helper()
72
+ matches := regexp.MustCompile(pattern).FindAllSubmatch(source, -1)
73
+ if len(matches) == 0 {
74
+ t.Fatalf("no string constants matched %q", pattern)
75
+ }
76
+ values := make([]string, 0, len(matches))
77
+ for _, match := range matches {
78
+ values = append(values, string(match[1]))
79
+ }
80
+ return values
81
+ }
82
+
83
+ func producedWireEdgeKinds(constants []string) []string {
84
+ values := map[string]bool{}
85
+ for _, constant := range constants {
86
+ kind := EdgeKind(constant)
87
+ origins := []string{""}
88
+ switch kind {
89
+ case EdgeValueCall:
90
+ origins = []string{"call", "new", "jsx", "tagged"}
91
+ case EdgeHeritage:
92
+ origins = []string{"extends", "implements"}
93
+ case EdgeMemberRelation:
94
+ origins = []string{"implements", "overrides"}
95
+ }
96
+ for _, origin := range origins {
97
+ values[wireEdgeKind(kind, origin)] = true
98
+ }
99
+ }
100
+ out := make([]string, 0, len(values))
101
+ for value := range values {
102
+ out = append(out, value)
103
+ }
104
+ return out
105
+ }
106
+
107
+ func assertStringSet(t *testing.T, name string, got, want []string) {
108
+ t.Helper()
109
+ got = append([]string{}, got...)
110
+ want = append([]string{}, want...)
111
+ sort.Strings(got)
112
+ sort.Strings(want)
113
+ if len(got) != len(want) {
114
+ t.Fatalf("%s = %v, want %v", name, got, want)
115
+ }
116
+ for i := range got {
117
+ if got[i] != want[i] {
118
+ t.Fatalf("%s = %v, want %v", name, got, want)
119
+ }
120
+ }
121
+ }
@@ -87,7 +87,7 @@ export const enum Mode {
87
87
  union := map[string]bool{
88
88
  "export": true, "default": true, "declare": true, "abstract": true,
89
89
  "static": true, "readonly": true, "async": true, "const": true,
90
- "public": true, "private": true, "protected": true, "optional": true,
90
+ "public": true, "private": true, "protected": true,
91
91
  }
92
92
  for _, n := range dump.Nodes {
93
93
  for _, m := range n.Modifiers {
@@ -5,6 +5,7 @@ import (
5
5
  "encoding/hex"
6
6
  "sort"
7
7
 
8
+ shimast "github.com/microsoft/typescript-go/shim/ast"
8
9
  shimcore "github.com/microsoft/typescript-go/shim/core"
9
10
 
10
11
  "github.com/samchon/ttsc/packages/ttsc/driver"
@@ -273,7 +274,16 @@ func NewProvenance(
273
274
  // every file the graph would otherwise only have bound. Callers that do not
274
275
  // publish diagnostics should not call it.
275
276
  func NewDiagnostics(prog *driver.Program) []Diagnostic {
276
- raw := prog.Diagnostics()
277
+ return newDiagnostics(prog.Diagnostics())
278
+ }
279
+
280
+ // NewDiagnosticsForFiles projects compiler findings for one invalidated source
281
+ // closure without forcing semantic diagnostics for every unchanged file.
282
+ func NewDiagnosticsForFiles(prog *driver.Program, files []*shimast.SourceFile) []Diagnostic {
283
+ return newDiagnostics(prog.DiagnosticsForFiles(files))
284
+ }
285
+
286
+ func newDiagnostics(raw []driver.Diagnostic) []Diagnostic {
277
287
  out := make([]Diagnostic, 0, len(raw))
278
288
  for _, diagnostic := range raw {
279
289
  out = append(out, Diagnostic{
@@ -18,8 +18,6 @@
18
18
  package graph
19
19
 
20
20
  import (
21
- "strings"
22
-
23
21
  shimast "github.com/microsoft/typescript-go/shim/ast"
24
22
  shimchecker "github.com/microsoft/typescript-go/shim/checker"
25
23
  )
@@ -80,8 +78,7 @@ func Resolve(checker *shimchecker.Checker, ref *shimast.Node) *Target {
80
78
  target.End = declaration.End()
81
79
  if file := shimast.GetSourceFileOfNode(declaration); file != nil {
82
80
  target.File = file.FileName()
83
- target.External = file.IsDeclarationFile ||
84
- strings.Contains(target.File, "/node_modules/")
81
+ target.External = !IsWorkspaceSourceFile(file)
85
82
  }
86
83
  }
87
84
  return target
@@ -0,0 +1,23 @@
1
+ package graph
2
+
3
+ import (
4
+ "strings"
5
+
6
+ shimast "github.com/microsoft/typescript-go/shim/ast"
7
+ )
8
+
9
+ // IsWorkspaceSourceFile reports whether file owns declarations and outgoing
10
+ // facts in the project graph. Declaration files and sources physically resident
11
+ // under node_modules are external boundary inputs even when the checker loads a
12
+ // package's raw TypeScript entry for type checking.
13
+ //
14
+ // Symlinked workspace packages remain authored source: the checker resolves the
15
+ // default preserveSymlinks=false path to their real workspace location before
16
+ // this predicate sees it.
17
+ func IsWorkspaceSourceFile(file *shimast.SourceFile) bool {
18
+ if file == nil || file.IsDeclarationFile {
19
+ return false
20
+ }
21
+ normalized := strings.ReplaceAll(file.FileName(), "\\", "/")
22
+ return !strings.Contains("/"+strings.TrimPrefix(normalized, "/"), "/node_modules/")
23
+ }
@@ -0,0 +1,29 @@
1
+ package graph
2
+
3
+ import "testing"
4
+
5
+ // TestWireNodeIDsShareAliasResolution verifies the public resident shard
6
+ // helper keeps declarations from one physical source distinct and stable.
7
+ //
8
+ // External declarations commonly share a package source file. Mapping the IDs
9
+ // as one batch must preserve the shared source coordinate without collisions.
10
+ //
11
+ // 1. Build two declaration IDs owned by one physical dependency source.
12
+ // 2. Map both IDs through the public batch helper.
13
+ // 3. Require distinct IDs with the same portable source coordinate.
14
+ func TestWireNodeIDsShareAliasResolution(t *testing.T) {
15
+ source := "C:/checkout/app/node_modules/pkg/index.d.ts"
16
+ first := nodeID(source, "First", NodeClass)
17
+ second := nodeID(source, "Second", NodeClass)
18
+ wire, err := WireNodeIDs("C:/checkout/app", []string{first, second})
19
+ if err != nil {
20
+ t.Fatal(err)
21
+ }
22
+ if wire[first] == wire[second] {
23
+ t.Fatalf("wire IDs collide: %q", wire[first])
24
+ }
25
+ expectedSource := "node_modules/pkg/index.d.ts"
26
+ if nodeFile(wire[first]) != expectedSource || nodeFile(wire[second]) != expectedSource {
27
+ t.Fatalf("wire source coordinates = %q and %q, want %q", nodeFile(wire[first]), nodeFile(wire[second]), expectedSource)
28
+ }
29
+ }
@@ -0,0 +1,94 @@
1
+ //go:build windows
2
+
3
+ package lspserver
4
+
5
+ import (
6
+ "os"
7
+ "os/exec"
8
+ "path/filepath"
9
+ "strings"
10
+ "testing"
11
+ )
12
+
13
+ // TestLauncherReloadDirectoryFingerprintMatchesGo verifies that the JavaScript
14
+ // launcher and Go host serialize the same Windows physical directory identity.
15
+ //
16
+ // The launcher formerly retained a physical drive volume while Go lowercased
17
+ // it, and the two sides also disagreed about case folding for a missing suffix.
18
+ // A stable launcher snapshot was therefore rejected as stale during native
19
+ // watcher registration.
20
+ //
21
+ // 1. Ask the built JavaScript launcher to fingerprint one existing directory
22
+ // and one missing descendant under an ordinary case-insensitive parent.
23
+ // 2. Pass each launcher-produced digest through the real Go snapshot
24
+ // normalization path.
25
+ // 3. Prove the Go currentness validator accepts both unchanged directories.
26
+ func TestLauncherReloadDirectoryFingerprintMatchesGo(t *testing.T) {
27
+ root := t.TempDir()
28
+ existing := filepath.Join(root, "ExistingDirectory")
29
+ if err := os.Mkdir(existing, 0o755); err != nil {
30
+ t.Fatal(err)
31
+ }
32
+ for _, directory := range []string{
33
+ existing,
34
+ filepath.Join(root, "MissingDirectory"),
35
+ } {
36
+ digest := launcherReloadDirectoryDigest(t, root, directory)
37
+ snapshot, err := normalizeLSPProjectInputSnapshot(
38
+ LSPProjectInputSnapshot{
39
+ Root: root,
40
+ ReloadDirectories: []string{directory},
41
+ ReloadDirectoryDigests: map[string]string{
42
+ directory: digest,
43
+ },
44
+ },
45
+ root,
46
+ )
47
+ if err != nil {
48
+ t.Fatalf("normalize launcher fingerprint for %q: %v", directory, err)
49
+ }
50
+ if !projectInputReloadFingerprintsAreCurrent(snapshot) {
51
+ t.Fatalf("launcher fingerprint for %q began stale in Go", directory)
52
+ }
53
+ }
54
+ }
55
+
56
+ func launcherReloadDirectoryDigest(
57
+ t *testing.T,
58
+ root string,
59
+ directory string,
60
+ ) string {
61
+ t.Helper()
62
+ module, err := filepath.Abs(
63
+ filepath.Join("..", "..", "lib", "launcher", "internal", "runTtscserver.js"),
64
+ )
65
+ if err != nil {
66
+ t.Fatal(err)
67
+ }
68
+ script := `
69
+ import { pathToFileURL } from "node:url";
70
+ const launcher = await import(pathToFileURL(process.argv[1]).href);
71
+ const location = process.argv[2];
72
+ const snapshot = launcher.fingerprintInitialLSPProjectInputSnapshot({
73
+ files: [],
74
+ globs: [],
75
+ reloadDirectories: [location],
76
+ root: process.argv[3],
77
+ });
78
+ process.stdout.write(snapshot.reloadDirectoryDigests[location]);
79
+ `
80
+ command := exec.Command(
81
+ "node",
82
+ "--input-type=module",
83
+ "--eval",
84
+ script,
85
+ module,
86
+ directory,
87
+ root,
88
+ )
89
+ output, err := command.CombinedOutput()
90
+ if err != nil {
91
+ t.Fatalf("launcher fingerprint for %q: %v\n%s", directory, err, output)
92
+ }
93
+ return strings.TrimSpace(string(output))
94
+ }
@@ -0,0 +1,16 @@
1
+ //go:build !windows
2
+
3
+ package lspserver
4
+
5
+ import "strings"
6
+
7
+ func projectInputGlobMatchesCandidate(
8
+ pattern string,
9
+ candidate string,
10
+ ) bool {
11
+ return matchProjectInputGlob(
12
+ strings.Split(projectInputPathKey(realProjectInputPath(pattern)), "/"),
13
+ strings.Split(projectInputPathKey(realProjectInputPath(candidate)), "/"),
14
+ nil,
15
+ )
16
+ }