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,60 @@
|
|
|
1
|
+
package graph
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
shimast "github.com/microsoft/typescript-go/shim/ast"
|
|
5
|
+
shimchecker "github.com/microsoft/typescript-go/shim/checker"
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
// markExports flags every node reachable through a source file's export table as
|
|
9
|
+
// Exported. It runs on the checker, so it sees the real public surface: an
|
|
10
|
+
// inline `export class`, a separate `export { Foo }`, a re-export
|
|
11
|
+
// `export { Foo } from "./foo"`, and a barrel `export *` all land here, where a
|
|
12
|
+
// purely syntactic `export`-modifier scan would miss the re-export forms that
|
|
13
|
+
// carry most of a monorepo's public API.
|
|
14
|
+
func (g *Graph) markExports(checker *shimchecker.Checker, file *shimast.SourceFile) {
|
|
15
|
+
moduleSymbol := checker.GetSymbolAtLocation(file.AsNode())
|
|
16
|
+
if moduleSymbol == nil {
|
|
17
|
+
return
|
|
18
|
+
}
|
|
19
|
+
for _, export := range shimchecker.Checker_getExportsOfModule(checker, moduleSymbol) {
|
|
20
|
+
symbol := export
|
|
21
|
+
// A re-export is an alias; unwrap it to the declaration it points at so the
|
|
22
|
+
// exported flag lands on the real node, not a re-exporting index file's
|
|
23
|
+
// local alias symbol (which has no node of its own).
|
|
24
|
+
if symbol.Flags&shimast.SymbolFlagsAlias != 0 {
|
|
25
|
+
if aliased := shimchecker.Checker_getAliasedSymbol(checker, symbol); aliased != nil {
|
|
26
|
+
symbol = aliased
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
g.markExportedSymbol(symbol)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// markExportedSymbol sets Exported on the node a resolved export symbol declares,
|
|
34
|
+
// when the graph recorded one. A symbol whose declaration the graph does not
|
|
35
|
+
// model as a node (a re-exported value from a dependency, a kind it does not
|
|
36
|
+
// track) is skipped rather than fabricated.
|
|
37
|
+
func (g *Graph) markExportedSymbol(symbol *shimast.Symbol) {
|
|
38
|
+
declaration := declarationNode(symbol)
|
|
39
|
+
if declaration == nil {
|
|
40
|
+
return
|
|
41
|
+
}
|
|
42
|
+
file := shimast.GetSourceFileOfNode(declaration)
|
|
43
|
+
if file == nil {
|
|
44
|
+
return
|
|
45
|
+
}
|
|
46
|
+
kind := symbolNodeKind(symbol)
|
|
47
|
+
if kind == "" {
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
name := qualifiedName(symbol)
|
|
51
|
+
if kind == NodeMethod {
|
|
52
|
+
name = methodName(symbol)
|
|
53
|
+
}
|
|
54
|
+
if name == "" {
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
if node, ok := g.Nodes[nodeID(file.FileName(), name, kind)]; ok {
|
|
58
|
+
node.Exported = true
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
package graph
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"path/filepath"
|
|
5
|
+
"testing"
|
|
6
|
+
|
|
7
|
+
"github.com/samchon/ttsc/packages/ttsc/driver"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
// TestFileDiagnosticsMatchTsgoCodeAndLocation verifies that FileDiagnostics
|
|
11
|
+
// surfaces the same semantic error code and line tsgo emits, scoped to one file.
|
|
12
|
+
// This is validation gate 4 from issue #259: a deliberate type error must yield
|
|
13
|
+
// the exact tsc code/location from the graph's shared Program, proving the
|
|
14
|
+
// diagnostics path is the real checker and not a re-implementation.
|
|
15
|
+
//
|
|
16
|
+
// 1. Compile a fixture whose only file assigns a string to a number binding.
|
|
17
|
+
// 2. Ask FileDiagnostics for that file.
|
|
18
|
+
// 3. Assert a TS2322 (not assignable) diagnostic on line 1, and that an
|
|
19
|
+
// unrelated path yields none.
|
|
20
|
+
func TestFileDiagnosticsMatchTsgoCodeAndLocation(t *testing.T) {
|
|
21
|
+
root := t.TempDir()
|
|
22
|
+
writeFile(t, filepath.Join(root, "tsconfig.json"), fixtureTSConfig)
|
|
23
|
+
writeFile(t, filepath.Join(root, "src", "main.ts"), `export const broken: number = "not a number";
|
|
24
|
+
`)
|
|
25
|
+
|
|
26
|
+
prog, diags, err := driver.LoadProgram(root, "tsconfig.json", driver.LoadProgramOptions{})
|
|
27
|
+
if err != nil {
|
|
28
|
+
t.Fatal(err)
|
|
29
|
+
}
|
|
30
|
+
// LoadProgram reports only config/parse diagnostics; the type error is
|
|
31
|
+
// syntactically valid, so it surfaces through Program.Diagnostics() instead.
|
|
32
|
+
if len(diags) != 0 {
|
|
33
|
+
t.Fatalf("unexpected parse diagnostics: %v", diags)
|
|
34
|
+
}
|
|
35
|
+
defer func() { _ = prog.Close() }()
|
|
36
|
+
|
|
37
|
+
main := sourceFile(t, prog, "main.ts").FileName()
|
|
38
|
+
got := FileDiagnostics(prog, main)
|
|
39
|
+
|
|
40
|
+
var match *driver.Diagnostic
|
|
41
|
+
for i := range got {
|
|
42
|
+
if got[i].Code == 2322 {
|
|
43
|
+
match = &got[i]
|
|
44
|
+
break
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if match == nil {
|
|
48
|
+
t.Fatalf("expected a TS2322 diagnostic for the bad assignment, got %v", got)
|
|
49
|
+
}
|
|
50
|
+
if match.Line != 1 {
|
|
51
|
+
t.Fatalf("TS2322 reported on line %d, expected line 1", match.Line)
|
|
52
|
+
}
|
|
53
|
+
if match.File != main {
|
|
54
|
+
t.Fatalf("diagnostic file %q is not the queried file %q", match.File, main)
|
|
55
|
+
}
|
|
56
|
+
if other := FileDiagnostics(prog, filepath.Join(root, "src", "absent.ts")); len(other) != 0 {
|
|
57
|
+
t.Fatalf("a file with no diagnostics returned %d", len(other))
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
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
|
+
// TestFileDiagnosticsReportCodeColumnAndMessage verifies that FileDiagnostics
|
|
12
|
+
// surfaces the full tsgo-reported location and text of a type error, not just
|
|
13
|
+
// its code and line: the column points at the offending initializer and the
|
|
14
|
+
// message carries the human-readable reason.
|
|
15
|
+
//
|
|
16
|
+
// The existing match-code-and-location probe pins Line and Code; this one
|
|
17
|
+
// strengthens the contract to Column and Message so a regression that kept the
|
|
18
|
+
// right code but lost the precise span (or the text) is caught. The oracle is
|
|
19
|
+
// the real checker: tsgo attributes the assignability error to the declared
|
|
20
|
+
// binding, so on `export const broken: number = "nope";` the diagnostic sits at
|
|
21
|
+
// `broken` — 1-based column 14 (`export const ` is 13 characters).
|
|
22
|
+
//
|
|
23
|
+
// 1. Compile a fixture whose only file assigns a string to a number binding.
|
|
24
|
+
// 2. Ask FileDiagnostics for that file.
|
|
25
|
+
// 3. Assert the TS2322 diagnostic sits at line 1 / column 14 and its message
|
|
26
|
+
// contains "not assignable".
|
|
27
|
+
func TestFileDiagnosticsReportCodeColumnAndMessage(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 const broken: number = "nope";
|
|
31
|
+
`)
|
|
32
|
+
|
|
33
|
+
prog, diags, err := driver.LoadProgram(root, "tsconfig.json", driver.LoadProgramOptions{})
|
|
34
|
+
if err != nil {
|
|
35
|
+
t.Fatal(err)
|
|
36
|
+
}
|
|
37
|
+
// The bad assignment is syntactically valid, so it surfaces through
|
|
38
|
+
// Program.Diagnostics() rather than the config/parse diagnostics LoadProgram
|
|
39
|
+
// returns.
|
|
40
|
+
if len(diags) != 0 {
|
|
41
|
+
t.Fatalf("unexpected parse diagnostics: %v", diags)
|
|
42
|
+
}
|
|
43
|
+
defer func() { _ = prog.Close() }()
|
|
44
|
+
|
|
45
|
+
main := sourceFile(t, prog, "main.ts").FileName()
|
|
46
|
+
got := FileDiagnostics(prog, main)
|
|
47
|
+
|
|
48
|
+
var match *driver.Diagnostic
|
|
49
|
+
for i := range got {
|
|
50
|
+
if got[i].Code == 2322 {
|
|
51
|
+
match = &got[i]
|
|
52
|
+
break
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if match == nil {
|
|
56
|
+
t.Fatalf("expected a TS2322 diagnostic for the bad assignment, got %v", got)
|
|
57
|
+
}
|
|
58
|
+
if match.Line != 1 {
|
|
59
|
+
t.Fatalf("TS2322 reported on line %d, expected line 1", match.Line)
|
|
60
|
+
}
|
|
61
|
+
// Oracle column: tsgo attributes the assignability error to the declared
|
|
62
|
+
// binding `broken`, which begins at the 14th character of the line.
|
|
63
|
+
if match.Column != 14 {
|
|
64
|
+
t.Fatalf("TS2322 reported at column %d, expected column 14 (the binding)", match.Column)
|
|
65
|
+
}
|
|
66
|
+
if !strings.Contains(match.Message, "not assignable") {
|
|
67
|
+
t.Fatalf("TS2322 message %q does not mention 'not assignable'", match.Message)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
package graph
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"path/filepath"
|
|
5
|
+
"testing"
|
|
6
|
+
|
|
7
|
+
"github.com/samchon/ttsc/packages/ttsc/driver"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
// TestFunctionLocalsDoNotCollide verifies that a function-local declaration is
|
|
11
|
+
// not minted as a graph node. Build records top-level declarations, namespace
|
|
12
|
+
// members, and class/interface members only; a body-scoped local resolved as an
|
|
13
|
+
// edge target has an unqualified, position-free id, so two same-named locals in
|
|
14
|
+
// different scopes would key the same node and merge — fabricating a false edge
|
|
15
|
+
// from each unrelated caller to one phantom callable.
|
|
16
|
+
func TestFunctionLocalsDoNotCollide(t *testing.T) {
|
|
17
|
+
root := t.TempDir()
|
|
18
|
+
writeFile(t, filepath.Join(root, "tsconfig.json"), `{
|
|
19
|
+
"compilerOptions": {
|
|
20
|
+
"target": "ES2022",
|
|
21
|
+
"module": "commonjs",
|
|
22
|
+
"strict": true,
|
|
23
|
+
"rootDir": "src",
|
|
24
|
+
"outDir": "dist"
|
|
25
|
+
},
|
|
26
|
+
"files": ["src/main.ts"]
|
|
27
|
+
}
|
|
28
|
+
`)
|
|
29
|
+
writeFile(t, filepath.Join(root, "src", "main.ts"), `export function outerA(): number {
|
|
30
|
+
function inner(): number {
|
|
31
|
+
return 1;
|
|
32
|
+
}
|
|
33
|
+
return inner();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function outerB(): number {
|
|
37
|
+
function inner(): number {
|
|
38
|
+
return 2;
|
|
39
|
+
}
|
|
40
|
+
return inner();
|
|
41
|
+
}
|
|
42
|
+
`)
|
|
43
|
+
|
|
44
|
+
prog, diags, err := driver.LoadProgram(root, "tsconfig.json", driver.LoadProgramOptions{})
|
|
45
|
+
if err != nil {
|
|
46
|
+
t.Fatal(err)
|
|
47
|
+
}
|
|
48
|
+
if len(diags) != 0 {
|
|
49
|
+
t.Fatalf("unexpected diagnostics: %v", diags)
|
|
50
|
+
}
|
|
51
|
+
defer func() { _ = prog.Close() }()
|
|
52
|
+
|
|
53
|
+
graph := Build(prog)
|
|
54
|
+
path := sourceFile(t, prog, "main.ts").FileName()
|
|
55
|
+
|
|
56
|
+
// No function-local node is minted...
|
|
57
|
+
if _, ok := graph.Nodes[nodeID(path, "inner", NodeFunction)]; ok {
|
|
58
|
+
t.Fatalf("a function-local 'inner' was minted as a node (would collide across scopes)")
|
|
59
|
+
}
|
|
60
|
+
// ...so no edge points at a phantom shared 'inner'.
|
|
61
|
+
for _, edge := range graph.Edges {
|
|
62
|
+
if to := graph.Nodes[edge.To]; to != nil && to.Name == "inner" {
|
|
63
|
+
t.Fatalf("edge to a function-local 'inner' (false cross-scope merge): %v", edge)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
package graph
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"os/exec"
|
|
5
|
+
"strings"
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
// GitIgnoredFiles returns the graph's source files that git ignores: generated
|
|
9
|
+
// output an author does not navigate, such as a Prisma client or other codegen
|
|
10
|
+
// emitted as real .ts into the source tree (the .d.ts emit is already dropped by
|
|
11
|
+
// driver.SourceFiles, but a generated .ts becomes a node and, being large and
|
|
12
|
+
// highly connected, otherwise dominates ranking and floods responses). Callers
|
|
13
|
+
// de-surface them: the MCP matcher keeps them reachable as edge targets and by
|
|
14
|
+
// exact name; the full-graph dump drops them from the viewer payload entirely.
|
|
15
|
+
//
|
|
16
|
+
// Empty when cwd is unset, the tree is not a git work tree, or git is
|
|
17
|
+
// unavailable, so a non-git project is unaffected.
|
|
18
|
+
func GitIgnoredFiles(cwd string, g *Graph) map[string]bool {
|
|
19
|
+
if cwd == "" || g == nil {
|
|
20
|
+
return nil
|
|
21
|
+
}
|
|
22
|
+
seen := make(map[string]bool)
|
|
23
|
+
var paths []string
|
|
24
|
+
for _, node := range g.Nodes {
|
|
25
|
+
f := node.File
|
|
26
|
+
if f == "" || strings.HasPrefix(f, "bundled:///") || seen[f] {
|
|
27
|
+
continue
|
|
28
|
+
}
|
|
29
|
+
seen[f] = true
|
|
30
|
+
paths = append(paths, f)
|
|
31
|
+
}
|
|
32
|
+
if len(paths) == 0 {
|
|
33
|
+
return nil
|
|
34
|
+
}
|
|
35
|
+
cmd := exec.Command("git", "-C", cwd, "check-ignore", "--stdin")
|
|
36
|
+
cmd.Stdin = strings.NewReader(strings.Join(paths, "\n") + "\n")
|
|
37
|
+
// check-ignore exits 0 with the ignored paths on stdout, 1 (an error to
|
|
38
|
+
// Output) with no output when none match, and 128 when git cannot run. Only
|
|
39
|
+
// stdout matters: parse it whenever it is non-empty, ignore the exit code.
|
|
40
|
+
out, _ := cmd.Output()
|
|
41
|
+
ignored := make(map[string]bool)
|
|
42
|
+
for _, line := range strings.Split(strings.TrimSpace(string(out)), "\n") {
|
|
43
|
+
if line != "" {
|
|
44
|
+
ignored[line] = true
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if len(ignored) == 0 {
|
|
48
|
+
return nil
|
|
49
|
+
}
|
|
50
|
+
return ignored
|
|
51
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
package graph
|
|
2
|
+
|
|
3
|
+
// NodeKind classifies a graph node by what its symbol declares.
|
|
4
|
+
type NodeKind string
|
|
5
|
+
|
|
6
|
+
const (
|
|
7
|
+
NodeFunction NodeKind = "function"
|
|
8
|
+
NodeClass NodeKind = "class"
|
|
9
|
+
NodeInterface NodeKind = "interface"
|
|
10
|
+
NodeTypeAlias NodeKind = "type"
|
|
11
|
+
NodeEnum NodeKind = "enum"
|
|
12
|
+
NodeVariable NodeKind = "variable"
|
|
13
|
+
// NodeMethod is a class or interface member (a method, constructor, or
|
|
14
|
+
// accessor). Its id is class-qualified ("path#Class.method:method") so a
|
|
15
|
+
// resolved method call lands on the same node the build pass recorded.
|
|
16
|
+
NodeMethod NodeKind = "method"
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
// Node is one declared symbol. Its ID is position-invariant, built from the file
|
|
20
|
+
// realpath, the declared name, and the kind, so inserting a line above a
|
|
21
|
+
// declaration does not re-key it. That keeps a future incremental layer from
|
|
22
|
+
// churning the whole graph on every edit, which a byte-offset key would force.
|
|
23
|
+
type Node struct {
|
|
24
|
+
ID string
|
|
25
|
+
Name string
|
|
26
|
+
// Simple is the unqualified declared name (`create`, `OrderService`), taken
|
|
27
|
+
// straight from the declaration's symbol. Name may join an owner chain to a
|
|
28
|
+
// member with a single dot, and a quoted member name can itself contain a dot
|
|
29
|
+
// (`"a.b"` → Name `C.a.b`), so the simple/qualified boundary cannot be
|
|
30
|
+
// recovered from Name by splitting on a dot. Recording it here keeps the dump
|
|
31
|
+
// split exact instead of guessing.
|
|
32
|
+
Simple string
|
|
33
|
+
Kind NodeKind
|
|
34
|
+
File string
|
|
35
|
+
External bool
|
|
36
|
+
// Exported marks a node that is part of its module's export surface, resolved
|
|
37
|
+
// through the checker's export table so a re-export (`export { Foo } from`) or
|
|
38
|
+
// a barrel (`export *`) counts, not only an inline `export` modifier. It is
|
|
39
|
+
// the signal a public-API projection filters on.
|
|
40
|
+
Exported bool
|
|
41
|
+
// Modifiers holds the declaration's syntactic modifiers as wire strings (a
|
|
42
|
+
// subset of the TtscGraphNodeModifier union: export/default/declare/abstract/
|
|
43
|
+
// static/readonly/async/const/public/private/protected). It is recorded from
|
|
44
|
+
// the declaration's combined modifier flags during the build pass and emitted
|
|
45
|
+
// for projections that filter on visibility and shape.
|
|
46
|
+
Modifiers []string
|
|
47
|
+
// Pos and End bound the declaration in its source file (byte offsets). They
|
|
48
|
+
// are for display, never identity, so an edit that shifts them does not re-key
|
|
49
|
+
// the node.
|
|
50
|
+
Pos int
|
|
51
|
+
End int
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// EdgeKind classifies a relationship between two nodes.
|
|
55
|
+
type EdgeKind string
|
|
56
|
+
|
|
57
|
+
const (
|
|
58
|
+
// EdgeHeritage is an `extends` / `implements` relationship from a class or
|
|
59
|
+
// interface to a base it derives from.
|
|
60
|
+
EdgeHeritage EdgeKind = "heritage"
|
|
61
|
+
// EdgeValueCall is a runtime use from one declaration of the function, method,
|
|
62
|
+
// or constructor it invokes: a call, a `new T()`, a `<Component/>` JSX use, or
|
|
63
|
+
// a tagged-template tag. Uses of a dependency's method are not modeled (the
|
|
64
|
+
// boundary stops at the external type).
|
|
65
|
+
EdgeValueCall EdgeKind = "value-call"
|
|
66
|
+
// EdgeValueAccess is a runtime property/accessor read or write. It is kept
|
|
67
|
+
// separate from calls so architecture flows can follow lazy getter/property
|
|
68
|
+
// behavior without pretending those reads invoke a function.
|
|
69
|
+
EdgeValueAccess EdgeKind = "value-access"
|
|
70
|
+
// EdgeTypeRef is a type-position reference from one declaration to a named
|
|
71
|
+
// type it mentions (a parameter, return, property, or alias type). It is not a
|
|
72
|
+
// runtime call, so an impact query can filter value edges from type edges.
|
|
73
|
+
EdgeTypeRef EdgeKind = "type-ref"
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
// Edge is a directed, checker-resolved relationship from one node to another,
|
|
77
|
+
// both referenced by Node.ID. Pos and End bound the source expression in the
|
|
78
|
+
// From node's file that produced the edge. They are evidence, not identity; a
|
|
79
|
+
// duplicate relationship keeps the first source-order span.
|
|
80
|
+
type Edge struct {
|
|
81
|
+
From string
|
|
82
|
+
To string
|
|
83
|
+
Kind EdgeKind
|
|
84
|
+
// Origin records the syntactic form a value-call or heritage edge came from,
|
|
85
|
+
// so the JSON dump can split one internal kind into the finer schema kinds
|
|
86
|
+
// (calls / instantiates / renders, extends / implements) without the
|
|
87
|
+
// MCP-facing model losing the distinction. It is "" for kinds that need no
|
|
88
|
+
// split (type-ref, value-access). For EdgeValueCall it is "call", "new",
|
|
89
|
+
// "jsx", or "tagged"; for EdgeHeritage it is "extends" or "implements".
|
|
90
|
+
Origin string
|
|
91
|
+
Pos int
|
|
92
|
+
End int
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Graph is the in-memory adjacency the MCP tools query. Edges are added by the
|
|
96
|
+
// resolution pass on top of the declaration nodes Build records.
|
|
97
|
+
type Graph struct {
|
|
98
|
+
Nodes map[string]*Node
|
|
99
|
+
Edges []*Edge
|
|
100
|
+
// Decorators holds the decorators written on the workspace's declarations,
|
|
101
|
+
// captured syntactically so the JSON dump can emit `decorates` edges and a
|
|
102
|
+
// consumer can interpret `@Controller`/`@Get` conventions without re-parsing
|
|
103
|
+
// source. It is dump-only metadata, separate from Edges so the existing
|
|
104
|
+
// checker-resolved relationships are untouched.
|
|
105
|
+
Decorators []*Decorator
|
|
106
|
+
// bodyNodes tracks whether a callable node's display span is the overload
|
|
107
|
+
// implementation rather than an overload signature. It is build-only metadata
|
|
108
|
+
// and intentionally stays out of JSON dumps.
|
|
109
|
+
bodyNodes map[string]bool
|
|
110
|
+
// seen deduplicates edges in O(1) during construction, so building a graph
|
|
111
|
+
// with N edges is O(N), not O(N²). Keyed by from\x00to\x00kind.
|
|
112
|
+
seen map[string]struct{}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// nodeID builds the position-invariant identity for a symbol named name,
|
|
116
|
+
// declared as kind in the source file at path.
|
|
117
|
+
func nodeID(path string, name string, kind NodeKind) string {
|
|
118
|
+
return path + "#" + name + ":" + string(kind)
|
|
119
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
package graph
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"path/filepath"
|
|
5
|
+
"testing"
|
|
6
|
+
|
|
7
|
+
"github.com/samchon/ttsc/packages/ttsc/driver"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
// TestHeritageEdgesCoverImplementsAndInterfaceExtends verifies that
|
|
11
|
+
// collectHeritage spans both heritage-bearing declaration kinds and both clause
|
|
12
|
+
// keywords: an interface `extends` and a class `implements` each yield a
|
|
13
|
+
// heritage edge to the same base, while a class that only mentions the base in a
|
|
14
|
+
// parameter type position yields a type-ref edge, never a heritage edge.
|
|
15
|
+
//
|
|
16
|
+
// The negative twin pins the boundary collectHeritage must hold: heritage is an
|
|
17
|
+
// `extends`/`implements` relationship, so a base reached only through a method
|
|
18
|
+
// parameter type (`greet(b: Base)`) must not leak into the heritage set; a path
|
|
19
|
+
// heuristic that treated any mention of Base as inheritance would over-match
|
|
20
|
+
// here.
|
|
21
|
+
//
|
|
22
|
+
// 1. Compile a fixture with `interface Derived extends Base`,
|
|
23
|
+
// `class Impl implements Base`, and `class Unrelated` whose only Base
|
|
24
|
+
// reference is the parameter type of a method.
|
|
25
|
+
// 2. Build the graph.
|
|
26
|
+
// 3. Assert heritage edges Derived->Base and Impl->Base exist, and that
|
|
27
|
+
// Unrelated->Base is a type-ref edge but not a heritage edge.
|
|
28
|
+
func TestHeritageEdgesCoverImplementsAndInterfaceExtends(t *testing.T) {
|
|
29
|
+
root := t.TempDir()
|
|
30
|
+
writeFile(t, filepath.Join(root, "tsconfig.json"), fixtureTSConfig)
|
|
31
|
+
writeFile(t, filepath.Join(root, "src", "main.ts"), `export interface Base {}
|
|
32
|
+
export interface Derived extends Base {}
|
|
33
|
+
export class Impl implements Base {}
|
|
34
|
+
export class Unrelated {
|
|
35
|
+
greet(b: Base): void {
|
|
36
|
+
void b;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
`)
|
|
40
|
+
|
|
41
|
+
prog, diags, err := driver.LoadProgram(root, "tsconfig.json", driver.LoadProgramOptions{})
|
|
42
|
+
if err != nil {
|
|
43
|
+
t.Fatal(err)
|
|
44
|
+
}
|
|
45
|
+
if len(diags) != 0 {
|
|
46
|
+
t.Fatalf("unexpected diagnostics: %v", diags)
|
|
47
|
+
}
|
|
48
|
+
defer func() { _ = prog.Close() }()
|
|
49
|
+
|
|
50
|
+
graph := Build(prog)
|
|
51
|
+
path := sourceFile(t, prog, "main.ts").FileName()
|
|
52
|
+
|
|
53
|
+
base := nodeID(path, "Base", NodeInterface)
|
|
54
|
+
derived := nodeID(path, "Derived", NodeInterface)
|
|
55
|
+
impl := nodeID(path, "Impl", NodeClass)
|
|
56
|
+
unrelated := nodeID(path, "Unrelated", NodeClass)
|
|
57
|
+
greet := nodeID(path, "Unrelated.greet", NodeMethod)
|
|
58
|
+
|
|
59
|
+
// Interface `extends`: Derived inherits Base.
|
|
60
|
+
if !hasEdge(graph, derived, base, EdgeHeritage) {
|
|
61
|
+
t.Fatalf("missing heritage edge Derived -> Base (interface extends); edges: %v", graph.Edges)
|
|
62
|
+
}
|
|
63
|
+
// Class `implements`: Impl declares Base as a heritage base.
|
|
64
|
+
if !hasEdge(graph, impl, base, EdgeHeritage) {
|
|
65
|
+
t.Fatalf("missing heritage edge Impl -> Base (class implements); edges: %v", graph.Edges)
|
|
66
|
+
}
|
|
67
|
+
// Negative twin: the base named only in a method parameter type is a type-ref
|
|
68
|
+
// dependency of that method (Unrelated.greet -> Base), never a heritage edge —
|
|
69
|
+
// not from the class and not from the method.
|
|
70
|
+
if hasEdge(graph, unrelated, base, EdgeHeritage) || hasEdge(graph, greet, base, EdgeHeritage) {
|
|
71
|
+
t.Fatalf("a parameter-type reference to Base was misclassified as a heritage edge; edges: %v", graph.Edges)
|
|
72
|
+
}
|
|
73
|
+
if !hasEdge(graph, greet, base, EdgeTypeRef) {
|
|
74
|
+
t.Fatalf("missing type-ref edge Unrelated.greet -> Base (parameter type); edges: %v", graph.Edges)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -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
|
+
// TestHeritageEdgesKeepExtendsAndImplementsToSameBase verifies that a class which
|
|
11
|
+
// both extends and implements the same base records two distinct heritage edges
|
|
12
|
+
// — `extends` and `implements` — rather than collapsing to one. The edge dedup
|
|
13
|
+
// keys on the emitted wire kind, so two relationships to one target that mean
|
|
14
|
+
// different things both survive; a dedup on the internal edge kind alone would
|
|
15
|
+
// drop the second clause and the dump would claim only inheritance or only
|
|
16
|
+
// conformance, never both.
|
|
17
|
+
//
|
|
18
|
+
// 1. Compile a fixture with `class Derived extends Base implements Base`.
|
|
19
|
+
// 2. Build the graph.
|
|
20
|
+
// 3. Assert both an `extends` and an `implements` heritage edge Derived -> Base,
|
|
21
|
+
// and that they map to the distinct wire kinds the dump emits.
|
|
22
|
+
func TestHeritageEdgesKeepExtendsAndImplementsToSameBase(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 class Base {
|
|
26
|
+
value(): number {
|
|
27
|
+
return 1;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export class Derived extends Base implements Base {}
|
|
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
|
+
derived := nodeID(path, "Derived", NodeClass)
|
|
46
|
+
base := nodeID(path, "Base", NodeClass)
|
|
47
|
+
|
|
48
|
+
wireKinds := map[string]bool{}
|
|
49
|
+
for _, edge := range graph.Edges {
|
|
50
|
+
if edge.From != derived || edge.To != base || edge.Kind != EdgeHeritage {
|
|
51
|
+
continue
|
|
52
|
+
}
|
|
53
|
+
wireKinds[dumpEdgeKind(edge)] = true
|
|
54
|
+
}
|
|
55
|
+
if !wireKinds["extends"] || !wireKinds["implements"] {
|
|
56
|
+
t.Fatalf(
|
|
57
|
+
"Derived -> Base heritage edges = %v; want both extends and implements; edges: %v",
|
|
58
|
+
wireKinds, graph.Edges,
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
package graph
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"path/filepath"
|
|
5
|
+
"testing"
|
|
6
|
+
|
|
7
|
+
"github.com/samchon/ttsc/packages/ttsc/driver"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
// TestHeritageEdgesRecordExtendsVersusImplementsOrigin verifies that a heritage
|
|
11
|
+
// edge carries the clause keyword as Origin, so the dump can split the single
|
|
12
|
+
// internal heritage kind into the schema's `extends` and `implements`: a class
|
|
13
|
+
// superclass records "extends", a class interface list records "implements".
|
|
14
|
+
//
|
|
15
|
+
// One class declaration carries both clauses at once, so the test pins that the
|
|
16
|
+
// keyword — not the base's declaration kind — selects the origin: `extends Sup`
|
|
17
|
+
// and `implements Iface` on the same class must come out as different origins.
|
|
18
|
+
//
|
|
19
|
+
// 1. Compile `class Sub extends Sup implements Iface` plus the base class and
|
|
20
|
+
// interface.
|
|
21
|
+
// 2. Build the graph.
|
|
22
|
+
// 3. Assert the Sub->Sup heritage edge has Origin "extends" and the Sub->Iface
|
|
23
|
+
// heritage edge has Origin "implements".
|
|
24
|
+
func TestHeritageEdgesRecordExtendsVersusImplementsOrigin(t *testing.T) {
|
|
25
|
+
root := t.TempDir()
|
|
26
|
+
writeFile(t, filepath.Join(root, "tsconfig.json"), fixtureTSConfig)
|
|
27
|
+
writeFile(t, filepath.Join(root, "src", "main.ts"), `export interface Iface {}
|
|
28
|
+
export class Sup {}
|
|
29
|
+
export class Sub extends Sup implements Iface {}
|
|
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
|
+
graph := Build(prog)
|
|
42
|
+
path := sourceFile(t, prog, "main.ts").FileName()
|
|
43
|
+
|
|
44
|
+
sub := nodeID(path, "Sub", NodeClass)
|
|
45
|
+
sup := nodeID(path, "Sup", NodeClass)
|
|
46
|
+
iface := nodeID(path, "Iface", NodeInterface)
|
|
47
|
+
|
|
48
|
+
if got := edgeOrigin(graph, sub, sup, EdgeHeritage); got != "extends" {
|
|
49
|
+
t.Fatalf("Sub -> Sup: want heritage Origin \"extends\", got %q; edges: %v", got, graph.Edges)
|
|
50
|
+
}
|
|
51
|
+
if got := edgeOrigin(graph, sub, iface, EdgeHeritage); got != "implements" {
|
|
52
|
+
t.Fatalf("Sub -> Iface: want heritage Origin \"implements\", got %q; edges: %v", got, graph.Edges)
|
|
53
|
+
}
|
|
54
|
+
}
|