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,122 @@
|
|
|
1
|
+
package graph
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
shimast "github.com/microsoft/typescript-go/shim/ast"
|
|
5
|
+
)
|
|
6
|
+
|
|
7
|
+
// DecoratorArgument is one argument written on a decorator. Literal holds the
|
|
8
|
+
// statically-resolved value when the argument is a string or boolean literal
|
|
9
|
+
// and is nil otherwise; Text is always the argument's printable source.
|
|
10
|
+
type DecoratorArgument struct {
|
|
11
|
+
Text string
|
|
12
|
+
Literal any
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Decorator is a decorator as written on a workspace declaration, captured so a
|
|
16
|
+
// consumer can interpret a decorator convention (`@Controller('users')`,
|
|
17
|
+
// `@Get(':id')`) without re-parsing source. Target is the id of the graph node
|
|
18
|
+
// the decorator is applied to; Pos/End bound the decorator for evidence.
|
|
19
|
+
type Decorator struct {
|
|
20
|
+
Target string
|
|
21
|
+
Name string
|
|
22
|
+
Arguments []DecoratorArgument
|
|
23
|
+
File string
|
|
24
|
+
Pos int
|
|
25
|
+
End int
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// collectDecorators records the decorators on each class plus its methods and
|
|
29
|
+
// properties, descending into namespace bodies. It is syntactic — a decorator's
|
|
30
|
+
// name and literal arguments are read straight from the AST — because a consumer
|
|
31
|
+
// keys on the written convention name, not the resolved decorator symbol.
|
|
32
|
+
func (g *Graph) collectDecorators(path string, statements []*shimast.Node) {
|
|
33
|
+
for _, statement := range statements {
|
|
34
|
+
switch statement.Kind {
|
|
35
|
+
case shimast.KindClassDeclaration:
|
|
36
|
+
if id := topLevelID(path, statement, NodeClass); id != "" {
|
|
37
|
+
g.nodeDecorators(id, path, statement)
|
|
38
|
+
}
|
|
39
|
+
g.memberDecorators(path, statement)
|
|
40
|
+
case shimast.KindModuleDeclaration:
|
|
41
|
+
g.collectDecorators(path, moduleStatements(statement))
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// memberDecorators records the decorators on the methods and properties of a
|
|
47
|
+
// class declaration, attributed to their member nodes.
|
|
48
|
+
func (g *Graph) memberDecorators(path string, statement *shimast.Node) {
|
|
49
|
+
for _, member := range classMembers(statement) {
|
|
50
|
+
name := methodName(member.Symbol())
|
|
51
|
+
if name == "" {
|
|
52
|
+
continue
|
|
53
|
+
}
|
|
54
|
+
switch {
|
|
55
|
+
case isMethodMember(member.Kind):
|
|
56
|
+
g.nodeDecorators(nodeID(path, name, NodeMethod), path, member)
|
|
57
|
+
case isPropertyMember(member.Kind):
|
|
58
|
+
g.nodeDecorators(nodeID(path, name, NodeVariable), path, member)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// nodeDecorators appends a Decorator fact for every decorator on node, targeting
|
|
64
|
+
// the graph node identified by targetID.
|
|
65
|
+
func (g *Graph) nodeDecorators(targetID, path string, node *shimast.Node) {
|
|
66
|
+
for _, dec := range node.Decorators() {
|
|
67
|
+
fact := decoratorFact(dec)
|
|
68
|
+
if fact == nil {
|
|
69
|
+
continue
|
|
70
|
+
}
|
|
71
|
+
fact.Target = targetID
|
|
72
|
+
fact.File = path
|
|
73
|
+
g.Decorators = append(g.Decorators, fact)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// decoratorFact reads a decorator node into a Decorator, or nil when its
|
|
78
|
+
// expression has no recoverable name. `@Get(':id')` yields name "Get" with one
|
|
79
|
+
// literal argument; a bare `@Injectable` yields name "Injectable" and no
|
|
80
|
+
// arguments.
|
|
81
|
+
func decoratorFact(dec *shimast.Node) *Decorator {
|
|
82
|
+
decl := dec.AsDecorator()
|
|
83
|
+
if decl == nil || decl.Expression == nil {
|
|
84
|
+
return nil
|
|
85
|
+
}
|
|
86
|
+
fact := &Decorator{Pos: dec.Pos(), End: dec.End()}
|
|
87
|
+
expr := decl.Expression
|
|
88
|
+
if expr.Kind == shimast.KindCallExpression {
|
|
89
|
+
call := expr.AsCallExpression()
|
|
90
|
+
if call == nil || call.Expression == nil {
|
|
91
|
+
return nil
|
|
92
|
+
}
|
|
93
|
+
fact.Name = shimast.NodeText(call.Expression)
|
|
94
|
+
if call.Arguments != nil {
|
|
95
|
+
for _, arg := range call.Arguments.Nodes {
|
|
96
|
+
fact.Arguments = append(fact.Arguments, decoratorArg(arg))
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
} else {
|
|
100
|
+
fact.Name = shimast.NodeText(expr)
|
|
101
|
+
}
|
|
102
|
+
if fact.Name == "" {
|
|
103
|
+
return nil
|
|
104
|
+
}
|
|
105
|
+
return fact
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// decoratorArg captures one decorator argument. Only string and boolean literals
|
|
109
|
+
// resolve to a Literal — the values a consumer can use directly — so a
|
|
110
|
+
// non-literal argument keeps its source Text with a nil Literal.
|
|
111
|
+
func decoratorArg(arg *shimast.Node) DecoratorArgument {
|
|
112
|
+
out := DecoratorArgument{Text: shimast.NodeText(arg)}
|
|
113
|
+
switch arg.Kind {
|
|
114
|
+
case shimast.KindStringLiteral, shimast.KindNoSubstitutionTemplateLiteral:
|
|
115
|
+
out.Literal = arg.Text()
|
|
116
|
+
case shimast.KindTrueKeyword:
|
|
117
|
+
out.Literal = true
|
|
118
|
+
case shimast.KindFalseKeyword:
|
|
119
|
+
out.Literal = false
|
|
120
|
+
}
|
|
121
|
+
return out
|
|
122
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
package graph
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"path/filepath"
|
|
5
|
+
"testing"
|
|
6
|
+
|
|
7
|
+
"github.com/samchon/ttsc/packages/ttsc/driver"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
// TestDecoratorsCaptureNameAndLiteralArguments verifies that collectDecorators
|
|
11
|
+
// records each decorator on a class and on its methods with the convention name
|
|
12
|
+
// and the statically-resolved literal arguments, attributed to the decorated
|
|
13
|
+
// node — the facts a consumer reads to interpret a decorator convention without
|
|
14
|
+
// re-parsing source.
|
|
15
|
+
//
|
|
16
|
+
// A convention's value lives in the decorator's literal argument, so the test
|
|
17
|
+
// pins both axes a consumer depends on: the decorator name (`Controller`,
|
|
18
|
+
// `Get`) and the unquoted literal value (`users`, `:id`), each on the right
|
|
19
|
+
// target node. A bare method with no decorator is the negative twin: it must
|
|
20
|
+
// contribute no decorator fact.
|
|
21
|
+
//
|
|
22
|
+
// 1. Compile a controller-shaped fixture with @Controller("users") on the class
|
|
23
|
+
// and @Get(":id") on one method, plus an undecorated method, using local
|
|
24
|
+
// decorator factories so the program type-checks with no dependency.
|
|
25
|
+
// 2. Build the graph.
|
|
26
|
+
// 3. Assert a Decorator targets the class with name "Controller" / literal
|
|
27
|
+
// "users", one targets the method with name "Get" / literal ":id", and the
|
|
28
|
+
// undecorated method has no fact.
|
|
29
|
+
func TestDecoratorsCaptureNameAndLiteralArguments(t *testing.T) {
|
|
30
|
+
root := t.TempDir()
|
|
31
|
+
writeFile(t, filepath.Join(root, "tsconfig.json"), fixtureTSConfig)
|
|
32
|
+
writeFile(t, filepath.Join(root, "src", "main.ts"), `function Controller(prefix: string): any {
|
|
33
|
+
void prefix;
|
|
34
|
+
return () => {};
|
|
35
|
+
}
|
|
36
|
+
function Get(path: string): any {
|
|
37
|
+
void path;
|
|
38
|
+
return () => {};
|
|
39
|
+
}
|
|
40
|
+
@Controller("users")
|
|
41
|
+
export class UsersController {
|
|
42
|
+
@Get(":id")
|
|
43
|
+
find(): void {}
|
|
44
|
+
plain(): void {}
|
|
45
|
+
}
|
|
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
|
+
controller := nodeID(path, "UsersController", NodeClass)
|
|
61
|
+
find := nodeID(path, "UsersController.find", NodeMethod)
|
|
62
|
+
plain := nodeID(path, "UsersController.plain", NodeMethod)
|
|
63
|
+
|
|
64
|
+
classDec := findDecorator(graph, controller, "Controller")
|
|
65
|
+
if classDec == nil {
|
|
66
|
+
t.Fatalf("missing @Controller fact on the class; decorators: %v", graph.Decorators)
|
|
67
|
+
}
|
|
68
|
+
if len(classDec.Arguments) != 1 || classDec.Arguments[0].Literal != "users" {
|
|
69
|
+
t.Fatalf("@Controller args: want one literal \"users\", got %v", classDec.Arguments)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
methodDec := findDecorator(graph, find, "Get")
|
|
73
|
+
if methodDec == nil {
|
|
74
|
+
t.Fatalf("missing @Get fact on UsersController.find; decorators: %v", graph.Decorators)
|
|
75
|
+
}
|
|
76
|
+
if len(methodDec.Arguments) != 1 || methodDec.Arguments[0].Literal != ":id" {
|
|
77
|
+
t.Fatalf("@Get args: want one literal \":id\", got %v", methodDec.Arguments)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Negative twin: an undecorated method contributes no decorator fact, so a
|
|
81
|
+
// consumer sees no decorator on it.
|
|
82
|
+
for _, d := range graph.Decorators {
|
|
83
|
+
if d.Target == plain {
|
|
84
|
+
t.Fatalf("undecorated method UsersController.plain gained a decorator fact: %+v", d)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// findDecorator returns the first decorator fact targeting target with the given
|
|
90
|
+
// name, or nil.
|
|
91
|
+
func findDecorator(graph *Graph, target, name string) *Decorator {
|
|
92
|
+
for _, d := range graph.Decorators {
|
|
93
|
+
if d.Target == target && d.Name == name {
|
|
94
|
+
return d
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return nil
|
|
98
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
package graph
|
|
2
|
+
|
|
3
|
+
import "github.com/samchon/ttsc/packages/ttsc/driver"
|
|
4
|
+
|
|
5
|
+
// FileDiagnostics returns the resident program's compiler diagnostics for the
|
|
6
|
+
// source file at path, in the same code and location tsgo reports. Because the
|
|
7
|
+
// graph rides the already-open Program, this is one Program.Diagnostics() call
|
|
8
|
+
// over the warm checker, not a second compile.
|
|
9
|
+
//
|
|
10
|
+
// This is the compiler-only slice: it carries the TypeScript semantic
|
|
11
|
+
// diagnostics, not @ttsc/lint or transform-plugin findings. The MCP server fuses
|
|
12
|
+
// those onto the graph separately, through diagnostic providers the launcher
|
|
13
|
+
// feeds (see mcp.DiagnosticProvider). This helper and its tests pin the
|
|
14
|
+
// compiler-diagnostic conversion (code, column, message) on its own.
|
|
15
|
+
func FileDiagnostics(prog *driver.Program, path string) []driver.Diagnostic {
|
|
16
|
+
out := make([]driver.Diagnostic, 0)
|
|
17
|
+
for _, diagnostic := range prog.Diagnostics() {
|
|
18
|
+
if diagnostic.File == path {
|
|
19
|
+
out = append(out, diagnostic)
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return out
|
|
23
|
+
}
|
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
package graph
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"encoding/json"
|
|
5
|
+
"path/filepath"
|
|
6
|
+
"sort"
|
|
7
|
+
"strings"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
// dump.go projects a built graph onto the JSON wire contract `ttscgraph dump`
|
|
11
|
+
// prints — the IGraphDump shape the @ttsc/graph engine loads (and the 3D viewer
|
|
12
|
+
// reduces). The internal Node/Edge model stays narrow (so the resident MCP path
|
|
13
|
+
// is untouched); the richer schema is produced here:
|
|
14
|
+
//
|
|
15
|
+
// - internal node kinds map straight through (NodeTypeAlias is already "type");
|
|
16
|
+
// - one EdgeValueCall splits by Origin into "calls" / "instantiates" /
|
|
17
|
+
// "renders", and EdgeHeritage into "extends" / "implements";
|
|
18
|
+
// - every edge is tagged checker-resolved / high (the graph's whole contract);
|
|
19
|
+
// - byte spans become 1-based line/col Evidence, with a short excerpt on edges;
|
|
20
|
+
// - decorator facts ride on their target node;
|
|
21
|
+
// - file paths are project-relative and the output is sorted, so the dump is
|
|
22
|
+
// deterministic and diffable.
|
|
23
|
+
//
|
|
24
|
+
// Structural derivations the schema also defines (file nodes, contains/exports
|
|
25
|
+
// edges) are left to the TypeScript loader, which has the node set in hand and
|
|
26
|
+
// is where the redesign keeps that logic.
|
|
27
|
+
|
|
28
|
+
// DumpEvidence is a 1-based source span grounding a node declaration or an edge
|
|
29
|
+
// expression. It is display/expansion only, never identity.
|
|
30
|
+
type DumpEvidence struct {
|
|
31
|
+
File string `json:"file"`
|
|
32
|
+
StartLine int `json:"startLine"`
|
|
33
|
+
StartCol int `json:"startCol,omitempty"`
|
|
34
|
+
EndLine int `json:"endLine,omitempty"`
|
|
35
|
+
EndCol int `json:"endCol,omitempty"`
|
|
36
|
+
Text string `json:"text,omitempty"`
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// DumpDecoratorArgument is one decorator argument; Literal is set only for a
|
|
40
|
+
// statically-resolved string or boolean literal.
|
|
41
|
+
type DumpDecoratorArgument struct {
|
|
42
|
+
Text string `json:"text"`
|
|
43
|
+
Literal any `json:"literal,omitempty"`
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// DumpDecorator is a decorator as written on a declaration, carried on its
|
|
47
|
+
// target node for a consumer to interpret.
|
|
48
|
+
type DumpDecorator struct {
|
|
49
|
+
Name string `json:"name"`
|
|
50
|
+
Arguments []DumpDecoratorArgument `json:"arguments"`
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// DumpNode is the wire shape of a graph node. Lowercase json keys are the
|
|
54
|
+
// contract; the Go field names are not.
|
|
55
|
+
type DumpNode struct {
|
|
56
|
+
ID string `json:"id"`
|
|
57
|
+
Kind string `json:"kind"`
|
|
58
|
+
Name string `json:"name"`
|
|
59
|
+
QualifiedName string `json:"qualifiedName,omitempty"`
|
|
60
|
+
File string `json:"file"`
|
|
61
|
+
External bool `json:"external"`
|
|
62
|
+
Ignored bool `json:"ignored,omitempty"`
|
|
63
|
+
Exported bool `json:"exported,omitempty"`
|
|
64
|
+
Modifiers []string `json:"modifiers,omitempty"`
|
|
65
|
+
Evidence *DumpEvidence `json:"evidence,omitempty"`
|
|
66
|
+
Decorators []DumpDecorator `json:"decorators,omitempty"`
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// DumpEdge is the wire shape of a graph edge. Lowercase json keys are the
|
|
70
|
+
// contract; the Go field names are not.
|
|
71
|
+
type DumpEdge struct {
|
|
72
|
+
From string `json:"from"`
|
|
73
|
+
To string `json:"to"`
|
|
74
|
+
Kind string `json:"kind"`
|
|
75
|
+
Evidence *DumpEvidence `json:"evidence,omitempty"`
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Dump is the IGraphDump envelope: the project it was built for and the full
|
|
79
|
+
// node and edge sets with none of the MCP response caps.
|
|
80
|
+
type Dump struct {
|
|
81
|
+
Project string `json:"project"`
|
|
82
|
+
Tsconfig string `json:"tsconfig"`
|
|
83
|
+
Nodes []DumpNode `json:"nodes"`
|
|
84
|
+
Edges []DumpEdge `json:"edges"`
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// NewDump projects a built graph onto the export shape. project is the absolute
|
|
88
|
+
// project root used to relativize file paths and node ids; ignored is the
|
|
89
|
+
// git-ignored source set (nil for a non-git project); sources maps a source
|
|
90
|
+
// file's path to its text so byte spans become line/col evidence (nil omits
|
|
91
|
+
// evidence).
|
|
92
|
+
func NewDump(g *Graph, project, tsconfig string, ignored map[string]bool, sources map[string]string) Dump {
|
|
93
|
+
ctx := newDumpContext(project, sources)
|
|
94
|
+
|
|
95
|
+
// Decorators ride on their target node; group by the internal node id before
|
|
96
|
+
// ids are relativized for output.
|
|
97
|
+
decByNode := make(map[string][]DumpDecorator, len(g.Decorators))
|
|
98
|
+
for _, d := range g.Decorators {
|
|
99
|
+
args := make([]DumpDecoratorArgument, 0, len(d.Arguments))
|
|
100
|
+
for _, a := range d.Arguments {
|
|
101
|
+
args = append(args, DumpDecoratorArgument{Text: a.Text, Literal: a.Literal})
|
|
102
|
+
}
|
|
103
|
+
decByNode[d.Target] = append(decByNode[d.Target], DumpDecorator{Name: d.Name, Arguments: args})
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
nodes := make([]DumpNode, 0, len(g.Nodes))
|
|
107
|
+
for _, n := range g.Nodes {
|
|
108
|
+
name, qualified := nodeNames(n)
|
|
109
|
+
nodes = append(nodes, DumpNode{
|
|
110
|
+
ID: ctx.relID(n.ID),
|
|
111
|
+
Kind: string(n.Kind),
|
|
112
|
+
Name: name,
|
|
113
|
+
QualifiedName: qualified,
|
|
114
|
+
File: ctx.rel(n.File),
|
|
115
|
+
External: n.External,
|
|
116
|
+
Ignored: ignored[n.File],
|
|
117
|
+
Exported: n.Exported,
|
|
118
|
+
Modifiers: n.Modifiers,
|
|
119
|
+
Evidence: ctx.evidence(n.File, n.Pos, n.End, false),
|
|
120
|
+
Decorators: decByNode[n.ID],
|
|
121
|
+
})
|
|
122
|
+
}
|
|
123
|
+
sort.Slice(nodes, func(i, j int) bool { return nodes[i].ID < nodes[j].ID })
|
|
124
|
+
|
|
125
|
+
edges := make([]DumpEdge, 0, len(g.Edges))
|
|
126
|
+
for _, e := range g.Edges {
|
|
127
|
+
edges = append(edges, DumpEdge{
|
|
128
|
+
From: ctx.relID(e.From),
|
|
129
|
+
To: ctx.relID(e.To),
|
|
130
|
+
Kind: dumpEdgeKind(e),
|
|
131
|
+
Evidence: ctx.edgeEvidence(e),
|
|
132
|
+
})
|
|
133
|
+
}
|
|
134
|
+
sort.Slice(edges, func(i, j int) bool {
|
|
135
|
+
if edges[i].From != edges[j].From {
|
|
136
|
+
return edges[i].From < edges[j].From
|
|
137
|
+
}
|
|
138
|
+
if edges[i].To != edges[j].To {
|
|
139
|
+
return edges[i].To < edges[j].To
|
|
140
|
+
}
|
|
141
|
+
return edges[i].Kind < edges[j].Kind
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
return Dump{
|
|
145
|
+
Project: project,
|
|
146
|
+
Tsconfig: tsconfig,
|
|
147
|
+
Nodes: nodes,
|
|
148
|
+
Edges: edges,
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// MarshalDump serializes a built graph to the export JSON, indented when pretty.
|
|
153
|
+
// See NewDump for the parameters.
|
|
154
|
+
func MarshalDump(g *Graph, project, tsconfig string, ignored map[string]bool, sources map[string]string, pretty bool) ([]byte, error) {
|
|
155
|
+
d := NewDump(g, project, tsconfig, ignored, sources)
|
|
156
|
+
if pretty {
|
|
157
|
+
return json.MarshalIndent(d, "", " ")
|
|
158
|
+
}
|
|
159
|
+
return json.Marshal(d)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// dumpEdgeKind maps an internal edge kind, refined by Edge.Origin, onto the
|
|
163
|
+
// schema's finer relationship kind.
|
|
164
|
+
func dumpEdgeKind(e *Edge) string {
|
|
165
|
+
return wireEdgeKind(e.Kind, e.Origin)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// wireEdgeKind maps an internal edge kind, refined by its origin, onto the
|
|
169
|
+
// schema's finer relationship kind. It is the edge's emitted identity, so the
|
|
170
|
+
// dedup keys on it: two uses of one target that differ only in a form mapping to
|
|
171
|
+
// the same wire kind (a plain call and a tagged-template call, both `calls`)
|
|
172
|
+
// collapse to one edge, while forms that mean distinct relationships (`calls` vs
|
|
173
|
+
// `instantiates`, `extends` vs `implements`) are each kept.
|
|
174
|
+
func wireEdgeKind(kind EdgeKind, origin string) string {
|
|
175
|
+
switch kind {
|
|
176
|
+
case EdgeValueCall:
|
|
177
|
+
switch origin {
|
|
178
|
+
case "new":
|
|
179
|
+
return "instantiates"
|
|
180
|
+
case "jsx":
|
|
181
|
+
return "renders"
|
|
182
|
+
default:
|
|
183
|
+
return "calls"
|
|
184
|
+
}
|
|
185
|
+
case EdgeValueAccess:
|
|
186
|
+
return "accesses"
|
|
187
|
+
case EdgeTypeRef:
|
|
188
|
+
return "type_ref"
|
|
189
|
+
case EdgeHeritage:
|
|
190
|
+
if origin == "extends" {
|
|
191
|
+
return "extends"
|
|
192
|
+
}
|
|
193
|
+
return "implements"
|
|
194
|
+
default:
|
|
195
|
+
return string(kind)
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// nodeNames returns a node's simple name and, when it is owner-qualified, its
|
|
200
|
+
// full qualified form for the wire. The simple name is the symbol's own name
|
|
201
|
+
// recorded at build time, so a quoted member whose name contains a dot
|
|
202
|
+
// (`"a.b"` → Name `C.a.b`) splits exactly; the qualified form is the full Name
|
|
203
|
+
// when it differs from the simple name, and "" for a top-level declaration.
|
|
204
|
+
// A node without a recorded simple name (a future virtual node) falls back to
|
|
205
|
+
// the last dot-separated segment.
|
|
206
|
+
func nodeNames(n *Node) (simple, qualified string) {
|
|
207
|
+
if n.Simple == "" {
|
|
208
|
+
if dot := strings.LastIndex(n.Name, "."); dot >= 0 {
|
|
209
|
+
return n.Name[dot+1:], n.Name
|
|
210
|
+
}
|
|
211
|
+
return n.Name, ""
|
|
212
|
+
}
|
|
213
|
+
if n.Simple == n.Name {
|
|
214
|
+
return n.Simple, ""
|
|
215
|
+
}
|
|
216
|
+
return n.Simple, n.Name
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// dumpContext relativizes paths and turns byte spans into line/col evidence,
|
|
220
|
+
// caching a per-file line index so a large file's many edges cost O(log n) each
|
|
221
|
+
// instead of a re-scan.
|
|
222
|
+
type dumpContext struct {
|
|
223
|
+
project string
|
|
224
|
+
sources map[string]string
|
|
225
|
+
lines map[string]lineStarts
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
func newDumpContext(project string, sources map[string]string) *dumpContext {
|
|
229
|
+
return &dumpContext{
|
|
230
|
+
project: strings.TrimRight(filepath.ToSlash(project), "/"),
|
|
231
|
+
sources: sources,
|
|
232
|
+
lines: map[string]lineStarts{},
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// rel makes a source file path project-relative; an external path keeps its
|
|
237
|
+
// node_modules-relative tail so a dependency leaf stays readable.
|
|
238
|
+
func (c *dumpContext) rel(file string) string {
|
|
239
|
+
f := filepath.ToSlash(file)
|
|
240
|
+
if c.project != "" {
|
|
241
|
+
if f == c.project {
|
|
242
|
+
return ""
|
|
243
|
+
}
|
|
244
|
+
if strings.HasPrefix(f, c.project+"/") {
|
|
245
|
+
return f[len(c.project)+1:]
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
if i := strings.LastIndex(f, "/node_modules/"); i >= 0 {
|
|
249
|
+
return f[i+1:]
|
|
250
|
+
}
|
|
251
|
+
return f
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// relID relativizes the path portion of a node id ("path#qualifiedName:kind").
|
|
255
|
+
// An id with no path (a future virtual node) is returned unchanged.
|
|
256
|
+
func (c *dumpContext) relID(id string) string {
|
|
257
|
+
hash := strings.Index(id, "#")
|
|
258
|
+
if hash < 0 {
|
|
259
|
+
return id
|
|
260
|
+
}
|
|
261
|
+
return c.rel(id[:hash]) + id[hash:]
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// evidence builds the line/col span for a byte range in file, or nil when the
|
|
265
|
+
// span is absent or no source is available. includeText attaches a one-line
|
|
266
|
+
// excerpt (used for edge expressions, not declaration bodies).
|
|
267
|
+
func (c *dumpContext) evidence(file string, pos, end int, includeText bool) *DumpEvidence {
|
|
268
|
+
if pos < 0 || c.sources == nil {
|
|
269
|
+
return nil
|
|
270
|
+
}
|
|
271
|
+
text, ok := c.sources[file]
|
|
272
|
+
if !ok {
|
|
273
|
+
return nil
|
|
274
|
+
}
|
|
275
|
+
ls := c.lines[file]
|
|
276
|
+
if ls == nil {
|
|
277
|
+
ls = newLineStarts(text)
|
|
278
|
+
c.lines[file] = ls
|
|
279
|
+
}
|
|
280
|
+
if pos > len(text) {
|
|
281
|
+
return nil
|
|
282
|
+
}
|
|
283
|
+
// Node.Pos() and an expression's Pos() are the full-start: they include the
|
|
284
|
+
// leading whitespace and doc comments before the token. Advance to the first
|
|
285
|
+
// code character so the line/column point at the declaration, not its banner
|
|
286
|
+
// or its indentation.
|
|
287
|
+
pos = firstCodeOffset(text, pos)
|
|
288
|
+
sl, sc := ls.at(pos)
|
|
289
|
+
ev := &DumpEvidence{File: c.rel(file), StartLine: sl, StartCol: sc}
|
|
290
|
+
if end > pos && end <= len(text) {
|
|
291
|
+
ev.EndLine, ev.EndCol = ls.at(end)
|
|
292
|
+
if includeText {
|
|
293
|
+
ev.Text = excerpt(text[pos:end])
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return ev
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// firstCodeOffset advances past leading trivia — whitespace, // line comments,
|
|
300
|
+
// and /* */ block comments — from pos to the first code byte, or len(text) if
|
|
301
|
+
// the rest is all trivia. It mirrors how a token's real start is found, so an
|
|
302
|
+
// evidence span lands on the declaration rather than the comment above it.
|
|
303
|
+
func firstCodeOffset(text string, pos int) int {
|
|
304
|
+
i := pos
|
|
305
|
+
for i < len(text) {
|
|
306
|
+
switch {
|
|
307
|
+
case text[i] == ' ' || text[i] == '\t' || text[i] == '\r' || text[i] == '\n':
|
|
308
|
+
i++
|
|
309
|
+
case text[i] == '/' && i+1 < len(text) && text[i+1] == '/':
|
|
310
|
+
i += 2
|
|
311
|
+
for i < len(text) && text[i] != '\n' {
|
|
312
|
+
i++
|
|
313
|
+
}
|
|
314
|
+
case text[i] == '/' && i+1 < len(text) && text[i+1] == '*':
|
|
315
|
+
i += 2
|
|
316
|
+
for i+1 < len(text) && !(text[i] == '*' && text[i+1] == '/') {
|
|
317
|
+
i++
|
|
318
|
+
}
|
|
319
|
+
if i+1 < len(text) {
|
|
320
|
+
i += 2
|
|
321
|
+
} else {
|
|
322
|
+
i = len(text)
|
|
323
|
+
}
|
|
324
|
+
default:
|
|
325
|
+
return i
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return i
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// edgeEvidence is the evidence for an edge's source expression, with a short
|
|
332
|
+
// excerpt of the expression text.
|
|
333
|
+
func (c *dumpContext) edgeEvidence(e *Edge) *DumpEvidence {
|
|
334
|
+
// The edge's span lives in the From node's file.
|
|
335
|
+
file := nodeFile(e.From)
|
|
336
|
+
if file == "" {
|
|
337
|
+
return nil
|
|
338
|
+
}
|
|
339
|
+
return c.evidence(file, e.Pos, e.End, true)
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// nodeFile recovers the source file path embedded in a node id
|
|
343
|
+
// ("path#qualifiedName:kind"); "" for an id without a path.
|
|
344
|
+
func nodeFile(id string) string {
|
|
345
|
+
if hash := strings.Index(id, "#"); hash >= 0 {
|
|
346
|
+
return id[:hash]
|
|
347
|
+
}
|
|
348
|
+
return ""
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// excerpt is a single-line, length-capped slice of source text for inline
|
|
352
|
+
// evidence.
|
|
353
|
+
func excerpt(s string) string {
|
|
354
|
+
s = strings.TrimSpace(s)
|
|
355
|
+
if i := strings.IndexByte(s, '\n'); i >= 0 {
|
|
356
|
+
s = s[:i]
|
|
357
|
+
}
|
|
358
|
+
if len(s) > 160 {
|
|
359
|
+
// Drop a trailing partial UTF-8 rune so the excerpt cannot end mid-character.
|
|
360
|
+
s = strings.TrimSpace(strings.ToValidUTF8(s[:160], "")) + "…"
|
|
361
|
+
}
|
|
362
|
+
return s
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// lineStarts holds the byte offset of each line's start, so an offset maps to a
|
|
366
|
+
// 1-based line/column by binary search.
|
|
367
|
+
type lineStarts []int
|
|
368
|
+
|
|
369
|
+
func newLineStarts(text string) lineStarts {
|
|
370
|
+
starts := make(lineStarts, 1, 1+strings.Count(text, "\n"))
|
|
371
|
+
starts[0] = 0
|
|
372
|
+
for i := 0; i < len(text); i++ {
|
|
373
|
+
if text[i] == '\n' {
|
|
374
|
+
starts = append(starts, i+1)
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
return starts
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// at returns the 1-based line and column of a byte offset.
|
|
381
|
+
func (ls lineStarts) at(offset int) (line, col int) {
|
|
382
|
+
if offset < 0 || len(ls) == 0 {
|
|
383
|
+
return 0, 0
|
|
384
|
+
}
|
|
385
|
+
i := sort.Search(len(ls), func(i int) bool { return ls[i] > offset }) - 1
|
|
386
|
+
if i < 0 {
|
|
387
|
+
i = 0
|
|
388
|
+
}
|
|
389
|
+
return i + 1, offset - ls[i] + 1
|
|
390
|
+
}
|