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.
Files changed (74) hide show
  1. package/README.md +96 -25
  2. package/cmd/graphbench/main.go +135 -0
  3. package/cmd/graphdump/main.go +60 -0
  4. package/cmd/ttscgraph/dump.go +69 -0
  5. package/cmd/ttscgraph/main.go +91 -0
  6. package/cmd/ttscgraph/run_dump_prints_the_graph_as_json_test.go +80 -0
  7. package/cmd/ttscgraph/run_help_version_and_bad_flags_exit_codes_test.go +75 -0
  8. package/cmd/utility-host/main.go +4 -1
  9. package/driver/overlay.go +64 -0
  10. package/driver/program.go +8 -1
  11. package/driver/session.go +85 -0
  12. package/internal/graph/build.go +330 -0
  13. package/internal/graph/build_records_a_node_per_top_level_declaration_test.go +96 -0
  14. package/internal/graph/decorators.go +122 -0
  15. package/internal/graph/decorators_capture_name_and_literal_arguments_test.go +98 -0
  16. package/internal/graph/diagnostics.go +23 -0
  17. package/internal/graph/dump.go +390 -0
  18. package/internal/graph/dump_evidence_skips_leading_trivia_test.go +92 -0
  19. package/internal/graph/dump_serializes_the_full_graph_test.go +146 -0
  20. package/internal/graph/edge_spans_point_to_resolved_usage_test.go +90 -0
  21. package/internal/graph/edges.go +538 -0
  22. package/internal/graph/edges_cover_class_level_references_test.go +93 -0
  23. package/internal/graph/enum_initializer_calls_are_edges_test.go +63 -0
  24. package/internal/graph/exports.go +60 -0
  25. package/internal/graph/file_diagnostics_match_tsgo_code_and_location_test.go +59 -0
  26. package/internal/graph/file_diagnostics_report_code_column_and_message_test.go +69 -0
  27. package/internal/graph/function_locals_do_not_collide_test.go +66 -0
  28. package/internal/graph/gitignore.go +51 -0
  29. package/internal/graph/graph.go +119 -0
  30. package/internal/graph/heritage_edges_cover_implements_and_interface_extends_test.go +76 -0
  31. package/internal/graph/heritage_edges_keep_extends_and_implements_to_same_base_test.go +61 -0
  32. package/internal/graph/heritage_edges_record_extends_versus_implements_origin_test.go +54 -0
  33. package/internal/graph/heritage_edges_traverse_barrels_and_stop_at_external_leaves_test.go +112 -0
  34. package/internal/graph/namespace_dotted_form_qualifies_deeply_test.go +60 -0
  35. package/internal/graph/namespace_members_become_qualified_nodes_and_edges_test.go +100 -0
  36. package/internal/graph/node_identity_is_position_invariant_test.go +84 -0
  37. package/internal/graph/node_modifiers_emit_union_strings_test.go +118 -0
  38. package/internal/graph/nodes_are_marked_exported_through_the_export_table_test.go +61 -0
  39. package/internal/graph/overload_implementation_replaces_signature_test.go +54 -0
  40. package/internal/graph/property_member_edges_remain_owner_visible_test.go +83 -0
  41. package/internal/graph/resolve.go +102 -0
  42. package/internal/graph/resolve_prefers_implementation_declaration_test.go +50 -0
  43. package/internal/graph/resolve_unwraps_barrel_reexport_to_real_declaration_test.go +230 -0
  44. package/internal/graph/type_ref_edges_cover_typeof_as_satisfies_test.go +79 -0
  45. package/internal/graph/type_ref_edges_resolve_named_types_across_files_test.go +52 -0
  46. package/internal/graph/value_access_edges_cover_properties_test.go +96 -0
  47. package/internal/graph/value_call_edges_cover_jsx_components_test.go +82 -0
  48. package/internal/graph/value_call_edges_cover_methods_and_constructors_test.go +70 -0
  49. package/internal/graph/value_call_edges_cover_variable_bound_callables_test.go +46 -0
  50. package/internal/graph/value_call_edges_do_not_double_record_invoked_member_access_test.go +65 -0
  51. package/internal/graph/value_call_edges_record_construct_versus_call_origin_test.go +75 -0
  52. package/internal/graph/value_call_edges_resolve_across_files_and_dedupe_test.go +55 -0
  53. package/internal/graph/value_call_edges_skip_self_and_unresolved_test.go +74 -0
  54. package/lib/TtscService.d.ts +55 -0
  55. package/lib/TtscService.js +93 -0
  56. package/lib/TtscService.js.map +1 -0
  57. package/lib/compiler/internal/residentTransformProcess.d.ts +47 -0
  58. package/lib/compiler/internal/residentTransformProcess.js +187 -0
  59. package/lib/compiler/internal/residentTransformProcess.js.map +1 -0
  60. package/lib/compiler/internal/startResidentTransform.d.ts +26 -0
  61. package/lib/compiler/internal/startResidentTransform.js +98 -0
  62. package/lib/compiler/internal/startResidentTransform.js.map +1 -0
  63. package/lib/index.d.ts +1 -0
  64. package/lib/index.js +1 -0
  65. package/lib/index.js.map +1 -1
  66. package/package.json +8 -8
  67. package/shim/ast/shim.go +6 -0
  68. package/shim/checker/shim.go +22 -12
  69. package/src/TtscService.ts +94 -0
  70. package/src/compiler/internal/residentTransformProcess.ts +216 -0
  71. package/src/compiler/internal/startResidentTransform.ts +126 -0
  72. package/src/index.ts +1 -0
  73. package/utility/host.go +8 -2
  74. 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
+ }