ttsc 0.16.3 → 0.16.5
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/README.md +96 -25
- package/cmd/graphbench/main.go +135 -0
- package/cmd/graphdump/main.go +60 -0
- package/cmd/ttscgraph/dump.go +69 -0
- package/cmd/ttscgraph/main.go +91 -0
- package/cmd/ttscgraph/run_dump_prints_the_graph_as_json_test.go +80 -0
- package/cmd/ttscgraph/run_help_version_and_bad_flags_exit_codes_test.go +75 -0
- package/cmd/utility-host/main.go +4 -1
- package/driver/overlay.go +64 -0
- package/driver/program.go +8 -1
- package/driver/session.go +85 -0
- package/internal/graph/build.go +330 -0
- package/internal/graph/build_records_a_node_per_top_level_declaration_test.go +96 -0
- package/internal/graph/decorators.go +122 -0
- package/internal/graph/decorators_capture_name_and_literal_arguments_test.go +98 -0
- package/internal/graph/diagnostics.go +23 -0
- package/internal/graph/dump.go +390 -0
- package/internal/graph/dump_evidence_skips_leading_trivia_test.go +92 -0
- package/internal/graph/dump_serializes_the_full_graph_test.go +146 -0
- package/internal/graph/edge_spans_point_to_resolved_usage_test.go +90 -0
- package/internal/graph/edges.go +538 -0
- package/internal/graph/edges_cover_class_level_references_test.go +93 -0
- package/internal/graph/enum_initializer_calls_are_edges_test.go +63 -0
- package/internal/graph/exports.go +60 -0
- package/internal/graph/file_diagnostics_match_tsgo_code_and_location_test.go +59 -0
- package/internal/graph/file_diagnostics_report_code_column_and_message_test.go +69 -0
- package/internal/graph/function_locals_do_not_collide_test.go +66 -0
- package/internal/graph/gitignore.go +51 -0
- package/internal/graph/graph.go +119 -0
- package/internal/graph/heritage_edges_cover_implements_and_interface_extends_test.go +76 -0
- package/internal/graph/heritage_edges_keep_extends_and_implements_to_same_base_test.go +61 -0
- package/internal/graph/heritage_edges_record_extends_versus_implements_origin_test.go +54 -0
- package/internal/graph/heritage_edges_traverse_barrels_and_stop_at_external_leaves_test.go +112 -0
- package/internal/graph/namespace_dotted_form_qualifies_deeply_test.go +60 -0
- package/internal/graph/namespace_members_become_qualified_nodes_and_edges_test.go +100 -0
- package/internal/graph/node_identity_is_position_invariant_test.go +84 -0
- package/internal/graph/node_modifiers_emit_union_strings_test.go +118 -0
- package/internal/graph/nodes_are_marked_exported_through_the_export_table_test.go +61 -0
- package/internal/graph/overload_implementation_replaces_signature_test.go +54 -0
- package/internal/graph/property_member_edges_remain_owner_visible_test.go +83 -0
- package/internal/graph/resolve.go +102 -0
- package/internal/graph/resolve_prefers_implementation_declaration_test.go +50 -0
- package/internal/graph/resolve_unwraps_barrel_reexport_to_real_declaration_test.go +230 -0
- package/internal/graph/type_ref_edges_cover_typeof_as_satisfies_test.go +79 -0
- package/internal/graph/type_ref_edges_resolve_named_types_across_files_test.go +52 -0
- package/internal/graph/value_access_edges_cover_properties_test.go +96 -0
- package/internal/graph/value_call_edges_cover_jsx_components_test.go +82 -0
- package/internal/graph/value_call_edges_cover_methods_and_constructors_test.go +70 -0
- package/internal/graph/value_call_edges_cover_variable_bound_callables_test.go +46 -0
- package/internal/graph/value_call_edges_do_not_double_record_invoked_member_access_test.go +65 -0
- package/internal/graph/value_call_edges_record_construct_versus_call_origin_test.go +75 -0
- package/internal/graph/value_call_edges_resolve_across_files_and_dedupe_test.go +55 -0
- package/internal/graph/value_call_edges_skip_self_and_unresolved_test.go +74 -0
- package/lib/TtscService.d.ts +55 -0
- package/lib/TtscService.js +93 -0
- package/lib/TtscService.js.map +1 -0
- package/lib/compiler/internal/residentTransformProcess.d.ts +47 -0
- package/lib/compiler/internal/residentTransformProcess.js +187 -0
- package/lib/compiler/internal/residentTransformProcess.js.map +1 -0
- package/lib/compiler/internal/startResidentTransform.d.ts +26 -0
- package/lib/compiler/internal/startResidentTransform.js +98 -0
- package/lib/compiler/internal/startResidentTransform.js.map +1 -0
- package/lib/index.d.ts +1 -0
- package/lib/index.js +1 -0
- package/lib/index.js.map +1 -1
- package/package.json +8 -8
- package/shim/ast/shim.go +6 -0
- package/shim/checker/shim.go +22 -12
- package/src/TtscService.ts +94 -0
- package/src/compiler/internal/residentTransformProcess.ts +216 -0
- package/src/compiler/internal/startResidentTransform.ts +126 -0
- package/src/index.ts +1 -0
- package/utility/host.go +8 -2
- package/utility/serve.go +158 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
package graph
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"path/filepath"
|
|
5
|
+
"testing"
|
|
6
|
+
|
|
7
|
+
"github.com/samchon/ttsc/packages/ttsc/driver"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
// TestValueCallEdgesCoverMethodsAndConstructors verifies that the value-call walk
|
|
11
|
+
// reaches what the checker resolves but the first cut dropped: a method-to-method
|
|
12
|
+
// call lands on the callee's method node, and a `new T()` lands on T's class node,
|
|
13
|
+
// both attributed to the calling method.
|
|
14
|
+
//
|
|
15
|
+
// Before method nodes existed, a callee that resolved to a method symbol was
|
|
16
|
+
// dropped (no node to point at) and a `new` expression was never visited, so a
|
|
17
|
+
// class whose logic lived in method bodies showed almost no outgoing edges. This
|
|
18
|
+
// pins that gap closed — the load-bearing reason a model could trust the graph
|
|
19
|
+
// for an architecture question instead of re-reading the source.
|
|
20
|
+
//
|
|
21
|
+
// 1. Compile a fixture where Controller.handle calls Service.run (via a parameter
|
|
22
|
+
// and a constructed value) and constructs a Service.
|
|
23
|
+
// 2. Build the graph.
|
|
24
|
+
// 3. Assert handle -> Service.run (value-call, deduped) and handle -> Service
|
|
25
|
+
// (the new-expression constructor edge) both exist.
|
|
26
|
+
func TestValueCallEdgesCoverMethodsAndConstructors(t *testing.T) {
|
|
27
|
+
root := t.TempDir()
|
|
28
|
+
writeFile(t, filepath.Join(root, "tsconfig.json"), fixtureTSConfig)
|
|
29
|
+
writeFile(t, filepath.Join(root, "src", "main.ts"), `export class Service {
|
|
30
|
+
run(): void {}
|
|
31
|
+
}
|
|
32
|
+
export class Controller {
|
|
33
|
+
handle(s: Service): void {
|
|
34
|
+
s.run();
|
|
35
|
+
const made = new Service();
|
|
36
|
+
made.run();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
`)
|
|
40
|
+
|
|
41
|
+
prog, diags, err := driver.LoadProgram(root, "tsconfig.json", driver.LoadProgramOptions{})
|
|
42
|
+
if err != nil {
|
|
43
|
+
t.Fatal(err)
|
|
44
|
+
}
|
|
45
|
+
if len(diags) != 0 {
|
|
46
|
+
t.Fatalf("unexpected diagnostics: %v", diags)
|
|
47
|
+
}
|
|
48
|
+
defer func() { _ = prog.Close() }()
|
|
49
|
+
|
|
50
|
+
graph := Build(prog)
|
|
51
|
+
path := sourceFile(t, prog, "main.ts").FileName()
|
|
52
|
+
|
|
53
|
+
handle := nodeID(path, "Controller.handle", NodeMethod)
|
|
54
|
+
run := nodeID(path, "Service.run", NodeMethod)
|
|
55
|
+
service := nodeID(path, "Service", NodeClass)
|
|
56
|
+
|
|
57
|
+
// The callee method node both endpoints need must exist as a real node.
|
|
58
|
+
if _, ok := graph.Nodes[run]; !ok {
|
|
59
|
+
t.Fatalf("Build did not record the method node Service.run; have %v", nodeIDSet(graph))
|
|
60
|
+
}
|
|
61
|
+
// Method-to-method: handle calls Service.run (from s.run() and made.run(),
|
|
62
|
+
// deduped to a single edge) — the call the first cut dropped.
|
|
63
|
+
if !hasEdge(graph, handle, run, EdgeValueCall) {
|
|
64
|
+
t.Fatalf("missing value-call edge Controller.handle -> Service.run; edges: %v", graph.Edges)
|
|
65
|
+
}
|
|
66
|
+
// Constructor: `new Service()` is a value-call from handle to the class node.
|
|
67
|
+
if !hasEdge(graph, handle, service, EdgeValueCall) {
|
|
68
|
+
t.Fatalf("missing value-call edge Controller.handle -> Service (new); edges: %v", graph.Edges)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
package graph
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"path/filepath"
|
|
5
|
+
"testing"
|
|
6
|
+
|
|
7
|
+
"github.com/samchon/ttsc/packages/ttsc/driver"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
// TestValueCallEdgesCoverVariableBoundCallables verifies that a call made inside a
|
|
11
|
+
// top-level variable-bound function (a `const fn = () => …`) is an edge from that
|
|
12
|
+
// variable node. The first cut never walked a variable initializer, so an
|
|
13
|
+
// arrow-const that called a function produced no edge — a common modern-TS shape
|
|
14
|
+
// the graph silently missed.
|
|
15
|
+
//
|
|
16
|
+
// 1. Compile a fixture where `const handler = () => { helper(); }`.
|
|
17
|
+
// 2. Build the graph.
|
|
18
|
+
// 3. Assert a handler -> helper value-call edge exists.
|
|
19
|
+
func TestValueCallEdgesCoverVariableBoundCallables(t *testing.T) {
|
|
20
|
+
root := t.TempDir()
|
|
21
|
+
writeFile(t, filepath.Join(root, "tsconfig.json"), fixtureTSConfig)
|
|
22
|
+
writeFile(t, filepath.Join(root, "src", "main.ts"), `export function helper(): void {}
|
|
23
|
+
export const handler = (): void => {
|
|
24
|
+
helper();
|
|
25
|
+
};
|
|
26
|
+
`)
|
|
27
|
+
|
|
28
|
+
prog, diags, err := driver.LoadProgram(root, "tsconfig.json", driver.LoadProgramOptions{})
|
|
29
|
+
if err != nil {
|
|
30
|
+
t.Fatal(err)
|
|
31
|
+
}
|
|
32
|
+
if len(diags) != 0 {
|
|
33
|
+
t.Fatalf("unexpected diagnostics: %v", diags)
|
|
34
|
+
}
|
|
35
|
+
defer func() { _ = prog.Close() }()
|
|
36
|
+
|
|
37
|
+
graph := Build(prog)
|
|
38
|
+
path := sourceFile(t, prog, "main.ts").FileName()
|
|
39
|
+
|
|
40
|
+
handler := nodeID(path, "handler", NodeVariable)
|
|
41
|
+
helper := nodeID(path, "helper", NodeFunction)
|
|
42
|
+
|
|
43
|
+
if !hasEdge(graph, handler, helper, EdgeValueCall) {
|
|
44
|
+
t.Fatalf("missing value-call edge handler -> helper (variable-bound callable body); edges: %v", graph.Edges)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
package graph
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"path/filepath"
|
|
5
|
+
"testing"
|
|
6
|
+
|
|
7
|
+
"github.com/samchon/ttsc/packages/ttsc/driver"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
// TestValueCallEdgesDoNotDoubleRecordInvokedMemberAccess verifies a property
|
|
11
|
+
// access used as an invocation target remains only a value-call edge.
|
|
12
|
+
//
|
|
13
|
+
// Splitting value-call from value-access is useful only if method calls and
|
|
14
|
+
// tagged-template calls do not also look like plain property reads. Otherwise MCP
|
|
15
|
+
// flow ranking cannot tell invoked behavior from state/accessor evidence.
|
|
16
|
+
//
|
|
17
|
+
// 1. Compile method, constructor, and tagged-template calls whose callee/tag is
|
|
18
|
+
// a property access expression.
|
|
19
|
+
// 2. Build the graph.
|
|
20
|
+
// 3. Assert each target has a value-call edge and no duplicate value-access
|
|
21
|
+
// edge from the same caller.
|
|
22
|
+
func TestValueCallEdgesDoNotDoubleRecordInvokedMemberAccess(t *testing.T) {
|
|
23
|
+
root := t.TempDir()
|
|
24
|
+
writeFile(t, filepath.Join(root, "tsconfig.json"), fixtureTSConfig)
|
|
25
|
+
writeFile(t, filepath.Join(root, "src", "main.ts"), "export namespace Providers {\n"+
|
|
26
|
+
" export class Service {\n"+
|
|
27
|
+
" run(): void {}\n"+
|
|
28
|
+
" }\n"+
|
|
29
|
+
"}\n"+
|
|
30
|
+
"export namespace Tags {\n"+
|
|
31
|
+
" export function html(strings: TemplateStringsArray): string {\n"+
|
|
32
|
+
" return strings[0]\n"+
|
|
33
|
+
" }\n"+
|
|
34
|
+
"}\n"+
|
|
35
|
+
"export function handle(service: Providers.Service): string {\n"+
|
|
36
|
+
" service.run()\n"+
|
|
37
|
+
" new Providers.Service()\n"+
|
|
38
|
+
" return Tags.html`ok`\n"+
|
|
39
|
+
"}\n")
|
|
40
|
+
|
|
41
|
+
prog, diags, err := driver.LoadProgram(root, "tsconfig.json", driver.LoadProgramOptions{})
|
|
42
|
+
if err != nil {
|
|
43
|
+
t.Fatal(err)
|
|
44
|
+
}
|
|
45
|
+
if len(diags) != 0 {
|
|
46
|
+
t.Fatalf("unexpected diagnostics: %v", diags)
|
|
47
|
+
}
|
|
48
|
+
defer func() { _ = prog.Close() }()
|
|
49
|
+
|
|
50
|
+
graph := Build(prog)
|
|
51
|
+
path := sourceFile(t, prog, "main.ts").FileName()
|
|
52
|
+
handle := nodeID(path, "handle", NodeFunction)
|
|
53
|
+
run := nodeID(path, "Providers.Service.run", NodeMethod)
|
|
54
|
+
service := nodeID(path, "Providers.Service", NodeClass)
|
|
55
|
+
html := nodeID(path, "Tags.html", NodeFunction)
|
|
56
|
+
|
|
57
|
+
for _, target := range []string{run, service, html} {
|
|
58
|
+
if !hasEdge(graph, handle, target, EdgeValueCall) {
|
|
59
|
+
t.Fatalf("missing value-call edge handle -> %s; edges: %v", target, graph.Edges)
|
|
60
|
+
}
|
|
61
|
+
if hasEdge(graph, handle, target, EdgeValueAccess) {
|
|
62
|
+
t.Fatalf("invoked member access was also recorded as value-access handle -> %s; edges: %v", target, graph.Edges)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
package graph
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"path/filepath"
|
|
5
|
+
"testing"
|
|
6
|
+
|
|
7
|
+
"github.com/samchon/ttsc/packages/ttsc/driver"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
// TestValueCallEdgesRecordConstructVersusCallOrigin verifies that a value-call
|
|
11
|
+
// edge carries the syntactic Origin the dump splits into the schema's `calls`
|
|
12
|
+
// versus `instantiates` kinds: a plain call records "call", a `new T()` records
|
|
13
|
+
// "new".
|
|
14
|
+
//
|
|
15
|
+
// The internal model keeps one EdgeValueCall kind so the existing MCP stays
|
|
16
|
+
// untouched; the finer distinction the redesigned graph schema requires
|
|
17
|
+
// (`new Foo()` is an instantiation, not a call) rides on Edge.Origin. Without it
|
|
18
|
+
// the dump could not recover a constructor from an invocation after the fact, so
|
|
19
|
+
// this pins the discriminator at its source.
|
|
20
|
+
//
|
|
21
|
+
// 1. Compile a fixture where Controller.handle both calls Service.run and
|
|
22
|
+
// constructs `new Service()`.
|
|
23
|
+
// 2. Build the graph.
|
|
24
|
+
// 3. Assert the handle->Service.run edge has Origin "call" and the
|
|
25
|
+
// handle->Service edge has Origin "new".
|
|
26
|
+
func TestValueCallEdgesRecordConstructVersusCallOrigin(t *testing.T) {
|
|
27
|
+
root := t.TempDir()
|
|
28
|
+
writeFile(t, filepath.Join(root, "tsconfig.json"), fixtureTSConfig)
|
|
29
|
+
writeFile(t, filepath.Join(root, "src", "main.ts"), `export class Service {
|
|
30
|
+
run(): void {}
|
|
31
|
+
}
|
|
32
|
+
export class Controller {
|
|
33
|
+
handle(s: Service): void {
|
|
34
|
+
s.run();
|
|
35
|
+
const made = new Service();
|
|
36
|
+
void made;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
`)
|
|
40
|
+
|
|
41
|
+
prog, diags, err := driver.LoadProgram(root, "tsconfig.json", driver.LoadProgramOptions{})
|
|
42
|
+
if err != nil {
|
|
43
|
+
t.Fatal(err)
|
|
44
|
+
}
|
|
45
|
+
if len(diags) != 0 {
|
|
46
|
+
t.Fatalf("unexpected diagnostics: %v", diags)
|
|
47
|
+
}
|
|
48
|
+
defer func() { _ = prog.Close() }()
|
|
49
|
+
|
|
50
|
+
graph := Build(prog)
|
|
51
|
+
path := sourceFile(t, prog, "main.ts").FileName()
|
|
52
|
+
|
|
53
|
+
handle := nodeID(path, "Controller.handle", NodeMethod)
|
|
54
|
+
run := nodeID(path, "Service.run", NodeMethod)
|
|
55
|
+
service := nodeID(path, "Service", NodeClass)
|
|
56
|
+
|
|
57
|
+
if got := edgeOrigin(graph, handle, run, EdgeValueCall); got != "call" {
|
|
58
|
+
t.Fatalf("Controller.handle -> Service.run: want Origin \"call\", got %q; edges: %v", got, graph.Edges)
|
|
59
|
+
}
|
|
60
|
+
if got := edgeOrigin(graph, handle, service, EdgeValueCall); got != "new" {
|
|
61
|
+
t.Fatalf("Controller.handle -> Service (new): want Origin \"new\", got %q; edges: %v", got, graph.Edges)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// edgeOrigin returns the Origin of the first from->to edge of kind, or the
|
|
66
|
+
// sentinel "<missing>" when no such edge exists, so a test distinguishes a wrong
|
|
67
|
+
// origin from an absent edge.
|
|
68
|
+
func edgeOrigin(graph *Graph, from, to string, kind EdgeKind) string {
|
|
69
|
+
for _, edge := range graph.Edges {
|
|
70
|
+
if edge.From == from && edge.To == to && edge.Kind == kind {
|
|
71
|
+
return edge.Origin
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return "<missing>"
|
|
75
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
package graph
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"path/filepath"
|
|
5
|
+
"testing"
|
|
6
|
+
|
|
7
|
+
"github.com/samchon/ttsc/packages/ttsc/driver"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
// TestValueCallEdgesResolveAcrossFilesAndDedupe verifies that a runtime call is
|
|
11
|
+
// recorded as a single value-call edge to the callee's real declaration, even
|
|
12
|
+
// when the caller invokes it from several sites. Cross-file resolution proves the
|
|
13
|
+
// edge rides the checker; the dedup keeps the call graph one-edge-per-pair so an
|
|
14
|
+
// impact query is not skewed by how many times a function is called.
|
|
15
|
+
//
|
|
16
|
+
// 1. Compile a fixture where caller() calls helper() (declared in another file)
|
|
17
|
+
// twice.
|
|
18
|
+
// 2. Build the graph.
|
|
19
|
+
// 3. Assert exactly one caller -> helper value-call edge.
|
|
20
|
+
func TestValueCallEdgesResolveAcrossFilesAndDedupe(t *testing.T) {
|
|
21
|
+
root := t.TempDir()
|
|
22
|
+
writeFile(t, filepath.Join(root, "tsconfig.json"), fixtureTSConfig)
|
|
23
|
+
writeFile(t, filepath.Join(root, "src", "util.ts"), `export function helper(): number {
|
|
24
|
+
return 1;
|
|
25
|
+
}
|
|
26
|
+
`)
|
|
27
|
+
writeFile(t, filepath.Join(root, "src", "main.ts"), `import { helper } from "./util";
|
|
28
|
+
export function caller(): number {
|
|
29
|
+
return helper() + helper();
|
|
30
|
+
}
|
|
31
|
+
`)
|
|
32
|
+
|
|
33
|
+
prog, diags, err := driver.LoadProgram(root, "tsconfig.json", driver.LoadProgramOptions{})
|
|
34
|
+
if err != nil {
|
|
35
|
+
t.Fatal(err)
|
|
36
|
+
}
|
|
37
|
+
if len(diags) != 0 {
|
|
38
|
+
t.Fatalf("unexpected diagnostics: %v", diags)
|
|
39
|
+
}
|
|
40
|
+
defer func() { _ = prog.Close() }()
|
|
41
|
+
|
|
42
|
+
graph := Build(prog)
|
|
43
|
+
caller := nodeID(sourceFile(t, prog, "main.ts").FileName(), "caller", NodeFunction)
|
|
44
|
+
helper := nodeID(sourceFile(t, prog, "util.ts").FileName(), "helper", NodeFunction)
|
|
45
|
+
|
|
46
|
+
count := 0
|
|
47
|
+
for _, edge := range graph.Edges {
|
|
48
|
+
if edge.From == caller && edge.To == helper && edge.Kind == EdgeValueCall {
|
|
49
|
+
count++
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if count != 1 {
|
|
53
|
+
t.Fatalf("expected exactly one caller -> helper value-call edge, got %d; edges: %v", count, graph.Edges)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
package graph
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"path/filepath"
|
|
5
|
+
"testing"
|
|
6
|
+
|
|
7
|
+
"github.com/samchon/ttsc/packages/ttsc/driver"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
// TestValueCallEdgesSkipSelfAndUnresolved verifies that callEdge drops the two
|
|
11
|
+
// edges it is documented to skip while still recording an ordinary cross-call:
|
|
12
|
+
// a self-call (`rec` calling `rec`) yields no edge because `to == from`, and a
|
|
13
|
+
// call whose callee the checker cannot bind to a declaration (a method on an
|
|
14
|
+
// `any`-typed value) yields no edge because Resolve returns nil.
|
|
15
|
+
//
|
|
16
|
+
// The positive caller->helper edge is the load-bearing twin: without it the test
|
|
17
|
+
// would also pass if callEdge recorded nothing at all, so it proves the two skips
|
|
18
|
+
// are specific to a self-call and an unresolved callee, not a blanket failure to
|
|
19
|
+
// emit value-call edges.
|
|
20
|
+
//
|
|
21
|
+
// 1. Compile a fixture with `rec()` returning `rec()`, `caller()` calling a
|
|
22
|
+
// separate `helper()`, and `dynamic(host: any)` calling `host.run()`.
|
|
23
|
+
// 2. Build the graph.
|
|
24
|
+
// 3. Assert no rec->rec edge and no edge out of dynamic, but a caller->helper
|
|
25
|
+
// value-call edge.
|
|
26
|
+
func TestValueCallEdgesSkipSelfAndUnresolved(t *testing.T) {
|
|
27
|
+
root := t.TempDir()
|
|
28
|
+
writeFile(t, filepath.Join(root, "tsconfig.json"), fixtureTSConfig)
|
|
29
|
+
writeFile(t, filepath.Join(root, "src", "main.ts"), `export function rec(): unknown {
|
|
30
|
+
return rec();
|
|
31
|
+
}
|
|
32
|
+
export function helper(): void {}
|
|
33
|
+
export function caller(): void {
|
|
34
|
+
helper();
|
|
35
|
+
}
|
|
36
|
+
export function dynamic(host: any): void {
|
|
37
|
+
host.run();
|
|
38
|
+
}
|
|
39
|
+
`)
|
|
40
|
+
|
|
41
|
+
prog, diags, err := driver.LoadProgram(root, "tsconfig.json", driver.LoadProgramOptions{})
|
|
42
|
+
if err != nil {
|
|
43
|
+
t.Fatal(err)
|
|
44
|
+
}
|
|
45
|
+
if len(diags) != 0 {
|
|
46
|
+
t.Fatalf("unexpected diagnostics: %v", diags)
|
|
47
|
+
}
|
|
48
|
+
defer func() { _ = prog.Close() }()
|
|
49
|
+
|
|
50
|
+
graph := Build(prog)
|
|
51
|
+
path := sourceFile(t, prog, "main.ts").FileName()
|
|
52
|
+
|
|
53
|
+
rec := nodeID(path, "rec", NodeFunction)
|
|
54
|
+
helper := nodeID(path, "helper", NodeFunction)
|
|
55
|
+
caller := nodeID(path, "caller", NodeFunction)
|
|
56
|
+
dynamic := nodeID(path, "dynamic", NodeFunction)
|
|
57
|
+
|
|
58
|
+
// Self-call: callEdge skips `to == from`, so rec never points at itself.
|
|
59
|
+
if hasEdge(graph, rec, rec, EdgeValueCall) {
|
|
60
|
+
t.Fatalf("a self-call recorded a rec -> rec value-call edge; edges: %v", graph.Edges)
|
|
61
|
+
}
|
|
62
|
+
// Unresolved callee: `host.run()` binds to no declaration, so dynamic gains no
|
|
63
|
+
// outgoing value-call edge.
|
|
64
|
+
for _, edge := range graph.Edges {
|
|
65
|
+
if edge.From == dynamic && edge.Kind == EdgeValueCall {
|
|
66
|
+
t.Fatalf("an unresolved callee recorded a value-call edge out of dynamic: %+v", edge)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// Positive twin: an ordinary cross-call is still recorded, proving the skips
|
|
70
|
+
// above are specific and not a blanket suppression.
|
|
71
|
+
if !hasEdge(graph, caller, helper, EdgeValueCall) {
|
|
72
|
+
t.Fatalf("missing value-call edge caller -> helper; edges: %v", graph.Edges)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { ITtscCompilerContext } from "./structures/ITtscCompilerContext";
|
|
2
|
+
/**
|
|
3
|
+
* Resident, incremental transform service for the `ttsc` TypeScript-Go
|
|
4
|
+
* pipeline.
|
|
5
|
+
*
|
|
6
|
+
* Where {@link TtscCompiler.transform} spawns a fresh process and recompiles the
|
|
7
|
+
* whole project on every call, `TtscService` keeps one long-lived host warm: it
|
|
8
|
+
* compiles the project once and then answers per-file transform requests from
|
|
9
|
+
* that warm program. A single service instance transforms many files (a watch
|
|
10
|
+
* server, an editor session, a codegen tool) while paying the project compile
|
|
11
|
+
* once instead of once per file. Sharing one host across separate worker
|
|
12
|
+
* processes (a Metro worker pool) is tracked in samchon/ttsc#255.
|
|
13
|
+
*
|
|
14
|
+
* The shape mirrors a legacy TypeScript `LanguageService`: construct it against
|
|
15
|
+
* a project context, ask it to transform individual files, and dispose it when
|
|
16
|
+
* done. Construction is synchronous (it launches the host); the host compiles
|
|
17
|
+
* in the background, so the first {@link transformFile} resolves once that
|
|
18
|
+
* compile lands.
|
|
19
|
+
*
|
|
20
|
+
* Resident mode runs through the linked-plugin shared host, so the project must
|
|
21
|
+
* declare at least one transform-stage plugin; the constructor throws
|
|
22
|
+
* otherwise. It does not run check-stage plugins (unlike
|
|
23
|
+
* {@link TtscCompiler.transform}); only the program's own type-checking gates an
|
|
24
|
+
* {@link updateFile}.
|
|
25
|
+
*/
|
|
26
|
+
export declare class TtscService {
|
|
27
|
+
private readonly resident;
|
|
28
|
+
private readonly projectRoot;
|
|
29
|
+
/**
|
|
30
|
+
* Create a service bound to the given project context and launch its resident
|
|
31
|
+
* host. The context is the same shape {@link TtscCompiler} accepts; it is not
|
|
32
|
+
* replaceable per call.
|
|
33
|
+
*/
|
|
34
|
+
constructor(context?: ITtscCompilerContext);
|
|
35
|
+
/**
|
|
36
|
+
* Return the transformed TypeScript source for one file, or `undefined` when
|
|
37
|
+
* the resident program does not contain it (for example a file excluded from
|
|
38
|
+
* the tsconfig). A relative `fileName` is resolved against the project root.
|
|
39
|
+
*
|
|
40
|
+
* Rejects when the resident host failed to compile the project; the rejection
|
|
41
|
+
* carries the host's diagnostics so callers can surface a real build error.
|
|
42
|
+
*/
|
|
43
|
+
transformFile(fileName: string): Promise<string | undefined>;
|
|
44
|
+
/**
|
|
45
|
+
* Apply new in-memory content for one file and re-transform the project, so a
|
|
46
|
+
* subsequent {@link transformFile} reflects the edit without restarting the
|
|
47
|
+
* host. Returns whether the re-transform succeeded; `false` means the edit
|
|
48
|
+
* did not compile and the previous transform is still in effect. A relative
|
|
49
|
+
* `fileName` is resolved against the project root.
|
|
50
|
+
*/
|
|
51
|
+
updateFile(fileName: string, content: string): Promise<boolean>;
|
|
52
|
+
/** Terminate the resident host and reject any in-flight requests. */
|
|
53
|
+
dispose(): void;
|
|
54
|
+
private absolutePath;
|
|
55
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.TtscService = void 0;
|
|
7
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
8
|
+
const startResidentTransform_1 = require("./compiler/internal/startResidentTransform");
|
|
9
|
+
/**
|
|
10
|
+
* Resident, incremental transform service for the `ttsc` TypeScript-Go
|
|
11
|
+
* pipeline.
|
|
12
|
+
*
|
|
13
|
+
* Where {@link TtscCompiler.transform} spawns a fresh process and recompiles the
|
|
14
|
+
* whole project on every call, `TtscService` keeps one long-lived host warm: it
|
|
15
|
+
* compiles the project once and then answers per-file transform requests from
|
|
16
|
+
* that warm program. A single service instance transforms many files (a watch
|
|
17
|
+
* server, an editor session, a codegen tool) while paying the project compile
|
|
18
|
+
* once instead of once per file. Sharing one host across separate worker
|
|
19
|
+
* processes (a Metro worker pool) is tracked in samchon/ttsc#255.
|
|
20
|
+
*
|
|
21
|
+
* The shape mirrors a legacy TypeScript `LanguageService`: construct it against
|
|
22
|
+
* a project context, ask it to transform individual files, and dispose it when
|
|
23
|
+
* done. Construction is synchronous (it launches the host); the host compiles
|
|
24
|
+
* in the background, so the first {@link transformFile} resolves once that
|
|
25
|
+
* compile lands.
|
|
26
|
+
*
|
|
27
|
+
* Resident mode runs through the linked-plugin shared host, so the project must
|
|
28
|
+
* declare at least one transform-stage plugin; the constructor throws
|
|
29
|
+
* otherwise. It does not run check-stage plugins (unlike
|
|
30
|
+
* {@link TtscCompiler.transform}); only the program's own type-checking gates an
|
|
31
|
+
* {@link updateFile}.
|
|
32
|
+
*/
|
|
33
|
+
class TtscService {
|
|
34
|
+
resident;
|
|
35
|
+
projectRoot;
|
|
36
|
+
/**
|
|
37
|
+
* Create a service bound to the given project context and launch its resident
|
|
38
|
+
* host. The context is the same shape {@link TtscCompiler} accepts; it is not
|
|
39
|
+
* replaceable per call.
|
|
40
|
+
*/
|
|
41
|
+
constructor(context = {}) {
|
|
42
|
+
const started = (0, startResidentTransform_1.startResidentTransform)({
|
|
43
|
+
...context,
|
|
44
|
+
env: context.env ? { ...context.env } : undefined,
|
|
45
|
+
plugins: Array.isArray(context.plugins)
|
|
46
|
+
? [...context.plugins]
|
|
47
|
+
: context.plugins,
|
|
48
|
+
});
|
|
49
|
+
this.resident = started.process;
|
|
50
|
+
this.projectRoot = started.projectRoot;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Return the transformed TypeScript source for one file, or `undefined` when
|
|
54
|
+
* the resident program does not contain it (for example a file excluded from
|
|
55
|
+
* the tsconfig). A relative `fileName` is resolved against the project root.
|
|
56
|
+
*
|
|
57
|
+
* Rejects when the resident host failed to compile the project; the rejection
|
|
58
|
+
* carries the host's diagnostics so callers can surface a real build error.
|
|
59
|
+
*/
|
|
60
|
+
async transformFile(fileName) {
|
|
61
|
+
const reply = await this.resident.request({
|
|
62
|
+
file: this.absolutePath(fileName),
|
|
63
|
+
});
|
|
64
|
+
return reply.found === true && typeof reply.typescript === "string"
|
|
65
|
+
? reply.typescript
|
|
66
|
+
: undefined;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Apply new in-memory content for one file and re-transform the project, so a
|
|
70
|
+
* subsequent {@link transformFile} reflects the edit without restarting the
|
|
71
|
+
* host. Returns whether the re-transform succeeded; `false` means the edit
|
|
72
|
+
* did not compile and the previous transform is still in effect. A relative
|
|
73
|
+
* `fileName` is resolved against the project root.
|
|
74
|
+
*/
|
|
75
|
+
async updateFile(fileName, content) {
|
|
76
|
+
const reply = await this.resident.request({
|
|
77
|
+
content,
|
|
78
|
+
update: this.absolutePath(fileName),
|
|
79
|
+
});
|
|
80
|
+
return reply.updated === true;
|
|
81
|
+
}
|
|
82
|
+
/** Terminate the resident host and reject any in-flight requests. */
|
|
83
|
+
dispose() {
|
|
84
|
+
this.resident.dispose();
|
|
85
|
+
}
|
|
86
|
+
absolutePath(fileName) {
|
|
87
|
+
return node_path_1.default.isAbsolute(fileName)
|
|
88
|
+
? fileName
|
|
89
|
+
: node_path_1.default.resolve(this.projectRoot, fileName);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
exports.TtscService = TtscService;
|
|
93
|
+
//# sourceMappingURL=TtscService.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TtscService.js","sourceRoot":"","sources":["../src/TtscService.ts"],"names":[],"mappings":";;;;;;AAAA,0DAA6B;AAG7B,uFAAoF;AAGpF;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH;IACmB,QAAQ,CAA2B;IACnC,WAAW,CAAS;IAErC;;;;OAIG;IACH,YAAmB,OAAO,GAAyB,EAAE;QACnD,MAAM,OAAO,GAAG,IAAA,+CAAsB,EAAC;YACrC,GAAG,OAAO;YACV,GAAG,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,SAAS;YACjD,OAAO,EAAE,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC;gBACrC,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC;gBACtB,CAAC,CAAC,OAAO,CAAC,OAAO;SACpB,CAAC,CAAC;QACH,IAAI,CAAC,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC;QAChC,IAAI,CAAC,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC;IACzC,CAAC;IAED;;;;;;;OAOG;IACI,KAAK,CAAC,aAAa,CAAC,QAAgB;QACzC,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC;YACxC,IAAI,EAAE,IAAI,CAAC,YAAY,CAAC,QAAQ,CAAC;SAClC,CAAC,CAAC;QACH,OAAO,KAAK,CAAC,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,CAAC,UAAU,KAAK,QAAQ;YACjE,CAAC,CAAC,KAAK,CAAC,UAAU;YAClB,CAAC,CAAC,SAAS,CAAC;IAChB,CAAC;IAED;;;;;;OAMG;IACI,KAAK,CAAC,UAAU,CAAC,QAAgB,EAAE,OAAe;QACvD,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC;YACxC,OAAO;YACP,MAAM,EAAE,IAAI,CAAC,YAAY,CAAC,QAAQ,CAAC;SACpC,CAAC,CAAC;QACH,OAAO,KAAK,CAAC,OAAO,KAAK,IAAI,CAAC;IAChC,CAAC;IAED,qEAAqE;IAC9D,OAAO;QACZ,IAAI,CAAC,QAAQ,CAAC,OAAO,EAAE,CAAC;IAC1B,CAAC;IAEO,YAAY,CAAC,QAAgB;QACnC,OAAO,mBAAI,CAAC,UAAU,CAAC,QAAQ,CAAC;YAC9B,CAAC,CAAC,QAAQ;YACV,CAAC,CAAC,mBAAI,CAAC,OAAO,CAAC,IAAI,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC;IAC/C,CAAC;CACF"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/** Options for spawning the resident transform host. */
|
|
2
|
+
export interface ResidentTransformProcessOptions {
|
|
3
|
+
args: readonly string[];
|
|
4
|
+
binary: string;
|
|
5
|
+
cwd?: string;
|
|
6
|
+
env?: NodeJS.ProcessEnv;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Async client for the long-lived `utility-host serve` process.
|
|
10
|
+
*
|
|
11
|
+
* The host transforms the whole project once at startup, caches every file's
|
|
12
|
+
* transformed TypeScript, then answers newline-delimited requests. This class
|
|
13
|
+
* speaks that protocol: each {@link request} writes one JSON line and the host
|
|
14
|
+
* replies with one line, matched FIFO. A transform request (`{"file":...}`) is
|
|
15
|
+
* answered with `{"typescript":...,"found":...}`, and an update request
|
|
16
|
+
* (`{"update":...,"content":...}`) with `{"updated":...}`.
|
|
17
|
+
*
|
|
18
|
+
* One resident process answers every request from one service instead of
|
|
19
|
+
* spawning a fresh `transform` subprocess per call, so a single process pays
|
|
20
|
+
* the project compile once (samchon/ttsc#255).
|
|
21
|
+
*/
|
|
22
|
+
export declare class ResidentTransformProcess {
|
|
23
|
+
private readonly child;
|
|
24
|
+
private readonly reader;
|
|
25
|
+
private readonly pending;
|
|
26
|
+
private stderr;
|
|
27
|
+
private failure;
|
|
28
|
+
constructor(options: ResidentTransformProcessOptions);
|
|
29
|
+
/**
|
|
30
|
+
* Send one request to the host and resolve with its parsed JSON reply. The
|
|
31
|
+
* host answers in FIFO order, so the caller interprets the reply shape by the
|
|
32
|
+
* payload it sent (a transform reply vs an update reply). Rejects when the
|
|
33
|
+
* host has already failed or exited, or if writing the request fails.
|
|
34
|
+
*/
|
|
35
|
+
request(payload: Record<string, unknown>): Promise<Record<string, unknown>>;
|
|
36
|
+
/**
|
|
37
|
+
* Terminate the resident process and reject any in-flight requests. Safe to
|
|
38
|
+
* call more than once.
|
|
39
|
+
*/
|
|
40
|
+
dispose(): void;
|
|
41
|
+
private onLine;
|
|
42
|
+
private onReaderClose;
|
|
43
|
+
private fail;
|
|
44
|
+
private rejectAll;
|
|
45
|
+
private removePending;
|
|
46
|
+
private exitError;
|
|
47
|
+
}
|