ttsc 0.16.3 → 0.16.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) 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 +120 -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 +374 -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 +581 -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 +122 -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_attribute_function_assignments_to_members_test.go +59 -0
  48. package/internal/graph/value_call_edges_cover_jsx_components_test.go +82 -0
  49. package/internal/graph/value_call_edges_cover_methods_and_constructors_test.go +70 -0
  50. package/internal/graph/value_call_edges_cover_variable_bound_callables_test.go +46 -0
  51. package/internal/graph/value_call_edges_do_not_double_record_invoked_member_access_test.go +65 -0
  52. package/internal/graph/value_call_edges_record_construct_versus_call_origin_test.go +75 -0
  53. package/internal/graph/value_call_edges_resolve_across_files_and_dedupe_test.go +55 -0
  54. package/internal/graph/value_call_edges_skip_self_and_unresolved_test.go +74 -0
  55. package/lib/TtscService.d.ts +55 -0
  56. package/lib/TtscService.js +93 -0
  57. package/lib/TtscService.js.map +1 -0
  58. package/lib/compiler/internal/residentTransformProcess.d.ts +47 -0
  59. package/lib/compiler/internal/residentTransformProcess.js +187 -0
  60. package/lib/compiler/internal/residentTransformProcess.js.map +1 -0
  61. package/lib/compiler/internal/startResidentTransform.d.ts +26 -0
  62. package/lib/compiler/internal/startResidentTransform.js +98 -0
  63. package/lib/compiler/internal/startResidentTransform.js.map +1 -0
  64. package/lib/index.d.ts +1 -0
  65. package/lib/index.js +1 -0
  66. package/lib/index.js.map +1 -1
  67. package/package.json +8 -8
  68. package/shim/ast/shim.go +6 -0
  69. package/shim/checker/shim.go +22 -12
  70. package/src/TtscService.ts +94 -0
  71. package/src/compiler/internal/residentTransformProcess.ts +216 -0
  72. package/src/compiler/internal/startResidentTransform.ts +126 -0
  73. package/src/index.ts +1 -0
  74. package/utility/host.go +8 -2
  75. package/utility/serve.go +158 -0
@@ -0,0 +1,120 @@
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.
10
+ type DecoratorArgument struct {
11
+ Literal any
12
+ }
13
+
14
+ // Decorator is a decorator as written on a workspace declaration, captured so a
15
+ // consumer can interpret a decorator convention (`@Controller('users')`,
16
+ // `@Get(':id')`) without re-parsing source. Target is the id of the graph node
17
+ // the decorator is applied to; Pos/End bound the decorator for evidence.
18
+ type Decorator struct {
19
+ Target string
20
+ Name string
21
+ Arguments []DecoratorArgument
22
+ File string
23
+ Pos int
24
+ End int
25
+ }
26
+
27
+ // collectDecorators records the decorators on each class plus its methods and
28
+ // properties, descending into namespace bodies. It is syntactic: a decorator's
29
+ // name and literal arguments are read straight from the AST because a consumer
30
+ // keys on the written convention name, not the resolved decorator symbol.
31
+ func (g *Graph) collectDecorators(path string, statements []*shimast.Node) {
32
+ for _, statement := range statements {
33
+ switch statement.Kind {
34
+ case shimast.KindClassDeclaration:
35
+ if id := topLevelID(path, statement, NodeClass); id != "" {
36
+ g.nodeDecorators(id, path, statement)
37
+ }
38
+ g.memberDecorators(path, statement)
39
+ case shimast.KindModuleDeclaration:
40
+ g.collectDecorators(path, moduleStatements(statement))
41
+ }
42
+ }
43
+ }
44
+
45
+ // memberDecorators records the decorators on the methods and properties of a
46
+ // class declaration, attributed to their member nodes.
47
+ func (g *Graph) memberDecorators(path string, statement *shimast.Node) {
48
+ for _, member := range classMembers(statement) {
49
+ name := methodName(member.Symbol())
50
+ if name == "" {
51
+ continue
52
+ }
53
+ switch {
54
+ case isMethodMember(member.Kind):
55
+ g.nodeDecorators(nodeID(path, name, NodeMethod), path, member)
56
+ case isPropertyMember(member.Kind):
57
+ g.nodeDecorators(nodeID(path, name, NodeVariable), path, member)
58
+ }
59
+ }
60
+ }
61
+
62
+ // nodeDecorators appends a Decorator fact for every decorator on node, targeting
63
+ // the graph node identified by targetID.
64
+ func (g *Graph) nodeDecorators(targetID, path string, node *shimast.Node) {
65
+ for _, dec := range node.Decorators() {
66
+ fact := decoratorFact(dec)
67
+ if fact == nil {
68
+ continue
69
+ }
70
+ fact.Target = targetID
71
+ fact.File = path
72
+ g.Decorators = append(g.Decorators, fact)
73
+ }
74
+ }
75
+
76
+ // decoratorFact reads a decorator node into a Decorator, or nil when its
77
+ // expression has no recoverable name. `@Get(':id')` yields name "Get" with one
78
+ // literal argument; a bare `@Injectable` yields name "Injectable" and no
79
+ // arguments.
80
+ func decoratorFact(dec *shimast.Node) *Decorator {
81
+ decl := dec.AsDecorator()
82
+ if decl == nil || decl.Expression == nil {
83
+ return nil
84
+ }
85
+ fact := &Decorator{Pos: dec.Pos(), End: dec.End()}
86
+ expr := decl.Expression
87
+ if expr.Kind == shimast.KindCallExpression {
88
+ call := expr.AsCallExpression()
89
+ if call == nil || call.Expression == nil {
90
+ return nil
91
+ }
92
+ fact.Name = shimast.NodeText(call.Expression)
93
+ if call.Arguments != nil {
94
+ for _, arg := range call.Arguments.Nodes {
95
+ fact.Arguments = append(fact.Arguments, decoratorArg(arg))
96
+ }
97
+ }
98
+ } else {
99
+ fact.Name = shimast.NodeText(expr)
100
+ }
101
+ if fact.Name == "" {
102
+ return nil
103
+ }
104
+ return fact
105
+ }
106
+
107
+ // decoratorArg captures one decorator argument. Only string and boolean
108
+ // literals resolve to a Literal, the values a consumer can use directly.
109
+ func decoratorArg(arg *shimast.Node) DecoratorArgument {
110
+ out := DecoratorArgument{}
111
+ switch arg.Kind {
112
+ case shimast.KindStringLiteral, shimast.KindNoSubstitutionTemplateLiteral:
113
+ out.Literal = arg.Text()
114
+ case shimast.KindTrueKeyword:
115
+ out.Literal = true
116
+ case shimast.KindFalseKeyword:
117
+ out.Literal = false
118
+ }
119
+ return out
120
+ }
@@ -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,374 @@
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 ranges;
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
+ }
37
+
38
+ // DumpDecoratorArgument is one decorator argument; Literal is set only for a
39
+ // statically-resolved string or boolean literal.
40
+ type DumpDecoratorArgument struct {
41
+ Literal any `json:"literal,omitempty"`
42
+ }
43
+
44
+ // DumpDecorator is a decorator as written on a declaration, carried on its
45
+ // target node for a consumer to interpret.
46
+ type DumpDecorator struct {
47
+ Name string `json:"name"`
48
+ Arguments []DumpDecoratorArgument `json:"arguments"`
49
+ }
50
+
51
+ // DumpNode is the wire shape of a graph node. Lowercase json keys are the
52
+ // contract; the Go field names are not.
53
+ type DumpNode struct {
54
+ ID string `json:"id"`
55
+ Kind string `json:"kind"`
56
+ Name string `json:"name"`
57
+ QualifiedName string `json:"qualifiedName,omitempty"`
58
+ File string `json:"file"`
59
+ External bool `json:"external"`
60
+ Ignored bool `json:"ignored,omitempty"`
61
+ Exported bool `json:"exported,omitempty"`
62
+ Modifiers []string `json:"modifiers,omitempty"`
63
+ Evidence *DumpEvidence `json:"evidence,omitempty"`
64
+ Implementation *DumpEvidence `json:"implementation,omitempty"`
65
+ Decorators []DumpDecorator `json:"decorators,omitempty"`
66
+ }
67
+
68
+ // DumpEdge is the wire shape of a graph edge. Lowercase json keys are the
69
+ // contract; the Go field names are not.
70
+ type DumpEdge struct {
71
+ From string `json:"from"`
72
+ To string `json:"to"`
73
+ Kind string `json:"kind"`
74
+ Evidence *DumpEvidence `json:"evidence,omitempty"`
75
+ }
76
+
77
+ // Dump is the IGraphDump envelope: the project it was built for and the full
78
+ // node and edge sets with none of the MCP response caps.
79
+ type Dump struct {
80
+ Project string `json:"project"`
81
+ Tsconfig string `json:"tsconfig"`
82
+ Nodes []DumpNode `json:"nodes"`
83
+ Edges []DumpEdge `json:"edges"`
84
+ }
85
+
86
+ // NewDump projects a built graph onto the export shape. project is the absolute
87
+ // project root used to relativize file paths and node ids; ignored is the
88
+ // git-ignored source set (nil for a non-git project); sources maps a source
89
+ // file's path to its text so byte spans become line/col evidence (nil omits
90
+ // evidence).
91
+ func NewDump(g *Graph, project, tsconfig string, ignored map[string]bool, sources map[string]string) Dump {
92
+ ctx := newDumpContext(project, sources)
93
+
94
+ // Decorators ride on their target node; group by the internal node id before
95
+ // ids are relativized for output.
96
+ decByNode := make(map[string][]DumpDecorator, len(g.Decorators))
97
+ for _, d := range g.Decorators {
98
+ args := make([]DumpDecoratorArgument, 0, len(d.Arguments))
99
+ for _, a := range d.Arguments {
100
+ if a.Literal == nil {
101
+ continue
102
+ }
103
+ args = append(args, DumpDecoratorArgument{Literal: a.Literal})
104
+ }
105
+ decByNode[d.Target] = append(decByNode[d.Target], DumpDecorator{Name: d.Name, Arguments: args})
106
+ }
107
+
108
+ nodes := make([]DumpNode, 0, len(g.Nodes))
109
+ for _, n := range g.Nodes {
110
+ name, qualified := nodeNames(n)
111
+ nodes = append(nodes, DumpNode{
112
+ ID: ctx.relID(n.ID),
113
+ Kind: string(n.Kind),
114
+ Name: name,
115
+ QualifiedName: qualified,
116
+ File: ctx.rel(n.File),
117
+ External: n.External,
118
+ Ignored: ignored[n.File],
119
+ Exported: n.Exported,
120
+ Modifiers: n.Modifiers,
121
+ Evidence: ctx.evidence(n.File, n.Pos, n.End),
122
+ Implementation: ctx.evidence(n.ImplementationFile, n.ImplementationPos, n.ImplementationEnd),
123
+ Decorators: decByNode[n.ID],
124
+ })
125
+ }
126
+ sort.Slice(nodes, func(i, j int) bool { return nodes[i].ID < nodes[j].ID })
127
+
128
+ edges := make([]DumpEdge, 0, len(g.Edges))
129
+ for _, e := range g.Edges {
130
+ edges = append(edges, DumpEdge{
131
+ From: ctx.relID(e.From),
132
+ To: ctx.relID(e.To),
133
+ Kind: dumpEdgeKind(e),
134
+ Evidence: ctx.edgeEvidence(e),
135
+ })
136
+ }
137
+ sort.Slice(edges, func(i, j int) bool {
138
+ if edges[i].From != edges[j].From {
139
+ return edges[i].From < edges[j].From
140
+ }
141
+ if edges[i].To != edges[j].To {
142
+ return edges[i].To < edges[j].To
143
+ }
144
+ return edges[i].Kind < edges[j].Kind
145
+ })
146
+
147
+ return Dump{
148
+ Project: project,
149
+ Tsconfig: tsconfig,
150
+ Nodes: nodes,
151
+ Edges: edges,
152
+ }
153
+ }
154
+
155
+ // MarshalDump serializes a built graph to the export JSON, indented when pretty.
156
+ // See NewDump for the parameters.
157
+ func MarshalDump(g *Graph, project, tsconfig string, ignored map[string]bool, sources map[string]string, pretty bool) ([]byte, error) {
158
+ d := NewDump(g, project, tsconfig, ignored, sources)
159
+ if pretty {
160
+ return json.MarshalIndent(d, "", " ")
161
+ }
162
+ return json.Marshal(d)
163
+ }
164
+
165
+ // dumpEdgeKind maps an internal edge kind, refined by Edge.Origin, onto the
166
+ // schema's finer relationship kind.
167
+ func dumpEdgeKind(e *Edge) string {
168
+ return wireEdgeKind(e.Kind, e.Origin)
169
+ }
170
+
171
+ // wireEdgeKind maps an internal edge kind, refined by its origin, onto the
172
+ // schema's finer relationship kind. It is the edge's emitted identity, so the
173
+ // dedup keys on it: two uses of one target that differ only in a form mapping to
174
+ // the same wire kind (a plain call and a tagged-template call, both `calls`)
175
+ // collapse to one edge, while forms that mean distinct relationships (`calls` vs
176
+ // `instantiates`, `extends` vs `implements`) are each kept.
177
+ func wireEdgeKind(kind EdgeKind, origin string) string {
178
+ switch kind {
179
+ case EdgeValueCall:
180
+ switch origin {
181
+ case "new":
182
+ return "instantiates"
183
+ case "jsx":
184
+ return "renders"
185
+ default:
186
+ return "calls"
187
+ }
188
+ case EdgeValueAccess:
189
+ return "accesses"
190
+ case EdgeTypeRef:
191
+ return "type_ref"
192
+ case EdgeHeritage:
193
+ if origin == "extends" {
194
+ return "extends"
195
+ }
196
+ return "implements"
197
+ default:
198
+ return string(kind)
199
+ }
200
+ }
201
+
202
+ // nodeNames returns a node's simple name and, when it is owner-qualified, its
203
+ // full qualified form for the wire. The simple name is the symbol's own name
204
+ // recorded at build time, so a quoted member whose name contains a dot
205
+ // (`"a.b"` becomes Name `C.a.b`) splits exactly; the qualified form is the full Name
206
+ // when it differs from the simple name, and "" for a top-level declaration.
207
+ // A node without a recorded simple name (a future virtual node) falls back to
208
+ // the last dot-separated segment.
209
+ func nodeNames(n *Node) (simple, qualified string) {
210
+ if n.Simple == "" {
211
+ if dot := strings.LastIndex(n.Name, "."); dot >= 0 {
212
+ return n.Name[dot+1:], n.Name
213
+ }
214
+ return n.Name, ""
215
+ }
216
+ if n.Simple == n.Name {
217
+ return n.Simple, ""
218
+ }
219
+ return n.Simple, n.Name
220
+ }
221
+
222
+ // dumpContext relativizes paths and turns byte spans into line/col evidence,
223
+ // caching a per-file line index so a large file's many edges cost O(log n) each
224
+ // instead of a re-scan.
225
+ type dumpContext struct {
226
+ project string
227
+ sources map[string]string
228
+ lines map[string]lineStarts
229
+ }
230
+
231
+ func newDumpContext(project string, sources map[string]string) *dumpContext {
232
+ return &dumpContext{
233
+ project: strings.TrimRight(filepath.ToSlash(project), "/"),
234
+ sources: sources,
235
+ lines: map[string]lineStarts{},
236
+ }
237
+ }
238
+
239
+ // rel makes a source file path project-relative; an external path keeps its
240
+ // node_modules-relative tail so a dependency leaf stays readable.
241
+ func (c *dumpContext) rel(file string) string {
242
+ f := filepath.ToSlash(file)
243
+ if c.project != "" {
244
+ if f == c.project {
245
+ return ""
246
+ }
247
+ if strings.HasPrefix(f, c.project+"/") {
248
+ return f[len(c.project)+1:]
249
+ }
250
+ }
251
+ if i := strings.LastIndex(f, "/node_modules/"); i >= 0 {
252
+ return f[i+1:]
253
+ }
254
+ return f
255
+ }
256
+
257
+ // relID relativizes the path portion of a node id ("path#qualifiedName:kind").
258
+ // An id with no path (a future virtual node) is returned unchanged.
259
+ func (c *dumpContext) relID(id string) string {
260
+ hash := strings.Index(id, "#")
261
+ if hash < 0 {
262
+ return id
263
+ }
264
+ return c.rel(id[:hash]) + id[hash:]
265
+ }
266
+
267
+ // evidence builds the line/col span for a byte range in file, or nil when the
268
+ // span is absent or no source is available.
269
+ func (c *dumpContext) evidence(file string, pos, end int) *DumpEvidence {
270
+ if pos < 0 || c.sources == nil {
271
+ return nil
272
+ }
273
+ text, ok := c.sources[file]
274
+ if !ok {
275
+ return nil
276
+ }
277
+ ls := c.lines[file]
278
+ if ls == nil {
279
+ ls = newLineStarts(text)
280
+ c.lines[file] = ls
281
+ }
282
+ if pos > len(text) {
283
+ return nil
284
+ }
285
+ // Node.Pos() and an expression's Pos() are the full-start: they include the
286
+ // leading whitespace and doc comments before the token. Advance to the first
287
+ // code character so the line/column point at the declaration, not its banner
288
+ // or its indentation.
289
+ pos = firstCodeOffset(text, pos)
290
+ sl, sc := ls.at(pos)
291
+ ev := &DumpEvidence{File: c.rel(file), StartLine: sl, StartCol: sc}
292
+ if end > pos && end <= len(text) {
293
+ ev.EndLine, ev.EndCol = ls.at(end)
294
+ }
295
+ return ev
296
+ }
297
+
298
+ // firstCodeOffset advances past leading trivia: whitespace, // line comments,
299
+ // and /* */ block comments from pos to the first code byte, or len(text) if
300
+ // the rest is all trivia. It mirrors how a token's real start is found, so an
301
+ // evidence span lands on the declaration rather than the comment above it.
302
+ func firstCodeOffset(text string, pos int) int {
303
+ i := pos
304
+ for i < len(text) {
305
+ switch {
306
+ case text[i] == ' ' || text[i] == '\t' || text[i] == '\r' || text[i] == '\n':
307
+ i++
308
+ case text[i] == '/' && i+1 < len(text) && text[i+1] == '/':
309
+ i += 2
310
+ for i < len(text) && text[i] != '\n' {
311
+ i++
312
+ }
313
+ case text[i] == '/' && i+1 < len(text) && text[i+1] == '*':
314
+ i += 2
315
+ for i+1 < len(text) && !(text[i] == '*' && text[i+1] == '/') {
316
+ i++
317
+ }
318
+ if i+1 < len(text) {
319
+ i += 2
320
+ } else {
321
+ i = len(text)
322
+ }
323
+ default:
324
+ return i
325
+ }
326
+ }
327
+ return i
328
+ }
329
+
330
+ // edgeEvidence is the evidence range for an edge's source expression.
331
+ func (c *dumpContext) edgeEvidence(e *Edge) *DumpEvidence {
332
+ // The edge's span lives in the From node's file.
333
+ file := nodeFile(e.From)
334
+ if file == "" {
335
+ return nil
336
+ }
337
+ return c.evidence(file, e.Pos, e.End)
338
+ }
339
+
340
+ // nodeFile recovers the source file path embedded in a node id
341
+ // ("path#qualifiedName:kind"); "" for an id without a path.
342
+ func nodeFile(id string) string {
343
+ if hash := strings.Index(id, "#"); hash >= 0 {
344
+ return id[:hash]
345
+ }
346
+ return ""
347
+ }
348
+
349
+ // lineStarts holds the byte offset of each line's start, so an offset maps to a
350
+ // 1-based line/column by binary search.
351
+ type lineStarts []int
352
+
353
+ func newLineStarts(text string) lineStarts {
354
+ starts := make(lineStarts, 1, 1+strings.Count(text, "\n"))
355
+ starts[0] = 0
356
+ for i := 0; i < len(text); i++ {
357
+ if text[i] == '\n' {
358
+ starts = append(starts, i+1)
359
+ }
360
+ }
361
+ return starts
362
+ }
363
+
364
+ // at returns the 1-based line and column of a byte offset.
365
+ func (ls lineStarts) at(offset int) (line, col int) {
366
+ if offset < 0 || len(ls) == 0 {
367
+ return 0, 0
368
+ }
369
+ i := sort.Search(len(ls), func(i int) bool { return ls[i] > offset }) - 1
370
+ if i < 0 {
371
+ i = 0
372
+ }
373
+ return i + 1, offset - ls[i] + 1
374
+ }