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.
- package/LICENSE +21 -21
- package/README.md +66 -63
- package/cmd/ttscgraph/serve.go +264 -48
- package/cmd/ttscgraph/serve_answers_a_project_that_implements_its_own_ambient_declaration_test.go +69 -0
- 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.sum +22 -22
- package/internal/graph/assigned_implementations_of_declaration_file_symbols_leave_no_edge_source_test.go +84 -0
- package/internal/graph/assigned_implementations_of_node_modules_symbols_leave_no_edge_source_test.go +85 -0
- 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 +73 -17
- 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_native_plugin_source.go +5 -6
- package/internal/lspserver/lsp_native_resident.go +17 -24
- package/lib/TtscService.d.ts +1 -6
- package/lib/TtscService.js +2 -2
- package/lib/TtscService.js.map +1 -1
- package/lib/compiler/internal/captureProcessOutput.d.ts +26 -0
- package/lib/compiler/internal/captureProcessOutput.js +92 -0
- package/lib/compiler/internal/captureProcessOutput.js.map +1 -0
- package/lib/compiler/internal/compileProjectInMemory.js +4 -7
- package/lib/compiler/internal/compileProjectInMemory.js.map +1 -1
- package/lib/compiler/internal/residentCheckProcess.d.ts +0 -2
- package/lib/compiler/internal/residentCheckProcess.js +1 -15
- package/lib/compiler/internal/residentCheckProcess.js.map +1 -1
- package/lib/compiler/internal/residentTransformProcess.d.ts +0 -6
- package/lib/compiler/internal/residentTransformProcess.js +0 -25
- package/lib/compiler/internal/residentTransformProcess.js.map +1 -1
- package/lib/compiler/internal/spawnNative.d.ts +14 -6
- package/lib/compiler/internal/spawnNative.js +32 -11
- package/lib/compiler/internal/spawnNative.js.map +1 -1
- package/lib/compiler/internal/startResidentTransform.d.ts +1 -5
- package/lib/compiler/internal/startResidentTransform.js +1 -3
- package/lib/compiler/internal/startResidentTransform.js.map +1 -1
- package/lib/launcher/internal/prepareExecution.d.ts +2 -1
- package/lib/launcher/internal/prepareExecution.js +269 -23
- package/lib/launcher/internal/prepareExecution.js.map +1 -1
- package/lib/launcher/internal/registerRuntimeHooks.js +6 -1
- package/lib/launcher/internal/registerRuntimeHooks.js.map +1 -1
- package/lib/launcher/internal/runTtscserver.js +4 -9
- package/lib/launcher/internal/runTtscserver.js.map +1 -1
- package/lib/launcher/internal/runTtsx.js +2 -55
- package/lib/launcher/internal/runTtsx.js.map +1 -1
- package/lib/launcher/internal/runtimeHooks.d.ts +25 -2
- package/lib/launcher/internal/runtimeHooks.js +344 -69
- package/lib/launcher/internal/runtimeHooks.js.map +1 -1
- package/lib/plugin/internal/buildSourcePlugin.js +31 -3
- package/lib/plugin/internal/buildSourcePlugin.js.map +1 -1
- package/lib/plugin/internal/descriptorProcessFailure.d.ts +23 -19
- package/lib/plugin/internal/descriptorProcessFailure.js +40 -51
- package/lib/plugin/internal/descriptorProcessFailure.js.map +1 -1
- package/lib/plugin/internal/loadProjectPlugins.d.ts +12 -0
- package/lib/plugin/internal/loadProjectPlugins.js +77 -28
- package/lib/plugin/internal/loadProjectPlugins.js.map +1 -1
- package/package.json +8 -8
- package/shim/ast/extra-shim.json +4 -4
- package/shim/ast/go.mod +14 -14
- package/shim/ast/go.sum +20 -20
- package/shim/astnav/go.mod +14 -14
- package/shim/astnav/go.sum +20 -20
- package/shim/bundled/extra-shim.json +1 -1
- package/shim/bundled/go.mod +5 -5
- package/shim/checker/extra-shim.json +31 -31
- package/shim/checker/go.mod +5 -5
- package/shim/compiler/extra-shim.json +4 -4
- package/shim/compiler/go.mod +14 -14
- package/shim/compiler/go.sum +22 -22
- package/shim/core/extra-shim.json +4 -4
- package/shim/core/go.mod +5 -5
- package/shim/diagnosticwriter/go.mod +5 -5
- package/shim/lsp/extra-shim.json +4 -4
- package/shim/lsp/go.mod +16 -16
- package/shim/lsp/go.sum +26 -26
- package/shim/parser/extra-shim.json +4 -4
- package/shim/parser/go.mod +5 -5
- package/shim/printer/go.mod +14 -14
- package/shim/printer/go.sum +22 -22
- package/shim/scanner/extra-shim.json +4 -4
- package/shim/scanner/go.mod +5 -5
- package/shim/transformers/go.mod +14 -14
- package/shim/transformers/go.sum +20 -20
- package/shim/tsoptions/extra-shim.json +4 -4
- package/shim/tsoptions/go.mod +5 -5
- package/shim/tspath/extra-shim.json +4 -4
- package/shim/tspath/go.mod +5 -5
- package/shim/vfs/cachedvfs/extra-shim.json +4 -4
- package/shim/vfs/cachedvfs/go.mod +5 -5
- package/shim/vfs/extra-shim.json +4 -4
- package/shim/vfs/go.mod +5 -5
- package/shim/vfs/osvfs/extra-shim.json +4 -4
- package/shim/vfs/osvfs/go.mod +5 -5
- package/src/TtscService.ts +8 -20
- package/src/compiler/internal/captureProcessOutput.ts +96 -0
- package/src/compiler/internal/compileProjectInMemory.ts +7 -7
- package/src/compiler/internal/residentCheckProcess.ts +1 -21
- package/src/compiler/internal/residentTransformProcess.ts +0 -42
- package/src/compiler/internal/spawnNative.ts +39 -18
- package/src/compiler/internal/startResidentTransform.ts +1 -12
- package/src/launcher/internal/prepareExecution.ts +297 -26
- package/src/launcher/internal/registerRuntimeHooks.ts +11 -2
- package/src/launcher/internal/runTtscserver.ts +4 -9
- package/src/launcher/internal/runTtsx.ts +2 -78
- package/src/launcher/internal/runtimeHooks.ts +385 -82
- package/src/plugin/internal/buildSourcePlugin.ts +36 -3
- package/src/plugin/internal/descriptorProcessFailure.ts +34 -65
- package/src/plugin/internal/loadProjectPlugins.ts +78 -39
- package/tools/gen_shims/go.mod +14 -14
- package/tools/gen_shims/go.sum +12 -12
- package/tools/shim_audit/go.mod +10 -10
- 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
|
+
}
|
package/internal/graph/assigned_implementations_of_node_modules_symbols_leave_no_edge_source_test.go
ADDED
|
@@ -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
|
+
}
|
package/internal/graph/build.go
CHANGED
|
@@ -8,23 +8,53 @@ import (
|
|
|
8
8
|
"github.com/samchon/ttsc/packages/ttsc/driver"
|
|
9
9
|
)
|
|
10
10
|
|
|
11
|
-
// Build walks the program's
|
|
12
|
-
// each top-level declaration.
|
|
13
|
-
//
|
|
14
|
-
//
|
|
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:
|
|
19
|
-
bodyNodes:
|
|
20
|
-
seen:
|
|
21
|
-
resolved:
|
|
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
|
+
}
|
package/internal/graph/build_keeps_source_distributed_dependencies_at_the_external_boundary_test.go
ADDED
|
@@ -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
|
+
}
|
package/internal/graph/dump.go
CHANGED
|
@@ -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
|
|
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:
|
|
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
|
|
290
|
+
return DumpFacts{}, ctx, err
|
|
259
291
|
}
|
|
260
|
-
return
|
|
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{
|
|
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
|
-
|
|
651
|
-
file
|
|
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.
|
|
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
|
+
}
|