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,538 @@
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.KindJsxSelfClosingElement:
308
+ // `<Component />` is a use of the component; an intrinsic tag (`<div />`)
309
+ // resolves to nothing and is dropped by callEdge.
310
+ if jsx := child.AsJsxSelfClosingElement(); jsx != nil && jsx.TagName != nil {
311
+ g.callEdge(checker, from, jsx.TagName, "jsx")
312
+ }
313
+ case shimast.KindJsxOpeningElement:
314
+ if jsx := child.AsJsxOpeningElement(); jsx != nil && jsx.TagName != nil {
315
+ g.callEdge(checker, from, jsx.TagName, "jsx")
316
+ }
317
+ }
318
+ g.callsWithin(checker, from, child)
319
+ return false
320
+ })
321
+ }
322
+
323
+ func isInvokedAccess(access *shimast.Node) bool {
324
+ parent := access.Parent
325
+ if parent == nil {
326
+ return false
327
+ }
328
+ switch parent.Kind {
329
+ case shimast.KindCallExpression:
330
+ call := parent.AsCallExpression()
331
+ return call != nil && call.Expression == access
332
+ case shimast.KindNewExpression:
333
+ newExpr := parent.AsNewExpression()
334
+ return newExpr != nil && newExpr.Expression == access
335
+ case shimast.KindTaggedTemplateExpression:
336
+ tagged := parent.AsTaggedTemplateExpression()
337
+ return tagged != nil && tagged.Tag == access
338
+ default:
339
+ return false
340
+ }
341
+ }
342
+
343
+ // callEdge resolves a callee expression to its declaration and records a
344
+ // value-call edge, skipping an unresolved callee and a self-call. origin records
345
+ // the call form ("call", "new", "jsx", "tagged") so the dump can split it into
346
+ // the schema's calls / instantiates / renders kinds.
347
+ func (g *Graph) callEdge(checker *shimchecker.Checker, from string, callee *shimast.Node, origin string) {
348
+ g.valueUseEdge(checker, from, callee, EdgeValueCall, origin)
349
+ }
350
+
351
+ // accessEdge resolves a property or element access to its declaration and
352
+ // records a value-access edge, skipping unresolved/external/self targets.
353
+ func (g *Graph) accessEdge(checker *shimchecker.Checker, from string, access *shimast.Node) {
354
+ g.valueUseEdge(checker, from, access, EdgeValueAccess, "")
355
+ }
356
+
357
+ func (g *Graph) valueUseEdge(checker *shimchecker.Checker, from string, targetExpr *shimast.Node, kind EdgeKind, origin string) {
358
+ if targetExpr == nil {
359
+ return
360
+ }
361
+ target := Resolve(checker, targetExpr)
362
+ if target == nil || target.Symbol == nil {
363
+ return
364
+ }
365
+ to := g.ensureTargetNode(target)
366
+ if to == "" || to == from {
367
+ return
368
+ }
369
+ g.addEdgeAt(from, to, kind, origin, targetExpr.Pos(), targetExpr.End())
370
+ }
371
+
372
+ // collectTypeRefs records a type-ref edge from each top-level function, class,
373
+ // interface, or type alias to every named type it references in a type position
374
+ // (parameter, return, property, and alias right-hand-side types). Type
375
+ // references are first-class edges, which fits the ttsc thesis that types are
376
+ // the unit of truth: an `import type` or annotation-only dependency relates two
377
+ // symbols without any runtime call.
378
+ func (g *Graph) collectTypeRefs(checker *shimchecker.Checker, file *shimast.SourceFile) {
379
+ forEachContainer(file.FileName(), file, func(from string, node *shimast.Node) {
380
+ g.typeRefsWithin(checker, from, node)
381
+ })
382
+ }
383
+
384
+ // typeRefsWithin walks node's subtree and records a type-ref edge from `from` to
385
+ // the resolved target of every type reference it finds. A plain named type is a
386
+ // KindTypeReference; the two other type-position shapes that name a symbol are a
387
+ // `typeof value` query and an `import("./m").Foo` type, whose name is an
388
+ // EntityName rather than a TypeReference, so each is matched explicitly. A
389
+ // surrounding `as` / `satisfies` expression needs no case of its own: the type
390
+ // it carries is itself one of these nodes, which the recursion reaches.
391
+ func (g *Graph) typeRefsWithin(checker *shimchecker.Checker, from string, node *shimast.Node) {
392
+ node.ForEachChild(func(child *shimast.Node) bool {
393
+ switch child.Kind {
394
+ case shimast.KindTypeReference:
395
+ if ref := child.AsTypeReferenceNode(); ref != nil && ref.TypeName != nil {
396
+ g.typeRefEdge(checker, from, ref.TypeName)
397
+ }
398
+ case shimast.KindTypeQuery:
399
+ // `typeof value` in a type position depends on that value's type.
400
+ if query := child.AsTypeQueryNode(); query != nil && query.ExprName != nil {
401
+ g.typeRefEdge(checker, from, query.ExprName)
402
+ }
403
+ case shimast.KindImportType:
404
+ // `import("./m").Foo` references Foo through a dynamic import type; the
405
+ // module argument is a string literal and resolves to nothing.
406
+ if imp := child.AsImportTypeNode(); imp != nil && imp.Qualifier != nil {
407
+ g.typeRefEdge(checker, from, imp.Qualifier)
408
+ }
409
+ }
410
+ g.typeRefsWithin(checker, from, child)
411
+ return false
412
+ })
413
+ }
414
+
415
+ // typeRefEdge resolves a type name to its declaration and records a type-ref
416
+ // edge, skipping an unresolved name and a self-reference.
417
+ func (g *Graph) typeRefEdge(checker *shimchecker.Checker, from string, typeName *shimast.Node) {
418
+ if typeName == nil {
419
+ return
420
+ }
421
+ target := Resolve(checker, typeName)
422
+ if target == nil || target.Symbol == nil {
423
+ return
424
+ }
425
+ to := g.ensureTargetNode(target)
426
+ if to == "" || to == from {
427
+ return
428
+ }
429
+ g.addEdgeAt(from, to, EdgeTypeRef, "", typeName.Pos(), typeName.End())
430
+ }
431
+
432
+ // ensureTargetNode returns the node id for a resolved edge target, creating the
433
+ // node when the resolution pass reached a symbol Build did not record: an
434
+ // external boundary leaf (node_modules / `.d.ts`), kept as a leaf so the graph
435
+ // stays "your code" without descending into a dependency's internals. Returns ""
436
+ // when the symbol is not a kind the graph models as a node.
437
+ func (g *Graph) ensureTargetNode(target *Target) string {
438
+ kind := symbolNodeKind(target.Symbol)
439
+ if kind == "" {
440
+ return ""
441
+ }
442
+ // A synthesized symbol without a declaration file would key a fileless ghost
443
+ // node ("#name:kind") that could collide across distinct symbols; skip it.
444
+ if target.File == "" {
445
+ return ""
446
+ }
447
+ if kind == NodeMethod {
448
+ // A method node is class-qualified and only modeled when it belongs to the
449
+ // workspace (Build recorded it). A call into a dependency's method stops at
450
+ // the boundary rather than spawning an external method leaf for every
451
+ // `.map` / `.push` into a library type.
452
+ name := methodName(target.Symbol)
453
+ if name == "" {
454
+ return ""
455
+ }
456
+ id := nodeID(target.File, name, NodeMethod)
457
+ if _, exists := g.Nodes[id]; exists {
458
+ return id
459
+ }
460
+ return ""
461
+ }
462
+ if kind == NodeVariable && target.External && target.Symbol.Flags&shimast.SymbolFlagsProperty != 0 {
463
+ return ""
464
+ }
465
+ name := qualifiedName(target.Symbol)
466
+ id := nodeID(target.File, name, kind)
467
+ if _, exists := g.Nodes[id]; exists {
468
+ return id
469
+ }
470
+ if !target.External {
471
+ // A workspace target Build did not record is a function-local or otherwise
472
+ // body-scoped declaration (Build records top-level declarations, namespace
473
+ // members, and class/interface members only). Its name is unqualified and
474
+ // position-free, so two same-named locals in different scopes would key the
475
+ // same id and merge into one node, fabricating false edges. Drop it — the
476
+ // same workspace-only discipline the NodeMethod branch already applies.
477
+ return ""
478
+ }
479
+ g.Nodes[id] = &Node{
480
+ ID: id,
481
+ Name: name,
482
+ Simple: simpleName(target.Symbol),
483
+ Kind: kind,
484
+ File: target.File,
485
+ External: true,
486
+ Pos: target.Pos,
487
+ End: target.End,
488
+ }
489
+ return id
490
+ }
491
+
492
+ // symbolNodeKind maps a resolved symbol's declarations/flags to a NodeKind, or
493
+ // "" when the symbol is not a kind the graph records as a node. Declaration kind
494
+ // wins over flags because property-like accessor symbols can otherwise be
495
+ // resolved as NodeVariable even though Build recorded the getter/setter as a
496
+ // NodeMethod.
497
+ func symbolNodeKind(symbol *shimast.Symbol) NodeKind {
498
+ for _, declaration := range symbol.Declarations {
499
+ switch declaration.Kind {
500
+ case shimast.KindClassDeclaration:
501
+ return NodeClass
502
+ case shimast.KindInterfaceDeclaration:
503
+ return NodeInterface
504
+ case shimast.KindTypeAliasDeclaration:
505
+ return NodeTypeAlias
506
+ case shimast.KindEnumDeclaration:
507
+ return NodeEnum
508
+ case shimast.KindFunctionDeclaration:
509
+ return NodeFunction
510
+ case shimast.KindMethodDeclaration, shimast.KindMethodSignature,
511
+ shimast.KindConstructor, shimast.KindGetAccessor, shimast.KindSetAccessor:
512
+ return NodeMethod
513
+ case shimast.KindPropertyDeclaration, shimast.KindPropertySignature,
514
+ shimast.KindVariableDeclaration:
515
+ return NodeVariable
516
+ }
517
+ }
518
+ switch {
519
+ case symbol.Flags&shimast.SymbolFlagsClass != 0:
520
+ return NodeClass
521
+ case symbol.Flags&shimast.SymbolFlagsInterface != 0:
522
+ return NodeInterface
523
+ case symbol.Flags&shimast.SymbolFlagsTypeAlias != 0:
524
+ return NodeTypeAlias
525
+ case symbol.Flags&shimast.SymbolFlagsEnum != 0:
526
+ return NodeEnum
527
+ case symbol.Flags&shimast.SymbolFlagsFunction != 0:
528
+ return NodeFunction
529
+ case symbol.Flags&(shimast.SymbolFlagsMethod|shimast.SymbolFlagsConstructor|shimast.SymbolFlagsGetAccessor|shimast.SymbolFlagsSetAccessor) != 0:
530
+ return NodeMethod
531
+ case symbol.Flags&shimast.SymbolFlagsProperty != 0:
532
+ return NodeVariable
533
+ case symbol.Flags&shimast.SymbolFlagsVariable != 0:
534
+ return NodeVariable
535
+ default:
536
+ return ""
537
+ }
538
+ }
@@ -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
+ }
@@ -0,0 +1,63 @@
1
+ package graph
2
+
3
+ import (
4
+ "path/filepath"
5
+ "testing"
6
+
7
+ "github.com/samchon/ttsc/packages/ttsc/driver"
8
+ )
9
+
10
+ // TestEnumInitializerCallsAreEdges pins that a value-call inside a (non-const)
11
+ // enum member initializer is recorded as an edge from the enum node. build.go
12
+ // records the enum as a node, but the edge pass must also walk the enum body, or
13
+ // the call in `A = base()` is silently dropped — the gap a round-1 reviewer found.
14
+ func TestEnumInitializerCallsAreEdges(t *testing.T) {
15
+ root := t.TempDir()
16
+ writeFile(t, filepath.Join(root, "tsconfig.json"), `{
17
+ "compilerOptions": {
18
+ "target": "ES2022",
19
+ "module": "commonjs",
20
+ "strict": true,
21
+ "rootDir": "src",
22
+ "outDir": "dist"
23
+ },
24
+ "files": ["src/main.ts"]
25
+ }
26
+ `)
27
+ writeFile(t, filepath.Join(root, "src", "main.ts"), `export function base(): number {
28
+ return 1;
29
+ }
30
+
31
+ export enum E {
32
+ A = base(),
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
+ enumID := nodeID(path, "E", NodeEnum)
48
+ baseID := nodeID(path, "base", NodeFunction)
49
+
50
+ if _, ok := graph.Nodes[enumID]; !ok {
51
+ t.Fatalf("enum E was not recorded as a node")
52
+ }
53
+ found := false
54
+ for _, edge := range graph.Edges {
55
+ if edge.From == enumID && edge.To == baseID && edge.Kind == EdgeValueCall {
56
+ found = true
57
+ break
58
+ }
59
+ }
60
+ if !found {
61
+ t.Fatalf("no value-call edge from enum E to base(); enum initializer call was dropped")
62
+ }
63
+ }