ttsc 0.16.2 → 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/lib/plugin/internal/loadProjectPlugins.js +17 -3
- package/lib/plugin/internal/loadProjectPlugins.js.map +1 -1
- package/lib/structures/ITtscPlugin.d.ts +7 -5
- package/lib/structures/ITtscPluginFactoryContext.d.ts +24 -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/src/plugin/internal/loadProjectPlugins.ts +17 -3
- package/src/structures/ITtscPlugin.ts +7 -5
- package/src/structures/ITtscPluginFactoryContext.ts +26 -1
- package/utility/host.go +8 -2
- package/utility/serve.go +158 -0
|
@@ -0,0 +1,112 @@
|
|
|
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
|
+
// TestHeritageEdgesTraverseBarrelsAndStopAtExternalLeaves verifies the two edge
|
|
12
|
+
// behaviors that distinguish a checker-resolved graph from a path-heuristic one:
|
|
13
|
+
// a heritage edge into a barrel-re-exported base lands on the sibling source
|
|
14
|
+
// that declares it (not the index file), and a heritage edge into a dependency
|
|
15
|
+
// becomes an external boundary leaf that the walk does not descend into.
|
|
16
|
+
//
|
|
17
|
+
// 1. Compile a fixture: `Sub extends Base` where Base is re-exported through a
|
|
18
|
+
// barrel, and `SubExt extends Ext` where Ext is declared in a node_modules
|
|
19
|
+
// `.d.ts`.
|
|
20
|
+
// 2. Build the graph.
|
|
21
|
+
// 3. Assert the Sub->Base edge targets the real declaration in impl.ts and is
|
|
22
|
+
// not external, while the SubExt->Ext edge targets an external leaf under
|
|
23
|
+
// node_modules.
|
|
24
|
+
func TestHeritageEdgesTraverseBarrelsAndStopAtExternalLeaves(t *testing.T) {
|
|
25
|
+
root := t.TempDir()
|
|
26
|
+
writeFile(t, filepath.Join(root, "tsconfig.json"), fixtureTSConfig)
|
|
27
|
+
writeFile(t, filepath.Join(root, "node_modules", "dep", "package.json"), `{
|
|
28
|
+
"name": "dep",
|
|
29
|
+
"version": "1.0.0",
|
|
30
|
+
"types": "index.d.ts"
|
|
31
|
+
}
|
|
32
|
+
`)
|
|
33
|
+
writeFile(t, filepath.Join(root, "node_modules", "dep", "index.d.ts"), `export declare class Ext {}
|
|
34
|
+
`)
|
|
35
|
+
writeFile(t, filepath.Join(root, "src", "impl.ts"), `export class Base {}
|
|
36
|
+
`)
|
|
37
|
+
writeFile(t, filepath.Join(root, "src", "index.ts"), `export { Base } from "./impl";
|
|
38
|
+
`)
|
|
39
|
+
writeFile(t, filepath.Join(root, "src", "main.ts"), `import { Base } from "./index";
|
|
40
|
+
import { Ext } from "dep";
|
|
41
|
+
export class Sub extends Base {}
|
|
42
|
+
export class SubExt extends Ext {}
|
|
43
|
+
`)
|
|
44
|
+
|
|
45
|
+
prog, diags, err := driver.LoadProgram(root, "tsconfig.json", driver.LoadProgramOptions{})
|
|
46
|
+
if err != nil {
|
|
47
|
+
t.Fatal(err)
|
|
48
|
+
}
|
|
49
|
+
if len(diags) != 0 {
|
|
50
|
+
t.Fatalf("unexpected diagnostics: %v", diags)
|
|
51
|
+
}
|
|
52
|
+
defer func() { _ = prog.Close() }()
|
|
53
|
+
|
|
54
|
+
graph := Build(prog)
|
|
55
|
+
mainPath := sourceFile(t, prog, "main.ts").FileName()
|
|
56
|
+
implPath := sourceFile(t, prog, "impl.ts").FileName()
|
|
57
|
+
|
|
58
|
+
// Barrel traversal: the base resolved to the sibling source, not the index.
|
|
59
|
+
base := nodeID(implPath, "Base", NodeClass)
|
|
60
|
+
if node, ok := graph.Nodes[base]; !ok || node.External {
|
|
61
|
+
t.Fatalf("Base node missing or misclassified external (barrel not traversed): %+v", node)
|
|
62
|
+
}
|
|
63
|
+
sub := nodeID(mainPath, "Sub", NodeClass)
|
|
64
|
+
if !hasEdge(graph, sub, base, EdgeHeritage) {
|
|
65
|
+
t.Fatalf("missing heritage edge Sub -> Base@impl.ts; edges: %v", graph.Edges)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// External leaf: the dependency base is a boundary leaf, kept as a node with
|
|
69
|
+
// its incoming edge but never walked into.
|
|
70
|
+
ext := findNodeByName(graph, "Ext")
|
|
71
|
+
if ext == nil {
|
|
72
|
+
t.Fatal("Ext base was not recorded as a node")
|
|
73
|
+
}
|
|
74
|
+
if !ext.External || !strings.Contains(ext.File, "/node_modules/") {
|
|
75
|
+
t.Fatalf("Ext should be an external node_modules leaf: %+v", ext)
|
|
76
|
+
}
|
|
77
|
+
subExt := nodeID(mainPath, "SubExt", NodeClass)
|
|
78
|
+
if !hasEdge(graph, subExt, ext.ID, EdgeHeritage) {
|
|
79
|
+
t.Fatalf("missing heritage edge SubExt -> Ext (external); edges: %v", graph.Edges)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// hasEdge reports whether the graph holds a from->to edge of the given kind.
|
|
84
|
+
func hasEdge(graph *Graph, from, to string, kind EdgeKind) bool {
|
|
85
|
+
for _, edge := range graph.Edges {
|
|
86
|
+
if edge.From == from && edge.To == to && edge.Kind == kind {
|
|
87
|
+
return true
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return false
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// hasDecorator reports whether the graph recorded a decorator named name on the
|
|
94
|
+
// node target.
|
|
95
|
+
func hasDecorator(graph *Graph, target, name string) bool {
|
|
96
|
+
for _, dec := range graph.Decorators {
|
|
97
|
+
if dec.Target == target && dec.Name == name {
|
|
98
|
+
return true
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return false
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// findNodeByName returns the first node whose declared name is name, or nil.
|
|
105
|
+
func findNodeByName(graph *Graph, name string) *Node {
|
|
106
|
+
for _, node := range graph.Nodes {
|
|
107
|
+
if node.Name == name {
|
|
108
|
+
return node
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return nil
|
|
112
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
package graph
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"path/filepath"
|
|
5
|
+
"testing"
|
|
6
|
+
|
|
7
|
+
"github.com/samchon/ttsc/packages/ttsc/driver"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
// TestNamespaceDottedFormQualifiesDeeply covers the chained-module shape that a
|
|
11
|
+
// single-level namespace test does not: `namespace A.B.C { … }` is parsed as a
|
|
12
|
+
// ModuleDeclaration whose body is another ModuleDeclaration, so moduleStatements
|
|
13
|
+
// must descend through the chain and qualifiedName must build the full dotted
|
|
14
|
+
// prefix. A regression in either would silently drop every declaration in a
|
|
15
|
+
// dotted (or deeply nested) namespace — the idiom of .d.ts-heavy and
|
|
16
|
+
// proto-generated codebases.
|
|
17
|
+
func TestNamespaceDottedFormQualifiesDeeply(t *testing.T) {
|
|
18
|
+
root := t.TempDir()
|
|
19
|
+
writeFile(t, filepath.Join(root, "tsconfig.json"), `{
|
|
20
|
+
"compilerOptions": {
|
|
21
|
+
"target": "ES2022",
|
|
22
|
+
"module": "commonjs",
|
|
23
|
+
"strict": true,
|
|
24
|
+
"rootDir": "src",
|
|
25
|
+
"outDir": "dist"
|
|
26
|
+
},
|
|
27
|
+
"files": ["src/main.ts"]
|
|
28
|
+
}
|
|
29
|
+
`)
|
|
30
|
+
writeFile(t, filepath.Join(root, "src", "main.ts"), `export namespace A.B.C {
|
|
31
|
+
export function deep(): void {}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function caller(): void {
|
|
35
|
+
A.B.C.deep();
|
|
36
|
+
}
|
|
37
|
+
`)
|
|
38
|
+
|
|
39
|
+
prog, diags, err := driver.LoadProgram(root, "tsconfig.json", driver.LoadProgramOptions{})
|
|
40
|
+
if err != nil {
|
|
41
|
+
t.Fatal(err)
|
|
42
|
+
}
|
|
43
|
+
if len(diags) != 0 {
|
|
44
|
+
t.Fatalf("unexpected diagnostics: %v", diags)
|
|
45
|
+
}
|
|
46
|
+
defer func() { _ = prog.Close() }()
|
|
47
|
+
|
|
48
|
+
graph := Build(prog)
|
|
49
|
+
path := sourceFile(t, prog, "main.ts").FileName()
|
|
50
|
+
|
|
51
|
+
deep := nodeID(path, "A.B.C.deep", NodeFunction)
|
|
52
|
+
caller := nodeID(path, "caller", NodeFunction)
|
|
53
|
+
|
|
54
|
+
if _, ok := graph.Nodes[deep]; !ok {
|
|
55
|
+
t.Fatalf("missing deeply-namespaced node %q; nodes: %v", deep, graph.Nodes)
|
|
56
|
+
}
|
|
57
|
+
if !hasEdge(graph, caller, deep, EdgeValueCall) {
|
|
58
|
+
t.Fatalf("missing value-call edge caller -> A.B.C.deep; edges: %v", graph.Edges)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
package graph
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"path/filepath"
|
|
5
|
+
"testing"
|
|
6
|
+
|
|
7
|
+
"github.com/samchon/ttsc/packages/ttsc/driver"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
// TestNamespaceMembersBecomeQualifiedNodesAndEdges verifies that declarations
|
|
11
|
+
// inside a `namespace` are first-class graph nodes, keyed by their
|
|
12
|
+
// namespace-qualified name, and that edges cross the namespace boundary in both
|
|
13
|
+
// directions. A top-level-statements-only walk recorded none of this: every
|
|
14
|
+
// namespaced declaration and every edge touching one was silently dropped.
|
|
15
|
+
//
|
|
16
|
+
// The fixture exercises four relationships:
|
|
17
|
+
//
|
|
18
|
+
// - intra-namespace call: Service.run -> Service.helper
|
|
19
|
+
// - top-level into a namespace: bootstrap -> Service.run
|
|
20
|
+
// - namespaced method type-ref: Service.Worker.process -> Payload
|
|
21
|
+
// - namespaced type referenced: WorkerRef -> Service.Worker
|
|
22
|
+
func TestNamespaceMembersBecomeQualifiedNodesAndEdges(t *testing.T) {
|
|
23
|
+
root := t.TempDir()
|
|
24
|
+
writeFile(t, filepath.Join(root, "tsconfig.json"), `{
|
|
25
|
+
"compilerOptions": {
|
|
26
|
+
"target": "ES2022",
|
|
27
|
+
"module": "commonjs",
|
|
28
|
+
"strict": true,
|
|
29
|
+
"rootDir": "src",
|
|
30
|
+
"outDir": "dist"
|
|
31
|
+
},
|
|
32
|
+
"files": ["src/main.ts"]
|
|
33
|
+
}
|
|
34
|
+
`)
|
|
35
|
+
writeFile(t, filepath.Join(root, "src", "main.ts"), `export interface Payload {}
|
|
36
|
+
|
|
37
|
+
export namespace Service {
|
|
38
|
+
export function helper(): void {}
|
|
39
|
+
|
|
40
|
+
export function run(): void {
|
|
41
|
+
helper();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export class Worker {
|
|
45
|
+
process(p: Payload): void {
|
|
46
|
+
void p;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function bootstrap(): void {
|
|
52
|
+
Service.run();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export type WorkerRef = Service.Worker;
|
|
56
|
+
`)
|
|
57
|
+
|
|
58
|
+
prog, diags, err := driver.LoadProgram(root, "tsconfig.json", driver.LoadProgramOptions{})
|
|
59
|
+
if err != nil {
|
|
60
|
+
t.Fatal(err)
|
|
61
|
+
}
|
|
62
|
+
if len(diags) != 0 {
|
|
63
|
+
t.Fatalf("unexpected diagnostics: %v", diags)
|
|
64
|
+
}
|
|
65
|
+
defer func() { _ = prog.Close() }()
|
|
66
|
+
|
|
67
|
+
graph := Build(prog)
|
|
68
|
+
path := sourceFile(t, prog, "main.ts").FileName()
|
|
69
|
+
|
|
70
|
+
serviceHelper := nodeID(path, "Service.helper", NodeFunction)
|
|
71
|
+
serviceRun := nodeID(path, "Service.run", NodeFunction)
|
|
72
|
+
serviceWorker := nodeID(path, "Service.Worker", NodeClass)
|
|
73
|
+
workerProcess := nodeID(path, "Service.Worker.process", NodeMethod)
|
|
74
|
+
bootstrap := nodeID(path, "bootstrap", NodeFunction)
|
|
75
|
+
workerRef := nodeID(path, "WorkerRef", NodeTypeAlias)
|
|
76
|
+
payload := nodeID(path, "Payload", NodeInterface)
|
|
77
|
+
|
|
78
|
+
// The namespaced declarations are nodes in their own right.
|
|
79
|
+
for _, id := range []string{serviceHelper, serviceRun, serviceWorker, workerProcess} {
|
|
80
|
+
if _, ok := graph.Nodes[id]; !ok {
|
|
81
|
+
t.Errorf("missing namespaced node %q", id)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if !hasEdge(graph, serviceRun, serviceHelper, EdgeValueCall) {
|
|
86
|
+
t.Errorf("missing value-call edge Service.run -> Service.helper (intra-namespace)")
|
|
87
|
+
}
|
|
88
|
+
if !hasEdge(graph, bootstrap, serviceRun, EdgeValueCall) {
|
|
89
|
+
t.Errorf("missing value-call edge bootstrap -> Service.run (top-level into namespace)")
|
|
90
|
+
}
|
|
91
|
+
if !hasEdge(graph, workerProcess, payload, EdgeTypeRef) {
|
|
92
|
+
t.Errorf("missing type-ref edge Service.Worker.process -> Payload (namespaced method)")
|
|
93
|
+
}
|
|
94
|
+
if !hasEdge(graph, workerRef, serviceWorker, EdgeTypeRef) {
|
|
95
|
+
t.Errorf("missing type-ref edge WorkerRef -> Service.Worker (namespaced type referenced)")
|
|
96
|
+
}
|
|
97
|
+
if t.Failed() {
|
|
98
|
+
t.Logf("nodes: %d, edges: %v", len(graph.Nodes), graph.Edges)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -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
|
+
// TestNodeIdentityIsPositionInvariant verifies that a declaration keeps the same
|
|
11
|
+
// node id after lines are inserted above it. A byte-offset or line-number key
|
|
12
|
+
// would re-key every declaration below an edit, forcing a future incremental
|
|
13
|
+
// layer to churn the whole graph; the realpath + name + kind key does not.
|
|
14
|
+
//
|
|
15
|
+
// The probe rewrites the same file with a comment and blank lines prepended,
|
|
16
|
+
// shifting every offset below, and asserts (a) the reload actually changed the
|
|
17
|
+
// source text, so the result is not a stale cached read, and (b) the node-id set
|
|
18
|
+
// is identical across the shift.
|
|
19
|
+
//
|
|
20
|
+
// 1. Compile a fixture, capture its node ids and source text.
|
|
21
|
+
// 2. Rewrite the same file with leading lines that shift every offset.
|
|
22
|
+
// 3. Assert the text changed but the node-id set did not.
|
|
23
|
+
func TestNodeIdentityIsPositionInvariant(t *testing.T) {
|
|
24
|
+
root := t.TempDir()
|
|
25
|
+
writeFile(t, filepath.Join(root, "tsconfig.json"), fixtureTSConfig)
|
|
26
|
+
main := filepath.Join(root, "src", "main.ts")
|
|
27
|
+
|
|
28
|
+
writeFile(t, main, `export function fn(): void {}
|
|
29
|
+
export const value = 1;
|
|
30
|
+
`)
|
|
31
|
+
firstIDs, firstText := buildSnapshot(t, root)
|
|
32
|
+
|
|
33
|
+
writeFile(t, main, `// a leading comment that shifts every offset below it
|
|
34
|
+
|
|
35
|
+
export function fn(): void {}
|
|
36
|
+
|
|
37
|
+
export const value = 1;
|
|
38
|
+
`)
|
|
39
|
+
secondIDs, secondText := buildSnapshot(t, root)
|
|
40
|
+
|
|
41
|
+
if firstText == secondText {
|
|
42
|
+
t.Fatal("the reload returned identical source text; the probe is reading a stale cache, not the shifted file")
|
|
43
|
+
}
|
|
44
|
+
if len(firstIDs) != 2 {
|
|
45
|
+
t.Fatalf("expected 2 nodes, got %d: %v", len(firstIDs), firstIDs)
|
|
46
|
+
}
|
|
47
|
+
if !equalStringSets(firstIDs, secondIDs) {
|
|
48
|
+
t.Fatalf("node identity changed when positions shifted:\n first=%v\n second=%v", firstIDs, secondIDs)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// buildSnapshot compiles root, then returns the built graph's node-id set and
|
|
53
|
+
// the source text of main.ts so the caller can confirm a reload took effect.
|
|
54
|
+
func buildSnapshot(t *testing.T, root string) (map[string]bool, string) {
|
|
55
|
+
t.Helper()
|
|
56
|
+
prog, diags, err := driver.LoadProgram(root, "tsconfig.json", driver.LoadProgramOptions{})
|
|
57
|
+
if err != nil {
|
|
58
|
+
t.Fatal(err)
|
|
59
|
+
}
|
|
60
|
+
if len(diags) != 0 {
|
|
61
|
+
t.Fatalf("unexpected diagnostics: %v", diags)
|
|
62
|
+
}
|
|
63
|
+
defer func() { _ = prog.Close() }()
|
|
64
|
+
|
|
65
|
+
text := sourceFile(t, prog, "main.ts").Text()
|
|
66
|
+
ids := map[string]bool{}
|
|
67
|
+
for id := range Build(prog).Nodes {
|
|
68
|
+
ids[id] = true
|
|
69
|
+
}
|
|
70
|
+
return ids, text
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// equalStringSets reports whether a and b hold the same keys.
|
|
74
|
+
func equalStringSets(a, b map[string]bool) bool {
|
|
75
|
+
if len(a) != len(b) {
|
|
76
|
+
return false
|
|
77
|
+
}
|
|
78
|
+
for key := range a {
|
|
79
|
+
if !b[key] {
|
|
80
|
+
return false
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return true
|
|
84
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
package graph
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"path/filepath"
|
|
5
|
+
"testing"
|
|
6
|
+
|
|
7
|
+
"github.com/samchon/ttsc/packages/ttsc/driver"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
// TestNodeModifiersEmitUnionStrings verifies that the dump records a declaration's
|
|
11
|
+
// syntactic modifiers as wire strings drawn only from the TtscGraphNodeModifier
|
|
12
|
+
// union, mapping the combined modifier flags of a class, a static/readonly
|
|
13
|
+
// property, an accessibility-qualified async method, and an exported const onto
|
|
14
|
+
// their union members.
|
|
15
|
+
//
|
|
16
|
+
// The TypeScript loader runs `typia.assert<ITtscGraphDump>` over this dump, so a
|
|
17
|
+
// modifier string outside the union would reject the whole graph. The assertion
|
|
18
|
+
// pins both the exact strings emitted and the negative twin — a member with no
|
|
19
|
+
// modifier must carry none — so an over-broad flag mapping cannot slip through.
|
|
20
|
+
//
|
|
21
|
+
// 1. Compile a fixture with `export abstract class`, a `static readonly`
|
|
22
|
+
// property, a `private async` method, a plain method, and `export const enum`
|
|
23
|
+
// (the `const` keyword that is a modifier, unlike a `const` variable).
|
|
24
|
+
// 2. Build and marshal the dump.
|
|
25
|
+
// 3. Assert each node's modifiers are exactly the expected union strings, the
|
|
26
|
+
// plain method has none, and every emitted string is a union member.
|
|
27
|
+
func TestNodeModifiersEmitUnionStrings(t *testing.T) {
|
|
28
|
+
root := t.TempDir()
|
|
29
|
+
writeFile(t, filepath.Join(root, "tsconfig.json"), fixtureTSConfig)
|
|
30
|
+
writeFile(t, filepath.Join(root, "src", "main.ts"), `export abstract class Service {
|
|
31
|
+
static readonly config: number = 1;
|
|
32
|
+
private async run(): Promise<void> {}
|
|
33
|
+
plain(): void {}
|
|
34
|
+
}
|
|
35
|
+
export const enum Mode {
|
|
36
|
+
On,
|
|
37
|
+
}
|
|
38
|
+
`)
|
|
39
|
+
|
|
40
|
+
prog, diags, err := driver.LoadProgram(root, "tsconfig.json", driver.LoadProgramOptions{})
|
|
41
|
+
if err != nil {
|
|
42
|
+
t.Fatal(err)
|
|
43
|
+
}
|
|
44
|
+
if len(diags) != 0 {
|
|
45
|
+
t.Fatalf("unexpected diagnostics: %v", diags)
|
|
46
|
+
}
|
|
47
|
+
defer func() { _ = prog.Close() }()
|
|
48
|
+
|
|
49
|
+
g := Build(prog)
|
|
50
|
+
dump := NewDump(g, root, "tsconfig.json", nil, SourceTexts(prog))
|
|
51
|
+
|
|
52
|
+
byID := make(map[string]DumpNode, len(dump.Nodes))
|
|
53
|
+
for _, n := range dump.Nodes {
|
|
54
|
+
byID[n.ID] = n
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
want := map[string][]string{
|
|
58
|
+
"src/main.ts#Service:class": {"export", "abstract"},
|
|
59
|
+
"src/main.ts#Service.config:variable": {"static", "readonly"},
|
|
60
|
+
"src/main.ts#Service.run:method": {"async", "private"},
|
|
61
|
+
"src/main.ts#Mode:enum": {"export", "const"},
|
|
62
|
+
}
|
|
63
|
+
for id, expected := range want {
|
|
64
|
+
node, ok := byID[id]
|
|
65
|
+
if !ok {
|
|
66
|
+
t.Fatalf("missing node %q; have %v", id, dumpNodeIDs(dump))
|
|
67
|
+
}
|
|
68
|
+
if !equalStrings(node.Modifiers, expected) {
|
|
69
|
+
t.Fatalf("node %q modifiers = %v, want %v", id, node.Modifiers, expected)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Negative twin: a member with no modifier keyword emits no modifiers.
|
|
74
|
+
plain, ok := byID["src/main.ts#Service.plain:method"]
|
|
75
|
+
if !ok {
|
|
76
|
+
t.Fatalf("missing plain method node; have %v", dumpNodeIDs(dump))
|
|
77
|
+
}
|
|
78
|
+
if len(plain.Modifiers) != 0 {
|
|
79
|
+
t.Fatalf("plain method modifiers = %v, want none", plain.Modifiers)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Every emitted modifier is a member of the TtscGraphNodeModifier union, since
|
|
83
|
+
// an unknown string would reject the dump under typia.assert.
|
|
84
|
+
union := map[string]bool{
|
|
85
|
+
"export": true, "default": true, "declare": true, "abstract": true,
|
|
86
|
+
"static": true, "readonly": true, "async": true, "const": true,
|
|
87
|
+
"public": true, "private": true, "protected": true, "optional": true,
|
|
88
|
+
}
|
|
89
|
+
for _, n := range dump.Nodes {
|
|
90
|
+
for _, m := range n.Modifiers {
|
|
91
|
+
if !union[m] {
|
|
92
|
+
t.Fatalf("node %q emitted non-union modifier %q", n.ID, m)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// equalStrings reports whether two string slices have the same elements in order.
|
|
99
|
+
func equalStrings(a, b []string) bool {
|
|
100
|
+
if len(a) != len(b) {
|
|
101
|
+
return false
|
|
102
|
+
}
|
|
103
|
+
for i := range a {
|
|
104
|
+
if a[i] != b[i] {
|
|
105
|
+
return false
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return true
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// dumpNodeIDs returns a dump's node ids for failure messages.
|
|
112
|
+
func dumpNodeIDs(d Dump) []string {
|
|
113
|
+
ids := make([]string, 0, len(d.Nodes))
|
|
114
|
+
for _, n := range d.Nodes {
|
|
115
|
+
ids = append(ids, n.ID)
|
|
116
|
+
}
|
|
117
|
+
return ids
|
|
118
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
package graph
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"path/filepath"
|
|
5
|
+
"testing"
|
|
6
|
+
|
|
7
|
+
"github.com/samchon/ttsc/packages/ttsc/driver"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
// TestNodesAreMarkedExportedThroughTheExportTable verifies that markExports flags
|
|
11
|
+
// a node as Exported from the checker's module export table, not from a syntactic
|
|
12
|
+
// `export` modifier — so a declaration exported by a separate `export { Service }`
|
|
13
|
+
// statement counts, while an unexported sibling does not.
|
|
14
|
+
//
|
|
15
|
+
// The export surface drives the public-API projection, and the codebases the
|
|
16
|
+
// graph targets export most of their surface through `export { … }` and barrel
|
|
17
|
+
// re-exports rather than inline modifiers. A modifier-only scan would miss
|
|
18
|
+
// Service here; the export-table walk must catch it, and must still leave the
|
|
19
|
+
// unexported Internal alone.
|
|
20
|
+
//
|
|
21
|
+
// 1. Compile a fixture declaring `class Service` and `class Internal`, exporting
|
|
22
|
+
// only Service through a trailing `export { Service }` statement.
|
|
23
|
+
// 2. Build the graph.
|
|
24
|
+
// 3. Assert the Service node is Exported and the Internal node is not.
|
|
25
|
+
func TestNodesAreMarkedExportedThroughTheExportTable(t *testing.T) {
|
|
26
|
+
root := t.TempDir()
|
|
27
|
+
writeFile(t, filepath.Join(root, "tsconfig.json"), fixtureTSConfig)
|
|
28
|
+
writeFile(t, filepath.Join(root, "src", "main.ts"), `class Service {}
|
|
29
|
+
class Internal {}
|
|
30
|
+
export { Service };
|
|
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
|
+
path := sourceFile(t, prog, "main.ts").FileName()
|
|
44
|
+
|
|
45
|
+
service := graph.Nodes[nodeID(path, "Service", NodeClass)]
|
|
46
|
+
internal := graph.Nodes[nodeID(path, "Internal", NodeClass)]
|
|
47
|
+
if service == nil || internal == nil {
|
|
48
|
+
t.Fatalf("Build did not record both classes; have %v", nodeIDSet(graph))
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Exported via a separate `export { Service }` statement — no inline modifier,
|
|
52
|
+
// so only the checker export table reveals it.
|
|
53
|
+
if !service.Exported {
|
|
54
|
+
t.Fatalf("Service should be marked Exported through the export table")
|
|
55
|
+
}
|
|
56
|
+
// Negative twin: Internal is never exported, so the public-API projection must
|
|
57
|
+
// not surface it.
|
|
58
|
+
if internal.Exported {
|
|
59
|
+
t.Fatalf("Internal is not exported but was marked Exported")
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
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
|
+
// TestOverloadImplementationReplacesSignature verifies that an overload set's
|
|
12
|
+
// graph node points at the executable implementation, not the first signature.
|
|
13
|
+
// Overload-heavy APIs otherwise force agents to re-open the file to see the
|
|
14
|
+
// calls that the graph already attributed to the node.
|
|
15
|
+
func TestOverloadImplementationReplacesSignature(t *testing.T) {
|
|
16
|
+
root := t.TempDir()
|
|
17
|
+
writeFile(t, filepath.Join(root, "tsconfig.json"), fixtureTSConfig)
|
|
18
|
+
writeFile(t, filepath.Join(root, "src", "main.ts"), `export class Query {
|
|
19
|
+
join(name: string): this
|
|
20
|
+
join(name: string, alias?: string): this {
|
|
21
|
+
return this.realJoin(name, alias)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
private realJoin(name: string, alias?: string): this {
|
|
25
|
+
return this
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
`)
|
|
29
|
+
|
|
30
|
+
prog, diags, err := driver.LoadProgram(root, "tsconfig.json", driver.LoadProgramOptions{})
|
|
31
|
+
if err != nil {
|
|
32
|
+
t.Fatal(err)
|
|
33
|
+
}
|
|
34
|
+
if len(diags) != 0 {
|
|
35
|
+
t.Fatalf("unexpected diagnostics: %v", diags)
|
|
36
|
+
}
|
|
37
|
+
defer func() { _ = prog.Close() }()
|
|
38
|
+
|
|
39
|
+
graph := Build(prog)
|
|
40
|
+
file := sourceFile(t, prog, "main.ts")
|
|
41
|
+
path := file.FileName()
|
|
42
|
+
id := nodeID(path, "Query.join", NodeMethod)
|
|
43
|
+
node := graph.Nodes[id]
|
|
44
|
+
if node == nil {
|
|
45
|
+
t.Fatalf("missing node Query.join; have %v", nodeIDSet(graph))
|
|
46
|
+
}
|
|
47
|
+
source := file.Text()[node.Pos:node.End]
|
|
48
|
+
if !strings.Contains(source, "return this.realJoin") {
|
|
49
|
+
t.Fatalf("expected Query.join node to cover the implementation body, got:\n%s", source)
|
|
50
|
+
}
|
|
51
|
+
if !hasEdge(graph, id, nodeID(path, "Query.realJoin", NodeMethod), EdgeValueCall) {
|
|
52
|
+
t.Fatalf("missing value-call edge Query.join -> Query.realJoin; edges: %v", graph.Edges)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
package graph
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"path/filepath"
|
|
5
|
+
"testing"
|
|
6
|
+
|
|
7
|
+
"github.com/samchon/ttsc/packages/ttsc/driver"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
// TestPropertyMemberEdgesRemainOwnerVisible verifies property-member nodes are
|
|
11
|
+
// additive rather than replacing class/interface owner-level edges.
|
|
12
|
+
//
|
|
13
|
+
// Property nodes give MCP precise evidence for state/property flows, but broad
|
|
14
|
+
// architecture questions still need `Service -> Dep` and `Service -> makeDep`
|
|
15
|
+
// without already knowing `Service.dep`.
|
|
16
|
+
//
|
|
17
|
+
// 1. Compile a class property with both a type reference and initializer call,
|
|
18
|
+
// plus an interface property signature.
|
|
19
|
+
// 2. Build the graph.
|
|
20
|
+
// 3. Assert both the owner node and the property node expose the same
|
|
21
|
+
// dependency evidence.
|
|
22
|
+
func TestPropertyMemberEdgesRemainOwnerVisible(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 interface Dep {
|
|
26
|
+
value: string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function makeDep(): Dep {
|
|
30
|
+
return { value: "ok" }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export class Service {
|
|
34
|
+
dep: Dep = makeDep()
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface Contract {
|
|
38
|
+
dep: Dep
|
|
39
|
+
}
|
|
40
|
+
`)
|
|
41
|
+
|
|
42
|
+
prog, diags, err := driver.LoadProgram(root, "tsconfig.json", driver.LoadProgramOptions{})
|
|
43
|
+
if err != nil {
|
|
44
|
+
t.Fatal(err)
|
|
45
|
+
}
|
|
46
|
+
if len(diags) != 0 {
|
|
47
|
+
t.Fatalf("unexpected diagnostics: %v", diags)
|
|
48
|
+
}
|
|
49
|
+
defer func() { _ = prog.Close() }()
|
|
50
|
+
|
|
51
|
+
graph := Build(prog)
|
|
52
|
+
path := sourceFile(t, prog, "main.ts").FileName()
|
|
53
|
+
dep := nodeID(path, "Dep", NodeInterface)
|
|
54
|
+
makeDep := nodeID(path, "makeDep", NodeFunction)
|
|
55
|
+
service := nodeID(path, "Service", NodeClass)
|
|
56
|
+
serviceDep := nodeID(path, "Service.dep", NodeVariable)
|
|
57
|
+
contract := nodeID(path, "Contract", NodeInterface)
|
|
58
|
+
contractDep := nodeID(path, "Contract.dep", NodeVariable)
|
|
59
|
+
|
|
60
|
+
for _, id := range []string{serviceDep, contractDep} {
|
|
61
|
+
if _, ok := graph.Nodes[id]; !ok {
|
|
62
|
+
t.Fatalf("missing property node %s; nodes: %v", id, nodeIDSet(graph))
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if !hasEdge(graph, serviceDep, dep, EdgeTypeRef) {
|
|
66
|
+
t.Fatalf("missing type-ref edge Service.dep -> Dep; edges: %v", graph.Edges)
|
|
67
|
+
}
|
|
68
|
+
if !hasEdge(graph, serviceDep, makeDep, EdgeValueCall) {
|
|
69
|
+
t.Fatalf("missing value-call edge Service.dep -> makeDep; edges: %v", graph.Edges)
|
|
70
|
+
}
|
|
71
|
+
if !hasEdge(graph, service, dep, EdgeTypeRef) {
|
|
72
|
+
t.Fatalf("missing owner type-ref edge Service -> Dep; edges: %v", graph.Edges)
|
|
73
|
+
}
|
|
74
|
+
if !hasEdge(graph, service, makeDep, EdgeValueCall) {
|
|
75
|
+
t.Fatalf("missing owner value-call edge Service -> makeDep; edges: %v", graph.Edges)
|
|
76
|
+
}
|
|
77
|
+
if !hasEdge(graph, contractDep, dep, EdgeTypeRef) {
|
|
78
|
+
t.Fatalf("missing type-ref edge Contract.dep -> Dep; edges: %v", graph.Edges)
|
|
79
|
+
}
|
|
80
|
+
if !hasEdge(graph, contract, dep, EdgeTypeRef) {
|
|
81
|
+
t.Fatalf("missing owner type-ref edge Contract -> Dep; edges: %v", graph.Edges)
|
|
82
|
+
}
|
|
83
|
+
}
|