ttsc 0.16.5 → 0.16.7

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.
@@ -1,7 +1,7 @@
1
1
  // Command graphdump builds the @ttsc/graph code graph for a project and prints
2
2
  // the entire graph (every node and every edge, with none of the MCP response
3
3
  // caps) as one JSON document. It is the repo-internal one-shot used by the
4
- // graph-bench viewer pipeline; the shipped equivalent is `ttscgraph dump`. Both
4
+ // benchmark graph viewer pipeline; the shipped equivalent is `ttscgraph dump`. Both
5
5
  // serialize through graph.MarshalDump.
6
6
  package main
7
7
 
@@ -6,9 +6,8 @@ import (
6
6
 
7
7
  // DecoratorArgument is one argument written on a decorator. Literal holds the
8
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.
9
+ // and is nil otherwise.
10
10
  type DecoratorArgument struct {
11
- Text string
12
11
  Literal any
13
12
  }
14
13
 
@@ -26,8 +25,8 @@ type Decorator struct {
26
25
  }
27
26
 
28
27
  // 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
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
31
30
  // keys on the written convention name, not the resolved decorator symbol.
32
31
  func (g *Graph) collectDecorators(path string, statements []*shimast.Node) {
33
32
  for _, statement := range statements {
@@ -105,11 +104,10 @@ func decoratorFact(dec *shimast.Node) *Decorator {
105
104
  return fact
106
105
  }
107
106
 
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.
107
+ // decoratorArg captures one decorator argument. Only string and boolean
108
+ // literals resolve to a Literal, the values a consumer can use directly.
111
109
  func decoratorArg(arg *shimast.Node) DecoratorArgument {
112
- out := DecoratorArgument{Text: shimast.NodeText(arg)}
110
+ out := DecoratorArgument{}
113
111
  switch arg.Kind {
114
112
  case shimast.KindStringLiteral, shimast.KindNoSubstitutionTemplateLiteral:
115
113
  out.Literal = arg.Text()
@@ -8,7 +8,7 @@ import (
8
8
  )
9
9
 
10
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
11
+ // prints: the IGraphDump shape the @ttsc/graph engine loads (and the 3D viewer
12
12
  // reduces). The internal Node/Edge model stays narrow (so the resident MCP path
13
13
  // is untouched); the richer schema is produced here:
14
14
  //
@@ -16,7 +16,7 @@ import (
16
16
  // - one EdgeValueCall splits by Origin into "calls" / "instantiates" /
17
17
  // "renders", and EdgeHeritage into "extends" / "implements";
18
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;
19
+ // - byte spans become 1-based line/col Evidence ranges;
20
20
  // - decorator facts ride on their target node;
21
21
  // - file paths are project-relative and the output is sorted, so the dump is
22
22
  // deterministic and diffable.
@@ -33,14 +33,12 @@ type DumpEvidence struct {
33
33
  StartCol int `json:"startCol,omitempty"`
34
34
  EndLine int `json:"endLine,omitempty"`
35
35
  EndCol int `json:"endCol,omitempty"`
36
- Text string `json:"text,omitempty"`
37
36
  }
38
37
 
39
38
  // DumpDecoratorArgument is one decorator argument; Literal is set only for a
40
39
  // statically-resolved string or boolean literal.
41
40
  type DumpDecoratorArgument struct {
42
- Text string `json:"text"`
43
- Literal any `json:"literal,omitempty"`
41
+ Literal any `json:"literal,omitempty"`
44
42
  }
45
43
 
46
44
  // DumpDecorator is a decorator as written on a declaration, carried on its
@@ -53,17 +51,18 @@ type DumpDecorator struct {
53
51
  // DumpNode is the wire shape of a graph node. Lowercase json keys are the
54
52
  // contract; the Go field names are not.
55
53
  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"`
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"`
67
66
  }
68
67
 
69
68
  // DumpEdge is the wire shape of a graph edge. Lowercase json keys are the
@@ -98,7 +97,10 @@ func NewDump(g *Graph, project, tsconfig string, ignored map[string]bool, source
98
97
  for _, d := range g.Decorators {
99
98
  args := make([]DumpDecoratorArgument, 0, len(d.Arguments))
100
99
  for _, a := range d.Arguments {
101
- args = append(args, DumpDecoratorArgument{Text: a.Text, Literal: a.Literal})
100
+ if a.Literal == nil {
101
+ continue
102
+ }
103
+ args = append(args, DumpDecoratorArgument{Literal: a.Literal})
102
104
  }
103
105
  decByNode[d.Target] = append(decByNode[d.Target], DumpDecorator{Name: d.Name, Arguments: args})
104
106
  }
@@ -107,17 +109,18 @@ func NewDump(g *Graph, project, tsconfig string, ignored map[string]bool, source
107
109
  for _, n := range g.Nodes {
108
110
  name, qualified := nodeNames(n)
109
111
  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],
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],
121
124
  })
122
125
  }
123
126
  sort.Slice(nodes, func(i, j int) bool { return nodes[i].ID < nodes[j].ID })
@@ -199,7 +202,7 @@ func wireEdgeKind(kind EdgeKind, origin string) string {
199
202
  // nodeNames returns a node's simple name and, when it is owner-qualified, its
200
203
  // full qualified form for the wire. The simple name is the symbol's own name
201
204
  // 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
205
+ // (`"a.b"` becomes Name `C.a.b`) splits exactly; the qualified form is the full Name
203
206
  // when it differs from the simple name, and "" for a top-level declaration.
204
207
  // A node without a recorded simple name (a future virtual node) falls back to
205
208
  // the last dot-separated segment.
@@ -262,9 +265,8 @@ func (c *dumpContext) relID(id string) string {
262
265
  }
263
266
 
264
267
  // 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
+ // span is absent or no source is available.
269
+ func (c *dumpContext) evidence(file string, pos, end int) *DumpEvidence {
268
270
  if pos < 0 || c.sources == nil {
269
271
  return nil
270
272
  }
@@ -289,15 +291,12 @@ func (c *dumpContext) evidence(file string, pos, end int, includeText bool) *Dum
289
291
  ev := &DumpEvidence{File: c.rel(file), StartLine: sl, StartCol: sc}
290
292
  if end > pos && end <= len(text) {
291
293
  ev.EndLine, ev.EndCol = ls.at(end)
292
- if includeText {
293
- ev.Text = excerpt(text[pos:end])
294
- }
295
294
  }
296
295
  return ev
297
296
  }
298
297
 
299
- // firstCodeOffset advances past leading trivia — whitespace, // line comments,
300
- // and /* */ block comments — from pos to the first code byte, or len(text) if
298
+ // firstCodeOffset advances past leading trivia: whitespace, // line comments,
299
+ // and /* */ block comments from pos to the first code byte, or len(text) if
301
300
  // the rest is all trivia. It mirrors how a token's real start is found, so an
302
301
  // evidence span lands on the declaration rather than the comment above it.
303
302
  func firstCodeOffset(text string, pos int) int {
@@ -328,15 +327,14 @@ func firstCodeOffset(text string, pos int) int {
328
327
  return i
329
328
  }
330
329
 
331
- // edgeEvidence is the evidence for an edge's source expression, with a short
332
- // excerpt of the expression text.
330
+ // edgeEvidence is the evidence range for an edge's source expression.
333
331
  func (c *dumpContext) edgeEvidence(e *Edge) *DumpEvidence {
334
332
  // The edge's span lives in the From node's file.
335
333
  file := nodeFile(e.From)
336
334
  if file == "" {
337
335
  return nil
338
336
  }
339
- return c.evidence(file, e.Pos, e.End, true)
337
+ return c.evidence(file, e.Pos, e.End)
340
338
  }
341
339
 
342
340
  // nodeFile recovers the source file path embedded in a node id
@@ -348,20 +346,6 @@ func nodeFile(id string) string {
348
346
  return ""
349
347
  }
350
348
 
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
349
  // lineStarts holds the byte offset of each line's start, so an offset maps to a
366
350
  // 1-based line/column by binary search.
367
351
  type lineStarts []int
@@ -130,7 +130,7 @@ export function main(): void {
130
130
  }
131
131
  // The dump carries no ceremony keys: it is wholly checker-resolved, so it has
132
132
  // no per-edge trust flags and no schema version to negotiate.
133
- for _, gone := range []string{`"schemaVersion"`, `"provenance":`, `"confidence":`} {
133
+ for _, gone := range []string{`"schemaVersion"`, `"provenance":`, `"confidence":`, `"text":`} {
134
134
  if strings.Contains(s, gone) {
135
135
  t.Fatalf("dump still emits removed key %s:\n%s", gone, s)
136
136
  }
@@ -304,6 +304,11 @@ func (g *Graph) callsWithin(checker *shimchecker.Checker, from string, node *shi
304
304
  if !isInvokedAccess(child) {
305
305
  g.accessEdge(checker, from, child)
306
306
  }
307
+ case shimast.KindBinaryExpression:
308
+ if target := g.assignedFunctionTarget(checker, from, child); target != "" {
309
+ g.recordImplementation(target, child)
310
+ g.callsWithin(checker, target, child.AsBinaryExpression().Right)
311
+ }
307
312
  case shimast.KindJsxSelfClosingElement:
308
313
  // `<Component />` is a use of the component; an intrinsic tag (`<div />`)
309
314
  // resolves to nothing and is dropped by callEdge.
@@ -320,6 +325,40 @@ func (g *Graph) callsWithin(checker *shimchecker.Checker, from string, node *shi
320
325
  })
321
326
  }
322
327
 
328
+ func (g *Graph) assignedFunctionTarget(checker *shimchecker.Checker, from string, node *shimast.Node) string {
329
+ binary := node.AsBinaryExpression()
330
+ if binary == nil ||
331
+ binary.OperatorToken == nil ||
332
+ binary.Right == nil ||
333
+ binary.OperatorToken.Kind != shimast.KindEqualsToken ||
334
+ !shimast.IsFunctionLike(binary.Right) {
335
+ return ""
336
+ }
337
+ target := Resolve(checker, binary.Left)
338
+ if target == nil || target.Symbol == nil {
339
+ return ""
340
+ }
341
+ to := g.ensureTargetNode(target)
342
+ if to == "" || to == from {
343
+ return ""
344
+ }
345
+ return to
346
+ }
347
+
348
+ func (g *Graph) recordImplementation(id string, assignment *shimast.Node) {
349
+ node := g.Nodes[id]
350
+ if node == nil || node.External || node.ImplementationFile != "" {
351
+ return
352
+ }
353
+ file := shimast.GetSourceFileOfNode(assignment)
354
+ if file == nil {
355
+ return
356
+ }
357
+ node.ImplementationFile = file.FileName()
358
+ node.ImplementationPos = assignment.Pos()
359
+ node.ImplementationEnd = assignment.End()
360
+ }
361
+
323
362
  func isInvokedAccess(access *shimast.Node) bool {
324
363
  parent := access.Parent
325
364
  if parent == nil {
@@ -406,6 +445,10 @@ func (g *Graph) typeRefsWithin(checker *shimchecker.Checker, from string, node *
406
445
  if imp := child.AsImportTypeNode(); imp != nil && imp.Qualifier != nil {
407
446
  g.typeRefEdge(checker, from, imp.Qualifier)
408
447
  }
448
+ case shimast.KindBinaryExpression:
449
+ if target := g.assignedFunctionTarget(checker, from, child); target != "" {
450
+ g.typeRefsWithin(checker, target, child.AsBinaryExpression().Right)
451
+ }
409
452
  }
410
453
  g.typeRefsWithin(checker, from, child)
411
454
  return false
@@ -47,8 +47,11 @@ type Node struct {
47
47
  // Pos and End bound the declaration in its source file (byte offsets). They
48
48
  // are for display, never identity, so an edit that shifts them does not re-key
49
49
  // the node.
50
- Pos int
51
- End int
50
+ Pos int
51
+ End int
52
+ ImplementationFile string
53
+ ImplementationPos int
54
+ ImplementationEnd int
52
55
  }
53
56
 
54
57
  // EdgeKind classifies a relationship between two nodes.
@@ -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
+ // TestValueCallEdgesAttributeFunctionAssignmentsToMembers verifies function
11
+ // assignments to resolved members are walked as that member's implementation.
12
+ //
13
+ // Libraries sometimes attach methods by assigning an arrow/function expression
14
+ // to a typed property instead of declaring a class method body. The class-level
15
+ // walk sees the assignment, but graph flow still needs the assigned member node
16
+ // to own calls made inside the RHS function.
17
+ //
18
+ // 1. Compile a fixture where `inst.run = () => helper()` assigns a typed class
19
+ // method implementation.
20
+ // 2. Build the graph.
21
+ // 3. Assert the `Service.run` method has a value-call edge to `helper`.
22
+ func TestValueCallEdgesAttributeFunctionAssignmentsToMembers(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 function helper(): void {}
26
+ export class Service {
27
+ run(): void {}
28
+ }
29
+ export function install(inst: Service): void {
30
+ inst.run = (): void => {
31
+ helper();
32
+ };
33
+ }
34
+ `)
35
+
36
+ prog, diags, err := driver.LoadProgram(root, "tsconfig.json", driver.LoadProgramOptions{})
37
+ if err != nil {
38
+ t.Fatal(err)
39
+ }
40
+ if len(diags) != 0 {
41
+ t.Fatalf("unexpected diagnostics: %v", diags)
42
+ }
43
+ defer func() { _ = prog.Close() }()
44
+
45
+ graph := Build(prog)
46
+ path := sourceFile(t, prog, "main.ts").FileName()
47
+
48
+ run := nodeID(path, "Service.run", NodeMethod)
49
+ helper := nodeID(path, "helper", NodeFunction)
50
+
51
+ if !hasEdge(graph, run, helper, EdgeValueCall) {
52
+ t.Fatalf("missing value-call edge Service.run -> helper (assigned implementation); edges: %v", graph.Edges)
53
+ }
54
+ if graph.Nodes[run].ImplementationFile != path ||
55
+ graph.Nodes[run].ImplementationPos <= 0 ||
56
+ graph.Nodes[run].ImplementationEnd <= graph.Nodes[run].ImplementationPos {
57
+ t.Fatalf("Service.run missing assigned implementation span: %+v", graph.Nodes[run])
58
+ }
59
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ttsc",
3
- "version": "0.16.5",
3
+ "version": "0.16.7",
4
4
  "description": "General-purpose TypeScript-Go compiler, runtime, plugin host, and LSP host.",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",
@@ -32,13 +32,13 @@
32
32
  "typescript": "7.0.1-rc"
33
33
  },
34
34
  "optionalDependencies": {
35
- "@ttsc/linux-x64": "0.16.5",
36
- "@ttsc/linux-arm64": "0.16.5",
37
- "@ttsc/darwin-x64": "0.16.5",
38
- "@ttsc/darwin-arm64": "0.16.5",
39
- "@ttsc/linux-arm": "0.16.5",
40
- "@ttsc/win32-x64": "0.16.5",
41
- "@ttsc/win32-arm64": "0.16.5"
35
+ "@ttsc/linux-arm": "0.16.7",
36
+ "@ttsc/linux-x64": "0.16.7",
37
+ "@ttsc/linux-arm64": "0.16.7",
38
+ "@ttsc/darwin-arm64": "0.16.7",
39
+ "@ttsc/win32-x64": "0.16.7",
40
+ "@ttsc/win32-arm64": "0.16.7",
41
+ "@ttsc/darwin-x64": "0.16.7"
42
42
  },
43
43
  "sideEffects": false,
44
44
  "files": [