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.
- package/README.md +96 -25
- package/cmd/graphbench/main.go +135 -0
- package/cmd/graphdump/main.go +60 -0
- package/cmd/ttscgraph/dump.go +69 -0
- package/cmd/ttscgraph/main.go +91 -0
- package/cmd/ttscgraph/run_dump_prints_the_graph_as_json_test.go +80 -0
- package/cmd/ttscgraph/run_help_version_and_bad_flags_exit_codes_test.go +75 -0
- package/cmd/utility-host/main.go +4 -1
- package/driver/overlay.go +64 -0
- package/driver/program.go +8 -1
- package/driver/session.go +85 -0
- package/internal/graph/build.go +330 -0
- package/internal/graph/build_records_a_node_per_top_level_declaration_test.go +96 -0
- package/internal/graph/decorators.go +122 -0
- package/internal/graph/decorators_capture_name_and_literal_arguments_test.go +98 -0
- package/internal/graph/diagnostics.go +23 -0
- package/internal/graph/dump.go +390 -0
- package/internal/graph/dump_evidence_skips_leading_trivia_test.go +92 -0
- package/internal/graph/dump_serializes_the_full_graph_test.go +146 -0
- package/internal/graph/edge_spans_point_to_resolved_usage_test.go +90 -0
- package/internal/graph/edges.go +538 -0
- package/internal/graph/edges_cover_class_level_references_test.go +93 -0
- package/internal/graph/enum_initializer_calls_are_edges_test.go +63 -0
- package/internal/graph/exports.go +60 -0
- package/internal/graph/file_diagnostics_match_tsgo_code_and_location_test.go +59 -0
- package/internal/graph/file_diagnostics_report_code_column_and_message_test.go +69 -0
- package/internal/graph/function_locals_do_not_collide_test.go +66 -0
- package/internal/graph/gitignore.go +51 -0
- package/internal/graph/graph.go +119 -0
- package/internal/graph/heritage_edges_cover_implements_and_interface_extends_test.go +76 -0
- package/internal/graph/heritage_edges_keep_extends_and_implements_to_same_base_test.go +61 -0
- package/internal/graph/heritage_edges_record_extends_versus_implements_origin_test.go +54 -0
- package/internal/graph/heritage_edges_traverse_barrels_and_stop_at_external_leaves_test.go +112 -0
- package/internal/graph/namespace_dotted_form_qualifies_deeply_test.go +60 -0
- package/internal/graph/namespace_members_become_qualified_nodes_and_edges_test.go +100 -0
- package/internal/graph/node_identity_is_position_invariant_test.go +84 -0
- package/internal/graph/node_modifiers_emit_union_strings_test.go +118 -0
- package/internal/graph/nodes_are_marked_exported_through_the_export_table_test.go +61 -0
- package/internal/graph/overload_implementation_replaces_signature_test.go +54 -0
- package/internal/graph/property_member_edges_remain_owner_visible_test.go +83 -0
- package/internal/graph/resolve.go +102 -0
- package/internal/graph/resolve_prefers_implementation_declaration_test.go +50 -0
- package/internal/graph/resolve_unwraps_barrel_reexport_to_real_declaration_test.go +230 -0
- package/internal/graph/type_ref_edges_cover_typeof_as_satisfies_test.go +79 -0
- package/internal/graph/type_ref_edges_resolve_named_types_across_files_test.go +52 -0
- package/internal/graph/value_access_edges_cover_properties_test.go +96 -0
- package/internal/graph/value_call_edges_cover_jsx_components_test.go +82 -0
- package/internal/graph/value_call_edges_cover_methods_and_constructors_test.go +70 -0
- package/internal/graph/value_call_edges_cover_variable_bound_callables_test.go +46 -0
- package/internal/graph/value_call_edges_do_not_double_record_invoked_member_access_test.go +65 -0
- package/internal/graph/value_call_edges_record_construct_versus_call_origin_test.go +75 -0
- package/internal/graph/value_call_edges_resolve_across_files_and_dedupe_test.go +55 -0
- package/internal/graph/value_call_edges_skip_self_and_unresolved_test.go +74 -0
- package/lib/TtscService.d.ts +55 -0
- package/lib/TtscService.js +93 -0
- package/lib/TtscService.js.map +1 -0
- package/lib/compiler/internal/residentTransformProcess.d.ts +47 -0
- package/lib/compiler/internal/residentTransformProcess.js +187 -0
- package/lib/compiler/internal/residentTransformProcess.js.map +1 -0
- package/lib/compiler/internal/startResidentTransform.d.ts +26 -0
- package/lib/compiler/internal/startResidentTransform.js +98 -0
- package/lib/compiler/internal/startResidentTransform.js.map +1 -0
- package/lib/index.d.ts +1 -0
- package/lib/index.js +1 -0
- package/lib/index.js.map +1 -1
- package/lib/plugin/internal/loadProjectPlugins.js +17 -3
- package/lib/plugin/internal/loadProjectPlugins.js.map +1 -1
- package/lib/structures/ITtscPlugin.d.ts +7 -5
- package/lib/structures/ITtscPluginFactoryContext.d.ts +24 -1
- package/package.json +8 -8
- package/shim/ast/shim.go +6 -0
- package/shim/checker/shim.go +22 -12
- package/src/TtscService.ts +94 -0
- package/src/compiler/internal/residentTransformProcess.ts +216 -0
- package/src/compiler/internal/startResidentTransform.ts +126 -0
- package/src/index.ts +1 -0
- package/src/plugin/internal/loadProjectPlugins.ts +17 -3
- package/src/structures/ITtscPlugin.ts +7 -5
- package/src/structures/ITtscPluginFactoryContext.ts +26 -1
- package/utility/host.go +8 -2
- package/utility/serve.go +158 -0
package/cmd/utility-host/main.go
CHANGED
|
@@ -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 :=
|
|
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
|
+
}
|