ttsc 0.23.0 → 0.25.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 (149) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +66 -63
  3. package/cmd/ttscgraph/serve.go +264 -48
  4. package/cmd/ttscgraph/serve_answers_a_project_that_implements_its_own_ambient_declaration_test.go +69 -0
  5. package/cmd/ttscgraph/serve_early_error_encode_failure_is_observable_test.go +40 -0
  6. package/cmd/ttscgraph/serve_graph_snapshot_version_matches_typescript_client_test.go +32 -0
  7. package/cmd/ttscgraph/serve_phase_trace_accounts_for_load_failure_test.go +45 -0
  8. package/cmd/ttscgraph/serve_phase_trace_is_opt_in_and_payload_free_test.go +52 -0
  9. package/cmd/ttscgraph/serve_protocol_negotiates_incremental_shards_test.go +44 -0
  10. package/cmd/ttscgraph/serve_session_reloads_commonjs_package_entry_for_esm_test.go +2 -2
  11. package/cmd/ttscgraph/serve_session_reloads_superseding_module_candidates_test.go +46 -20
  12. package/cmd/ttscgraph/serve_session_retries_failed_dump_test.go +9 -9
  13. package/cmd/ttscgraph/serve_shards.go +1390 -0
  14. package/cmd/ttscgraph/serve_shards_abandoned_generation_converges_on_retry_test.go +92 -0
  15. package/cmd/ttscgraph/serve_shards_delete_removed_source_test.go +57 -0
  16. package/cmd/ttscgraph/serve_shards_external_endpoints_test.go +53 -0
  17. package/cmd/ttscgraph/serve_shards_keep_source_distributed_dependencies_at_the_boundary_test.go +108 -0
  18. package/cmd/ttscgraph/serve_shards_match_full_dump_oracle_test.go +88 -0
  19. package/cmd/ttscgraph/serve_shards_merge_pending_and_new_edits_test.go +50 -0
  20. package/cmd/ttscgraph/serve_shards_prune_unreferenced_external_endpoints_test.go +65 -0
  21. package/cmd/ttscgraph/serve_shards_publish_created_source_test.go +92 -0
  22. package/cmd/ttscgraph/serve_shards_rebuild_declaration_edit_test.go +43 -0
  23. package/cmd/ttscgraph/serve_shards_refresh_cross_file_implementation_test.go +63 -0
  24. package/cmd/ttscgraph/serve_shards_refresh_dependent_diagnostics_test.go +80 -0
  25. package/cmd/ttscgraph/serve_shards_rename_moves_source_identity_test.go +95 -0
  26. package/cmd/ttscgraph/serve_shards_retry_preserves_committed_generation_test.go +48 -0
  27. package/cmd/ttscgraph/serve_shards_reuse_unaffected_sources_test.go +121 -0
  28. package/driver/declaration_shape.go +85 -0
  29. package/driver/program.go +39 -10
  30. package/go.sum +22 -22
  31. package/internal/graph/assigned_implementations_of_declaration_file_symbols_leave_no_edge_source_test.go +84 -0
  32. package/internal/graph/assigned_implementations_of_node_modules_symbols_leave_no_edge_source_test.go +85 -0
  33. package/internal/graph/build.go +62 -10
  34. package/internal/graph/build_files_reuses_committed_cross_file_endpoints_test.go +87 -0
  35. package/internal/graph/build_keeps_source_distributed_dependencies_at_the_external_boundary_test.go +74 -0
  36. package/internal/graph/dump.go +84 -43
  37. package/internal/graph/dump_path_mapper_caches_alias_resolution_test.go +29 -0
  38. package/internal/graph/dump_paths.go +134 -11
  39. package/internal/graph/dump_paths_canonicalize_windows_short_root_test.go +39 -0
  40. package/internal/graph/dump_rejects_provenance_only_path_errors_test.go +32 -0
  41. package/internal/graph/dump_serializes_the_full_graph_test.go +2 -2
  42. package/internal/graph/dump_symlink_project_publishes_matching_base_test.go +82 -0
  43. package/internal/graph/edges.go +73 -17
  44. package/internal/graph/exports.go +9 -2
  45. package/internal/graph/graph.go +47 -7
  46. package/internal/graph/graph_kind_contracts_match_their_producers_test.go +121 -0
  47. package/internal/graph/node_modifiers_emit_union_strings_test.go +1 -1
  48. package/internal/graph/provenance.go +11 -1
  49. package/internal/graph/resolve.go +1 -4
  50. package/internal/graph/source.go +23 -0
  51. package/internal/graph/wire_node_ids_share_alias_resolution_test.go +29 -0
  52. package/internal/lspserver/lsp_native_plugin_source.go +5 -6
  53. package/internal/lspserver/lsp_native_resident.go +17 -24
  54. package/lib/TtscService.d.ts +1 -6
  55. package/lib/TtscService.js +2 -2
  56. package/lib/TtscService.js.map +1 -1
  57. package/lib/compiler/internal/captureProcessOutput.d.ts +26 -0
  58. package/lib/compiler/internal/captureProcessOutput.js +92 -0
  59. package/lib/compiler/internal/captureProcessOutput.js.map +1 -0
  60. package/lib/compiler/internal/compileProjectInMemory.js +4 -7
  61. package/lib/compiler/internal/compileProjectInMemory.js.map +1 -1
  62. package/lib/compiler/internal/residentCheckProcess.d.ts +0 -2
  63. package/lib/compiler/internal/residentCheckProcess.js +1 -15
  64. package/lib/compiler/internal/residentCheckProcess.js.map +1 -1
  65. package/lib/compiler/internal/residentTransformProcess.d.ts +0 -6
  66. package/lib/compiler/internal/residentTransformProcess.js +0 -25
  67. package/lib/compiler/internal/residentTransformProcess.js.map +1 -1
  68. package/lib/compiler/internal/spawnNative.d.ts +14 -6
  69. package/lib/compiler/internal/spawnNative.js +32 -11
  70. package/lib/compiler/internal/spawnNative.js.map +1 -1
  71. package/lib/compiler/internal/startResidentTransform.d.ts +1 -5
  72. package/lib/compiler/internal/startResidentTransform.js +1 -3
  73. package/lib/compiler/internal/startResidentTransform.js.map +1 -1
  74. package/lib/launcher/internal/prepareExecution.d.ts +2 -1
  75. package/lib/launcher/internal/prepareExecution.js +269 -23
  76. package/lib/launcher/internal/prepareExecution.js.map +1 -1
  77. package/lib/launcher/internal/registerRuntimeHooks.js +6 -1
  78. package/lib/launcher/internal/registerRuntimeHooks.js.map +1 -1
  79. package/lib/launcher/internal/runTtscserver.js +4 -9
  80. package/lib/launcher/internal/runTtscserver.js.map +1 -1
  81. package/lib/launcher/internal/runTtsx.js +2 -55
  82. package/lib/launcher/internal/runTtsx.js.map +1 -1
  83. package/lib/launcher/internal/runtimeHooks.d.ts +25 -2
  84. package/lib/launcher/internal/runtimeHooks.js +344 -69
  85. package/lib/launcher/internal/runtimeHooks.js.map +1 -1
  86. package/lib/plugin/internal/buildSourcePlugin.js +31 -3
  87. package/lib/plugin/internal/buildSourcePlugin.js.map +1 -1
  88. package/lib/plugin/internal/descriptorProcessFailure.d.ts +23 -19
  89. package/lib/plugin/internal/descriptorProcessFailure.js +40 -51
  90. package/lib/plugin/internal/descriptorProcessFailure.js.map +1 -1
  91. package/lib/plugin/internal/loadProjectPlugins.d.ts +12 -0
  92. package/lib/plugin/internal/loadProjectPlugins.js +77 -28
  93. package/lib/plugin/internal/loadProjectPlugins.js.map +1 -1
  94. package/package.json +8 -8
  95. package/shim/ast/extra-shim.json +4 -4
  96. package/shim/ast/go.mod +14 -14
  97. package/shim/ast/go.sum +20 -20
  98. package/shim/astnav/go.mod +14 -14
  99. package/shim/astnav/go.sum +20 -20
  100. package/shim/bundled/extra-shim.json +1 -1
  101. package/shim/bundled/go.mod +5 -5
  102. package/shim/checker/extra-shim.json +31 -31
  103. package/shim/checker/go.mod +5 -5
  104. package/shim/compiler/extra-shim.json +4 -4
  105. package/shim/compiler/go.mod +14 -14
  106. package/shim/compiler/go.sum +22 -22
  107. package/shim/core/extra-shim.json +4 -4
  108. package/shim/core/go.mod +5 -5
  109. package/shim/diagnosticwriter/go.mod +5 -5
  110. package/shim/lsp/extra-shim.json +4 -4
  111. package/shim/lsp/go.mod +16 -16
  112. package/shim/lsp/go.sum +26 -26
  113. package/shim/parser/extra-shim.json +4 -4
  114. package/shim/parser/go.mod +5 -5
  115. package/shim/printer/go.mod +14 -14
  116. package/shim/printer/go.sum +22 -22
  117. package/shim/scanner/extra-shim.json +4 -4
  118. package/shim/scanner/go.mod +5 -5
  119. package/shim/transformers/go.mod +14 -14
  120. package/shim/transformers/go.sum +20 -20
  121. package/shim/tsoptions/extra-shim.json +4 -4
  122. package/shim/tsoptions/go.mod +5 -5
  123. package/shim/tspath/extra-shim.json +4 -4
  124. package/shim/tspath/go.mod +5 -5
  125. package/shim/vfs/cachedvfs/extra-shim.json +4 -4
  126. package/shim/vfs/cachedvfs/go.mod +5 -5
  127. package/shim/vfs/extra-shim.json +4 -4
  128. package/shim/vfs/go.mod +5 -5
  129. package/shim/vfs/osvfs/extra-shim.json +4 -4
  130. package/shim/vfs/osvfs/go.mod +5 -5
  131. package/src/TtscService.ts +8 -20
  132. package/src/compiler/internal/captureProcessOutput.ts +96 -0
  133. package/src/compiler/internal/compileProjectInMemory.ts +7 -7
  134. package/src/compiler/internal/residentCheckProcess.ts +1 -21
  135. package/src/compiler/internal/residentTransformProcess.ts +0 -42
  136. package/src/compiler/internal/spawnNative.ts +39 -18
  137. package/src/compiler/internal/startResidentTransform.ts +1 -12
  138. package/src/launcher/internal/prepareExecution.ts +297 -26
  139. package/src/launcher/internal/registerRuntimeHooks.ts +11 -2
  140. package/src/launcher/internal/runTtscserver.ts +4 -9
  141. package/src/launcher/internal/runTtsx.ts +2 -78
  142. package/src/launcher/internal/runtimeHooks.ts +385 -82
  143. package/src/plugin/internal/buildSourcePlugin.ts +36 -3
  144. package/src/plugin/internal/descriptorProcessFailure.ts +34 -65
  145. package/src/plugin/internal/loadProjectPlugins.ts +78 -39
  146. package/tools/gen_shims/go.mod +14 -14
  147. package/tools/gen_shims/go.sum +12 -12
  148. package/tools/shim_audit/go.mod +10 -10
  149. package/tools/shim_audit/go.sum +8 -8
@@ -0,0 +1,84 @@
1
+ package graph
2
+
3
+ import (
4
+ "path/filepath"
5
+ "testing"
6
+
7
+ "github.com/samchon/ttsc/packages/ttsc/driver"
8
+ )
9
+
10
+ // ambientGlobalFixtureTSConfig compiles a project that declares an ambient
11
+ // global in its own `.d.ts` and assigns the implementation in a sibling source.
12
+ // Both files are named because a global augmentation only reaches the program
13
+ // when its declaration file is an input.
14
+ const ambientGlobalFixtureTSConfig = `{
15
+ "compilerOptions": {
16
+ "target": "ES2022",
17
+ "module": "commonjs",
18
+ "strict": true,
19
+ "rootDir": "src",
20
+ "outDir": "dist"
21
+ },
22
+ "files": ["src/globals.d.ts", "src/main.ts"]
23
+ }
24
+ `
25
+
26
+ // TestAssignedImplementationsOfDeclarationFileSymbolsLeaveNoEdgeSource verifies
27
+ // that assigning a function to a symbol declared in a declaration file records
28
+ // no edge leaving the external boundary leaf that symbol enters the graph as.
29
+ //
30
+ // An external leaf has no outgoing side: the graph never walks a dependency's
31
+ // internals, and no shard may hold an edge whose source is one — an external
32
+ // node is owned by the non-source external shard, which is forbidden to own
33
+ // edges. Attributing the assigned body to it therefore did not merely misplace
34
+ // facts (with evidence offsets from the implementation read against the
35
+ // declaration file); it produced a snapshot `ttscgraph serve` could not
36
+ // assemble, so every request against such a project failed. The calls the body
37
+ // makes are not lost by refusing it: the module that runs the assignment owns
38
+ // them.
39
+ //
40
+ // 1. Compile a fixture whose `src/globals.d.ts` declares `var patched` in a
41
+ // global augmentation and whose `src/main.ts` assigns an arrow function to
42
+ // it that calls a local `helper`.
43
+ // 2. Build the graph.
44
+ // 3. Assert no edge leaves an external node, and that `main.ts`'s module node
45
+ // still owns the value-call edge to `helper`.
46
+ func TestAssignedImplementationsOfDeclarationFileSymbolsLeaveNoEdgeSource(t *testing.T) {
47
+ root := t.TempDir()
48
+ writeFile(t, filepath.Join(root, "tsconfig.json"), ambientGlobalFixtureTSConfig)
49
+ writeFile(t, filepath.Join(root, "src", "globals.d.ts"), `declare global {
50
+ var patched: (message: string) => void;
51
+ }
52
+ export {};
53
+ `)
54
+ writeFile(t, filepath.Join(root, "src", "main.ts"), `export function helper(): void {}
55
+ patched = (message: string): void => {
56
+ helper();
57
+ };
58
+ `)
59
+
60
+ prog, diags, err := driver.LoadProgram(root, "tsconfig.json", driver.LoadProgramOptions{})
61
+ if err != nil {
62
+ t.Fatal(err)
63
+ }
64
+ if len(diags) != 0 {
65
+ t.Fatalf("unexpected diagnostics: %v", diags)
66
+ }
67
+ defer func() { _ = prog.Close() }()
68
+
69
+ graph := Build(prog)
70
+ mainPath := sourceFile(t, prog, "main.ts").FileName()
71
+
72
+ for _, edge := range graph.Edges {
73
+ node, ok := graph.Nodes[edge.From]
74
+ if ok && node.External {
75
+ t.Fatalf("edge leaves the external boundary: %s -> %s (%s), source declared in %s", edge.From, edge.To, edge.Kind, node.File)
76
+ }
77
+ }
78
+
79
+ module := moduleID(mainPath)
80
+ helper := nodeID(mainPath, "helper", NodeFunction)
81
+ if !hasEdge(graph, module, helper, EdgeValueCall) {
82
+ t.Fatalf("the assigned body's call to helper is not owned by the running module; edges: %v", graph.Edges)
83
+ }
84
+ }
@@ -0,0 +1,85 @@
1
+ package graph
2
+
3
+ import (
4
+ "path/filepath"
5
+ "testing"
6
+
7
+ "github.com/samchon/ttsc/packages/ttsc/driver"
8
+ )
9
+
10
+ // nodeModulesGlobalFixtureTSConfig compiles a project whose ambient global is
11
+ // declared by a dependency's raw `.ts` source. Both inputs are named because
12
+ // the declaring file is reached by neither an import nor a glob, and no
13
+ // `rootDir` or `outDir` is set because a `node_modules` input under one would
14
+ // be an input outside the root.
15
+ const nodeModulesGlobalFixtureTSConfig = `{
16
+ "compilerOptions": {
17
+ "target": "ES2022",
18
+ "module": "commonjs",
19
+ "strict": true
20
+ },
21
+ "files": ["node_modules/dep/globals.ts", "src/main.ts"]
22
+ }
23
+ `
24
+
25
+ // TestAssignedImplementationsOfNodeModulesSymbolsLeaveNoEdgeSource verifies the
26
+ // other half of the external boundary: a symbol declared under `node_modules`
27
+ // gains no outgoing edge from an assignment either.
28
+ //
29
+ // `IsWorkspaceSourceFile` refuses a file for two disjoint reasons, and only one
30
+ // of them is being a declaration file. A dependency that ships raw TypeScript
31
+ // is not a declaration file and still owns no facts in this graph, so an
32
+ // assignment to something it declares must be refused by the same rule. Without
33
+ // this case the fix is pinned on one branch of the classifier and a change to
34
+ // the other would pass.
35
+ //
36
+ // 1. Compile a fixture whose `node_modules/dep/globals.ts` declares `var
37
+ // patched` in a global augmentation and whose `src/main.ts` assigns an arrow
38
+ // function to it that calls a local `helper`.
39
+ // 2. Build the graph.
40
+ // 3. Assert no edge leaves an external node, and that `main.ts`'s module node
41
+ // still owns the value-call edge to `helper`.
42
+ func TestAssignedImplementationsOfNodeModulesSymbolsLeaveNoEdgeSource(t *testing.T) {
43
+ root := t.TempDir()
44
+ writeFile(t, filepath.Join(root, "tsconfig.json"), nodeModulesGlobalFixtureTSConfig)
45
+ writeFile(t, filepath.Join(root, "node_modules", "dep", "package.json"), `{
46
+ "name": "dep",
47
+ "version": "1.0.0"
48
+ }
49
+ `)
50
+ writeFile(t, filepath.Join(root, "node_modules", "dep", "globals.ts"), `declare global {
51
+ var patched: (message: string) => void;
52
+ }
53
+ export {};
54
+ `)
55
+ writeFile(t, filepath.Join(root, "src", "main.ts"), `export function helper(): void {}
56
+ patched = (message: string): void => {
57
+ helper();
58
+ };
59
+ `)
60
+
61
+ prog, diags, err := driver.LoadProgram(root, "tsconfig.json", driver.LoadProgramOptions{})
62
+ if err != nil {
63
+ t.Fatal(err)
64
+ }
65
+ if len(diags) != 0 {
66
+ t.Fatalf("unexpected diagnostics: %v", diags)
67
+ }
68
+ defer func() { _ = prog.Close() }()
69
+
70
+ graph := Build(prog)
71
+ mainPath := sourceFile(t, prog, "main.ts").FileName()
72
+
73
+ for _, edge := range graph.Edges {
74
+ node, ok := graph.Nodes[edge.From]
75
+ if ok && node.External {
76
+ t.Fatalf("edge leaves the external boundary: %s -> %s (%s), source declared in %s", edge.From, edge.To, edge.Kind, node.File)
77
+ }
78
+ }
79
+
80
+ module := moduleID(mainPath)
81
+ helper := nodeID(mainPath, "helper", NodeFunction)
82
+ if !hasEdge(graph, module, helper, EdgeValueCall) {
83
+ t.Fatalf("the assigned body's call to helper is not owned by the running module; edges: %v", graph.Edges)
84
+ }
85
+ }
@@ -8,23 +8,53 @@ import (
8
8
  "github.com/samchon/ttsc/packages/ttsc/driver"
9
9
  )
10
10
 
11
- // Build walks the program's user-authored source files and records a node for
12
- // each top-level declaration. driver.SourceFiles already drops declaration files
13
- // and the program never compiles a dependency's `.ts`, so every node Build emits
14
- // is workspace source: External is false. External boundary leaves enter the
15
- // graph only as the resolved target of an edge (see Resolve).
11
+ // Build walks the program's workspace source files and records a node for
12
+ // each top-level declaration. The checker can load a dependency's raw `.ts`
13
+ // entry, so IsWorkspaceSourceFile owns the declaration boundary instead of
14
+ // assuming every non-declaration source is authored here. External boundary
15
+ // leaves enter the graph only as the resolved target of an edge (see Resolve).
16
16
  func Build(prog *driver.Program) *Graph {
17
+ return BuildFiles(prog, nil, nil)
18
+ }
19
+
20
+ // BuildFiles resolves graph facts owned by selected source files against one
21
+ // immutable Program. A nil selection preserves Build's complete-project
22
+ // behavior. A non-nil selection emits only replacement nodes and outgoing
23
+ // facts for those files while using baseNodes as a read-only endpoint index.
24
+ //
25
+ // This is the semantic primitive a resident shard store needs: an ordinary edit
26
+ // asks the new checker only about its invalidated closure instead of walking
27
+ // every declaration again, while cross-file targets retain the exact stable IDs
28
+ // established by the preceding committed generation.
29
+ func BuildFiles(prog *driver.Program, selected []string, baseNodes map[string]*Node) *Graph {
30
+ selectedFiles := map[string]bool{}
31
+ if selected != nil {
32
+ for _, file := range selected {
33
+ selectedFiles[file] = true
34
+ }
35
+ }
17
36
  g := &Graph{
18
- Nodes: map[string]*Node{},
19
- bodyNodes: map[string]bool{},
20
- seen: map[edgeKey]struct{}{},
21
- resolved: map[*shimast.Node]*Target{},
37
+ Nodes: map[string]*Node{},
38
+ bodyNodes: map[string]bool{},
39
+ seen: map[edgeKey]struct{}{},
40
+ resolved: map[*shimast.Node]*Target{},
41
+ edgeEvidenceFiles: map[string]string{},
42
+ baseNodes: baseNodes,
43
+ selectedFiles: selectedFiles,
44
+ ExportedTargets: map[string]bool{},
45
+ ImplementationSources: map[string]map[string]bool{},
22
46
  }
23
47
  for _, file := range prog.SourceFiles() {
48
+ if !IsWorkspaceSourceFile(file) {
49
+ continue
50
+ }
51
+ if selected != nil && !selectedFiles[file.FileName()] {
52
+ continue
53
+ }
24
54
  g.putModuleNode(file)
25
55
  collectDeclarations(g, file)
26
56
  }
27
- g.addEdges(prog)
57
+ g.addEdges(prog, selectedFiles, selected != nil)
28
58
  return g
29
59
  }
30
60
 
@@ -49,6 +79,28 @@ func SourceTexts(prog *driver.Program) map[string]string {
49
79
  return out
50
80
  }
51
81
 
82
+ // SourceTextsForFiles returns resident checker text only for the named source
83
+ // files. It is the partial-build counterpart of SourceTexts: selected graph
84
+ // shards still receive exact evidence and signatures, without walking or
85
+ // retaining every unchanged source body again.
86
+ func SourceTextsForFiles(prog *driver.Program, files []string) map[string]string {
87
+ if prog == nil || prog.TSProgram == nil {
88
+ return map[string]string{}
89
+ }
90
+ _ = prog.ApplyLinkedPlugins()
91
+ selected := make(map[string]bool, len(files))
92
+ for _, file := range files {
93
+ selected[file] = true
94
+ }
95
+ out := make(map[string]string, len(files))
96
+ for _, source := range prog.TSProgram.SourceFiles() {
97
+ if source != nil && selected[source.FileName()] {
98
+ out[source.FileName()] = source.Text()
99
+ }
100
+ }
101
+ return out
102
+ }
103
+
52
104
  // collectDeclarations records a node for each declaration statement in file,
53
105
  // plus a method node for each callable member of a class or interface, so
54
106
  // method-to-method calls have both endpoints. It descends into namespace bodies
@@ -0,0 +1,87 @@
1
+ package graph
2
+
3
+ import (
4
+ "path/filepath"
5
+ "testing"
6
+
7
+ "github.com/samchon/ttsc/packages/ttsc/driver"
8
+ )
9
+
10
+ // TestBuildFilesReusesCommittedCrossFileEndpoints verifies partial extraction
11
+ // resolves selected-file facts against, but does not re-emit, the preceding
12
+ // generation's unchanged declaration index.
13
+ //
14
+ // A member implementation edge is the boundary case: resolving the base class
15
+ // alone is insufficient because the edge targets the base member node. If a
16
+ // partial builder sees only its replacement file, that member disappears unless
17
+ // the committed endpoint index participates in checker resolution. Conversely,
18
+ // emitting the base and unrelated nodes again would turn a file edit back into
19
+ // a whole-project graph replacement.
20
+ //
21
+ // 1. Compile a base interface, one implementation and an unrelated source.
22
+ // 2. Build the complete generation, then rebuild only the implementation file
23
+ // against its node index.
24
+ // 3. Require the implementation/member facts and cross-file edge while
25
+ // rejecting re-emission of the base and unrelated nodes.
26
+ func TestBuildFilesReusesCommittedCrossFileEndpoints(t *testing.T) {
27
+ root := t.TempDir()
28
+ writeFile(t, filepath.Join(root, "tsconfig.json"), `{
29
+ "compilerOptions": {
30
+ "target": "ES2022",
31
+ "module": "commonjs",
32
+ "strict": true,
33
+ "rootDir": "src",
34
+ "outDir": "dist"
35
+ },
36
+ "include": ["src"]
37
+ }
38
+ `)
39
+ writeFile(t, filepath.Join(root, "src", "base.ts"), `export interface Base {
40
+ run(): void;
41
+ }
42
+ `)
43
+ writeFile(t, filepath.Join(root, "src", "impl.ts"), `import { Base } from "./base";
44
+ export class Impl implements Base {
45
+ run(): void {}
46
+ }
47
+ `)
48
+ writeFile(t, filepath.Join(root, "src", "unrelated.ts"), `export const unrelated = 1;
49
+ `)
50
+
51
+ prog, diags, err := driver.LoadProgram(root, "tsconfig.json", driver.LoadProgramOptions{})
52
+ if err != nil {
53
+ t.Fatal(err)
54
+ }
55
+ if len(diags) != 0 {
56
+ t.Fatalf("unexpected diagnostics: %v", diags)
57
+ }
58
+ defer func() { _ = prog.Close() }()
59
+
60
+ complete := Build(prog)
61
+ implFile := sourceFile(t, prog, "impl.ts").FileName()
62
+ baseFile := sourceFile(t, prog, "base.ts").FileName()
63
+ unrelatedFile := sourceFile(t, prog, "unrelated.ts").FileName()
64
+ partial := BuildFiles(prog, []string{implFile}, complete.Nodes)
65
+
66
+ implMethod := nodeID(implFile, "Impl.run", NodeMethod)
67
+ baseMethod := nodeID(baseFile, "Base.run", NodeMethod)
68
+ if partial.Nodes[implMethod] == nil {
69
+ t.Fatalf("partial build omitted selected method %s", implMethod)
70
+ }
71
+ for _, node := range partial.Nodes {
72
+ if node.File == baseFile || node.File == unrelatedFile {
73
+ t.Fatalf("partial build re-emitted unchanged node %s", node.ID)
74
+ }
75
+ }
76
+ found := false
77
+ for _, edge := range partial.Edges {
78
+ if edge.From == implMethod && edge.To == baseMethod &&
79
+ edge.Kind == EdgeMemberRelation && edge.Origin == "implements" {
80
+ found = true
81
+ break
82
+ }
83
+ }
84
+ if !found {
85
+ t.Fatalf("partial build omitted checker-valid member edge %s -> %s", implMethod, baseMethod)
86
+ }
87
+ }
@@ -0,0 +1,74 @@
1
+ package graph
2
+
3
+ import (
4
+ "path/filepath"
5
+ "strings"
6
+ "testing"
7
+
8
+ "github.com/samchon/ttsc/packages/ttsc/driver"
9
+ )
10
+
11
+ // TestBuildKeepsSourceDistributedDependenciesAtTheExternalBoundary proves a
12
+ // package whose public entry is raw TypeScript remains a referenced leaf rather
13
+ // than becoming authored graph content. The resident source text is retained as
14
+ // provenance evidence even though its declarations are not walked.
15
+ //
16
+ // 1. Load one workspace source that imports a raw TypeScript package entry.
17
+ // 2. Assert only the referenced dependency symbol becomes an external leaf.
18
+ // 3. Assert provenance retains the dependency source text without its facts.
19
+ func TestBuildKeepsSourceDistributedDependenciesAtTheExternalBoundary(t *testing.T) {
20
+ root := t.TempDir()
21
+ writeFile(t, filepath.Join(root, "tsconfig.json"), `{
22
+ "compilerOptions": { "target": "ES2022", "module": "commonjs", "strict": true },
23
+ "files": ["src/main.ts"]
24
+ }
25
+ `)
26
+ writeFile(t, filepath.Join(root, "node_modules", "dep-src", "package.json"), `{
27
+ "name": "dep-src",
28
+ "version": "1.0.0",
29
+ "main": "src/index.ts"
30
+ }
31
+ `)
32
+ writeFile(t, filepath.Join(root, "node_modules", "dep-src", "src", "index.ts"), `export function dependencyValue(): number { return 1; }
33
+ export function dependencyInternal(): number { return dependencyValue(); }
34
+ `)
35
+ writeFile(t, filepath.Join(root, "src", "main.ts"), `import { dependencyValue } from "dep-src";
36
+ export function workspaceValue(): number { return dependencyValue() + 1; }
37
+ `)
38
+
39
+ program, diagnostics, err := driver.LoadProgram(root, "tsconfig.json", driver.LoadProgramOptions{})
40
+ if err != nil {
41
+ t.Fatal(err)
42
+ }
43
+ if len(diagnostics) != 0 {
44
+ t.Fatalf("unexpected diagnostics: %v", diagnostics)
45
+ }
46
+ defer func() { _ = program.Close() }()
47
+
48
+ built := Build(program)
49
+ dependency := findNodeByName(built, "dependencyValue")
50
+ if dependency == nil || !dependency.External || !strings.Contains(filepath.ToSlash(dependency.File), "/node_modules/dep-src/") {
51
+ t.Fatalf("dependencyValue should be one external boundary leaf: %+v", dependency)
52
+ }
53
+ if internal := findNodeByName(built, "dependencyInternal"); internal != nil {
54
+ t.Fatalf("unreferenced dependency implementation leaked into the authored graph: %+v", internal)
55
+ }
56
+ workspace := findNodeByName(built, "workspaceValue")
57
+ if workspace == nil || workspace.External {
58
+ t.Fatalf("workspaceValue should remain authored source: %+v", workspace)
59
+ }
60
+ if !hasEdge(built, workspace.ID, dependency.ID, EdgeValueCall) {
61
+ t.Fatalf("missing workspaceValue -> dependencyValue boundary call: %v", built.Edges)
62
+ }
63
+
64
+ dependencyTextRetained := false
65
+ for file := range SourceTexts(program) {
66
+ if strings.Contains(filepath.ToSlash(file), "/node_modules/dep-src/") {
67
+ dependencyTextRetained = true
68
+ break
69
+ }
70
+ }
71
+ if !dependencyTextRetained {
72
+ t.Fatal("resident dependency source was removed from provenance text")
73
+ }
74
+ }
@@ -30,7 +30,8 @@ import (
30
30
  // expression. It is display/expansion only, never identity.
31
31
  //
32
32
  // File is omitted when the reader reconstructs it exactly: a node's span is in
33
- // the node's file, and an edge's span is in the file its `from` id names. The
33
+ // the node's file, and an ordinary edge's span is in the file its `from` id
34
+ // names. Cross-file assigned implementations keep the actual evidence file. The
34
35
  // path is long, it rode the wire once per node and once per edge, and on VS Code
35
36
  // those two copies are 55 MB of a 323 MB document that then has to be encoded,
36
37
  // piped, parsed and validated. An `implementation` span keeps its file — that one
@@ -131,6 +132,14 @@ type Dump struct {
131
132
  Edges []DumpEdge `json:"edges"`
132
133
  }
133
134
 
135
+ // DumpFacts is the path-normalized fact payload of one graph shard. It omits
136
+ // project-wide provenance and diagnostics so an incremental producer can
137
+ // project one invalidated closure without reconstructing a complete Dump.
138
+ type DumpFacts struct {
139
+ Nodes []DumpNode `json:"nodes"`
140
+ Edges []DumpEdge `json:"edges"`
141
+ }
142
+
134
143
  // DumpOrigin is the snapshot evidence a caller attaches to a dump: who built it
135
144
  // and what the same program generation had to say about the code. It is a
136
145
  // separate struct because only the commands that own a compiler session can
@@ -153,6 +162,61 @@ type DumpOrigin struct {
153
162
  // error before serialization when a path is on another filesystem root or two
154
163
  // physical sources would collide at one wire identity.
155
164
  func NewDump(g *Graph, project, tsconfig string, ignored map[string]bool, sources map[string]string, origin DumpOrigin) (Dump, error) {
165
+ facts, ctx, err := newDumpFacts(g, project, ignored, sources)
166
+ if err != nil {
167
+ return Dump{}, err
168
+ }
169
+
170
+ // The schema version describes this code, not the caller's belief about it,
171
+ // so stamp it here rather than trusting what came in.
172
+ provenance := ctx.provenance(origin.Provenance)
173
+ provenance.SchemaVersion = DumpSchemaVersion
174
+
175
+ // A nil slice encodes as JSON null, and null is not an empty list: a reader
176
+ // validating `string[]` rejects it, and a reader that does not would have to
177
+ // guess which of the two the producer meant. Every list on the wire is a list.
178
+ if provenance.Capabilities == nil {
179
+ provenance.Capabilities = []string{}
180
+ }
181
+ if provenance.Sources == nil {
182
+ provenance.Sources = []SourceDigest{}
183
+ }
184
+ if provenance.Universe.Configs == nil {
185
+ provenance.Universe.Configs = []FileDigest{}
186
+ }
187
+ if provenance.Universe.Roots == nil {
188
+ provenance.Universe.Roots = []RootFile{}
189
+ }
190
+
191
+ diagnostics := ctx.diagnostics(origin.Diagnostics)
192
+ if diagnostics == nil {
193
+ diagnostics = []Diagnostic{}
194
+ }
195
+ target := ctx.rel(tsconfig)
196
+ if err := ctx.pathError(); err != nil {
197
+ return Dump{}, err
198
+ }
199
+
200
+ return Dump{
201
+ Project: ctx.paths.project,
202
+ Tsconfig: target,
203
+ Provenance: provenance,
204
+ Diagnostics: diagnostics,
205
+ Nodes: facts.Nodes,
206
+ Edges: facts.Edges,
207
+ }, nil
208
+ }
209
+
210
+ // NewDumpFacts projects only one graph's nodes and edges through the complete
211
+ // dump path/evidence codec. The caller may pass a partial graph produced by
212
+ // BuildFiles; no project-wide provenance, diagnostic walk or full Dump is
213
+ // constructed.
214
+ func NewDumpFacts(g *Graph, project string, ignored map[string]bool, sources map[string]string) (DumpFacts, error) {
215
+ facts, _, err := newDumpFacts(g, project, ignored, sources)
216
+ return facts, err
217
+ }
218
+
219
+ func newDumpFacts(g *Graph, project string, ignored map[string]bool, sources map[string]string) (DumpFacts, *dumpContext, error) {
156
220
  ctx := newDumpContext(project, sources)
157
221
 
158
222
  // Decorators ride on their target node; group by the internal node id before
@@ -203,11 +267,13 @@ func NewDump(g *Graph, project, tsconfig string, ignored map[string]bool, source
203
267
 
204
268
  edges := make([]DumpEdge, 0, len(g.Edges))
205
269
  for _, e := range g.Edges {
270
+ includeEvidenceFile := e.File != "" && e.File != nodeFile(e.From)
271
+ evidence := ctx.edgeEvidence(e, includeEvidenceFile)
206
272
  edges = append(edges, DumpEdge{
207
273
  From: ctx.relID(e.From),
208
274
  To: ctx.relID(e.To),
209
275
  Kind: dumpEdgeKind(e),
210
- Evidence: withoutFile(ctx.edgeEvidence(e)),
276
+ Evidence: evidence,
211
277
  })
212
278
  }
213
279
  sort.Slice(edges, func(i, j int) bool {
@@ -220,44 +286,10 @@ func NewDump(g *Graph, project, tsconfig string, ignored map[string]bool, source
220
286
  return edges[i].Kind < edges[j].Kind
221
287
  })
222
288
 
223
- // The schema version describes this code, not the caller's belief about it,
224
- // so stamp it here rather than trusting what came in.
225
- provenance := ctx.provenance(origin.Provenance)
226
- provenance.SchemaVersion = DumpSchemaVersion
227
-
228
- // A nil slice encodes as JSON null, and null is not an empty list: a reader
229
- // validating `string[]` rejects it, and a reader that does not would have to
230
- // guess which of the two the producer meant. Every list on the wire is a list.
231
- if provenance.Capabilities == nil {
232
- provenance.Capabilities = []string{}
233
- }
234
- if provenance.Sources == nil {
235
- provenance.Sources = []SourceDigest{}
236
- }
237
- if provenance.Universe.Configs == nil {
238
- provenance.Universe.Configs = []FileDigest{}
239
- }
240
- if provenance.Universe.Roots == nil {
241
- provenance.Universe.Roots = []RootFile{}
242
- }
243
-
244
- diagnostics := ctx.diagnostics(origin.Diagnostics)
245
- if diagnostics == nil {
246
- diagnostics = []Diagnostic{}
247
- }
248
-
249
- dump := Dump{
250
- Project: project,
251
- Tsconfig: ctx.rel(tsconfig),
252
- Provenance: provenance,
253
- Diagnostics: diagnostics,
254
- Nodes: nodes,
255
- Edges: edges,
256
- }
257
289
  if err := ctx.pathError(); err != nil {
258
- return Dump{}, err
290
+ return DumpFacts{}, ctx, err
259
291
  }
260
- return dump, nil
292
+ return DumpFacts{Nodes: nodes, Edges: edges}, ctx, nil
261
293
  }
262
294
 
263
295
  // dumpEnumMembers projects an enum's members onto the wire shape, nil for a
@@ -443,6 +475,10 @@ func (c *dumpContext) relID(id string) string {
443
475
  // evidence builds the line/col span for a byte range in file, or nil when the
444
476
  // span is absent or no source is available.
445
477
  func (c *dumpContext) evidence(file string, pos, end int) *DumpEvidence {
478
+ return c.evidenceWithFile(file, pos, end, true)
479
+ }
480
+
481
+ func (c *dumpContext) evidenceWithFile(file string, pos, end int, includeFile bool) *DumpEvidence {
446
482
  if pos < 0 || c.sources == nil {
447
483
  return nil
448
484
  }
@@ -464,7 +500,10 @@ func (c *dumpContext) evidence(file string, pos, end int) *DumpEvidence {
464
500
  // or its indentation.
465
501
  pos = FirstCodeOffset(text, pos)
466
502
  sl, sc := ls.at(pos)
467
- ev := &DumpEvidence{File: c.rel(file), StartLine: sl, StartCol: sc}
503
+ ev := &DumpEvidence{StartLine: sl, StartCol: sc}
504
+ if includeFile {
505
+ ev.File = c.rel(file)
506
+ }
468
507
  if end > pos && end <= len(text) {
469
508
  ev.EndLine, ev.EndCol = ls.at(end)
470
509
  }
@@ -646,13 +685,15 @@ func regularExpressionEnd(text string, start int) int {
646
685
  }
647
686
 
648
687
  // edgeEvidence is the evidence range for an edge's source expression.
649
- func (c *dumpContext) edgeEvidence(e *Edge) *DumpEvidence {
650
- // The edge's span lives in the From node's file.
651
- file := nodeFile(e.From)
688
+ func (c *dumpContext) edgeEvidence(e *Edge, includeFile bool) *DumpEvidence {
689
+ file := e.File
690
+ if file == "" {
691
+ file = nodeFile(e.From)
692
+ }
652
693
  if file == "" {
653
694
  return nil
654
695
  }
655
- return c.evidence(file, e.Pos, e.End)
696
+ return c.evidenceWithFile(file, e.Pos, e.End, includeFile)
656
697
  }
657
698
 
658
699
  // withoutFile drops a span file the reader reconstructs from the node or edge
@@ -0,0 +1,29 @@
1
+ package graph
2
+
3
+ import "testing"
4
+
5
+ // TestDumpPathMapperCachesAliasResolution verifies repeated graph facts do not
6
+ // repeat filesystem canonicalization for the same raw source path.
7
+ //
8
+ // A large graph maps one source through its node, IDs and many edge endpoints;
9
+ // caching only after EvalSymlinks would turn those facts into repeated syscalls.
10
+ //
11
+ // 1. Install a counting canonicalizer on one mapper.
12
+ // 2. Map the same absolute source path repeatedly.
13
+ // 3. Require one canonicalization and one stable wire coordinate.
14
+ func TestDumpPathMapperCachesAliasResolution(t *testing.T) {
15
+ mapper := newDumpPathMapper("C:/checkout/app")
16
+ calls := 0
17
+ mapper.canonicalize = func(path string) string {
18
+ calls++
19
+ return path
20
+ }
21
+ for range 100 {
22
+ if wire := mapper.mapPath("C:/checkout/app/src/main.ts"); wire != "src/main.ts" {
23
+ t.Fatalf("wire path = %q, want src/main.ts", wire)
24
+ }
25
+ }
26
+ if calls != 1 {
27
+ t.Fatalf("canonicalizer calls = %d, want 1", calls)
28
+ }
29
+ }