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,581 @@
1
+ package graph
2
+
3
+ import (
4
+ shimast "github.com/microsoft/typescript-go/shim/ast"
5
+ shimchecker "github.com/microsoft/typescript-go/shim/checker"
6
+
7
+ "github.com/samchon/ttsc/packages/ttsc/driver"
8
+ )
9
+
10
+ // addEdges resolves the relationships between the declaration nodes Build
11
+ // recorded. It walks each source file again and, for every class or interface,
12
+ // resolves its heritage bases through the checker (unwrapping barrel re-exports
13
+ // to the real declaration) and links the declaration to that base, materializing
14
+ // an external boundary-leaf node when the base lives in node_modules or a `.d.ts`.
15
+ func (g *Graph) addEdges(prog *driver.Program) {
16
+ checker := prog.Checker
17
+ for _, file := range prog.SourceFiles() {
18
+ g.markExports(checker, file)
19
+ g.collectHeritage(checker, file)
20
+ g.collectCalls(checker, file)
21
+ g.collectTypeRefs(checker, file)
22
+ if file.Statements != nil {
23
+ g.collectDecorators(file.FileName(), file.Statements.Nodes)
24
+ }
25
+ }
26
+ }
27
+
28
+ // addEdge records a from->to edge of the given kind, skipping a duplicate so a
29
+ // caller that invokes the same function several times yields one edge, not one
30
+ // per call site. The dedup is an O(1) set lookup, so building N edges is O(N).
31
+ func (g *Graph) addEdge(from, to string, kind EdgeKind) {
32
+ g.addEdgeAt(from, to, kind, "", -1, -1)
33
+ }
34
+
35
+ func (g *Graph) addEdgeAt(from, to string, kind EdgeKind, origin string, pos, end int) {
36
+ // Key on the emitted wire kind, not the internal kind, so two uses of one
37
+ // target that surface as different relationships (a call and a `new`, an
38
+ // `extends` and an `implements` of the same base) are both kept, while
39
+ // repeated uses of the same form collapse to one edge.
40
+ key := from + "\x00" + to + "\x00" + wireEdgeKind(kind, origin)
41
+ if _, exists := g.seen[key]; exists {
42
+ return
43
+ }
44
+ g.seen[key] = struct{}{}
45
+ g.Edges = append(g.Edges, &Edge{From: from, To: to, Kind: kind, Origin: origin, Pos: pos, End: end})
46
+ }
47
+
48
+ // collectHeritage adds a heritage edge for every base of every class and
49
+ // interface in file, descending into namespace bodies so a namespaced class's
50
+ // bases are resolved too.
51
+ func (g *Graph) collectHeritage(checker *shimchecker.Checker, file *shimast.SourceFile) {
52
+ if file.Statements == nil {
53
+ return
54
+ }
55
+ g.collectHeritageIn(checker, file.FileName(), file.Statements.Nodes)
56
+ }
57
+
58
+ // collectHeritageIn adds heritage edges for the class/interface statements in a
59
+ // list — the file's top level, or a namespace body it recurses into.
60
+ func (g *Graph) collectHeritageIn(checker *shimchecker.Checker, path string, statements []*shimast.Node) {
61
+ for _, statement := range statements {
62
+ switch statement.Kind {
63
+ case shimast.KindClassDeclaration:
64
+ decl := statement.AsClassDeclaration()
65
+ if decl != nil && decl.HeritageClauses != nil {
66
+ g.heritageEdges(checker, path, statement, NodeClass, decl.HeritageClauses.Nodes)
67
+ }
68
+ case shimast.KindInterfaceDeclaration:
69
+ decl := statement.AsInterfaceDeclaration()
70
+ if decl != nil && decl.HeritageClauses != nil {
71
+ g.heritageEdges(checker, path, statement, NodeInterface, decl.HeritageClauses.Nodes)
72
+ }
73
+ case shimast.KindModuleDeclaration:
74
+ g.collectHeritageIn(checker, path, moduleStatements(statement))
75
+ }
76
+ }
77
+ }
78
+
79
+ // heritageEdges resolves each base expression of node's heritage clauses and
80
+ // records a heritage edge from node to the resolved base node.
81
+ func (g *Graph) heritageEdges(checker *shimchecker.Checker, path string, node *shimast.Node, kind NodeKind, clauses []*shimast.Node) {
82
+ symbol := node.Symbol()
83
+ if symbol == nil || symbol.Name == "" {
84
+ return
85
+ }
86
+ from := nodeID(path, qualifiedName(symbol), kind)
87
+ for _, clauseNode := range clauses {
88
+ clause := clauseNode.AsHeritageClause()
89
+ if clause == nil || clause.Types == nil {
90
+ continue
91
+ }
92
+ // The clause keyword splits one internal heritage kind into the schema's
93
+ // `extends` vs `implements`: an interface's bases and a class's superclass
94
+ // are `extends`; a class's interface list is `implements`.
95
+ origin := "implements"
96
+ if clause.Token == shimast.KindExtendsKeyword {
97
+ origin = "extends"
98
+ }
99
+ for _, typeNode := range clause.Types.Nodes {
100
+ base := typeNode.AsExpressionWithTypeArguments()
101
+ if base == nil || base.Expression == nil {
102
+ continue
103
+ }
104
+ target := Resolve(checker, base.Expression)
105
+ if target == nil || target.Symbol == nil {
106
+ continue
107
+ }
108
+ to := g.ensureTargetNode(target)
109
+ if to == "" {
110
+ continue
111
+ }
112
+ g.addEdgeAt(from, to, EdgeHeritage, origin, base.Expression.Pos(), base.Expression.End())
113
+ }
114
+ }
115
+ }
116
+
117
+ // collectCalls records a value-call edge from each declaration to every function,
118
+ // method, or constructor it invokes. The reference walk is attributed to the
119
+ // nearest enclosing graph node: a top-level function, a class/interface method, a
120
+ // top-level variable binding, or the class itself for a member that is not a
121
+ // method (a property initializer).
122
+ func (g *Graph) collectCalls(checker *shimchecker.Checker, file *shimast.SourceFile) {
123
+ forEachContainer(file.FileName(), file, func(from string, node *shimast.Node) {
124
+ g.callsWithin(checker, from, node)
125
+ })
126
+ }
127
+
128
+ // forEachContainer calls fn(nodeID, subtree) for every graph node that can hold
129
+ // call or type references, paired with the subtree to walk for it. A class or
130
+ // interface is split: each method member is attributed to its own method node, and
131
+ // every other member (a property initializer) to the type node, so a call made
132
+ // inside one method is not confused with another's.
133
+ func forEachContainer(path string, file *shimast.SourceFile, fn func(string, *shimast.Node)) {
134
+ if file.Statements == nil {
135
+ return
136
+ }
137
+ forEachContainerIn(path, file.Statements.Nodes, fn)
138
+ }
139
+
140
+ // forEachContainerIn pairs each graph node with its subtree for a statement list
141
+ // — the file's top level, or a namespace body it recurses into, so a call or
142
+ // type reference made inside a namespace member is attributed to that member.
143
+ func forEachContainerIn(path string, statements []*shimast.Node, fn func(string, *shimast.Node)) {
144
+ for _, statement := range statements {
145
+ switch statement.Kind {
146
+ case shimast.KindFunctionDeclaration:
147
+ if id := topLevelID(path, statement, NodeFunction); id != "" {
148
+ fn(id, statement)
149
+ }
150
+ case shimast.KindTypeAliasDeclaration:
151
+ if id := topLevelID(path, statement, NodeTypeAlias); id != "" {
152
+ fn(id, statement)
153
+ }
154
+ case shimast.KindEnumDeclaration:
155
+ // An enum is a recorded node (build.go), and a member initializer can call a
156
+ // function (`enum E { A = base() }`) or reference a type, so walk its body
157
+ // too or those edges are silently dropped.
158
+ if id := topLevelID(path, statement, NodeEnum); id != "" {
159
+ fn(id, statement)
160
+ }
161
+ case shimast.KindClassDeclaration:
162
+ forEachMember(path, statement, NodeClass, fn)
163
+ case shimast.KindInterfaceDeclaration:
164
+ forEachMember(path, statement, NodeInterface, fn)
165
+ case shimast.KindVariableStatement:
166
+ forEachVariable(path, statement, fn)
167
+ case shimast.KindModuleDeclaration:
168
+ forEachContainerIn(path, moduleStatements(statement), fn)
169
+ }
170
+ }
171
+ }
172
+
173
+ // topLevelID returns the node id for a named declaration, or "". The name is
174
+ // namespace-qualified, so a namespaced declaration lands on the node the build
175
+ // pass recorded.
176
+ func topLevelID(path string, statement *shimast.Node, kind NodeKind) string {
177
+ symbol := statement.Symbol()
178
+ if symbol == nil || symbol.Name == "" {
179
+ return ""
180
+ }
181
+ return nodeID(path, qualifiedName(symbol), kind)
182
+ }
183
+
184
+ // forEachMember attributes a class/interface's callable members to their method
185
+ // node. Property members are additive: their initializer/type subtree is walked
186
+ // once for the property node, and once for the owner type node, so precise member
187
+ // queries can land on `Class.prop` without making coarse class/interface queries
188
+ // lose dependency edges they historically owned.
189
+ func forEachMember(path string, statement *shimast.Node, kind NodeKind, fn func(string, *shimast.Node)) {
190
+ containerID := topLevelID(path, statement, kind)
191
+ for _, member := range classMembers(statement) {
192
+ if isMethodMember(member.Kind) {
193
+ if name := methodName(member.Symbol()); name != "" {
194
+ fn(nodeID(path, name, NodeMethod), member)
195
+ continue
196
+ }
197
+ }
198
+ if isPropertyMember(member.Kind) {
199
+ if name := methodName(member.Symbol()); name != "" {
200
+ fn(nodeID(path, name, NodeVariable), member)
201
+ }
202
+ }
203
+ if containerID != "" {
204
+ fn(containerID, member)
205
+ }
206
+ }
207
+ if containerID == "" {
208
+ return
209
+ }
210
+ // The references that live on the declaration itself rather than in a member
211
+ // belong to the type node: a decorator factory call (`@Injectable()`), a type
212
+ // parameter constraint (`<T extends Base>`), and a heritage type argument
213
+ // (`extends Base<Payload>`). The per-member walk above never sees these, so
214
+ // attribute each class-level subtree here or the edge is silently dropped.
215
+ for _, decorator := range statement.Decorators() {
216
+ fn(containerID, decorator)
217
+ }
218
+ for _, typeParam := range statement.TypeParameters() {
219
+ fn(containerID, typeParam)
220
+ }
221
+ for _, clause := range heritageClauses(statement) {
222
+ fn(containerID, clause)
223
+ }
224
+ }
225
+
226
+ // heritageClauses returns the heritage clause nodes (`extends` / `implements`)
227
+ // of a class or interface declaration, or nil for anything else. Their type
228
+ // arguments are type references attributed to the declaration; the base
229
+ // expressions themselves become heritage edges in collectHeritage.
230
+ func heritageClauses(statement *shimast.Node) []*shimast.Node {
231
+ switch statement.Kind {
232
+ case shimast.KindClassDeclaration:
233
+ if decl := statement.AsClassDeclaration(); decl != nil && decl.HeritageClauses != nil {
234
+ return decl.HeritageClauses.Nodes
235
+ }
236
+ case shimast.KindInterfaceDeclaration:
237
+ if decl := statement.AsInterfaceDeclaration(); decl != nil && decl.HeritageClauses != nil {
238
+ return decl.HeritageClauses.Nodes
239
+ }
240
+ }
241
+ return nil
242
+ }
243
+
244
+ // forEachVariable attributes each binding of a top-level variable statement to
245
+ // its variable node, so a call or type reference inside `const fn = () => …` is
246
+ // an edge from fn.
247
+ func forEachVariable(path string, statement *shimast.Node, fn func(string, *shimast.Node)) {
248
+ variables := statement.AsVariableStatement()
249
+ if variables == nil || variables.DeclarationList == nil {
250
+ return
251
+ }
252
+ list := variables.DeclarationList.AsVariableDeclarationList()
253
+ if list == nil || list.Declarations == nil {
254
+ return
255
+ }
256
+ for _, binding := range list.Declarations.Nodes {
257
+ symbol := binding.Symbol()
258
+ if symbol == nil || symbol.Name == "" {
259
+ continue
260
+ }
261
+ fn(nodeID(path, qualifiedName(symbol), NodeVariable), binding)
262
+ }
263
+ }
264
+
265
+ // callsWithin walks node's subtree and records runtime value-use edges from
266
+ // `from` to the resolved target: calls/new/tagged templates/JSX components as
267
+ // value-call edges, and property or element access as value-access edges.
268
+ func (g *Graph) callsWithin(checker *shimchecker.Checker, from string, node *shimast.Node) {
269
+ node.ForEachChild(func(child *shimast.Node) bool {
270
+ switch child.Kind {
271
+ case shimast.KindCallExpression:
272
+ // A decorator's own factory call (`@Column()`, `@Entity()`) is metadata,
273
+ // not a runtime call: the decoration is already a fact on the node's
274
+ // decorators. Emitting a calls edge to the decorator function instead
275
+ // makes ubiquitous decorators (every entity field is `@Column`) the
276
+ // busiest nodes in the graph and buries the real architecture, so skip
277
+ // the factory call itself while still walking its arguments below.
278
+ if child.Parent != nil && child.Parent.Kind == shimast.KindDecorator {
279
+ break
280
+ }
281
+ if call := child.AsCallExpression(); call != nil && call.Expression != nil {
282
+ g.callEdge(checker, from, call.Expression, "call")
283
+ }
284
+ case shimast.KindNewExpression:
285
+ if newExpr := child.AsNewExpression(); newExpr != nil && newExpr.Expression != nil {
286
+ g.callEdge(checker, from, newExpr.Expression, "new")
287
+ }
288
+ case shimast.KindTaggedTemplateExpression:
289
+ // A tagged template (styled`…`, gql`…`) is a call to its tag function.
290
+ if tagged := child.AsTaggedTemplateExpression(); tagged != nil && tagged.Tag != nil {
291
+ g.callEdge(checker, from, tagged.Tag, "tagged")
292
+ }
293
+ case shimast.KindPropertyAccessExpression:
294
+ // Accessor/property reads are runtime uses too. Without this edge the
295
+ // graph shows the constructor or method call but not the state or lazy
296
+ // property resolution that agents then reopen files to inspect.
297
+ if !isInvokedAccess(child) {
298
+ g.accessEdge(checker, from, child)
299
+ }
300
+ case shimast.KindElementAccessExpression:
301
+ // String-literal bracket access (`this["metadata"]`) can resolve to the
302
+ // same property/accessor symbol as dotted access. Dynamic indexes resolve
303
+ // to nothing or to external library members and are filtered below.
304
+ if !isInvokedAccess(child) {
305
+ g.accessEdge(checker, from, child)
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
+ }
312
+ case shimast.KindJsxSelfClosingElement:
313
+ // `<Component />` is a use of the component; an intrinsic tag (`<div />`)
314
+ // resolves to nothing and is dropped by callEdge.
315
+ if jsx := child.AsJsxSelfClosingElement(); jsx != nil && jsx.TagName != nil {
316
+ g.callEdge(checker, from, jsx.TagName, "jsx")
317
+ }
318
+ case shimast.KindJsxOpeningElement:
319
+ if jsx := child.AsJsxOpeningElement(); jsx != nil && jsx.TagName != nil {
320
+ g.callEdge(checker, from, jsx.TagName, "jsx")
321
+ }
322
+ }
323
+ g.callsWithin(checker, from, child)
324
+ return false
325
+ })
326
+ }
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
+
362
+ func isInvokedAccess(access *shimast.Node) bool {
363
+ parent := access.Parent
364
+ if parent == nil {
365
+ return false
366
+ }
367
+ switch parent.Kind {
368
+ case shimast.KindCallExpression:
369
+ call := parent.AsCallExpression()
370
+ return call != nil && call.Expression == access
371
+ case shimast.KindNewExpression:
372
+ newExpr := parent.AsNewExpression()
373
+ return newExpr != nil && newExpr.Expression == access
374
+ case shimast.KindTaggedTemplateExpression:
375
+ tagged := parent.AsTaggedTemplateExpression()
376
+ return tagged != nil && tagged.Tag == access
377
+ default:
378
+ return false
379
+ }
380
+ }
381
+
382
+ // callEdge resolves a callee expression to its declaration and records a
383
+ // value-call edge, skipping an unresolved callee and a self-call. origin records
384
+ // the call form ("call", "new", "jsx", "tagged") so the dump can split it into
385
+ // the schema's calls / instantiates / renders kinds.
386
+ func (g *Graph) callEdge(checker *shimchecker.Checker, from string, callee *shimast.Node, origin string) {
387
+ g.valueUseEdge(checker, from, callee, EdgeValueCall, origin)
388
+ }
389
+
390
+ // accessEdge resolves a property or element access to its declaration and
391
+ // records a value-access edge, skipping unresolved/external/self targets.
392
+ func (g *Graph) accessEdge(checker *shimchecker.Checker, from string, access *shimast.Node) {
393
+ g.valueUseEdge(checker, from, access, EdgeValueAccess, "")
394
+ }
395
+
396
+ func (g *Graph) valueUseEdge(checker *shimchecker.Checker, from string, targetExpr *shimast.Node, kind EdgeKind, origin string) {
397
+ if targetExpr == nil {
398
+ return
399
+ }
400
+ target := Resolve(checker, targetExpr)
401
+ if target == nil || target.Symbol == nil {
402
+ return
403
+ }
404
+ to := g.ensureTargetNode(target)
405
+ if to == "" || to == from {
406
+ return
407
+ }
408
+ g.addEdgeAt(from, to, kind, origin, targetExpr.Pos(), targetExpr.End())
409
+ }
410
+
411
+ // collectTypeRefs records a type-ref edge from each top-level function, class,
412
+ // interface, or type alias to every named type it references in a type position
413
+ // (parameter, return, property, and alias right-hand-side types). Type
414
+ // references are first-class edges, which fits the ttsc thesis that types are
415
+ // the unit of truth: an `import type` or annotation-only dependency relates two
416
+ // symbols without any runtime call.
417
+ func (g *Graph) collectTypeRefs(checker *shimchecker.Checker, file *shimast.SourceFile) {
418
+ forEachContainer(file.FileName(), file, func(from string, node *shimast.Node) {
419
+ g.typeRefsWithin(checker, from, node)
420
+ })
421
+ }
422
+
423
+ // typeRefsWithin walks node's subtree and records a type-ref edge from `from` to
424
+ // the resolved target of every type reference it finds. A plain named type is a
425
+ // KindTypeReference; the two other type-position shapes that name a symbol are a
426
+ // `typeof value` query and an `import("./m").Foo` type, whose name is an
427
+ // EntityName rather than a TypeReference, so each is matched explicitly. A
428
+ // surrounding `as` / `satisfies` expression needs no case of its own: the type
429
+ // it carries is itself one of these nodes, which the recursion reaches.
430
+ func (g *Graph) typeRefsWithin(checker *shimchecker.Checker, from string, node *shimast.Node) {
431
+ node.ForEachChild(func(child *shimast.Node) bool {
432
+ switch child.Kind {
433
+ case shimast.KindTypeReference:
434
+ if ref := child.AsTypeReferenceNode(); ref != nil && ref.TypeName != nil {
435
+ g.typeRefEdge(checker, from, ref.TypeName)
436
+ }
437
+ case shimast.KindTypeQuery:
438
+ // `typeof value` in a type position depends on that value's type.
439
+ if query := child.AsTypeQueryNode(); query != nil && query.ExprName != nil {
440
+ g.typeRefEdge(checker, from, query.ExprName)
441
+ }
442
+ case shimast.KindImportType:
443
+ // `import("./m").Foo` references Foo through a dynamic import type; the
444
+ // module argument is a string literal and resolves to nothing.
445
+ if imp := child.AsImportTypeNode(); imp != nil && imp.Qualifier != nil {
446
+ g.typeRefEdge(checker, from, imp.Qualifier)
447
+ }
448
+ case shimast.KindBinaryExpression:
449
+ if target := g.assignedFunctionTarget(checker, from, child); target != "" {
450
+ g.typeRefsWithin(checker, target, child.AsBinaryExpression().Right)
451
+ }
452
+ }
453
+ g.typeRefsWithin(checker, from, child)
454
+ return false
455
+ })
456
+ }
457
+
458
+ // typeRefEdge resolves a type name to its declaration and records a type-ref
459
+ // edge, skipping an unresolved name and a self-reference.
460
+ func (g *Graph) typeRefEdge(checker *shimchecker.Checker, from string, typeName *shimast.Node) {
461
+ if typeName == nil {
462
+ return
463
+ }
464
+ target := Resolve(checker, typeName)
465
+ if target == nil || target.Symbol == nil {
466
+ return
467
+ }
468
+ to := g.ensureTargetNode(target)
469
+ if to == "" || to == from {
470
+ return
471
+ }
472
+ g.addEdgeAt(from, to, EdgeTypeRef, "", typeName.Pos(), typeName.End())
473
+ }
474
+
475
+ // ensureTargetNode returns the node id for a resolved edge target, creating the
476
+ // node when the resolution pass reached a symbol Build did not record: an
477
+ // external boundary leaf (node_modules / `.d.ts`), kept as a leaf so the graph
478
+ // stays "your code" without descending into a dependency's internals. Returns ""
479
+ // when the symbol is not a kind the graph models as a node.
480
+ func (g *Graph) ensureTargetNode(target *Target) string {
481
+ kind := symbolNodeKind(target.Symbol)
482
+ if kind == "" {
483
+ return ""
484
+ }
485
+ // A synthesized symbol without a declaration file would key a fileless ghost
486
+ // node ("#name:kind") that could collide across distinct symbols; skip it.
487
+ if target.File == "" {
488
+ return ""
489
+ }
490
+ if kind == NodeMethod {
491
+ // A method node is class-qualified and only modeled when it belongs to the
492
+ // workspace (Build recorded it). A call into a dependency's method stops at
493
+ // the boundary rather than spawning an external method leaf for every
494
+ // `.map` / `.push` into a library type.
495
+ name := methodName(target.Symbol)
496
+ if name == "" {
497
+ return ""
498
+ }
499
+ id := nodeID(target.File, name, NodeMethod)
500
+ if _, exists := g.Nodes[id]; exists {
501
+ return id
502
+ }
503
+ return ""
504
+ }
505
+ if kind == NodeVariable && target.External && target.Symbol.Flags&shimast.SymbolFlagsProperty != 0 {
506
+ return ""
507
+ }
508
+ name := qualifiedName(target.Symbol)
509
+ id := nodeID(target.File, name, kind)
510
+ if _, exists := g.Nodes[id]; exists {
511
+ return id
512
+ }
513
+ if !target.External {
514
+ // A workspace target Build did not record is a function-local or otherwise
515
+ // body-scoped declaration (Build records top-level declarations, namespace
516
+ // members, and class/interface members only). Its name is unqualified and
517
+ // position-free, so two same-named locals in different scopes would key the
518
+ // same id and merge into one node, fabricating false edges. Drop it — the
519
+ // same workspace-only discipline the NodeMethod branch already applies.
520
+ return ""
521
+ }
522
+ g.Nodes[id] = &Node{
523
+ ID: id,
524
+ Name: name,
525
+ Simple: simpleName(target.Symbol),
526
+ Kind: kind,
527
+ File: target.File,
528
+ External: true,
529
+ Pos: target.Pos,
530
+ End: target.End,
531
+ }
532
+ return id
533
+ }
534
+
535
+ // symbolNodeKind maps a resolved symbol's declarations/flags to a NodeKind, or
536
+ // "" when the symbol is not a kind the graph records as a node. Declaration kind
537
+ // wins over flags because property-like accessor symbols can otherwise be
538
+ // resolved as NodeVariable even though Build recorded the getter/setter as a
539
+ // NodeMethod.
540
+ func symbolNodeKind(symbol *shimast.Symbol) NodeKind {
541
+ for _, declaration := range symbol.Declarations {
542
+ switch declaration.Kind {
543
+ case shimast.KindClassDeclaration:
544
+ return NodeClass
545
+ case shimast.KindInterfaceDeclaration:
546
+ return NodeInterface
547
+ case shimast.KindTypeAliasDeclaration:
548
+ return NodeTypeAlias
549
+ case shimast.KindEnumDeclaration:
550
+ return NodeEnum
551
+ case shimast.KindFunctionDeclaration:
552
+ return NodeFunction
553
+ case shimast.KindMethodDeclaration, shimast.KindMethodSignature,
554
+ shimast.KindConstructor, shimast.KindGetAccessor, shimast.KindSetAccessor:
555
+ return NodeMethod
556
+ case shimast.KindPropertyDeclaration, shimast.KindPropertySignature,
557
+ shimast.KindVariableDeclaration:
558
+ return NodeVariable
559
+ }
560
+ }
561
+ switch {
562
+ case symbol.Flags&shimast.SymbolFlagsClass != 0:
563
+ return NodeClass
564
+ case symbol.Flags&shimast.SymbolFlagsInterface != 0:
565
+ return NodeInterface
566
+ case symbol.Flags&shimast.SymbolFlagsTypeAlias != 0:
567
+ return NodeTypeAlias
568
+ case symbol.Flags&shimast.SymbolFlagsEnum != 0:
569
+ return NodeEnum
570
+ case symbol.Flags&shimast.SymbolFlagsFunction != 0:
571
+ return NodeFunction
572
+ case symbol.Flags&(shimast.SymbolFlagsMethod|shimast.SymbolFlagsConstructor|shimast.SymbolFlagsGetAccessor|shimast.SymbolFlagsSetAccessor) != 0:
573
+ return NodeMethod
574
+ case symbol.Flags&shimast.SymbolFlagsProperty != 0:
575
+ return NodeVariable
576
+ case symbol.Flags&shimast.SymbolFlagsVariable != 0:
577
+ return NodeVariable
578
+ default:
579
+ return ""
580
+ }
581
+ }
@@ -0,0 +1,93 @@
1
+ package graph
2
+
3
+ import (
4
+ "path/filepath"
5
+ "testing"
6
+
7
+ "github.com/samchon/ttsc/packages/ttsc/driver"
8
+ )
9
+
10
+ // TestEdgesCoverClassLevelReferences verifies that the references living on a
11
+ // class declaration itself — not in a member — are attributed to the class
12
+ // node. The method-node split walks members one at a time, so without an
13
+ // explicit class-level pass these edges are silently dropped:
14
+ //
15
+ // - a heritage type argument `extends Base<Payload>` -> type-ref to Payload
16
+ // - a type parameter constraint `<T extends Constraint>` -> type-ref to Constraint
17
+ //
18
+ // These are exactly the relationships generic-heavy codebases are built from, so
19
+ // a top-level-only-then-per-member walk that forgot them would blind the graph
20
+ // to generic-base edges.
21
+ //
22
+ // A decorator's own factory call (`@Injectable()`) is deliberately NOT a
23
+ // value-call edge: the decoration is a fact on the node's decorators, and a
24
+ // calls edge to the decorator function would make ubiquitous decorators the
25
+ // busiest nodes in the graph. The test pins both halves of that contract.
26
+ func TestEdgesCoverClassLevelReferences(t *testing.T) {
27
+ root := t.TempDir()
28
+ writeFile(t, filepath.Join(root, "tsconfig.json"), `{
29
+ "compilerOptions": {
30
+ "target": "ES2022",
31
+ "module": "commonjs",
32
+ "strict": true,
33
+ "experimentalDecorators": true,
34
+ "rootDir": "src",
35
+ "outDir": "dist"
36
+ },
37
+ "files": ["src/main.ts"]
38
+ }
39
+ `)
40
+ writeFile(t, filepath.Join(root, "src", "main.ts"), `export function Injectable() {
41
+ return function (_target: Function): void {};
42
+ }
43
+ export class Base<T> {
44
+ value!: T;
45
+ }
46
+ export interface Payload {}
47
+ export interface Constraint {}
48
+
49
+ @Injectable()
50
+ export class Service<T extends Constraint> extends Base<Payload> {}
51
+ `)
52
+
53
+ prog, diags, err := driver.LoadProgram(root, "tsconfig.json", driver.LoadProgramOptions{})
54
+ if err != nil {
55
+ t.Fatal(err)
56
+ }
57
+ if len(diags) != 0 {
58
+ t.Fatalf("unexpected diagnostics: %v", diags)
59
+ }
60
+ defer func() { _ = prog.Close() }()
61
+
62
+ graph := Build(prog)
63
+ path := sourceFile(t, prog, "main.ts").FileName()
64
+
65
+ service := nodeID(path, "Service", NodeClass)
66
+ injectable := nodeID(path, "Injectable", NodeFunction)
67
+ base := nodeID(path, "Base", NodeClass)
68
+ payload := nodeID(path, "Payload", NodeInterface)
69
+ constraint := nodeID(path, "Constraint", NodeInterface)
70
+
71
+ // The decorator factory call is a fact, not an edge: no value-call to
72
+ // Injectable, but a recorded decorator on the Service node.
73
+ if hasEdge(graph, service, injectable, EdgeValueCall) {
74
+ t.Errorf("decorator factory call leaked a value-call edge Service -> Injectable")
75
+ }
76
+ if !hasDecorator(graph, service, "Injectable") {
77
+ t.Errorf("missing decorator fact @Injectable on Service")
78
+ }
79
+ if !hasEdge(graph, service, payload, EdgeTypeRef) {
80
+ t.Errorf("missing type-ref edge Service -> Payload (heritage type argument)")
81
+ }
82
+ if !hasEdge(graph, service, constraint, EdgeTypeRef) {
83
+ t.Errorf("missing type-ref edge Service -> Constraint (type parameter constraint)")
84
+ }
85
+ // The base expression itself stays a heritage edge, unaffected by the
86
+ // class-level type-argument walk.
87
+ if !hasEdge(graph, service, base, EdgeHeritage) {
88
+ t.Errorf("missing heritage edge Service -> Base")
89
+ }
90
+ if t.Failed() {
91
+ t.Logf("edges: %v", graph.Edges)
92
+ }
93
+ }