ttsc 0.19.1 → 0.19.3
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 +68 -186
- package/cmd/graphdump/main.go +21 -1
- package/cmd/ttsc/api_compile.go +3 -13
- package/cmd/ttsc/api_transform.go +13 -0
- package/cmd/ttscgraph/dump.go +40 -1
- package/cmd/ttscgraph/serve.go +194 -25
- package/cmd/ttscgraph/serve_error_response_still_names_its_mode_test.go +73 -0
- package/cmd/ttscgraph/serve_protocol_fails_the_stream_on_an_unaddressable_request_line_test.go +43 -0
- package/cmd/ttscgraph/serve_protocol_version_matches_the_typescript_client_test.go +48 -0
- package/cmd/ttscgraph/serve_snapshot_proves_its_program_without_a_second_read_test.go +109 -0
- package/driver/graph.go +133 -0
- package/internal/graph/diagnostics.go +6 -5
- package/internal/graph/dump.go +107 -27
- package/internal/graph/dump_diagnostics_ride_the_generation_that_built_it_test.go +79 -0
- package/internal/graph/dump_evidence_skips_leading_trivia_test.go +1 -1
- package/internal/graph/dump_is_a_function_of_the_source_test.go +1 -1
- package/internal/graph/dump_never_serializes_a_list_as_null_test.go +76 -0
- package/internal/graph/dump_schema_version_matches_the_typescript_client_test.go +48 -0
- package/internal/graph/dump_serializes_the_full_graph_test.go +21 -7
- package/internal/graph/edges.go +12 -5
- package/internal/graph/enum_members_carry_their_names_on_the_enum_node_test.go +131 -0
- package/internal/graph/graph.go +34 -0
- package/internal/graph/literals.go +175 -0
- package/internal/graph/literals_are_absent_when_the_type_admits_more_than_it_can_name_test.go +74 -0
- package/internal/graph/literals_cover_enum_members_the_graph_has_no_nodes_for_test.go +79 -0
- package/internal/graph/literals_do_not_depend_on_declaration_line_wrapping_test.go +84 -0
- package/internal/graph/literals_render_every_value_in_typescript_source_form_test.go +77 -0
- package/internal/graph/literals_resolve_through_alias_indirection_test.go +64 -0
- package/internal/graph/node_modifiers_emit_union_strings_test.go +1 -1
- package/internal/graph/provenance.go +312 -0
- package/lib/TtscCompiler.js +9 -4
- package/lib/TtscCompiler.js.map +1 -1
- package/lib/compiler/internal/transformProjectInMemory.d.ts +4 -0
- package/lib/compiler/internal/transformProjectInMemory.js +67 -12
- package/lib/compiler/internal/transformProjectInMemory.js.map +1 -1
- package/lib/structures/ITtscCompilerTransformation.d.ts +98 -0
- package/package.json +8 -8
- package/shim/checker/shim.go +13 -0
- package/shim/compiler/incremental.go +58 -0
- package/shim/core/shim.go +5 -0
- package/src/TtscCompiler.ts +19 -4
- package/src/compiler/internal/transformProjectInMemory.ts +106 -12
- package/src/structures/ITtscCompilerTransformation.ts +107 -0
- package/tools/shim_audit/baseline.json +2 -1
- package/tools/shim_audit/main.go +17 -0
- package/utility/host.go +17 -15
- package/cmd/ttscgraph/serve_protocol_rejects_invalid_request_line_test.go +0 -43
package/driver/graph.go
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
package driver
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"path/filepath"
|
|
5
|
+
"sort"
|
|
6
|
+
"strings"
|
|
7
|
+
|
|
8
|
+
"github.com/microsoft/typescript-go/shim/ast"
|
|
9
|
+
shimcompiler "github.com/microsoft/typescript-go/shim/compiler"
|
|
10
|
+
shimtspath "github.com/microsoft/typescript-go/shim/tspath"
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
// bundledScheme prefixes the virtual paths of the TypeScript-Go standard
|
|
14
|
+
// library files embedded in the binary. They are not filesystem inputs — they
|
|
15
|
+
// change only with the compiler itself — so the reference graph excludes them.
|
|
16
|
+
const bundledScheme = "bundled:///"
|
|
17
|
+
|
|
18
|
+
// TransformGraph is the host-owned reference-graph section of a transform
|
|
19
|
+
// envelope (`graph` in the stdout JSON). It carries the language-semantic
|
|
20
|
+
// input set of a transform under `tsc --incremental` semantics, so cache
|
|
21
|
+
// layers (bundler filesystem caches, watch graphs) can register every file
|
|
22
|
+
// whose content can influence a transformed module's output:
|
|
23
|
+
//
|
|
24
|
+
// - Edges maps each file to its direct resolved references — imports,
|
|
25
|
+
// re-exports, `/// <reference>` targets, type reference directives, and
|
|
26
|
+
// ambient-module declaration files, type-only edges included. Direct
|
|
27
|
+
// edges are the minimal sufficient statistic; consumers that need a flat
|
|
28
|
+
// per-file list compute the reachability closure themselves.
|
|
29
|
+
// - Globals lists the files that contribute to the global scope (ambient
|
|
30
|
+
// declaration files, script files, global augmentations, `typeRoots`
|
|
31
|
+
// entries). A change to any of them can affect every file.
|
|
32
|
+
// - Configs lists the project tsconfig followed by its `extends` ancestry.
|
|
33
|
+
//
|
|
34
|
+
// Keys and values use the same convention as the envelope's `typescript`
|
|
35
|
+
// map: project-relative slash paths, falling back to slash-normalized
|
|
36
|
+
// absolute paths outside the project root (see TransformOutputKey).
|
|
37
|
+
type TransformGraph struct {
|
|
38
|
+
Edges map[string][]string `json:"edges"`
|
|
39
|
+
Globals []string `json:"globals"`
|
|
40
|
+
Configs []string `json:"configs"`
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// NewTransformGraph computes the reference graph of a loaded program, keyed
|
|
44
|
+
// relative to cwd exactly like the transform envelope's `typescript` map.
|
|
45
|
+
// Hosts stamp the result into their stdout envelope's `graph` field;
|
|
46
|
+
// `cmd/ttsc api-transform` and the linked-plugin utility host both do.
|
|
47
|
+
// Returns nil only for a nil or unloaded program.
|
|
48
|
+
func NewTransformGraph(prog *Program, cwd string) *TransformGraph {
|
|
49
|
+
if prog == nil || prog.TSProgram == nil {
|
|
50
|
+
return nil
|
|
51
|
+
}
|
|
52
|
+
graph := &TransformGraph{
|
|
53
|
+
Edges: map[string][]string{},
|
|
54
|
+
Globals: []string{},
|
|
55
|
+
Configs: []string{},
|
|
56
|
+
}
|
|
57
|
+
for _, file := range prog.TSProgram.SourceFiles() {
|
|
58
|
+
fileName := file.FileName()
|
|
59
|
+
if strings.HasPrefix(fileName, bundledScheme) {
|
|
60
|
+
continue
|
|
61
|
+
}
|
|
62
|
+
key := TransformOutputKey(cwd, fileName)
|
|
63
|
+
if shimcompiler.FileAffectsGlobalScope(file) {
|
|
64
|
+
graph.Globals = append(graph.Globals, key)
|
|
65
|
+
}
|
|
66
|
+
targets := referenceTargets(prog, cwd, file)
|
|
67
|
+
if len(targets) != 0 {
|
|
68
|
+
graph.Edges[key] = targets
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
sort.Strings(graph.Globals)
|
|
72
|
+
graph.Configs = configChain(prog, cwd)
|
|
73
|
+
return graph
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// referenceTargets resolves one file's direct reference set to sorted envelope
|
|
77
|
+
// keys, dropping bundled library files and the file itself.
|
|
78
|
+
func referenceTargets(prog *Program, cwd string, file *ast.SourceFile) []string {
|
|
79
|
+
paths := shimcompiler.GetReferencedFilePaths(prog.TSProgram, file)
|
|
80
|
+
targets := make([]string, 0, len(paths))
|
|
81
|
+
for _, path := range paths {
|
|
82
|
+
fileName := path
|
|
83
|
+
// Referenced paths are case-canonicalized tspath.Path values; recover the
|
|
84
|
+
// real spelling from the program so consumers can compare them against
|
|
85
|
+
// filesystem paths byte-for-byte.
|
|
86
|
+
if resolved := prog.TSProgram.GetSourceFileByPath(shimtspath.Path(path)); resolved != nil {
|
|
87
|
+
fileName = resolved.FileName()
|
|
88
|
+
}
|
|
89
|
+
if fileName == file.FileName() || strings.HasPrefix(fileName, bundledScheme) {
|
|
90
|
+
continue
|
|
91
|
+
}
|
|
92
|
+
targets = append(targets, TransformOutputKey(cwd, fileName))
|
|
93
|
+
}
|
|
94
|
+
sort.Strings(targets)
|
|
95
|
+
return targets
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// configChain returns the project tsconfig followed by its `extends` ancestry
|
|
99
|
+
// as envelope keys. An inferred (config-less) program yields an empty list.
|
|
100
|
+
func configChain(prog *Program, cwd string) []string {
|
|
101
|
+
configs := []string{}
|
|
102
|
+
parsed := prog.ParsedConfig
|
|
103
|
+
if parsed == nil || parsed.ConfigFile == nil {
|
|
104
|
+
return configs
|
|
105
|
+
}
|
|
106
|
+
if source := parsed.ConfigFile.SourceFile; source != nil {
|
|
107
|
+
configs = append(configs, TransformOutputKey(cwd, source.FileName()))
|
|
108
|
+
}
|
|
109
|
+
for _, extended := range parsed.ExtendedSourceFiles() {
|
|
110
|
+
configs = append(configs, TransformOutputKey(cwd, extended))
|
|
111
|
+
}
|
|
112
|
+
return configs
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// TransformOutputKey converts an absolute fileName to the key used by the
|
|
116
|
+
// transform and compile envelopes: a slash-separated path relative to cwd,
|
|
117
|
+
// falling back to the slash-normalized absolute path when the file lives
|
|
118
|
+
// outside the project root. Every envelope section (`typescript`, `graph`,
|
|
119
|
+
// `dependencies` producers) must share this one implementation so a consumer
|
|
120
|
+
// can join sections by key.
|
|
121
|
+
func TransformOutputKey(cwd, fileName string) string {
|
|
122
|
+
rel, err := filepath.Rel(cwd, fileName)
|
|
123
|
+
if err != nil || isOutsideRelativePath(rel) {
|
|
124
|
+
return filepath.ToSlash(fileName)
|
|
125
|
+
}
|
|
126
|
+
return filepath.ToSlash(rel)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// isOutsideRelativePath reports whether rel escapes the project root (starts
|
|
130
|
+
// with ".." or is exactly "..").
|
|
131
|
+
func isOutsideRelativePath(rel string) bool {
|
|
132
|
+
return rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator))
|
|
133
|
+
}
|
|
@@ -8,11 +8,12 @@ import "github.com/samchon/ttsc/packages/ttsc/driver"
|
|
|
8
8
|
// over the warm checker, not a second compile.
|
|
9
9
|
//
|
|
10
10
|
// This is the compiler-only slice: it carries the TypeScript semantic
|
|
11
|
-
// diagnostics, not @ttsc/lint or transform-plugin findings.
|
|
12
|
-
//
|
|
13
|
-
// This
|
|
14
|
-
//
|
|
15
|
-
//
|
|
11
|
+
// diagnostics, not @ttsc/lint or transform-plugin findings.
|
|
12
|
+
//
|
|
13
|
+
// This is the per-file view. The dump publishes the whole-program view through
|
|
14
|
+
// NewDiagnostics, which groups and relativizes the same driver.Diagnostic values
|
|
15
|
+
// in one pass; prefer it when every file is wanted, because filtering per file
|
|
16
|
+
// here rescans the program's diagnostics once per call.
|
|
16
17
|
func FileDiagnostics(prog *driver.Program, path string) []driver.Diagnostic {
|
|
17
18
|
out := make([]driver.Diagnostic, 0)
|
|
18
19
|
for _, diagnostic := range prog.Diagnostics() {
|
package/internal/graph/dump.go
CHANGED
|
@@ -17,7 +17,6 @@ import (
|
|
|
17
17
|
// - internal node kinds map straight through (NodeTypeAlias is already "type");
|
|
18
18
|
// - one EdgeValueCall splits by Origin into "calls" / "instantiates" /
|
|
19
19
|
// "renders", and EdgeHeritage into "extends" / "implements";
|
|
20
|
-
// - every edge is tagged checker-resolved / high (the graph's whole contract);
|
|
21
20
|
// - byte spans become 1-based line/col Evidence ranges;
|
|
22
21
|
// - decorator facts ride on their target node;
|
|
23
22
|
// - file paths are project-relative and the output is sorted, so the dump is
|
|
@@ -57,6 +56,15 @@ type DumpDecorator struct {
|
|
|
57
56
|
Arguments []DumpDecoratorArgument `json:"arguments"`
|
|
58
57
|
}
|
|
59
58
|
|
|
59
|
+
// DumpEnumMember is one member of an enum on the wire: the name a caller writes
|
|
60
|
+
// and the value it carries. `value` is omitted for a member the checker could
|
|
61
|
+
// not fold to a constant — the name still stands, and the name is what a caller
|
|
62
|
+
// asking about the enum came for.
|
|
63
|
+
type DumpEnumMember struct {
|
|
64
|
+
Name string `json:"name"`
|
|
65
|
+
Value string `json:"value,omitempty"`
|
|
66
|
+
}
|
|
67
|
+
|
|
60
68
|
// DumpNode is the wire shape of a graph node. Lowercase json keys are the
|
|
61
69
|
// contract; the Go field names are not.
|
|
62
70
|
type DumpNode struct {
|
|
@@ -70,6 +78,8 @@ type DumpNode struct {
|
|
|
70
78
|
Exported bool `json:"exported,omitempty"`
|
|
71
79
|
Closure bool `json:"closure,omitempty"`
|
|
72
80
|
Modifiers []string `json:"modifiers,omitempty"`
|
|
81
|
+
Literals []string `json:"literals,omitempty"`
|
|
82
|
+
EnumMembers []DumpEnumMember `json:"enumMembers,omitempty"`
|
|
73
83
|
Evidence *DumpEvidence `json:"evidence,omitempty"`
|
|
74
84
|
Implementation *DumpEvidence `json:"implementation,omitempty"`
|
|
75
85
|
Decorators []DumpDecorator `json:"decorators,omitempty"`
|
|
@@ -84,21 +94,46 @@ type DumpEdge struct {
|
|
|
84
94
|
Evidence *DumpEvidence `json:"evidence,omitempty"`
|
|
85
95
|
}
|
|
86
96
|
|
|
87
|
-
// Dump is the IGraphDump envelope: the project it was built for
|
|
88
|
-
// node and edge sets with none
|
|
97
|
+
// Dump is the IGraphDump envelope: the project it was built for, the evidence
|
|
98
|
+
// about the program that produced it, and the full node and edge sets with none
|
|
99
|
+
// of the MCP response caps.
|
|
89
100
|
type Dump struct {
|
|
90
|
-
Project string
|
|
91
|
-
Tsconfig string
|
|
92
|
-
|
|
93
|
-
|
|
101
|
+
Project string `json:"project"`
|
|
102
|
+
Tsconfig string `json:"tsconfig"`
|
|
103
|
+
// Provenance proves the rest of this dump came from one Program. It rides the
|
|
104
|
+
// body rather than the serve envelope so a dump written to a file by the
|
|
105
|
+
// one-shot command keeps its evidence, and so a consumer holding only the
|
|
106
|
+
// parsed dump never has to ask where it came from.
|
|
107
|
+
Provenance Provenance `json:"provenance"`
|
|
108
|
+
// Diagnostics are the compiler's findings for the same program generation
|
|
109
|
+
// that produced Nodes and Edges. Empty means the program had none, not that
|
|
110
|
+
// they were not collected; the producer states that in its capabilities.
|
|
111
|
+
Diagnostics []Diagnostic `json:"diagnostics"`
|
|
112
|
+
Nodes []DumpNode `json:"nodes"`
|
|
113
|
+
Edges []DumpEdge `json:"edges"`
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// DumpOrigin is the snapshot evidence a caller attaches to a dump: who built it
|
|
117
|
+
// and what the same program generation had to say about the code. It is a
|
|
118
|
+
// separate struct because only the commands that own a compiler session can
|
|
119
|
+
// produce it, while the graph projection below is pure.
|
|
120
|
+
type DumpOrigin struct {
|
|
121
|
+
// Provenance identifies the producing program. NewDump always stamps the
|
|
122
|
+
// schema version itself, so a caller cannot publish a wrong one.
|
|
123
|
+
Provenance Provenance
|
|
124
|
+
|
|
125
|
+
// Diagnostics are the compiler findings for the producing generation, or nil
|
|
126
|
+
// when the caller did not collect them.
|
|
127
|
+
Diagnostics []Diagnostic
|
|
94
128
|
}
|
|
95
129
|
|
|
96
130
|
// NewDump projects a built graph onto the export shape. project is the absolute
|
|
97
131
|
// project root used to relativize file paths and node ids; ignored is the
|
|
98
132
|
// git-ignored source set (nil for a non-git project); sources maps a source
|
|
99
133
|
// file's path to its text so byte spans become line/col evidence (nil omits
|
|
100
|
-
// evidence)
|
|
101
|
-
|
|
134
|
+
// evidence); origin is the snapshot evidence that proves where the facts came
|
|
135
|
+
// from.
|
|
136
|
+
func NewDump(g *Graph, project, tsconfig string, ignored map[string]bool, sources map[string]string, origin DumpOrigin) Dump {
|
|
102
137
|
ctx := newDumpContext(project, sources)
|
|
103
138
|
|
|
104
139
|
// Decorators ride on their target node; group by the internal node id before
|
|
@@ -124,16 +159,20 @@ func NewDump(g *Graph, project, tsconfig string, ignored map[string]bool, source
|
|
|
124
159
|
name, qualified = ctx.rel(name), ""
|
|
125
160
|
}
|
|
126
161
|
nodes = append(nodes, DumpNode{
|
|
127
|
-
ID:
|
|
128
|
-
Kind:
|
|
129
|
-
Name:
|
|
130
|
-
QualifiedName:
|
|
131
|
-
File:
|
|
132
|
-
External:
|
|
133
|
-
Ignored:
|
|
134
|
-
Exported:
|
|
135
|
-
Closure:
|
|
136
|
-
Modifiers:
|
|
162
|
+
ID: ctx.relID(n.ID),
|
|
163
|
+
Kind: string(n.Kind),
|
|
164
|
+
Name: name,
|
|
165
|
+
QualifiedName: qualified,
|
|
166
|
+
File: ctx.rel(n.File),
|
|
167
|
+
External: n.External,
|
|
168
|
+
Ignored: ignored[n.File],
|
|
169
|
+
Exported: n.Exported,
|
|
170
|
+
Closure: n.Closure,
|
|
171
|
+
Modifiers: n.Modifiers,
|
|
172
|
+
// Uncapped, like every other fact here: the dump is the whole graph and
|
|
173
|
+
// the MCP layer is what applies a response cap and marks it.
|
|
174
|
+
Literals: n.Literals,
|
|
175
|
+
EnumMembers: dumpEnumMembers(n.EnumMembers),
|
|
137
176
|
Evidence: withoutFile(ctx.evidence(n.File, n.Pos, n.End)),
|
|
138
177
|
Implementation: ctx.evidence(n.ImplementationFile, n.ImplementationPos, n.ImplementationEnd),
|
|
139
178
|
Decorators: decByNode[n.ID],
|
|
@@ -160,18 +199,59 @@ func NewDump(g *Graph, project, tsconfig string, ignored map[string]bool, source
|
|
|
160
199
|
return edges[i].Kind < edges[j].Kind
|
|
161
200
|
})
|
|
162
201
|
|
|
202
|
+
// The schema version describes this code, not the caller's belief about it,
|
|
203
|
+
// so stamp it here rather than trusting what came in.
|
|
204
|
+
provenance := origin.Provenance
|
|
205
|
+
provenance.SchemaVersion = DumpSchemaVersion
|
|
206
|
+
|
|
207
|
+
// A nil slice encodes as JSON null, and null is not an empty list: a reader
|
|
208
|
+
// validating `string[]` rejects it, and a reader that does not would have to
|
|
209
|
+
// guess which of the two the producer meant. Every list on the wire is a list.
|
|
210
|
+
if provenance.Capabilities == nil {
|
|
211
|
+
provenance.Capabilities = []string{}
|
|
212
|
+
}
|
|
213
|
+
if provenance.Sources == nil {
|
|
214
|
+
provenance.Sources = []SourceDigest{}
|
|
215
|
+
}
|
|
216
|
+
if provenance.Universe.Configs == nil {
|
|
217
|
+
provenance.Universe.Configs = []FileDigest{}
|
|
218
|
+
}
|
|
219
|
+
if provenance.Universe.Roots == nil {
|
|
220
|
+
provenance.Universe.Roots = []RootFile{}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
diagnostics := origin.Diagnostics
|
|
224
|
+
if diagnostics == nil {
|
|
225
|
+
diagnostics = []Diagnostic{}
|
|
226
|
+
}
|
|
227
|
+
|
|
163
228
|
return Dump{
|
|
164
|
-
Project:
|
|
165
|
-
Tsconfig:
|
|
166
|
-
|
|
167
|
-
|
|
229
|
+
Project: project,
|
|
230
|
+
Tsconfig: tsconfig,
|
|
231
|
+
Provenance: provenance,
|
|
232
|
+
Diagnostics: diagnostics,
|
|
233
|
+
Nodes: nodes,
|
|
234
|
+
Edges: edges,
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// dumpEnumMembers projects an enum's members onto the wire shape, nil for a
|
|
239
|
+
// node that declares none so the key stays off every other kind.
|
|
240
|
+
func dumpEnumMembers(members []EnumMember) []DumpEnumMember {
|
|
241
|
+
if len(members) == 0 {
|
|
242
|
+
return nil
|
|
243
|
+
}
|
|
244
|
+
out := make([]DumpEnumMember, 0, len(members))
|
|
245
|
+
for _, member := range members {
|
|
246
|
+
out = append(out, DumpEnumMember{Name: member.Name, Value: member.Value})
|
|
168
247
|
}
|
|
248
|
+
return out
|
|
169
249
|
}
|
|
170
250
|
|
|
171
251
|
// MarshalDump serializes a built graph to the export JSON, indented when pretty.
|
|
172
252
|
// See NewDump for the parameters.
|
|
173
|
-
func MarshalDump(g *Graph, project, tsconfig string, ignored map[string]bool, sources map[string]string, pretty bool) ([]byte, error) {
|
|
174
|
-
d := NewDump(g, project, tsconfig, ignored, sources)
|
|
253
|
+
func MarshalDump(g *Graph, project, tsconfig string, ignored map[string]bool, sources map[string]string, origin DumpOrigin, pretty bool) ([]byte, error) {
|
|
254
|
+
d := NewDump(g, project, tsconfig, ignored, sources, origin)
|
|
175
255
|
if pretty {
|
|
176
256
|
return json.MarshalIndent(d, "", " ")
|
|
177
257
|
}
|
|
@@ -186,13 +266,13 @@ func MarshalDump(g *Graph, project, tsconfig string, ignored map[string]bool, so
|
|
|
186
266
|
// string. On VS Code the document is 323 MB, so the conversion was a second full
|
|
187
267
|
// copy of it held live beside the first — half a gigabyte of peak heap that
|
|
188
268
|
// bought nothing, because the bytes were already exactly what stdout wanted.
|
|
189
|
-
func EncodeDump(w io.Writer, g *Graph, project, tsconfig string, ignored map[string]bool, sources map[string]string, pretty bool) error {
|
|
269
|
+
func EncodeDump(w io.Writer, g *Graph, project, tsconfig string, ignored map[string]bool, sources map[string]string, origin DumpOrigin, pretty bool) error {
|
|
190
270
|
buffered := bufio.NewWriterSize(w, 1<<20)
|
|
191
271
|
encoder := json.NewEncoder(buffered)
|
|
192
272
|
if pretty {
|
|
193
273
|
encoder.SetIndent("", " ")
|
|
194
274
|
}
|
|
195
|
-
if err := encoder.Encode(NewDump(g, project, tsconfig, ignored, sources)); err != nil {
|
|
275
|
+
if err := encoder.Encode(NewDump(g, project, tsconfig, ignored, sources, origin)); err != nil {
|
|
196
276
|
return err
|
|
197
277
|
}
|
|
198
278
|
return buffered.Flush()
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
package graph
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"path/filepath"
|
|
5
|
+
"strings"
|
|
6
|
+
"testing"
|
|
7
|
+
|
|
8
|
+
"github.com/samchon/ttsc/packages/ttsc/driver"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
// TestDumpDiagnosticsRideTheGenerationThatBuiltIt verifies the dump carries the
|
|
12
|
+
// compiler's findings for the same program that produced its nodes and edges,
|
|
13
|
+
// relativized like every other path on the wire.
|
|
14
|
+
//
|
|
15
|
+
// FileDiagnostics was written, tested, and then called by nothing: its own
|
|
16
|
+
// comment said "the dump and MCP results carry no diagnostics", so a consumer
|
|
17
|
+
// that wanted to know whether the code it was reading even compiles had to run a
|
|
18
|
+
// second compile to find out — against a program that, by then, was not this one.
|
|
19
|
+
// The value of riding along is precisely that the answer belongs to this
|
|
20
|
+
// snapshot; a diagnostics list assembled separately would be a different
|
|
21
|
+
// program's opinion.
|
|
22
|
+
//
|
|
23
|
+
// 1. Build a fixture whose only file assigns a string to a number binding.
|
|
24
|
+
// 2. Dump it.
|
|
25
|
+
// 3. Assert the TS2322 sits in the dump, at the location tsgo reports, against
|
|
26
|
+
// a project-relative path.
|
|
27
|
+
func TestDumpDiagnosticsRideTheGenerationThatBuiltIt(t *testing.T) {
|
|
28
|
+
root := t.TempDir()
|
|
29
|
+
writeFile(t, filepath.Join(root, "tsconfig.json"), fixtureTSConfig)
|
|
30
|
+
writeFile(t, filepath.Join(root, "src", "main.ts"), `export const broken: number = "nope";
|
|
31
|
+
`)
|
|
32
|
+
|
|
33
|
+
prog, _, err := driver.LoadProgram(root, "tsconfig.json", driver.LoadProgramOptions{})
|
|
34
|
+
if err != nil {
|
|
35
|
+
t.Fatal(err)
|
|
36
|
+
}
|
|
37
|
+
defer func() { _ = prog.Close() }()
|
|
38
|
+
|
|
39
|
+
dump := NewDump(Build(prog), root, "tsconfig.json", nil, SourceTexts(prog), DumpOrigin{
|
|
40
|
+
Diagnostics: NewDiagnostics(prog, root),
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
var match *Diagnostic
|
|
44
|
+
for i := range dump.Diagnostics {
|
|
45
|
+
if dump.Diagnostics[i].Code == 2322 {
|
|
46
|
+
match = &dump.Diagnostics[i]
|
|
47
|
+
break
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if match == nil {
|
|
51
|
+
t.Fatalf("the dump carried no TS2322 for the bad assignment: %v", dump.Diagnostics)
|
|
52
|
+
}
|
|
53
|
+
// The same oracle the FileDiagnostics probe uses: tsgo attributes the
|
|
54
|
+
// assignability error to the declared binding, 1-based column 14.
|
|
55
|
+
if match.Line != 1 || match.Column != 14 {
|
|
56
|
+
t.Fatalf("TS2322 at line %d column %d, expected line 1 column 14", match.Line, match.Column)
|
|
57
|
+
}
|
|
58
|
+
if !strings.Contains(match.Message, "not assignable") {
|
|
59
|
+
t.Fatalf("TS2322 message %q does not mention 'not assignable'", match.Message)
|
|
60
|
+
}
|
|
61
|
+
if match.Category != "error" {
|
|
62
|
+
t.Fatalf("TS2322 category %q, want error", match.Category)
|
|
63
|
+
}
|
|
64
|
+
// Paths on the wire are project-relative; an absolute one would leak the
|
|
65
|
+
// producer's disk layout into a document a consumer reads elsewhere.
|
|
66
|
+
if match.File != "src/main.ts" {
|
|
67
|
+
t.Fatalf("diagnostic file %q, want the project-relative src/main.ts", match.File)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// The negative twin: a caller that does not collect diagnostics publishes an
|
|
71
|
+
// empty list, never a nil that would encode as JSON null.
|
|
72
|
+
quiet := NewDump(Build(prog), root, "tsconfig.json", nil, SourceTexts(prog), DumpOrigin{})
|
|
73
|
+
if quiet.Diagnostics == nil {
|
|
74
|
+
t.Fatal("an uncollected diagnostics list is nil, so it serializes as null rather than []")
|
|
75
|
+
}
|
|
76
|
+
if len(quiet.Diagnostics) != 0 {
|
|
77
|
+
t.Fatalf("an uncollected diagnostics list invented %d entries", len(quiet.Diagnostics))
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -46,7 +46,7 @@ export function main(): void {
|
|
|
46
46
|
defer func() { _ = prog.Close() }()
|
|
47
47
|
|
|
48
48
|
g := Build(prog)
|
|
49
|
-
data, err := MarshalDump(g, root, "tsconfig.json", nil, SourceTexts(prog), false)
|
|
49
|
+
data, err := MarshalDump(g, root, "tsconfig.json", nil, SourceTexts(prog), DumpOrigin{}, false)
|
|
50
50
|
if err != nil {
|
|
51
51
|
t.Fatalf("MarshalDump: %v", err)
|
|
52
52
|
}
|
|
@@ -63,7 +63,7 @@ func dumpBytes(t *testing.T, root string) []byte {
|
|
|
63
63
|
}
|
|
64
64
|
defer func() { _ = prog.Close() }()
|
|
65
65
|
|
|
66
|
-
data, err := MarshalDump(Build(prog), root, "tsconfig.json", nil, SourceTexts(prog), false)
|
|
66
|
+
data, err := MarshalDump(Build(prog), root, "tsconfig.json", nil, SourceTexts(prog), DumpOrigin{}, false)
|
|
67
67
|
if err != nil {
|
|
68
68
|
t.Fatalf("MarshalDump: %v", err)
|
|
69
69
|
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
package graph
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"encoding/json"
|
|
5
|
+
"path/filepath"
|
|
6
|
+
"strings"
|
|
7
|
+
"testing"
|
|
8
|
+
|
|
9
|
+
"github.com/samchon/ttsc/packages/ttsc/driver"
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
// TestDumpNeverSerializesAListAsNull verifies every list the provenance contract
|
|
13
|
+
// declares reaches the wire as a list, even when the producer filled none of it.
|
|
14
|
+
//
|
|
15
|
+
// A nil Go slice encodes as `null`, and the TypeScript mirror types these as
|
|
16
|
+
// `string[]` / `T[]`, so a nil would fail validation at the consumer rather than
|
|
17
|
+
// here — and a consumer that did not validate would have to guess whether `null`
|
|
18
|
+
// meant "empty" or "absent". The distinction the contract actually uses for
|
|
19
|
+
// absence is `capabilities`, not the difference between `null` and `[]`, so the
|
|
20
|
+
// wire must never offer the second question.
|
|
21
|
+
//
|
|
22
|
+
// 1. Build a dump whose origin declares nothing at all.
|
|
23
|
+
// 2. Assert it still parses into lists, not nulls.
|
|
24
|
+
func TestDumpNeverSerializesAListAsNull(t *testing.T) {
|
|
25
|
+
root := t.TempDir()
|
|
26
|
+
writeFile(t, filepath.Join(root, "tsconfig.json"), fixtureTSConfig)
|
|
27
|
+
writeFile(t, filepath.Join(root, "src", "main.ts"), "export const value = 1;\n")
|
|
28
|
+
|
|
29
|
+
prog, _, err := driver.LoadProgram(root, "tsconfig.json", driver.LoadProgramOptions{})
|
|
30
|
+
if err != nil {
|
|
31
|
+
t.Fatal(err)
|
|
32
|
+
}
|
|
33
|
+
defer func() { _ = prog.Close() }()
|
|
34
|
+
|
|
35
|
+
// The emptiest origin a caller can pass: every list below is nil in Go.
|
|
36
|
+
data, err := MarshalDump(Build(prog), root, "tsconfig.json", nil, SourceTexts(prog), DumpOrigin{}, false)
|
|
37
|
+
if err != nil {
|
|
38
|
+
t.Fatal(err)
|
|
39
|
+
}
|
|
40
|
+
if strings.Contains(string(data), ":null") {
|
|
41
|
+
t.Fatalf("dump serialized a null where a list belongs:\n%s", data)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
var parsed struct {
|
|
45
|
+
Provenance struct {
|
|
46
|
+
Capabilities []string `json:"capabilities"`
|
|
47
|
+
Sources []any `json:"sources"`
|
|
48
|
+
Universe struct {
|
|
49
|
+
Configs []any `json:"configs"`
|
|
50
|
+
Roots []any `json:"roots"`
|
|
51
|
+
} `json:"universe"`
|
|
52
|
+
} `json:"provenance"`
|
|
53
|
+
Diagnostics []any `json:"diagnostics"`
|
|
54
|
+
}
|
|
55
|
+
if err := json.Unmarshal(data, &parsed); err != nil {
|
|
56
|
+
t.Fatal(err)
|
|
57
|
+
}
|
|
58
|
+
for name, list := range map[string]any{
|
|
59
|
+
"provenance.capabilities": parsed.Provenance.Capabilities,
|
|
60
|
+
"provenance.sources": parsed.Provenance.Sources,
|
|
61
|
+
"provenance.universe.configs": parsed.Provenance.Universe.Configs,
|
|
62
|
+
"provenance.universe.roots": parsed.Provenance.Universe.Roots,
|
|
63
|
+
"diagnostics": parsed.Diagnostics,
|
|
64
|
+
} {
|
|
65
|
+
switch typed := list.(type) {
|
|
66
|
+
case []string:
|
|
67
|
+
if typed == nil {
|
|
68
|
+
t.Fatalf("%s came back nil, so it rode the wire as null", name)
|
|
69
|
+
}
|
|
70
|
+
case []any:
|
|
71
|
+
if typed == nil {
|
|
72
|
+
t.Fatalf("%s came back nil, so it rode the wire as null", name)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
package graph
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"bytes"
|
|
5
|
+
"os"
|
|
6
|
+
"path/filepath"
|
|
7
|
+
"regexp"
|
|
8
|
+
"strconv"
|
|
9
|
+
"testing"
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
// TestDumpSchemaVersionMatchesTheTypescriptClient verifies the Go producer and
|
|
13
|
+
// the TypeScript reader agree on the dump schema version.
|
|
14
|
+
//
|
|
15
|
+
// The dump body is mirrored by hand: Dump here, ITtscGraphDump in @ttsc/graph.
|
|
16
|
+
// A dump also outlives the process that wrote it — the one-shot command writes
|
|
17
|
+
// JSON to a file that any later build may read — so the version is what tells a
|
|
18
|
+
// reader whether the document in front of it is the document it understands. It
|
|
19
|
+
// can only do that while both sides mean the same number by it.
|
|
20
|
+
//
|
|
21
|
+
// 1. Read DUMP_SCHEMA_VERSION out of the TypeScript reader's source.
|
|
22
|
+
// 2. Compare it to DumpSchemaVersion.
|
|
23
|
+
func TestDumpSchemaVersionMatchesTheTypescriptClient(t *testing.T) {
|
|
24
|
+
reader := filepath.Join("..", "..", "..", "graph", "src", "model", "loadGraph.ts")
|
|
25
|
+
source, err := os.ReadFile(reader)
|
|
26
|
+
if err != nil {
|
|
27
|
+
t.Fatalf("read the TypeScript reader: %v", err)
|
|
28
|
+
}
|
|
29
|
+
// The checkout is CRLF on Windows, and a line-anchored match would never
|
|
30
|
+
// see the end of a line that ends in a carriage return.
|
|
31
|
+
source = bytes.ReplaceAll(source, []byte("\r\n"), []byte("\n"))
|
|
32
|
+
match := regexp.MustCompile(`(?m)^(?:export )?const DUMP_SCHEMA_VERSION = (\d+);$`).FindSubmatch(source)
|
|
33
|
+
if match == nil {
|
|
34
|
+
t.Fatalf("no `const DUMP_SCHEMA_VERSION = <n>;` in %s; if it moved, this gate must follow it", reader)
|
|
35
|
+
}
|
|
36
|
+
declared, err := strconv.Atoi(string(match[1]))
|
|
37
|
+
if err != nil {
|
|
38
|
+
t.Fatalf("DUMP_SCHEMA_VERSION is not a number: %v", err)
|
|
39
|
+
}
|
|
40
|
+
if declared != DumpSchemaVersion {
|
|
41
|
+
t.Fatalf(
|
|
42
|
+
"dump schema drifted: Go produces v%d, %s reads v%d",
|
|
43
|
+
DumpSchemaVersion,
|
|
44
|
+
reader,
|
|
45
|
+
declared,
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -46,7 +46,7 @@ export function main(): void {
|
|
|
46
46
|
g := Build(prog)
|
|
47
47
|
sources := SourceTexts(prog)
|
|
48
48
|
|
|
49
|
-
data, err := MarshalDump(g, root, "tsconfig.json", nil, sources, false)
|
|
49
|
+
data, err := MarshalDump(g, root, "tsconfig.json", nil, sources, DumpOrigin{}, false)
|
|
50
50
|
if err != nil {
|
|
51
51
|
t.Fatalf("MarshalDump: %v", err)
|
|
52
52
|
}
|
|
@@ -138,17 +138,31 @@ export function main(): void {
|
|
|
138
138
|
t.Fatalf("dump leaked Go field name %s:\n%s", leaked, s)
|
|
139
139
|
}
|
|
140
140
|
}
|
|
141
|
-
//
|
|
142
|
-
//
|
|
143
|
-
//
|
|
144
|
-
//
|
|
145
|
-
|
|
141
|
+
// Every edge is checker-resolved by construction, so no record carries a
|
|
142
|
+
// per-edge trust flag saying so — that is the `provenance`/`confidence` pair
|
|
143
|
+
// 6d74a88c3 removed, and it stays removed. The dump's own `provenance` is a
|
|
144
|
+
// different thing at a different level: it describes the one program that
|
|
145
|
+
// produced every record, which no record can state about itself.
|
|
146
|
+
//
|
|
147
|
+
// No source body text rides the wire either. The manifest names its fields
|
|
148
|
+
// `checkerDigest` and `diskDigest` precisely so a digest can never be read as
|
|
149
|
+
// the text it stands for.
|
|
150
|
+
for _, gone := range []string{`"confidence":`, `"text":`} {
|
|
146
151
|
if strings.Contains(s, gone) {
|
|
147
152
|
t.Fatalf("dump still emits removed key %s:\n%s", gone, s)
|
|
148
153
|
}
|
|
149
154
|
}
|
|
155
|
+
for _, edge := range dump.Edges {
|
|
156
|
+
encoded, err := json.Marshal(edge)
|
|
157
|
+
if err != nil {
|
|
158
|
+
t.Fatalf("marshal edge %s->%s: %v", edge.From, edge.To, err)
|
|
159
|
+
}
|
|
160
|
+
if strings.Contains(string(encoded), `"provenance"`) {
|
|
161
|
+
t.Fatalf("edge regained a per-edge trust flag:\n%s", encoded)
|
|
162
|
+
}
|
|
163
|
+
}
|
|
150
164
|
|
|
151
|
-
pretty, err := MarshalDump(g, root, "tsconfig.json", nil, sources, true)
|
|
165
|
+
pretty, err := MarshalDump(g, root, "tsconfig.json", nil, sources, DumpOrigin{}, true)
|
|
152
166
|
if err != nil {
|
|
153
167
|
t.Fatalf("MarshalDump pretty: %v", err)
|
|
154
168
|
}
|
package/internal/graph/edges.go
CHANGED
|
@@ -7,11 +7,17 @@ import (
|
|
|
7
7
|
"github.com/samchon/ttsc/packages/ttsc/driver"
|
|
8
8
|
)
|
|
9
9
|
|
|
10
|
-
// addEdges
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
10
|
+
// addEdges is the checker pass over the declaration nodes Build recorded. It
|
|
11
|
+
// walks each source file again and, for every class or interface, resolves its
|
|
12
|
+
// heritage bases through the checker (unwrapping barrel re-exports to the real
|
|
13
|
+
// declaration) and links the declaration to that base, materializing an external
|
|
14
|
+
// boundary-leaf node when the base lives in node_modules or a `.d.ts`.
|
|
15
|
+
//
|
|
16
|
+
// Not everything it resolves is an edge. A fact that belongs to one declaration
|
|
17
|
+
// rather than to a pair of them rides on its own node: a decorator on the target
|
|
18
|
+
// it annotates, a literal value set on the type that admits it. Both need the
|
|
19
|
+
// same checker and the same walk, so they are resolved here and written to the
|
|
20
|
+
// node the build pass keyed.
|
|
15
21
|
func (g *Graph) addEdges(prog *driver.Program) {
|
|
16
22
|
checker := prog.Checker
|
|
17
23
|
for _, file := range prog.SourceFiles() {
|
|
@@ -19,6 +25,7 @@ func (g *Graph) addEdges(prog *driver.Program) {
|
|
|
19
25
|
g.collectHeritage(checker, file)
|
|
20
26
|
g.collectCalls(checker, file)
|
|
21
27
|
g.collectTypeRefs(checker, file)
|
|
28
|
+
g.collectLiterals(checker, file)
|
|
22
29
|
if file.Statements != nil {
|
|
23
30
|
g.collectDecorators(file.FileName(), file.Statements.Nodes)
|
|
24
31
|
}
|