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.
Files changed (47) hide show
  1. package/README.md +68 -186
  2. package/cmd/graphdump/main.go +21 -1
  3. package/cmd/ttsc/api_compile.go +3 -13
  4. package/cmd/ttsc/api_transform.go +13 -0
  5. package/cmd/ttscgraph/dump.go +40 -1
  6. package/cmd/ttscgraph/serve.go +194 -25
  7. package/cmd/ttscgraph/serve_error_response_still_names_its_mode_test.go +73 -0
  8. package/cmd/ttscgraph/serve_protocol_fails_the_stream_on_an_unaddressable_request_line_test.go +43 -0
  9. package/cmd/ttscgraph/serve_protocol_version_matches_the_typescript_client_test.go +48 -0
  10. package/cmd/ttscgraph/serve_snapshot_proves_its_program_without_a_second_read_test.go +109 -0
  11. package/driver/graph.go +133 -0
  12. package/internal/graph/diagnostics.go +6 -5
  13. package/internal/graph/dump.go +107 -27
  14. package/internal/graph/dump_diagnostics_ride_the_generation_that_built_it_test.go +79 -0
  15. package/internal/graph/dump_evidence_skips_leading_trivia_test.go +1 -1
  16. package/internal/graph/dump_is_a_function_of_the_source_test.go +1 -1
  17. package/internal/graph/dump_never_serializes_a_list_as_null_test.go +76 -0
  18. package/internal/graph/dump_schema_version_matches_the_typescript_client_test.go +48 -0
  19. package/internal/graph/dump_serializes_the_full_graph_test.go +21 -7
  20. package/internal/graph/edges.go +12 -5
  21. package/internal/graph/enum_members_carry_their_names_on_the_enum_node_test.go +131 -0
  22. package/internal/graph/graph.go +34 -0
  23. package/internal/graph/literals.go +175 -0
  24. package/internal/graph/literals_are_absent_when_the_type_admits_more_than_it_can_name_test.go +74 -0
  25. package/internal/graph/literals_cover_enum_members_the_graph_has_no_nodes_for_test.go +79 -0
  26. package/internal/graph/literals_do_not_depend_on_declaration_line_wrapping_test.go +84 -0
  27. package/internal/graph/literals_render_every_value_in_typescript_source_form_test.go +77 -0
  28. package/internal/graph/literals_resolve_through_alias_indirection_test.go +64 -0
  29. package/internal/graph/node_modifiers_emit_union_strings_test.go +1 -1
  30. package/internal/graph/provenance.go +312 -0
  31. package/lib/TtscCompiler.js +9 -4
  32. package/lib/TtscCompiler.js.map +1 -1
  33. package/lib/compiler/internal/transformProjectInMemory.d.ts +4 -0
  34. package/lib/compiler/internal/transformProjectInMemory.js +67 -12
  35. package/lib/compiler/internal/transformProjectInMemory.js.map +1 -1
  36. package/lib/structures/ITtscCompilerTransformation.d.ts +98 -0
  37. package/package.json +8 -8
  38. package/shim/checker/shim.go +13 -0
  39. package/shim/compiler/incremental.go +58 -0
  40. package/shim/core/shim.go +5 -0
  41. package/src/TtscCompiler.ts +19 -4
  42. package/src/compiler/internal/transformProjectInMemory.ts +106 -12
  43. package/src/structures/ITtscCompilerTransformation.ts +107 -0
  44. package/tools/shim_audit/baseline.json +2 -1
  45. package/tools/shim_audit/main.go +17 -0
  46. package/utility/host.go +17 -15
  47. package/cmd/ttscgraph/serve_protocol_rejects_invalid_request_line_test.go +0 -43
@@ -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. Nothing in the
12
- // graph pipeline calls it yet; the dump and MCP results carry no diagnostics.
13
- // This helper and its tests pin the compiler-diagnostic conversion (code,
14
- // column, message) so a future diagnostics integration starts from a proven
15
- // producer.
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() {
@@ -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 and the full
88
- // node and edge sets with none of the MCP response caps.
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 `json:"project"`
91
- Tsconfig string `json:"tsconfig"`
92
- Nodes []DumpNode `json:"nodes"`
93
- Edges []DumpEdge `json:"edges"`
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
- func NewDump(g *Graph, project, tsconfig string, ignored map[string]bool, sources map[string]string) Dump {
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: ctx.relID(n.ID),
128
- Kind: string(n.Kind),
129
- Name: name,
130
- QualifiedName: qualified,
131
- File: ctx.rel(n.File),
132
- External: n.External,
133
- Ignored: ignored[n.File],
134
- Exported: n.Exported,
135
- Closure: n.Closure,
136
- Modifiers: n.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: project,
165
- Tsconfig: tsconfig,
166
- Nodes: nodes,
167
- Edges: edges,
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
- // The dump carries no ceremony keys: it is wholly checker-resolved, so it has
142
- // no per-edge trust flags and no schema version to negotiate. It also carries
143
- // no keys the schema stopped promising: no inlined evidence text and no
144
- // diagnostics array.
145
- for _, gone := range []string{`"schemaVersion"`, `"provenance":`, `"confidence":`, `"text":`, `"diagnostics":`} {
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
  }
@@ -7,11 +7,17 @@ import (
7
7
  "github.com/samchon/ttsc/packages/ttsc/driver"
8
8
  )
9
9
 
10
- // addEdges resolves the relationships between the declaration nodes Build
11
- // recorded. It walks each source file again and, for every class or interface,
12
- // resolves its heritage bases through the checker (unwrapping barrel re-exports
13
- // to the real declaration) and links the declaration to that base, materializing
14
- // an external boundary-leaf node when the base lives in node_modules or a `.d.ts`.
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
  }