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.
- package/cmd/ttscgraph/serve.go +264 -48
- package/cmd/ttscgraph/serve_early_error_encode_failure_is_observable_test.go +40 -0
- package/cmd/ttscgraph/serve_graph_snapshot_version_matches_typescript_client_test.go +32 -0
- package/cmd/ttscgraph/serve_phase_trace_accounts_for_load_failure_test.go +45 -0
- package/cmd/ttscgraph/serve_phase_trace_is_opt_in_and_payload_free_test.go +52 -0
- package/cmd/ttscgraph/serve_protocol_negotiates_incremental_shards_test.go +44 -0
- package/cmd/ttscgraph/serve_session_reloads_commonjs_package_entry_for_esm_test.go +2 -2
- package/cmd/ttscgraph/serve_session_reloads_superseding_module_candidates_test.go +46 -20
- package/cmd/ttscgraph/serve_session_retries_failed_dump_test.go +9 -9
- package/cmd/ttscgraph/serve_shards.go +1390 -0
- package/cmd/ttscgraph/serve_shards_abandoned_generation_converges_on_retry_test.go +92 -0
- package/cmd/ttscgraph/serve_shards_delete_removed_source_test.go +57 -0
- package/cmd/ttscgraph/serve_shards_external_endpoints_test.go +53 -0
- package/cmd/ttscgraph/serve_shards_keep_source_distributed_dependencies_at_the_boundary_test.go +108 -0
- package/cmd/ttscgraph/serve_shards_match_full_dump_oracle_test.go +88 -0
- package/cmd/ttscgraph/serve_shards_merge_pending_and_new_edits_test.go +50 -0
- package/cmd/ttscgraph/serve_shards_prune_unreferenced_external_endpoints_test.go +65 -0
- package/cmd/ttscgraph/serve_shards_publish_created_source_test.go +92 -0
- package/cmd/ttscgraph/serve_shards_rebuild_declaration_edit_test.go +43 -0
- package/cmd/ttscgraph/serve_shards_refresh_cross_file_implementation_test.go +63 -0
- package/cmd/ttscgraph/serve_shards_refresh_dependent_diagnostics_test.go +80 -0
- package/cmd/ttscgraph/serve_shards_rename_moves_source_identity_test.go +95 -0
- package/cmd/ttscgraph/serve_shards_retry_preserves_committed_generation_test.go +48 -0
- package/cmd/ttscgraph/serve_shards_reuse_unaffected_sources_test.go +121 -0
- package/driver/declaration_shape.go +85 -0
- package/driver/program.go +39 -10
- package/go.mod +1 -1
- package/internal/graph/build.go +62 -10
- package/internal/graph/build_files_reuses_committed_cross_file_endpoints_test.go +87 -0
- package/internal/graph/build_keeps_source_distributed_dependencies_at_the_external_boundary_test.go +74 -0
- package/internal/graph/dump.go +84 -43
- package/internal/graph/dump_path_mapper_caches_alias_resolution_test.go +29 -0
- package/internal/graph/dump_paths.go +134 -11
- package/internal/graph/dump_paths_canonicalize_windows_short_root_test.go +39 -0
- package/internal/graph/dump_rejects_provenance_only_path_errors_test.go +32 -0
- package/internal/graph/dump_serializes_the_full_graph_test.go +2 -2
- package/internal/graph/dump_symlink_project_publishes_matching_base_test.go +82 -0
- package/internal/graph/edges.go +58 -16
- package/internal/graph/exports.go +9 -2
- package/internal/graph/graph.go +47 -7
- package/internal/graph/graph_kind_contracts_match_their_producers_test.go +121 -0
- package/internal/graph/node_modifiers_emit_union_strings_test.go +1 -1
- package/internal/graph/provenance.go +11 -1
- package/internal/graph/resolve.go +1 -4
- package/internal/graph/source.go +23 -0
- package/internal/graph/wire_node_ids_share_alias_resolution_test.go +29 -0
- package/internal/lspserver/lsp_launcher_reload_directory_fingerprint_parity_windows_test.go +94 -0
- package/internal/lspserver/lsp_project_input_glob_path_other.go +16 -0
- package/internal/lspserver/lsp_project_input_glob_path_windows.go +36 -0
- package/internal/lspserver/lsp_project_input_path_key_other.go +17 -0
- package/internal/lspserver/lsp_project_input_path_key_windows.go +228 -0
- package/internal/lspserver/lsp_project_input_path_key_windows_test.go +460 -0
- package/internal/lspserver/lsp_project_inputs.go +117 -49
- package/internal/lspserver/lsp_reload_directories_separate_glob_territory_test.go +181 -0
- package/lib/flags/schema.js +1 -1
- package/lib/flags/schema.js.map +1 -1
- package/lib/internal/projectInputPathIdentity.d.ts +13 -5
- package/lib/internal/projectInputPathIdentity.js +95 -41
- package/lib/internal/projectInputPathIdentity.js.map +1 -1
- package/lib/internal/windowsDirectoryCaseSensitivity.d.ts +10 -0
- package/lib/internal/windowsDirectoryCaseSensitivity.js +34 -0
- package/lib/internal/windowsDirectoryCaseSensitivity.js.map +1 -0
- package/lib/launcher/internal/runTtsc.js +5 -0
- package/lib/launcher/internal/runTtsc.js.map +1 -1
- package/lib/launcher/internal/runTtscserver.js +59 -8
- package/lib/launcher/internal/runTtscserver.js.map +1 -1
- package/lib/launcher/internal/runTtsx.js +61 -7
- package/lib/launcher/internal/runTtsx.js.map +1 -1
- package/lib/launcher/internal/runtimeHooks.d.ts +6 -5
- package/lib/launcher/internal/runtimeHooks.js +7 -15
- package/lib/launcher/internal/runtimeHooks.js.map +1 -1
- package/lib/launcher/internal/watchTopology.d.ts +82 -8
- package/lib/launcher/internal/watchTopology.js +535 -187
- package/lib/launcher/internal/watchTopology.js.map +1 -1
- package/lib/plugin/internal/descriptorProcessFailure.d.ts +32 -0
- package/lib/plugin/internal/descriptorProcessFailure.js +73 -0
- package/lib/plugin/internal/descriptorProcessFailure.js.map +1 -0
- package/lib/plugin/internal/loadProjectPlugins.js +29 -8
- package/lib/plugin/internal/loadProjectPlugins.js.map +1 -1
- package/package.json +12 -8
- package/src/flags/schema.ts +2 -1
- package/src/internal/projectInputPathIdentity.ts +150 -46
- package/src/internal/windowsDirectoryCaseSensitivity.ts +33 -0
- package/src/launcher/internal/runTtsc.ts +7 -0
- package/src/launcher/internal/runTtscserver.ts +81 -11
- package/src/launcher/internal/runTtsx.ts +87 -10
- package/src/launcher/internal/runtimeHooks.ts +16 -16
- package/src/launcher/internal/watchTopology.ts +679 -208
- package/src/plugin/internal/descriptorProcessFailure.ts +100 -0
- package/src/plugin/internal/loadProjectPlugins.ts +41 -8
- 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
|
+
}
|
package/internal/graph/edges.go
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
620
|
-
if
|
|
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
|
-
|
|
628
|
-
|
|
629
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
111
|
+
node, ok := g.lookupNode(nodeID(file.FileName(), name, kind))
|
|
112
112
|
if !ok {
|
|
113
113
|
return "", false
|
|
114
114
|
}
|
|
115
|
-
node.
|
|
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
|
}
|
package/internal/graph/graph.go
CHANGED
|
@@ -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
|
|
175
|
-
//
|
|
176
|
-
//
|
|
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
|
|
200
|
-
// consumer can interpret `@Controller`/`@Get` conventions without
|
|
201
|
-
// source. It is dump-only metadata, separate from Edges so the
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
+
}
|