ttsc 0.16.3 → 0.16.6
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 +120 -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 +374 -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 +581 -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 +122 -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_attribute_function_assignments_to_members_test.go +59 -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,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
|
+
}
|
|
@@ -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
|
+
}
|