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,102 @@
|
|
|
1
|
+
// Package graph builds a checker-resolved code reference graph over a tsgo
|
|
2
|
+
// Program: symbols as nodes, type-resolved relationships as edges. Because the
|
|
3
|
+
// graph rides ttsc's in-process Checker, every edge is resolved by the real
|
|
4
|
+
// type checker rather than a syntactic heuristic, so an edge can be tagged
|
|
5
|
+
// "checker-resolved" instead of "guessed".
|
|
6
|
+
//
|
|
7
|
+
// resolve.go holds the load-bearing primitive the rest of the graph depends on:
|
|
8
|
+
// following a reference to the true declaration the checker binds it to. The
|
|
9
|
+
// hard case is the barrel re-export, where `pkg/index.ts` re-exports a sibling's
|
|
10
|
+
// symbol; that shape carries almost every cross-package edge in a monorepo.
|
|
11
|
+
// Stopping at GetSymbolAtLocation lands on the local import alias and severs the
|
|
12
|
+
// edge at the index file, which collapses the output back to tree-sitter
|
|
13
|
+
// quality. Unwrapping the alias chain (Checker_getAliasedSymbol) lands on the
|
|
14
|
+
// sibling source that actually declares the symbol. Resolve does that unwrap,
|
|
15
|
+
// then classifies where the declaration lives so a node_modules / `.d.ts`
|
|
16
|
+
// boundary becomes an external leaf instead of pulling a dependency's internals
|
|
17
|
+
// into the graph.
|
|
18
|
+
package graph
|
|
19
|
+
|
|
20
|
+
import (
|
|
21
|
+
"strings"
|
|
22
|
+
|
|
23
|
+
shimast "github.com/microsoft/typescript-go/shim/ast"
|
|
24
|
+
shimchecker "github.com/microsoft/typescript-go/shim/checker"
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
// Target is the resolved endpoint of a reference: the declaration symbol the
|
|
28
|
+
// checker binds it to, the source file that declares it, and whether that file
|
|
29
|
+
// sits outside the workspace (a node_modules or `.d.ts` boundary leaf).
|
|
30
|
+
type Target struct {
|
|
31
|
+
Symbol *shimast.Symbol
|
|
32
|
+
File string
|
|
33
|
+
External bool
|
|
34
|
+
Pos int
|
|
35
|
+
End int
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Resolve follows ref to the real declaration the checker binds it to. It
|
|
39
|
+
// unwraps import/export alias chains so a reference through a barrel re-export
|
|
40
|
+
// lands on the sibling source that declares the symbol, not the re-exporting
|
|
41
|
+
// index file. It returns nil when the checker cannot bind ref to a symbol (a
|
|
42
|
+
// numeric literal, a punctuation token, an unresolved name).
|
|
43
|
+
func Resolve(checker *shimchecker.Checker, ref *shimast.Node) *Target {
|
|
44
|
+
symbol := checker.GetSymbolAtLocation(ref)
|
|
45
|
+
if symbol == nil {
|
|
46
|
+
return nil
|
|
47
|
+
}
|
|
48
|
+
if symbol.Flags&shimast.SymbolFlagsAlias != 0 {
|
|
49
|
+
if aliased := shimchecker.Checker_getAliasedSymbol(checker, symbol); aliased != nil {
|
|
50
|
+
symbol = aliased
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
target := &Target{Symbol: symbol}
|
|
54
|
+
if declaration := declarationNode(symbol); declaration != nil {
|
|
55
|
+
target.Pos = declaration.Pos()
|
|
56
|
+
target.End = declaration.End()
|
|
57
|
+
if file := shimast.GetSourceFileOfNode(declaration); file != nil {
|
|
58
|
+
target.File = file.FileName()
|
|
59
|
+
target.External = file.IsDeclarationFile ||
|
|
60
|
+
strings.Contains(target.File, "/node_modules/")
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return target
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// declarationFile returns the source file of symbol's first declaration, or nil
|
|
67
|
+
// when the symbol carries no declaration (an intrinsic or synthesized symbol).
|
|
68
|
+
// The checker resolves symlinks to realpath because preserveSymlinks defaults to
|
|
69
|
+
// false, so a pnpm `workspace:*` sibling resolves to its real source here and is
|
|
70
|
+
// not misclassified as external by Resolve.
|
|
71
|
+
func declarationFile(symbol *shimast.Symbol) *shimast.SourceFile {
|
|
72
|
+
if declaration := declarationNode(symbol); declaration != nil {
|
|
73
|
+
return shimast.GetSourceFileOfNode(declaration)
|
|
74
|
+
}
|
|
75
|
+
return nil
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
func declarationNode(symbol *shimast.Symbol) *shimast.Node {
|
|
79
|
+
if len(symbol.Declarations) == 0 {
|
|
80
|
+
return nil
|
|
81
|
+
}
|
|
82
|
+
// Prefer a non-declaration-file declaration. A declaration-merged symbol (a
|
|
83
|
+
// class paired with an interface, or a function with a namespace) can list a
|
|
84
|
+
// `.d.ts` declaration first; classifying by it would mark a real workspace
|
|
85
|
+
// symbol external and sever it from the graph.
|
|
86
|
+
for _, declaration := range symbol.Declarations {
|
|
87
|
+
if file := shimast.GetSourceFileOfNode(declaration); file != nil && !file.IsDeclarationFile && declaration.Body() != nil {
|
|
88
|
+
return declaration
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
for _, declaration := range symbol.Declarations {
|
|
92
|
+
if file := shimast.GetSourceFileOfNode(declaration); file != nil && !file.IsDeclarationFile {
|
|
93
|
+
return declaration
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
for _, declaration := range symbol.Declarations {
|
|
97
|
+
if declaration.Body() != nil {
|
|
98
|
+
return declaration
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return symbol.Declarations[0]
|
|
102
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
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
|
+
// TestResolvePrefersImplementationDeclarationSpan verifies overloaded callables
|
|
12
|
+
// resolve to the executable implementation span, not the first signature.
|
|
13
|
+
//
|
|
14
|
+
// Without this, MCP answers can show only an overload header and omit the body
|
|
15
|
+
// that explains downstream calls, pushing agents back to raw file reads.
|
|
16
|
+
//
|
|
17
|
+
// 1. Compile an overloaded function whose implementation contains a unique
|
|
18
|
+
// expression.
|
|
19
|
+
// 2. Resolve the function symbol.
|
|
20
|
+
// 3. Assert the selected target span includes the implementation body.
|
|
21
|
+
func TestResolvePrefersImplementationDeclarationSpan(t *testing.T) {
|
|
22
|
+
root := t.TempDir()
|
|
23
|
+
writeFile(t, filepath.Join(root, "tsconfig.json"), fixtureTSConfig)
|
|
24
|
+
writeFile(t, filepath.Join(root, "src", "main.ts"), `export function api(value: string): string
|
|
25
|
+
export function api(value: string): string {
|
|
26
|
+
return value.toUpperCase()
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
api("x")
|
|
30
|
+
`)
|
|
31
|
+
|
|
32
|
+
prog, diags, err := driver.LoadProgram(root, "tsconfig.json", driver.LoadProgramOptions{})
|
|
33
|
+
if err != nil {
|
|
34
|
+
t.Fatal(err)
|
|
35
|
+
}
|
|
36
|
+
if len(diags) != 0 {
|
|
37
|
+
t.Fatalf("unexpected diagnostics: %v", diags)
|
|
38
|
+
}
|
|
39
|
+
defer func() { _ = prog.Close() }()
|
|
40
|
+
|
|
41
|
+
file := sourceFile(t, prog, "main.ts")
|
|
42
|
+
target := Resolve(prog.Checker, identifier(t, file, "api"))
|
|
43
|
+
if target == nil {
|
|
44
|
+
t.Fatal("Resolve returned nil for api")
|
|
45
|
+
}
|
|
46
|
+
source := file.Text()[target.Pos:target.End]
|
|
47
|
+
if !strings.Contains(source, "toUpperCase") {
|
|
48
|
+
t.Fatalf("expected Resolve to report the implementation span, got:\n%s", source)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
package graph
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"os"
|
|
5
|
+
"path/filepath"
|
|
6
|
+
"strings"
|
|
7
|
+
"testing"
|
|
8
|
+
|
|
9
|
+
shimast "github.com/microsoft/typescript-go/shim/ast"
|
|
10
|
+
|
|
11
|
+
"github.com/samchon/ttsc/packages/ttsc/driver"
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
// TestResolveUnwrapsBarrelReexportToRealDeclaration is a traversal-completeness
|
|
15
|
+
// probe, not a unit test of a single branch: it runs a real Checker over a
|
|
16
|
+
// ttsc-owned fixture and asserts the load-bearing graph primitive
|
|
17
|
+
// (GetSymbolAtLocation -> Checker_getAliasedSymbol -> declaration) reaches the
|
|
18
|
+
// sibling source that actually declares a symbol, instead of dead-ending on the
|
|
19
|
+
// barrel re-export. This is the differentiator the whole package rests on: a
|
|
20
|
+
// path-heuristic tool stops at the index file and severs every cross-package
|
|
21
|
+
// edge, so if this surface ever regresses the graph silently collapses back to
|
|
22
|
+
// tree-sitter quality. Mirrors the naive-vs-bridged shape of the base-chain
|
|
23
|
+
// probe in packages/lint/test/shim.
|
|
24
|
+
//
|
|
25
|
+
// 1. Compile a fixture where main.ts imports `target` through a barrel
|
|
26
|
+
// (`index.ts` re-exports it from `impl.ts`).
|
|
27
|
+
// 2. Resolve the call-site reference two ways: the naive `GetSymbolAtLocation`
|
|
28
|
+
// stop (the local import alias) and the bridged `Resolve` (alias unwrapped).
|
|
29
|
+
// 3. Assert the naive stop lands on the import in main.ts (the edge is severed
|
|
30
|
+
// there) while `Resolve` lands on the real declaration in impl.ts, and that
|
|
31
|
+
// a workspace declaration is not classified as an external boundary leaf.
|
|
32
|
+
func TestResolveUnwrapsBarrelReexportToRealDeclaration(t *testing.T) {
|
|
33
|
+
root := t.TempDir()
|
|
34
|
+
writeFile(t, filepath.Join(root, "tsconfig.json"), `{
|
|
35
|
+
"compilerOptions": {
|
|
36
|
+
"target": "ES2022",
|
|
37
|
+
"module": "commonjs",
|
|
38
|
+
"strict": true,
|
|
39
|
+
"rootDir": "src",
|
|
40
|
+
"outDir": "dist"
|
|
41
|
+
},
|
|
42
|
+
"files": ["src/main.ts"]
|
|
43
|
+
}
|
|
44
|
+
`)
|
|
45
|
+
writeFile(t, filepath.Join(root, "src", "impl.ts"), `export function target(): number {
|
|
46
|
+
return 1;
|
|
47
|
+
}
|
|
48
|
+
`)
|
|
49
|
+
// The barrel: re-exports the sibling's symbol without redeclaring it. This is
|
|
50
|
+
// the alias hop that severs the edge for a path-string resolver.
|
|
51
|
+
writeFile(t, filepath.Join(root, "src", "index.ts"), `export { target } from "./impl";
|
|
52
|
+
`)
|
|
53
|
+
writeFile(t, filepath.Join(root, "src", "main.ts"), `import { target } from "./index";
|
|
54
|
+
const value = 1;
|
|
55
|
+
const echoed = value;
|
|
56
|
+
target();
|
|
57
|
+
void echoed;
|
|
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
|
+
checker := prog.Checker
|
|
70
|
+
if checker == nil {
|
|
71
|
+
t.Fatal("LoadProgram did not acquire a checker")
|
|
72
|
+
}
|
|
73
|
+
main := sourceFile(t, prog, "main.ts")
|
|
74
|
+
|
|
75
|
+
// The naive stop: GetSymbolAtLocation alone binds the call-site reference to
|
|
76
|
+
// the LOCAL import alias, whose declaration is the import specifier in main.ts.
|
|
77
|
+
// A resolver that stops here severs the edge at the import and never reaches
|
|
78
|
+
// the package that declares target.
|
|
79
|
+
callee := calleeIdentifier(t, main)
|
|
80
|
+
raw := checker.GetSymbolAtLocation(callee)
|
|
81
|
+
if raw == nil {
|
|
82
|
+
t.Fatal("GetSymbolAtLocation returned nil for the call-site reference")
|
|
83
|
+
}
|
|
84
|
+
if raw.Flags&shimast.SymbolFlagsAlias == 0 {
|
|
85
|
+
t.Fatal("premise broken: the call-site reference is not an alias, so there is no barrel hop to unwrap")
|
|
86
|
+
}
|
|
87
|
+
if rawFile := declarationFile(raw); rawFile == nil || !strings.HasSuffix(rawFile.FileName(), "main.ts") {
|
|
88
|
+
t.Fatalf("naive stop did not land on the import in main.ts: %v", raw.Declarations)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// The bridged walk: Resolve unwraps the alias chain to the real declaration.
|
|
92
|
+
resolved := Resolve(checker, callee)
|
|
93
|
+
if resolved == nil {
|
|
94
|
+
t.Fatal("Resolve returned nil for the call-site reference")
|
|
95
|
+
}
|
|
96
|
+
if resolved.Symbol == nil || resolved.Symbol.Name != "target" {
|
|
97
|
+
t.Fatalf("Resolve did not land on the target symbol: %+v", resolved.Symbol)
|
|
98
|
+
}
|
|
99
|
+
if !strings.HasSuffix(resolved.File, "impl.ts") {
|
|
100
|
+
t.Fatalf("Resolve dead-ended before impl.ts: got %q (a barrel-severed resolver would report index.ts)", resolved.File)
|
|
101
|
+
}
|
|
102
|
+
if strings.HasSuffix(resolved.File, "index.ts") {
|
|
103
|
+
t.Fatalf("Resolve stopped at the re-exporting barrel index.ts instead of the declaration")
|
|
104
|
+
}
|
|
105
|
+
if resolved.External {
|
|
106
|
+
t.Fatalf("a workspace declaration (%q) was misclassified as an external boundary leaf", resolved.File)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// A directly-declared local reference skips the alias unwrap and is a
|
|
110
|
+
// workspace node, not an external leaf.
|
|
111
|
+
local := Resolve(checker, identifier(t, main, "value"))
|
|
112
|
+
if local == nil || local.Symbol == nil || local.Symbol.Name != "value" {
|
|
113
|
+
t.Fatalf("Resolve did not bind the local const reference: %+v", local)
|
|
114
|
+
}
|
|
115
|
+
if !strings.HasSuffix(local.File, "main.ts") || local.External {
|
|
116
|
+
t.Fatalf("local const resolved to the wrong place: %+v", local)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// A node the checker cannot bind to a symbol resolves to nil rather than
|
|
120
|
+
// panicking, so the extraction walk can skip it.
|
|
121
|
+
if got := Resolve(checker, numericLiteral(t, main)); got != nil {
|
|
122
|
+
t.Fatalf("Resolve bound a numeric literal to a symbol: %+v", got)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// sourceFile returns the resident program's source file whose path ends with
|
|
127
|
+
// suffix, failing the test when no such file is loaded.
|
|
128
|
+
func sourceFile(t *testing.T, prog *driver.Program, suffix string) *shimast.SourceFile {
|
|
129
|
+
t.Helper()
|
|
130
|
+
for _, file := range prog.SourceFiles() {
|
|
131
|
+
if strings.HasSuffix(file.FileName(), suffix) {
|
|
132
|
+
return file
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
t.Fatalf("source file %q not found in program", suffix)
|
|
136
|
+
return nil
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// calleeIdentifier returns the expression identifier of the first call
|
|
140
|
+
// expression in file (the `target` in `target()`), failing when absent.
|
|
141
|
+
func calleeIdentifier(t *testing.T, file *shimast.SourceFile) *shimast.Node {
|
|
142
|
+
t.Helper()
|
|
143
|
+
var found *shimast.Node
|
|
144
|
+
walkNodes(file, func(node *shimast.Node) bool {
|
|
145
|
+
if node.Kind == shimast.KindCallExpression {
|
|
146
|
+
if call := node.AsCallExpression(); call != nil {
|
|
147
|
+
found = call.Expression
|
|
148
|
+
return true
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return false
|
|
152
|
+
})
|
|
153
|
+
if found == nil {
|
|
154
|
+
t.Fatal("could not locate a call-site identifier in the fixture")
|
|
155
|
+
}
|
|
156
|
+
return found
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// identifier returns the first identifier node in file whose text is name,
|
|
160
|
+
// failing when absent.
|
|
161
|
+
func identifier(t *testing.T, file *shimast.SourceFile, name string) *shimast.Node {
|
|
162
|
+
t.Helper()
|
|
163
|
+
var found *shimast.Node
|
|
164
|
+
walkNodes(file, func(node *shimast.Node) bool {
|
|
165
|
+
if node.Kind == shimast.KindIdentifier && shimast.NodeText(node) == name {
|
|
166
|
+
found = node
|
|
167
|
+
return true
|
|
168
|
+
}
|
|
169
|
+
return false
|
|
170
|
+
})
|
|
171
|
+
if found == nil {
|
|
172
|
+
t.Fatalf("identifier %q not found in the fixture", name)
|
|
173
|
+
}
|
|
174
|
+
return found
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// numericLiteral returns the first numeric literal node in file, failing when
|
|
178
|
+
// absent.
|
|
179
|
+
func numericLiteral(t *testing.T, file *shimast.SourceFile) *shimast.Node {
|
|
180
|
+
t.Helper()
|
|
181
|
+
var found *shimast.Node
|
|
182
|
+
walkNodes(file, func(node *shimast.Node) bool {
|
|
183
|
+
if node.Kind == shimast.KindNumericLiteral {
|
|
184
|
+
found = node
|
|
185
|
+
return true
|
|
186
|
+
}
|
|
187
|
+
return false
|
|
188
|
+
})
|
|
189
|
+
if found == nil {
|
|
190
|
+
t.Fatal("numeric literal not found in the fixture")
|
|
191
|
+
}
|
|
192
|
+
return found
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// walkNodes performs a pre-order walk of file's statement subtrees, calling
|
|
196
|
+
// visit on each node and stopping the whole walk as soon as visit returns true.
|
|
197
|
+
func walkNodes(file *shimast.SourceFile, visit func(*shimast.Node) bool) {
|
|
198
|
+
if file.Statements == nil {
|
|
199
|
+
return
|
|
200
|
+
}
|
|
201
|
+
var recur func(node *shimast.Node) bool
|
|
202
|
+
recur = func(node *shimast.Node) bool {
|
|
203
|
+
if node == nil {
|
|
204
|
+
return false
|
|
205
|
+
}
|
|
206
|
+
if visit(node) {
|
|
207
|
+
return true
|
|
208
|
+
}
|
|
209
|
+
return node.ForEachChild(func(child *shimast.Node) bool {
|
|
210
|
+
return recur(child)
|
|
211
|
+
})
|
|
212
|
+
}
|
|
213
|
+
for _, stmt := range file.Statements.Nodes {
|
|
214
|
+
if recur(stmt) {
|
|
215
|
+
return
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// writeFile writes content to path, creating parent directories, failing the
|
|
221
|
+
// test on any error.
|
|
222
|
+
func writeFile(t *testing.T, path, content string) {
|
|
223
|
+
t.Helper()
|
|
224
|
+
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
|
225
|
+
t.Fatal(err)
|
|
226
|
+
}
|
|
227
|
+
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
|
|
228
|
+
t.Fatal(err)
|
|
229
|
+
}
|
|
230
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
package graph
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"path/filepath"
|
|
5
|
+
"testing"
|
|
6
|
+
|
|
7
|
+
"github.com/samchon/ttsc/packages/ttsc/driver"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
// TestTypeRefEdgesCoverTypeofAsSatisfies verifies that the type-position shapes
|
|
11
|
+
// beyond a plain `Foo` annotation are all resolved to type-ref edges:
|
|
12
|
+
//
|
|
13
|
+
// - `typeof value` -> the value it queries (an EntityName, not a TypeReference)
|
|
14
|
+
// - `x as Target` -> Target (the assertion's type)
|
|
15
|
+
// - `expr satisfies T` -> T (the satisfies type)
|
|
16
|
+
//
|
|
17
|
+
// `as` and `satisfies` carry an ordinary TypeReference the recursion already
|
|
18
|
+
// reaches; `typeof` names its value through an EntityName, which a
|
|
19
|
+
// TypeReference-only walk would miss. All three must produce an edge so an
|
|
20
|
+
// impact query sees the dependency.
|
|
21
|
+
func TestTypeRefEdgesCoverTypeofAsSatisfies(t *testing.T) {
|
|
22
|
+
root := t.TempDir()
|
|
23
|
+
writeFile(t, filepath.Join(root, "tsconfig.json"), `{
|
|
24
|
+
"compilerOptions": {
|
|
25
|
+
"target": "ES2022",
|
|
26
|
+
"module": "commonjs",
|
|
27
|
+
"strict": true,
|
|
28
|
+
"rootDir": "src",
|
|
29
|
+
"outDir": "dist"
|
|
30
|
+
},
|
|
31
|
+
"files": ["src/main.ts"]
|
|
32
|
+
}
|
|
33
|
+
`)
|
|
34
|
+
writeFile(t, filepath.Join(root, "src", "main.ts"), `export interface Config {}
|
|
35
|
+
export interface Target {}
|
|
36
|
+
|
|
37
|
+
export const settings = { mode: "fast" };
|
|
38
|
+
|
|
39
|
+
export type SettingsShape = typeof settings;
|
|
40
|
+
|
|
41
|
+
export function coerce(x: unknown): Target {
|
|
42
|
+
return x as Target;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const config = { a: 1 } satisfies Config;
|
|
46
|
+
`)
|
|
47
|
+
|
|
48
|
+
prog, diags, err := driver.LoadProgram(root, "tsconfig.json", driver.LoadProgramOptions{})
|
|
49
|
+
if err != nil {
|
|
50
|
+
t.Fatal(err)
|
|
51
|
+
}
|
|
52
|
+
if len(diags) != 0 {
|
|
53
|
+
t.Fatalf("unexpected diagnostics: %v", diags)
|
|
54
|
+
}
|
|
55
|
+
defer func() { _ = prog.Close() }()
|
|
56
|
+
|
|
57
|
+
graph := Build(prog)
|
|
58
|
+
path := sourceFile(t, prog, "main.ts").FileName()
|
|
59
|
+
|
|
60
|
+
settingsShape := nodeID(path, "SettingsShape", NodeTypeAlias)
|
|
61
|
+
settings := nodeID(path, "settings", NodeVariable)
|
|
62
|
+
coerce := nodeID(path, "coerce", NodeFunction)
|
|
63
|
+
target := nodeID(path, "Target", NodeInterface)
|
|
64
|
+
config := nodeID(path, "config", NodeVariable)
|
|
65
|
+
configType := nodeID(path, "Config", NodeInterface)
|
|
66
|
+
|
|
67
|
+
if !hasEdge(graph, settingsShape, settings, EdgeTypeRef) {
|
|
68
|
+
t.Errorf("missing type-ref edge SettingsShape -> settings (typeof query)")
|
|
69
|
+
}
|
|
70
|
+
if !hasEdge(graph, coerce, target, EdgeTypeRef) {
|
|
71
|
+
t.Errorf("missing type-ref edge coerce -> Target (as assertion)")
|
|
72
|
+
}
|
|
73
|
+
if !hasEdge(graph, config, configType, EdgeTypeRef) {
|
|
74
|
+
t.Errorf("missing type-ref edge config -> Config (satisfies)")
|
|
75
|
+
}
|
|
76
|
+
if t.Failed() {
|
|
77
|
+
t.Logf("edges: %v", graph.Edges)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
package graph
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"path/filepath"
|
|
5
|
+
"testing"
|
|
6
|
+
|
|
7
|
+
"github.com/samchon/ttsc/packages/ttsc/driver"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
// TestTypeRefEdgesResolveNamedTypesAcrossFiles verifies that a type-position
|
|
11
|
+
// reference to a named type in another file is recorded as a type-ref edge to
|
|
12
|
+
// that type's real declaration, and is kept distinct from a value-call. Treating
|
|
13
|
+
// type references as first-class edges lets an impact query separate "uses this
|
|
14
|
+
// at runtime" from "depends on this type", which fits the ttsc thesis.
|
|
15
|
+
//
|
|
16
|
+
// 1. Compile a fixture where use(c: Config) annotates a parameter with an
|
|
17
|
+
// interface declared in another file.
|
|
18
|
+
// 2. Build the graph.
|
|
19
|
+
// 3. Assert a use -> Config type-ref edge exists and is not a value-call.
|
|
20
|
+
func TestTypeRefEdgesResolveNamedTypesAcrossFiles(t *testing.T) {
|
|
21
|
+
root := t.TempDir()
|
|
22
|
+
writeFile(t, filepath.Join(root, "tsconfig.json"), fixtureTSConfig)
|
|
23
|
+
writeFile(t, filepath.Join(root, "src", "types.ts"), `export interface Config {
|
|
24
|
+
name: string;
|
|
25
|
+
}
|
|
26
|
+
`)
|
|
27
|
+
writeFile(t, filepath.Join(root, "src", "main.ts"), `import { Config } from "./types";
|
|
28
|
+
export function use(c: Config): string {
|
|
29
|
+
return c.name;
|
|
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
|
+
use := nodeID(sourceFile(t, prog, "main.ts").FileName(), "use", NodeFunction)
|
|
44
|
+
config := nodeID(sourceFile(t, prog, "types.ts").FileName(), "Config", NodeInterface)
|
|
45
|
+
|
|
46
|
+
if !hasEdge(graph, use, config, EdgeTypeRef) {
|
|
47
|
+
t.Fatalf("missing type-ref edge use -> Config; edges: %v", graph.Edges)
|
|
48
|
+
}
|
|
49
|
+
if hasEdge(graph, use, config, EdgeValueCall) {
|
|
50
|
+
t.Fatalf("a type reference was misclassified as a value-call edge")
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
package graph
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"path/filepath"
|
|
5
|
+
"testing"
|
|
6
|
+
|
|
7
|
+
"github.com/samchon/ttsc/packages/ttsc/driver"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
// TestValueAccessEdgesCoverPropertiesAndAccessors verifies property and
|
|
11
|
+
// accessor reads/writes become value-access edges, not value-call edges.
|
|
12
|
+
//
|
|
13
|
+
// Lazy getters and state properties are part of the runtime flow an agent
|
|
14
|
+
// needs, but they are not invocations. The graph must expose them without
|
|
15
|
+
// corrupting call-flow semantics.
|
|
16
|
+
//
|
|
17
|
+
// 1. Compile a class with a property initializer, getter, setter, dotted access,
|
|
18
|
+
// and string-literal bracket access.
|
|
19
|
+
// 2. Build the graph.
|
|
20
|
+
// 3. Assert property/getter/setter uses are value-access edges while the
|
|
21
|
+
// property initializer's real function call stays a value-call edge.
|
|
22
|
+
func TestValueAccessEdgesCoverPropertiesAndAccessors(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 function seed(): string[] {
|
|
26
|
+
return []
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class Store {
|
|
30
|
+
protected items: string[] = seed()
|
|
31
|
+
|
|
32
|
+
get count(): number {
|
|
33
|
+
return this.items.length
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
set count(value: number) {
|
|
37
|
+
this.items = Array(value).fill("")
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
read(): number {
|
|
41
|
+
return this.count + this.items.length + this["count"] + this["items"].length
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
write(): void {
|
|
45
|
+
this.count = 1
|
|
46
|
+
this["count"] = 2
|
|
47
|
+
this.items = []
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
`)
|
|
51
|
+
|
|
52
|
+
prog, diags, err := driver.LoadProgram(root, "tsconfig.json", driver.LoadProgramOptions{})
|
|
53
|
+
if err != nil {
|
|
54
|
+
t.Fatal(err)
|
|
55
|
+
}
|
|
56
|
+
if len(diags) != 0 {
|
|
57
|
+
t.Fatalf("unexpected diagnostics: %v", diags)
|
|
58
|
+
}
|
|
59
|
+
defer func() { _ = prog.Close() }()
|
|
60
|
+
|
|
61
|
+
graph := Build(prog)
|
|
62
|
+
path := sourceFile(t, prog, "main.ts").FileName()
|
|
63
|
+
read := nodeID(path, "Store.read", NodeMethod)
|
|
64
|
+
write := nodeID(path, "Store.write", NodeMethod)
|
|
65
|
+
count := nodeID(path, "Store.count", NodeMethod)
|
|
66
|
+
items := nodeID(path, "Store.items", NodeVariable)
|
|
67
|
+
seed := nodeID(path, "seed", NodeFunction)
|
|
68
|
+
|
|
69
|
+
if _, ok := graph.Nodes[items]; !ok {
|
|
70
|
+
t.Fatalf("Build did not record Store.items; have %v", nodeIDSet(graph))
|
|
71
|
+
}
|
|
72
|
+
if !hasEdge(graph, read, count, EdgeValueAccess) {
|
|
73
|
+
t.Fatalf("missing value-access edge Store.read -> Store.count; edges: %v", graph.Edges)
|
|
74
|
+
}
|
|
75
|
+
if !hasEdge(graph, read, items, EdgeValueAccess) {
|
|
76
|
+
t.Fatalf("missing value-access edge Store.read -> Store.items; edges: %v", graph.Edges)
|
|
77
|
+
}
|
|
78
|
+
if !hasEdge(graph, count, items, EdgeValueAccess) {
|
|
79
|
+
t.Fatalf("missing value-access edge Store.count -> Store.items; edges: %v", graph.Edges)
|
|
80
|
+
}
|
|
81
|
+
if !hasEdge(graph, write, count, EdgeValueAccess) {
|
|
82
|
+
t.Fatalf("missing value-access edge Store.write -> Store.count; edges: %v", graph.Edges)
|
|
83
|
+
}
|
|
84
|
+
if !hasEdge(graph, write, items, EdgeValueAccess) {
|
|
85
|
+
t.Fatalf("missing value-access edge Store.write -> Store.items; edges: %v", graph.Edges)
|
|
86
|
+
}
|
|
87
|
+
if !hasEdge(graph, items, seed, EdgeValueCall) {
|
|
88
|
+
t.Fatalf("missing value-call edge Store.items -> seed; edges: %v", graph.Edges)
|
|
89
|
+
}
|
|
90
|
+
if hasEdge(graph, read, count, EdgeValueCall) {
|
|
91
|
+
t.Fatalf("getter read was also recorded as value-call Store.read -> Store.count; edges: %v", graph.Edges)
|
|
92
|
+
}
|
|
93
|
+
if hasEdge(graph, write, count, EdgeValueCall) {
|
|
94
|
+
t.Fatalf("setter write was also recorded as value-call Store.write -> Store.count; edges: %v", graph.Edges)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
package graph
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"path/filepath"
|
|
5
|
+
"testing"
|
|
6
|
+
|
|
7
|
+
"github.com/samchon/ttsc/packages/ttsc/driver"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
// TestValueCallEdgesCoverJsxComponents verifies that a JSX component use
|
|
11
|
+
// (`<Child />`) is a value-call edge to the component — the relationship a React
|
|
12
|
+
// codebase is built from, which a call-expression-only walk silently dropped. An
|
|
13
|
+
// intrinsic tag (`<div />`) resolves to nothing and adds no edge, so the walk
|
|
14
|
+
// distinguishes a component use from a host element.
|
|
15
|
+
//
|
|
16
|
+
// 1. Compile a .tsx fixture where Parent renders <Child /> inside a <div>.
|
|
17
|
+
// 2. Build the graph.
|
|
18
|
+
// 3. Assert a Parent -> Child value-call edge exists.
|
|
19
|
+
func TestValueCallEdgesCoverJsxComponents(t *testing.T) {
|
|
20
|
+
root := t.TempDir()
|
|
21
|
+
writeFile(t, filepath.Join(root, "tsconfig.json"), `{
|
|
22
|
+
"compilerOptions": {
|
|
23
|
+
"target": "ES2022",
|
|
24
|
+
"module": "commonjs",
|
|
25
|
+
"strict": true,
|
|
26
|
+
"jsx": "preserve",
|
|
27
|
+
"rootDir": "src",
|
|
28
|
+
"outDir": "dist"
|
|
29
|
+
},
|
|
30
|
+
"files": ["src/main.tsx"]
|
|
31
|
+
}
|
|
32
|
+
`)
|
|
33
|
+
writeFile(t, filepath.Join(root, "src", "main.tsx"), `declare namespace JSX {
|
|
34
|
+
interface Element {}
|
|
35
|
+
interface IntrinsicElements {
|
|
36
|
+
div: {};
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export function Child(): JSX.Element {
|
|
40
|
+
return null as unknown as JSX.Element;
|
|
41
|
+
}
|
|
42
|
+
export function Parent(): JSX.Element {
|
|
43
|
+
return (
|
|
44
|
+
<div>
|
|
45
|
+
<Child />
|
|
46
|
+
</div>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
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
|
+
graph := Build(prog)
|
|
61
|
+
path := sourceFile(t, prog, "main.tsx").FileName()
|
|
62
|
+
|
|
63
|
+
parent := nodeID(path, "Parent", NodeFunction)
|
|
64
|
+
child := nodeID(path, "Child", NodeFunction)
|
|
65
|
+
|
|
66
|
+
if !hasEdge(graph, parent, child, EdgeValueCall) {
|
|
67
|
+
t.Fatalf("missing value-call edge Parent -> Child (JSX component use); edges: %v", graph.Edges)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// The intrinsic `<div>` host element resolves to nothing and must add no edge.
|
|
71
|
+
// Without this negative twin, a regression that started resolving intrinsic
|
|
72
|
+
// tags would fill every React codebase's call graph with host-element noise
|
|
73
|
+
// while the positive assertion above still passed.
|
|
74
|
+
for _, edge := range graph.Edges {
|
|
75
|
+
if edge.From != parent {
|
|
76
|
+
continue
|
|
77
|
+
}
|
|
78
|
+
if to := graph.Nodes[edge.To]; to != nil && to.Name == "div" {
|
|
79
|
+
t.Fatalf("intrinsic <div> produced a spurious value-call edge from Parent: %v", edge)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|