ttsc 0.16.2 → 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 (81) 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/lib/plugin/internal/loadProjectPlugins.js +17 -3
  67. package/lib/plugin/internal/loadProjectPlugins.js.map +1 -1
  68. package/lib/structures/ITtscPlugin.d.ts +7 -5
  69. package/lib/structures/ITtscPluginFactoryContext.d.ts +24 -1
  70. package/package.json +8 -8
  71. package/shim/ast/shim.go +6 -0
  72. package/shim/checker/shim.go +22 -12
  73. package/src/TtscService.ts +94 -0
  74. package/src/compiler/internal/residentTransformProcess.ts +216 -0
  75. package/src/compiler/internal/startResidentTransform.ts +126 -0
  76. package/src/index.ts +1 -0
  77. package/src/plugin/internal/loadProjectPlugins.ts +17 -3
  78. package/src/structures/ITtscPlugin.ts +7 -5
  79. package/src/structures/ITtscPluginFactoryContext.ts +26 -1
  80. package/utility/host.go +8 -2
  81. package/utility/serve.go +158 -0
@@ -28,6 +28,8 @@ func run(args []string) int {
28
28
  return utility.RunCheck(args[1:])
29
29
  case "transform":
30
30
  return utility.RunTransform(args[1:])
31
+ case "serve":
32
+ return utility.RunServe(os.Stdin, os.Stdout, args[1:])
31
33
  case "-h", "--help", "help":
32
34
  printHelp()
33
35
  return 0
@@ -45,5 +47,6 @@ func printHelp() {
45
47
  Usage:
46
48
  utility-host build --tsconfig=tsconfig.json --plugins-json='[...]'
47
49
  utility-host check --tsconfig=tsconfig.json --plugins-json='[...]'
48
- utility-host transform --tsconfig=tsconfig.json --plugins-json='[...]'`)
50
+ utility-host transform --tsconfig=tsconfig.json --plugins-json='[...]'
51
+ utility-host serve --tsconfig=tsconfig.json --plugins-json='[...]'`)
49
52
  }
@@ -0,0 +1,64 @@
1
+ package driver
2
+
3
+ import (
4
+ shimtspath "github.com/microsoft/typescript-go/shim/tspath"
5
+ "github.com/microsoft/typescript-go/shim/vfs"
6
+ )
7
+
8
+ // OverlayFS layers in-memory file contents over an inner filesystem. Overridden
9
+ // paths return the in-memory text from ReadFile and report present from
10
+ // FileExists; every other operation delegates to the inner FS. A resident
11
+ // Session or serve host uses this to feed a caller's unsaved buffer (for example
12
+ // Metro's per-file src, or an editor's edited file) to the compiler without
13
+ // writing to disk.
14
+ type OverlayFS struct {
15
+ vfs.FS
16
+ caseSensitive bool
17
+ overrides map[string]string
18
+ }
19
+
20
+ // NewOverlayFS returns an overlay over inner with no overrides set.
21
+ func NewOverlayFS(inner vfs.FS) *OverlayFS {
22
+ return &OverlayFS{
23
+ FS: inner,
24
+ caseSensitive: inner.UseCaseSensitiveFileNames(),
25
+ overrides: map[string]string{},
26
+ }
27
+ }
28
+
29
+ // key normalizes a path the same way the compiler keys its files, so an
30
+ // override registered for one spelling is found however the host asks for it.
31
+ func (o *OverlayFS) key(path string) string {
32
+ return shimtspath.GetCanonicalFileName(shimtspath.NormalizePath(path), o.caseSensitive)
33
+ }
34
+
35
+ // Set records an in-memory override for path.
36
+ func (o *OverlayFS) Set(path, content string) {
37
+ o.overrides[o.key(path)] = content
38
+ }
39
+
40
+ // Get returns the in-memory override for path, if one is set.
41
+ func (o *OverlayFS) Get(path string) (string, bool) {
42
+ content, ok := o.overrides[o.key(path)]
43
+ return content, ok
44
+ }
45
+
46
+ // Unset removes the in-memory override for path, so reads fall back to the
47
+ // inner filesystem.
48
+ func (o *OverlayFS) Unset(path string) {
49
+ delete(o.overrides, o.key(path))
50
+ }
51
+
52
+ func (o *OverlayFS) ReadFile(path string) (string, bool) {
53
+ if content, ok := o.overrides[o.key(path)]; ok {
54
+ return content, true
55
+ }
56
+ return o.FS.ReadFile(path)
57
+ }
58
+
59
+ func (o *OverlayFS) FileExists(path string) bool {
60
+ if _, ok := o.overrides[o.key(path)]; ok {
61
+ return true
62
+ }
63
+ return o.FS.FileExists(path)
64
+ }
package/driver/program.go CHANGED
@@ -215,6 +215,10 @@ type LoadProgramOptions struct {
215
215
  // TypeScript-Go's own command-line parser into a CompilerOptions overlay
216
216
  // that wins over the tsconfig, exactly as `tsgo`'s CLI merges them.
217
217
  TsgoArgs []string
218
+ // FS overrides the filesystem the program is built on. When nil, DefaultFS
219
+ // is used. A resident Session passes an overlay FS so in-memory edits stay
220
+ // visible to the program and to incremental UpdateProgram calls.
221
+ FS vfs.FS
218
222
  }
219
223
 
220
224
  // Close releases the checker pool lease acquired by LoadProgram.
@@ -342,7 +346,10 @@ func LoadProgram(cwd, tsconfigPath string, options LoadProgramOptions) (*Program
342
346
  if preamble != "" {
343
347
  options.SourcePreamble += preamble
344
348
  }
345
- fs := DefaultFS()
349
+ fs := options.FS
350
+ if fs == nil {
351
+ fs = DefaultFS()
352
+ }
346
353
  if options.SourcePreamble != "" {
347
354
  fs = sourcePreambleFS{
348
355
  FS: fs,
@@ -0,0 +1,85 @@
1
+ package driver
2
+
3
+ import (
4
+ shimtspath "github.com/microsoft/typescript-go/shim/tspath"
5
+ )
6
+
7
+ // Session is a resident compiler host for incremental type-checking: it keeps a
8
+ // loaded program alive and re-parses only the changed file on each edit (reusing
9
+ // the unchanged ASTs and the pinned checker through UpdateProgram), instead of
10
+ // recompiling the whole project per request.
11
+ //
12
+ // It is the driver-level incremental type-check primitive. The resident
13
+ // transform path (utility-host `serve`) deliberately does not use it: the
14
+ // linked-plugin pass mutates source ASTs in place, so a transform cannot reuse a
15
+ // warm clean program and must rebuild a fresh one per edit. Session therefore
16
+ // provides type-check reuse, not transform reuse (samchon/ttsc#255).
17
+ //
18
+ // Construct one per project (cwd absolute), feed file edits through Apply, and
19
+ // read the resident program's source through SourceText. Apply reuses the
20
+ // existing program when the edited file's import/reference graph is unchanged.
21
+ type Session struct {
22
+ cwd string
23
+ overlay *OverlayFS
24
+ prog *Program
25
+ }
26
+
27
+ // NewSession loads the project over an overlay filesystem and keeps the
28
+ // resulting program resident. cwd must be absolute; tsconfig may be relative.
29
+ func NewSession(cwd, tsconfig string, options LoadProgramOptions) (*Session, []Diagnostic, error) {
30
+ overlay := NewOverlayFS(DefaultFS())
31
+ options.FS = overlay
32
+ prog, diags, err := LoadProgram(cwd, tsconfig, options)
33
+ if err != nil {
34
+ return nil, diags, err
35
+ }
36
+ if prog == nil {
37
+ return nil, diags, nil
38
+ }
39
+ return &Session{cwd: cwd, overlay: overlay, prog: prog}, nil, nil
40
+ }
41
+
42
+ // Apply sets the in-memory content of one file and incrementally updates the
43
+ // resident program. It returns whether the update reused the existing program
44
+ // (true) or had to rebuild it because the file's import/reference graph changed
45
+ // (false).
46
+ func (s *Session) Apply(absPath, content string) bool {
47
+ s.overlay.Set(absPath, content)
48
+ name := absPath
49
+ if file := s.prog.SourceFile(absPath); file != nil {
50
+ name = file.FileName()
51
+ }
52
+ changed := shimtspath.ToPath(name, s.cwd, s.overlay.caseSensitive)
53
+ newHost := DefaultHost(s.cwd, s.overlay)
54
+ newProg, reused := s.prog.TSProgram.UpdateProgram(changed, newHost, nil)
55
+ if newProg != nil {
56
+ s.prog.TSProgram = newProg
57
+ s.prog.Host = newHost
58
+ }
59
+ return reused
60
+ }
61
+
62
+ // Program returns the resident program, reflecting every Apply so far. The
63
+ // handle changes when an edit reshapes the import graph, so callers should read
64
+ // it after Apply rather than caching it across edits.
65
+ func (s *Session) Program() *Program {
66
+ return s.prog
67
+ }
68
+
69
+ // SourceText returns the source text the resident program currently holds for
70
+ // absPath, or ("", false) when the program has no such file.
71
+ func (s *Session) SourceText(absPath string) (string, bool) {
72
+ file := s.prog.SourceFile(absPath)
73
+ if file == nil {
74
+ return "", false
75
+ }
76
+ return file.Text(), true
77
+ }
78
+
79
+ // Close releases the resident program's resources.
80
+ func (s *Session) Close() error {
81
+ if s.prog != nil {
82
+ return s.prog.Close()
83
+ }
84
+ return nil
85
+ }
@@ -0,0 +1,330 @@
1
+ package graph
2
+
3
+ import (
4
+ "strings"
5
+
6
+ shimast "github.com/microsoft/typescript-go/shim/ast"
7
+
8
+ "github.com/samchon/ttsc/packages/ttsc/driver"
9
+ )
10
+
11
+ // Build walks the program's user-authored source files and records a node for
12
+ // each top-level declaration. driver.SourceFiles already drops declaration files
13
+ // and the program never compiles a dependency's `.ts`, so every node Build emits
14
+ // is workspace source: External is false. External boundary leaves enter the
15
+ // graph only as the resolved target of an edge (see Resolve).
16
+ func Build(prog *driver.Program) *Graph {
17
+ g := &Graph{
18
+ Nodes: map[string]*Node{},
19
+ bodyNodes: map[string]bool{},
20
+ seen: map[string]struct{}{},
21
+ }
22
+ for _, file := range prog.SourceFiles() {
23
+ collectDeclarations(g, file)
24
+ }
25
+ g.addEdges(prog)
26
+ return g
27
+ }
28
+
29
+ // SourceTexts maps each program source file to its text — the evidence input
30
+ // NewDump needs to turn a node or edge byte span into a line/column.
31
+ func SourceTexts(prog *driver.Program) map[string]string {
32
+ files := prog.SourceFiles()
33
+ out := make(map[string]string, len(files))
34
+ for _, file := range files {
35
+ out[file.FileName()] = file.Text()
36
+ }
37
+ return out
38
+ }
39
+
40
+ // collectDeclarations records a node for each declaration statement in file,
41
+ // plus a method node for each callable member of a class or interface, so
42
+ // method-to-method calls have both endpoints. It descends into namespace bodies
43
+ // so a `namespace X { … }` member is a node too, keyed by its namespace-qualified
44
+ // name. This pass establishes the symbol nodes that cross-file edges connect.
45
+ func collectDeclarations(g *Graph, file *shimast.SourceFile) {
46
+ if file.Statements == nil {
47
+ return
48
+ }
49
+ collectStatements(g, file.FileName(), file.Statements.Nodes)
50
+ }
51
+
52
+ // collectStatements records the nodes for a statement list — the file's top
53
+ // level, or the body of a namespace it recurses into. A member's id is built
54
+ // from its symbol, which already carries the enclosing namespace in its parent
55
+ // chain, so a node recorded here and an edge target resolved later agree without
56
+ // the walk having to thread the namespace name through.
57
+ func collectStatements(g *Graph, path string, statements []*shimast.Node) {
58
+ for _, statement := range statements {
59
+ switch statement.Kind {
60
+ case shimast.KindFunctionDeclaration:
61
+ addNode(g, path, statement, NodeFunction)
62
+ case shimast.KindClassDeclaration:
63
+ addNode(g, path, statement, NodeClass)
64
+ collectMembers(g, path, statement)
65
+ case shimast.KindInterfaceDeclaration:
66
+ addNode(g, path, statement, NodeInterface)
67
+ collectMembers(g, path, statement)
68
+ case shimast.KindTypeAliasDeclaration:
69
+ addNode(g, path, statement, NodeTypeAlias)
70
+ case shimast.KindEnumDeclaration:
71
+ addNode(g, path, statement, NodeEnum)
72
+ case shimast.KindVariableStatement:
73
+ collectVariables(g, path, statement)
74
+ case shimast.KindModuleDeclaration:
75
+ // `namespace X { … }` — its members are declarations in their own right,
76
+ // so recurse into the body. The namespace itself is a grouping container,
77
+ // not a referenceable symbol the graph models as a node.
78
+ collectStatements(g, path, moduleStatements(statement))
79
+ }
80
+ }
81
+ }
82
+
83
+ // collectVariables records a variable node for each binding in a top-level
84
+ // variable statement (both bindings of `const a = 1, b = 2`).
85
+ func collectVariables(g *Graph, path string, statement *shimast.Node) {
86
+ variables := statement.AsVariableStatement()
87
+ if variables == nil || variables.DeclarationList == nil {
88
+ return
89
+ }
90
+ list := variables.DeclarationList.AsVariableDeclarationList()
91
+ if list == nil || list.Declarations == nil {
92
+ return
93
+ }
94
+ for _, binding := range list.Declarations.Nodes {
95
+ addNode(g, path, binding, NodeVariable)
96
+ }
97
+ }
98
+
99
+ // addNode records a node for the symbol declared by node under its
100
+ // position-invariant id. A declaration the checker did not bind to a single
101
+ // named symbol (a destructuring pattern) is skipped. Redeclarations keep the
102
+ // first node except callable overload sets, where the implementation body
103
+ // replaces earlier signature-only declarations so graph answers show executable
104
+ // code instead of just the overload header.
105
+ func addNode(g *Graph, path string, node *shimast.Node, kind NodeKind) {
106
+ symbol := node.Symbol()
107
+ if symbol == nil || symbol.Name == "" {
108
+ return
109
+ }
110
+ putDeclaredNode(g, path, qualifiedName(symbol), kind, node)
111
+ }
112
+
113
+ // collectMembers records callable members (method, constructor, accessor) and
114
+ // property members of a class or interface declaration, keyed by their
115
+ // class-qualified names so resolved member references land on the same node.
116
+ func collectMembers(g *Graph, path string, statement *shimast.Node) {
117
+ for _, member := range classMembers(statement) {
118
+ name := methodName(member.Symbol())
119
+ if name == "" {
120
+ continue
121
+ }
122
+ switch {
123
+ case isMethodMember(member.Kind):
124
+ putDeclaredNode(g, path, name, NodeMethod, member)
125
+ case isPropertyMember(member.Kind):
126
+ putDeclaredNode(g, path, name, NodeVariable, member)
127
+ }
128
+ }
129
+ }
130
+
131
+ func putDeclaredNode(g *Graph, path, name string, kind NodeKind, declaration *shimast.Node) {
132
+ id := nodeID(path, name, kind)
133
+ hasBody := declarationHasImplementation(declaration, kind)
134
+ if _, exists := g.Nodes[id]; exists {
135
+ if !hasBody || g.bodyNodes[id] {
136
+ return
137
+ }
138
+ }
139
+ g.Nodes[id] = &Node{
140
+ ID: id,
141
+ Name: name,
142
+ Simple: simpleName(declaration.Symbol()),
143
+ Kind: kind,
144
+ File: path,
145
+ Pos: declaration.Pos(),
146
+ End: declaration.End(),
147
+ Modifiers: declarationModifiers(declaration),
148
+ }
149
+ g.bodyNodes[id] = hasBody
150
+ }
151
+
152
+ // declarationModifiers maps a declaration's combined modifier flags onto the
153
+ // wire-string subset of the TtscGraphNodeModifier union. Only flags with an
154
+ // exact union member are emitted, in a stable order; an unknown string would
155
+ // break the TypeScript-side typia.assert on the dump. It returns nil when the
156
+ // declaration carries no recorded modifier.
157
+ func declarationModifiers(declaration *shimast.Node) []string {
158
+ flags := shimast.GetCombinedModifierFlags(declaration)
159
+ if flags == shimast.ModifierFlagsNone {
160
+ return nil
161
+ }
162
+ var modifiers []string
163
+ for _, m := range modifierFlagStrings {
164
+ if flags&m.flag != 0 {
165
+ modifiers = append(modifiers, m.text)
166
+ }
167
+ }
168
+ return modifiers
169
+ }
170
+
171
+ // modifierFlagStrings is the ordered flag-to-wire-string table
172
+ // declarationModifiers walks. The order fixes the emitted sequence so a dump is
173
+ // deterministic; ModifierFlagsAmbient is the `declare` keyword.
174
+ var modifierFlagStrings = []struct {
175
+ flag shimast.ModifierFlags
176
+ text string
177
+ }{
178
+ {shimast.ModifierFlagsExport, "export"},
179
+ {shimast.ModifierFlagsDefault, "default"},
180
+ {shimast.ModifierFlagsAmbient, "declare"},
181
+ {shimast.ModifierFlagsAbstract, "abstract"},
182
+ {shimast.ModifierFlagsStatic, "static"},
183
+ {shimast.ModifierFlagsReadonly, "readonly"},
184
+ {shimast.ModifierFlagsAsync, "async"},
185
+ {shimast.ModifierFlagsConst, "const"},
186
+ {shimast.ModifierFlagsPublic, "public"},
187
+ {shimast.ModifierFlagsPrivate, "private"},
188
+ {shimast.ModifierFlagsProtected, "protected"},
189
+ }
190
+
191
+ func declarationHasImplementation(declaration *shimast.Node, kind NodeKind) bool {
192
+ switch kind {
193
+ case NodeFunction, NodeMethod:
194
+ return declaration.Body() != nil
195
+ default:
196
+ return false
197
+ }
198
+ }
199
+
200
+ // classMembers returns the member nodes of a class or interface declaration, or
201
+ // nil for anything else.
202
+ func classMembers(statement *shimast.Node) []*shimast.Node {
203
+ switch statement.Kind {
204
+ case shimast.KindClassDeclaration:
205
+ if decl := statement.AsClassDeclaration(); decl != nil && decl.Members != nil {
206
+ return decl.Members.Nodes
207
+ }
208
+ case shimast.KindInterfaceDeclaration:
209
+ if decl := statement.AsInterfaceDeclaration(); decl != nil && decl.Members != nil {
210
+ return decl.Members.Nodes
211
+ }
212
+ }
213
+ return nil
214
+ }
215
+
216
+ // isMethodMember reports whether a class/interface member kind is a callable the
217
+ // graph models as a method node.
218
+ func isMethodMember(kind shimast.Kind) bool {
219
+ switch kind {
220
+ case shimast.KindMethodDeclaration, shimast.KindMethodSignature,
221
+ shimast.KindConstructor, shimast.KindGetAccessor, shimast.KindSetAccessor:
222
+ return true
223
+ default:
224
+ return false
225
+ }
226
+ }
227
+
228
+ func isPropertyMember(kind shimast.Kind) bool {
229
+ switch kind {
230
+ case shimast.KindPropertyDeclaration, shimast.KindPropertySignature:
231
+ return true
232
+ default:
233
+ return false
234
+ }
235
+ }
236
+
237
+ // methodName returns the qualified, printable name of a method symbol
238
+ // ("Class.method", or "Namespace.Class.method" for a method of a namespaced
239
+ // class), or "" when it has no named parent (a synthesized member).
240
+ // symbol.Parent is the class/interface symbol, set by the binder for every
241
+ // member.
242
+ func methodName(symbol *shimast.Symbol) string {
243
+ if symbol == nil || symbol.Name == "" || symbol.Parent == nil || symbol.Parent.Name == "" {
244
+ return ""
245
+ }
246
+ return qualifiedName(symbol)
247
+ }
248
+
249
+ // simpleName is the unqualified declared name of a symbol with no owner prefix,
250
+ // the same form qualifiedName uses for the trailing member. A constructor's
251
+ // internal-name prefix (\xFE) is escaped to "__" so the two agree.
252
+ func simpleName(symbol *shimast.Symbol) string {
253
+ if symbol == nil || symbol.Name == "" {
254
+ return ""
255
+ }
256
+ return strings.ReplaceAll(symbol.Name, "\xFE", "__")
257
+ }
258
+
259
+ // qualifiedName is the identity name of a symbol: its own name, prefixed by the
260
+ // dotted chain of every enclosing namespace and declaring class or interface. A
261
+ // declaration at a module's top level has no such container, so its name is
262
+ // returned unchanged, which keeps every existing top-level node id stable. A
263
+ // constructor's internal-name prefix (\xFE) is escaped to "__".
264
+ func qualifiedName(symbol *shimast.Symbol) string {
265
+ name := simpleName(symbol)
266
+ if name == "" {
267
+ return ""
268
+ }
269
+ if prefix := containerPrefix(symbol); prefix != "" {
270
+ return prefix + "." + name
271
+ }
272
+ return name
273
+ }
274
+
275
+ // containerPrefix returns the qualified name of symbol's enclosing namespace or
276
+ // declaring class/interface, or "" at a module's top level. The source-file
277
+ // module symbol is not a namespace — its declaration is the file, not a
278
+ // `namespace` block — so a top-level declaration gets no prefix.
279
+ func containerPrefix(symbol *shimast.Symbol) string {
280
+ parent := symbol.Parent
281
+ if parent == nil || parent.Name == "" {
282
+ return ""
283
+ }
284
+ if isNamespaceSymbol(parent) || isTypeContainerSymbol(parent) {
285
+ return qualifiedName(parent)
286
+ }
287
+ return ""
288
+ }
289
+
290
+ // isNamespaceSymbol reports whether symbol is declared by a `namespace` / `module`
291
+ // block, the container whose members the graph qualifies by name. A string-named
292
+ // ambient module (`declare module "x"`) and the `global` augmentation scope are
293
+ // also module declarations, but qualifying members by their quoted or internal
294
+ // names would produce malformed ids, so they are excluded.
295
+ func isNamespaceSymbol(symbol *shimast.Symbol) bool {
296
+ if strings.HasPrefix(symbol.Name, "\"") || strings.Contains(symbol.Name, "\xFE") {
297
+ return false
298
+ }
299
+ for _, declaration := range symbol.Declarations {
300
+ if declaration.Kind == shimast.KindModuleDeclaration {
301
+ return true
302
+ }
303
+ }
304
+ return false
305
+ }
306
+
307
+ // isTypeContainerSymbol reports whether symbol is a class or interface, whose
308
+ // members the graph qualifies ("Class.method").
309
+ func isTypeContainerSymbol(symbol *shimast.Symbol) bool {
310
+ return symbol.Flags&(shimast.SymbolFlagsClass|shimast.SymbolFlagsInterface) != 0
311
+ }
312
+
313
+ // moduleStatements returns the member statements inside a namespace/module body,
314
+ // or nil. `namespace A.B { … }` nests B's module declaration as A's body rather
315
+ // than a block, so descend through any chained module declarations to reach the
316
+ // block.
317
+ func moduleStatements(statement *shimast.Node) []*shimast.Node {
318
+ body := statement.Body()
319
+ for body != nil && body.Kind == shimast.KindModuleDeclaration {
320
+ body = body.Body()
321
+ }
322
+ if body == nil || body.Kind != shimast.KindModuleBlock {
323
+ return nil
324
+ }
325
+ block := body.AsModuleBlock()
326
+ if block == nil || block.Statements == nil {
327
+ return nil
328
+ }
329
+ return block.Statements.Nodes
330
+ }
@@ -0,0 +1,96 @@
1
+ package graph
2
+
3
+ import (
4
+ "path/filepath"
5
+ "testing"
6
+
7
+ "github.com/samchon/ttsc/packages/ttsc/driver"
8
+ )
9
+
10
+ // fixtureTSConfig is the minimal tsconfig the graph probes compile their
11
+ // single-file fixtures with. ES2022 + commonjs keeps lib resolution light and
12
+ // extensionless relative imports working.
13
+ const fixtureTSConfig = `{
14
+ "compilerOptions": {
15
+ "target": "ES2022",
16
+ "module": "commonjs",
17
+ "strict": true,
18
+ "rootDir": "src",
19
+ "outDir": "dist"
20
+ },
21
+ "files": ["src/main.ts"]
22
+ }
23
+ `
24
+
25
+ // TestBuildRecordsANodePerTopLevelDeclaration verifies that Build records one
26
+ // graph node for each kind of top-level declaration, keyed by its
27
+ // position-invariant id, and classifies workspace source as non-external.
28
+ //
29
+ // It pins the declaration-to-node mapping the rest of the graph is laid over: a
30
+ // missing kind here is an edge with no endpoint later. driver.SourceFiles drops
31
+ // declaration files, so every node must report External=false.
32
+ //
33
+ // 1. Compile a fixture with a function, class, interface, type alias, enum, and
34
+ // const declaration.
35
+ // 2. Build the graph.
36
+ // 3. Assert exactly those six nodes exist with the right kind and name, none
37
+ // marked external.
38
+ func TestBuildRecordsANodePerTopLevelDeclaration(t *testing.T) {
39
+ root := t.TempDir()
40
+ writeFile(t, filepath.Join(root, "tsconfig.json"), fixtureTSConfig)
41
+ writeFile(t, filepath.Join(root, "src", "main.ts"), `export function fn(): void {}
42
+ export class Cls {}
43
+ export interface Iface {}
44
+ export type Alias = number;
45
+ export enum En {
46
+ A,
47
+ }
48
+ export const value = 1;
49
+ `)
50
+
51
+ prog, diags, err := driver.LoadProgram(root, "tsconfig.json", driver.LoadProgramOptions{})
52
+ if err != nil {
53
+ t.Fatal(err)
54
+ }
55
+ if len(diags) != 0 {
56
+ t.Fatalf("unexpected diagnostics: %v", diags)
57
+ }
58
+ defer func() { _ = prog.Close() }()
59
+
60
+ graph := Build(prog)
61
+ path := sourceFile(t, prog, "main.ts").FileName()
62
+
63
+ want := map[string]NodeKind{
64
+ "fn": NodeFunction,
65
+ "Cls": NodeClass,
66
+ "Iface": NodeInterface,
67
+ "Alias": NodeTypeAlias,
68
+ "En": NodeEnum,
69
+ "value": NodeVariable,
70
+ }
71
+ if len(graph.Nodes) != len(want) {
72
+ t.Fatalf("expected %d nodes, got %d: %v", len(want), len(graph.Nodes), nodeIDSet(graph))
73
+ }
74
+ for name, kind := range want {
75
+ id := nodeID(path, name, kind)
76
+ node, ok := graph.Nodes[id]
77
+ if !ok {
78
+ t.Fatalf("missing node for %s (%s); have %v", name, kind, nodeIDSet(graph))
79
+ }
80
+ if node.Name != name || node.Kind != kind {
81
+ t.Fatalf("node %s: got name=%q kind=%q", id, node.Name, node.Kind)
82
+ }
83
+ if node.External {
84
+ t.Fatalf("workspace declaration %s misclassified as external", id)
85
+ }
86
+ }
87
+ }
88
+
89
+ // nodeIDSet returns the graph's node ids as a slice for failure messages.
90
+ func nodeIDSet(graph *Graph) []string {
91
+ ids := make([]string, 0, len(graph.Nodes))
92
+ for id := range graph.Nodes {
93
+ ids = append(ids, id)
94
+ }
95
+ return ids
96
+ }