ttsc 0.19.1 → 0.19.2
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 +69 -17
- 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/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/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
|
|
@@ -84,21 +83,46 @@ type DumpEdge struct {
|
|
|
84
83
|
Evidence *DumpEvidence `json:"evidence,omitempty"`
|
|
85
84
|
}
|
|
86
85
|
|
|
87
|
-
// Dump is the IGraphDump envelope: the project it was built for
|
|
88
|
-
// node and edge sets with none
|
|
86
|
+
// Dump is the IGraphDump envelope: the project it was built for, the evidence
|
|
87
|
+
// about the program that produced it, and the full node and edge sets with none
|
|
88
|
+
// of the MCP response caps.
|
|
89
89
|
type Dump struct {
|
|
90
|
-
Project string
|
|
91
|
-
Tsconfig string
|
|
92
|
-
|
|
93
|
-
|
|
90
|
+
Project string `json:"project"`
|
|
91
|
+
Tsconfig string `json:"tsconfig"`
|
|
92
|
+
// Provenance proves the rest of this dump came from one Program. It rides the
|
|
93
|
+
// body rather than the serve envelope so a dump written to a file by the
|
|
94
|
+
// one-shot command keeps its evidence, and so a consumer holding only the
|
|
95
|
+
// parsed dump never has to ask where it came from.
|
|
96
|
+
Provenance Provenance `json:"provenance"`
|
|
97
|
+
// Diagnostics are the compiler's findings for the same program generation
|
|
98
|
+
// that produced Nodes and Edges. Empty means the program had none, not that
|
|
99
|
+
// they were not collected; the producer states that in its capabilities.
|
|
100
|
+
Diagnostics []Diagnostic `json:"diagnostics"`
|
|
101
|
+
Nodes []DumpNode `json:"nodes"`
|
|
102
|
+
Edges []DumpEdge `json:"edges"`
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// DumpOrigin is the snapshot evidence a caller attaches to a dump: who built it
|
|
106
|
+
// and what the same program generation had to say about the code. It is a
|
|
107
|
+
// separate struct because only the commands that own a compiler session can
|
|
108
|
+
// produce it, while the graph projection below is pure.
|
|
109
|
+
type DumpOrigin struct {
|
|
110
|
+
// Provenance identifies the producing program. NewDump always stamps the
|
|
111
|
+
// schema version itself, so a caller cannot publish a wrong one.
|
|
112
|
+
Provenance Provenance
|
|
113
|
+
|
|
114
|
+
// Diagnostics are the compiler findings for the producing generation, or nil
|
|
115
|
+
// when the caller did not collect them.
|
|
116
|
+
Diagnostics []Diagnostic
|
|
94
117
|
}
|
|
95
118
|
|
|
96
119
|
// NewDump projects a built graph onto the export shape. project is the absolute
|
|
97
120
|
// project root used to relativize file paths and node ids; ignored is the
|
|
98
121
|
// git-ignored source set (nil for a non-git project); sources maps a source
|
|
99
122
|
// file's path to its text so byte spans become line/col evidence (nil omits
|
|
100
|
-
// evidence)
|
|
101
|
-
|
|
123
|
+
// evidence); origin is the snapshot evidence that proves where the facts came
|
|
124
|
+
// from.
|
|
125
|
+
func NewDump(g *Graph, project, tsconfig string, ignored map[string]bool, sources map[string]string, origin DumpOrigin) Dump {
|
|
102
126
|
ctx := newDumpContext(project, sources)
|
|
103
127
|
|
|
104
128
|
// Decorators ride on their target node; group by the internal node id before
|
|
@@ -160,18 +184,46 @@ func NewDump(g *Graph, project, tsconfig string, ignored map[string]bool, source
|
|
|
160
184
|
return edges[i].Kind < edges[j].Kind
|
|
161
185
|
})
|
|
162
186
|
|
|
187
|
+
// The schema version describes this code, not the caller's belief about it,
|
|
188
|
+
// so stamp it here rather than trusting what came in.
|
|
189
|
+
provenance := origin.Provenance
|
|
190
|
+
provenance.SchemaVersion = DumpSchemaVersion
|
|
191
|
+
|
|
192
|
+
// A nil slice encodes as JSON null, and null is not an empty list: a reader
|
|
193
|
+
// validating `string[]` rejects it, and a reader that does not would have to
|
|
194
|
+
// guess which of the two the producer meant. Every list on the wire is a list.
|
|
195
|
+
if provenance.Capabilities == nil {
|
|
196
|
+
provenance.Capabilities = []string{}
|
|
197
|
+
}
|
|
198
|
+
if provenance.Sources == nil {
|
|
199
|
+
provenance.Sources = []SourceDigest{}
|
|
200
|
+
}
|
|
201
|
+
if provenance.Universe.Configs == nil {
|
|
202
|
+
provenance.Universe.Configs = []FileDigest{}
|
|
203
|
+
}
|
|
204
|
+
if provenance.Universe.Roots == nil {
|
|
205
|
+
provenance.Universe.Roots = []RootFile{}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
diagnostics := origin.Diagnostics
|
|
209
|
+
if diagnostics == nil {
|
|
210
|
+
diagnostics = []Diagnostic{}
|
|
211
|
+
}
|
|
212
|
+
|
|
163
213
|
return Dump{
|
|
164
|
-
Project:
|
|
165
|
-
Tsconfig:
|
|
166
|
-
|
|
167
|
-
|
|
214
|
+
Project: project,
|
|
215
|
+
Tsconfig: tsconfig,
|
|
216
|
+
Provenance: provenance,
|
|
217
|
+
Diagnostics: diagnostics,
|
|
218
|
+
Nodes: nodes,
|
|
219
|
+
Edges: edges,
|
|
168
220
|
}
|
|
169
221
|
}
|
|
170
222
|
|
|
171
223
|
// MarshalDump serializes a built graph to the export JSON, indented when pretty.
|
|
172
224
|
// 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)
|
|
225
|
+
func MarshalDump(g *Graph, project, tsconfig string, ignored map[string]bool, sources map[string]string, origin DumpOrigin, pretty bool) ([]byte, error) {
|
|
226
|
+
d := NewDump(g, project, tsconfig, ignored, sources, origin)
|
|
175
227
|
if pretty {
|
|
176
228
|
return json.MarshalIndent(d, "", " ")
|
|
177
229
|
}
|
|
@@ -186,13 +238,13 @@ func MarshalDump(g *Graph, project, tsconfig string, ignored map[string]bool, so
|
|
|
186
238
|
// string. On VS Code the document is 323 MB, so the conversion was a second full
|
|
187
239
|
// copy of it held live beside the first — half a gigabyte of peak heap that
|
|
188
240
|
// 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 {
|
|
241
|
+
func EncodeDump(w io.Writer, g *Graph, project, tsconfig string, ignored map[string]bool, sources map[string]string, origin DumpOrigin, pretty bool) error {
|
|
190
242
|
buffered := bufio.NewWriterSize(w, 1<<20)
|
|
191
243
|
encoder := json.NewEncoder(buffered)
|
|
192
244
|
if pretty {
|
|
193
245
|
encoder.SetIndent("", " ")
|
|
194
246
|
}
|
|
195
|
-
if err := encoder.Encode(NewDump(g, project, tsconfig, ignored, sources)); err != nil {
|
|
247
|
+
if err := encoder.Encode(NewDump(g, project, tsconfig, ignored, sources, origin)); err != nil {
|
|
196
248
|
return err
|
|
197
249
|
}
|
|
198
250
|
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)^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
|
}
|
|
@@ -47,7 +47,7 @@ export const enum Mode {
|
|
|
47
47
|
defer func() { _ = prog.Close() }()
|
|
48
48
|
|
|
49
49
|
g := Build(prog)
|
|
50
|
-
dump := NewDump(g, root, "tsconfig.json", nil, SourceTexts(prog))
|
|
50
|
+
dump := NewDump(g, root, "tsconfig.json", nil, SourceTexts(prog), DumpOrigin{})
|
|
51
51
|
|
|
52
52
|
byID := make(map[string]DumpNode, len(dump.Nodes))
|
|
53
53
|
for _, n := range dump.Nodes {
|