ttsc 0.19.2 → 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.
@@ -56,6 +56,15 @@ type DumpDecorator struct {
56
56
  Arguments []DumpDecoratorArgument `json:"arguments"`
57
57
  }
58
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
+
59
68
  // DumpNode is the wire shape of a graph node. Lowercase json keys are the
60
69
  // contract; the Go field names are not.
61
70
  type DumpNode struct {
@@ -69,6 +78,8 @@ type DumpNode struct {
69
78
  Exported bool `json:"exported,omitempty"`
70
79
  Closure bool `json:"closure,omitempty"`
71
80
  Modifiers []string `json:"modifiers,omitempty"`
81
+ Literals []string `json:"literals,omitempty"`
82
+ EnumMembers []DumpEnumMember `json:"enumMembers,omitempty"`
72
83
  Evidence *DumpEvidence `json:"evidence,omitempty"`
73
84
  Implementation *DumpEvidence `json:"implementation,omitempty"`
74
85
  Decorators []DumpDecorator `json:"decorators,omitempty"`
@@ -148,16 +159,20 @@ func NewDump(g *Graph, project, tsconfig string, ignored map[string]bool, source
148
159
  name, qualified = ctx.rel(name), ""
149
160
  }
150
161
  nodes = append(nodes, DumpNode{
151
- ID: ctx.relID(n.ID),
152
- Kind: string(n.Kind),
153
- Name: name,
154
- QualifiedName: qualified,
155
- File: ctx.rel(n.File),
156
- External: n.External,
157
- Ignored: ignored[n.File],
158
- Exported: n.Exported,
159
- Closure: n.Closure,
160
- 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),
161
176
  Evidence: withoutFile(ctx.evidence(n.File, n.Pos, n.End)),
162
177
  Implementation: ctx.evidence(n.ImplementationFile, n.ImplementationPos, n.ImplementationEnd),
163
178
  Decorators: decByNode[n.ID],
@@ -220,6 +235,19 @@ func NewDump(g *Graph, project, tsconfig string, ignored map[string]bool, source
220
235
  }
221
236
  }
222
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})
247
+ }
248
+ return out
249
+ }
250
+
223
251
  // MarshalDump serializes a built graph to the export JSON, indented when pretty.
224
252
  // See NewDump for the parameters.
225
253
  func MarshalDump(g *Graph, project, tsconfig string, ignored map[string]bool, sources map[string]string, origin DumpOrigin, pretty bool) ([]byte, error) {
@@ -29,7 +29,7 @@ func TestDumpSchemaVersionMatchesTheTypescriptClient(t *testing.T) {
29
29
  // The checkout is CRLF on Windows, and a line-anchored match would never
30
30
  // see the end of a line that ends in a carriage return.
31
31
  source = bytes.ReplaceAll(source, []byte("\r\n"), []byte("\n"))
32
- match := regexp.MustCompile(`(?m)^const DUMP_SCHEMA_VERSION = (\d+);$`).FindSubmatch(source)
32
+ match := regexp.MustCompile(`(?m)^(?:export )?const DUMP_SCHEMA_VERSION = (\d+);$`).FindSubmatch(source)
33
33
  if match == nil {
34
34
  t.Fatalf("no `const DUMP_SCHEMA_VERSION = <n>;` in %s; if it moved, this gate must follow it", reader)
35
35
  }
@@ -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
  }
@@ -0,0 +1,131 @@
1
+ package graph
2
+
3
+ import (
4
+ "path/filepath"
5
+ "testing"
6
+
7
+ "github.com/samchon/ttsc/packages/ttsc/driver"
8
+ )
9
+
10
+ // TestEnumMembersCarryTheirNamesOnTheEnumNode verifies that an enum records the
11
+ // name and value of each member on its own node, and that nothing else does.
12
+ //
13
+ // The enum's node was always in the graph and had nothing in it (#738). Its
14
+ // signature stops at the `{`, its members are not nodes, so the outline a class
15
+ // gets is empty for an enum — and #732 gave it only values. The code writes
16
+ // `Colors.Red`, never `"red"`, so a caller that had already named the enum
17
+ // still opened the file for the one fact it came for.
18
+ //
19
+ // The names ride on the enum rather than on member nodes on purpose. A member
20
+ // node would be indexing what `grep -rn "Colors.Red"` answers exactly, and it
21
+ // would put leaves into tour flows to do it. Filling in a node the graph holds
22
+ // is indexing; minting nodes to carry detail is not.
23
+ //
24
+ // 1. Compile a fixture with a string enum, an implicitly numbered one, and a
25
+ // class beside them.
26
+ // 2. Build the graph.
27
+ // 3. Assert each enum node carries its members name-and-value, that no member
28
+ // became a node, and that a class node carries none of this.
29
+ func TestEnumMembersCarryTheirNamesOnTheEnumNode(t *testing.T) {
30
+ root := t.TempDir()
31
+ writeFile(t, filepath.Join(root, "tsconfig.json"), fixtureTSConfig)
32
+ writeFile(t, filepath.Join(root, "src", "main.ts"), `export enum Colors {
33
+ Red = 'red',
34
+ Green = 'green',
35
+ }
36
+
37
+ export enum Implicit {
38
+ First,
39
+ Second,
40
+ }
41
+
42
+ // Two members, one value: a type folds these together, a declaration does not.
43
+ export enum Dup {
44
+ A = 'x',
45
+ B = 'x',
46
+ C = 'y',
47
+ }
48
+
49
+ export class Cls {
50
+ public value = 1;
51
+ }
52
+ `)
53
+
54
+ prog, diags, err := driver.LoadProgram(root, "tsconfig.json", driver.LoadProgramOptions{})
55
+ if err != nil {
56
+ t.Fatal(err)
57
+ }
58
+ if len(diags) != 0 {
59
+ t.Fatalf("unexpected diagnostics: %v", diags)
60
+ }
61
+ defer func() { _ = prog.Close() }()
62
+
63
+ graph := Build(prog)
64
+ path := sourceFile(t, prog, "main.ts").FileName()
65
+
66
+ colors, ok := graph.Nodes[nodeID(path, "Colors", NodeEnum)]
67
+ if !ok {
68
+ t.Fatalf("missing enum node; nodes: %v", nodeIDSet(graph))
69
+ }
70
+ want := []EnumMember{{Name: "Red", Value: `"red"`}, {Name: "Green", Value: `"green"`}}
71
+ if len(colors.EnumMembers) != len(want) {
72
+ t.Fatalf("Colors reported %v, want %v", colors.EnumMembers, want)
73
+ }
74
+ for i, member := range want {
75
+ if colors.EnumMembers[i] != member {
76
+ t.Fatalf("Colors member %d is %v, want %v", i, colors.EnumMembers[i], member)
77
+ }
78
+ }
79
+
80
+ // The values exist only in the checker here, so a scrape of the source could
81
+ // not produce this pairing.
82
+ implicit := graph.Nodes[nodeID(path, "Implicit", NodeEnum)]
83
+ if implicit == nil ||
84
+ len(implicit.EnumMembers) != 2 ||
85
+ implicit.EnumMembers[0] != (EnumMember{Name: "First", Value: "0"}) ||
86
+ implicit.EnumMembers[1] != (EnumMember{Name: "Second", Value: "1"}) {
87
+ t.Fatalf("implicitly numbered enum did not pair its members: %v", implicit.EnumMembers)
88
+ }
89
+
90
+ // Two members carrying one value. The declared type folds them into a single
91
+ // constituent — a type is a set — so reading the list off the type reports A
92
+ // and C and drops B, silently, which is #732's defect from #732's instinct:
93
+ // taking a declaration fact from a type. The list is the declaration's.
94
+ dup := graph.Nodes[nodeID(path, "Dup", NodeEnum)]
95
+ if dup == nil {
96
+ t.Fatalf("missing Dup; nodes: %v", nodeIDSet(graph))
97
+ }
98
+ if names := memberNames(dup.EnumMembers); len(names) != 3 ||
99
+ names[0] != "A" || names[1] != "B" || names[2] != "C" {
100
+ t.Fatalf("a member sharing another's value was dropped: %v", names)
101
+ }
102
+ // And the value set is right to say `"x"` once: two members, two names, but
103
+ // the values they admit really are two.
104
+ if len(dup.Literals) != 2 {
105
+ t.Fatalf("the value set should hold each distinct value once: %v", dup.Literals)
106
+ }
107
+
108
+ // The line this fix holds: the members are facts on the enum, not nodes of
109
+ // their own. A node per member would index what grep already answers.
110
+ for id := range graph.Nodes {
111
+ if id == nodeID(path, "Colors.Red", NodeVariable) ||
112
+ id == nodeID(path, "Red", NodeVariable) {
113
+ t.Fatalf("an enum member became a node (%s); it is a fact on the enum", id)
114
+ }
115
+ }
116
+
117
+ // The negative twin: this rides on enums only. A class's fields are member
118
+ // nodes and its outline comes from those.
119
+ if cls := graph.Nodes[nodeID(path, "Cls", NodeClass)]; cls == nil ||
120
+ len(cls.EnumMembers) != 0 {
121
+ t.Fatalf("a class node carried enum members: %v", cls)
122
+ }
123
+ }
124
+
125
+ func memberNames(members []EnumMember) []string {
126
+ out := make([]string, 0, len(members))
127
+ for _, member := range members {
128
+ out = append(out, member.Name)
129
+ }
130
+ return out
131
+ }
@@ -58,6 +58,32 @@ type Node struct {
58
58
  // the declaration's combined modifier flags during the build pass and emitted
59
59
  // for projections that filter on visibility and shape.
60
60
  Modifiers []string
61
+ // Literals is the complete value set of a type alias or enum whose declared
62
+ // type the checker resolved to literals, each rendered in TypeScript source
63
+ // form ("a", 1, true, null). It is set only when every constituent is
64
+ // enumerable, so a present list is the whole type and never a sample of it;
65
+ // a union that mixes in `string`, a type parameter, or a computed enum member
66
+ // has no complete answer and gets none.
67
+ //
68
+ // It is a checker fact because nothing else is sound. The value set was read
69
+ // off the declaration's source text for a while, which made the answer a
70
+ // function of line wrapping rather than of the type: a union written one
71
+ // member per line reported the members that fit in the snippet, an enum
72
+ // written across lines reported nothing at all, and `type I = Kind | 'f'`
73
+ // reported `'f'` while the members reaching it through `Kind` vanished (#732).
74
+ // The checker has already resolved every one of them, indirection included.
75
+ Literals []string
76
+ // EnumMembers is what an enum declares, in checker order: the name a caller
77
+ // writes and the value it carries. Empty for every other kind.
78
+ //
79
+ // The enum's node was always here and had nothing in it. `literals` says what
80
+ // values the enum admits, which answers a serializer; the code says
81
+ // `Colors.Red`, so a caller that had already named the enum still opened the
82
+ // file to learn what to type (#738). The members are not nodes of their own —
83
+ // `Colors.Red` is a literal string a grep finds exactly, and minting a node
84
+ // per member would grow the graph and put leaves into tour flows to index
85
+ // what grep already does. This fills in the node that exists instead.
86
+ EnumMembers []EnumMember
61
87
  // Pos and End bound the declaration in its source file (byte offsets). They
62
88
  // are for display, never identity, so an edit that shifts them does not re-key
63
89
  // the node.
@@ -68,6 +94,14 @@ type Node struct {
68
94
  ImplementationEnd int
69
95
  }
70
96
 
97
+ // EnumMember is one member of an enum: the name a caller writes and the value
98
+ // it carries, in TypeScript source form. Value is empty when the checker could
99
+ // not fold the member's initializer to a constant.
100
+ type EnumMember struct {
101
+ Name string
102
+ Value string
103
+ }
104
+
71
105
  // EdgeKind classifies a relationship between two nodes.
72
106
  type EdgeKind string
73
107
 
@@ -0,0 +1,175 @@
1
+ package graph
2
+
3
+ import (
4
+ shimast "github.com/microsoft/typescript-go/shim/ast"
5
+ shimchecker "github.com/microsoft/typescript-go/shim/checker"
6
+ )
7
+
8
+ // collectLiterals records the value set of every type alias and enum in file
9
+ // whose declared type resolves to literals, so a consumer asking what values a
10
+ // type admits is answered from the checker rather than from the declaration's
11
+ // source text.
12
+ //
13
+ // It runs on the checker for the reason markExports does: the syntactic reading
14
+ // is not the real answer. A union's members are only spelled out at the
15
+ // declaration when nothing indirects — `type Indirect = Kind | 'f'` names one
16
+ // literal and reaches five more through an alias, and only the checker has
17
+ // resolved that. It has also already flattened nested unions, dropped
18
+ // duplicates, and reduced subtypes, so the constituents it hands back are the
19
+ // type's members exactly once each.
20
+ //
21
+ // Enums come here too, and they need it more. An enum's members are not nodes
22
+ // of their own — the build pass records member nodes for classes and interfaces
23
+ // only — so its node is the only place anything about them can ride. It carries
24
+ // two facts, and they are not the same fact: the value set the enum admits,
25
+ // deduped because a type is a set, and the members it declares, which is the
26
+ // list a caller writes from and which nothing folds.
27
+ func (g *Graph) collectLiterals(checker *shimchecker.Checker, file *shimast.SourceFile) {
28
+ if file.Statements == nil {
29
+ return
30
+ }
31
+ g.collectLiteralsIn(checker, file.FileName(), file.Statements.Nodes)
32
+ }
33
+
34
+ // collectLiteralsIn walks a statement list — a file's top level, or a namespace
35
+ // body it recurses into — and records the value set of each type alias and enum
36
+ // it holds. It mirrors collectStatements' descent so a `namespace X { type K =
37
+ // … }` is resolved on the node the build pass keyed by its qualified name.
38
+ func (g *Graph) collectLiteralsIn(checker *shimchecker.Checker, path string, statements []*shimast.Node) {
39
+ for _, statement := range statements {
40
+ switch statement.Kind {
41
+ case shimast.KindTypeAliasDeclaration:
42
+ g.putLiterals(checker, path, statement, NodeTypeAlias)
43
+ case shimast.KindEnumDeclaration:
44
+ g.putLiterals(checker, path, statement, NodeEnum)
45
+ case shimast.KindModuleDeclaration:
46
+ g.collectLiteralsIn(checker, path, moduleStatements(statement))
47
+ }
48
+ }
49
+ }
50
+
51
+ // putLiterals resolves the declared type of statement's symbol and records its
52
+ // value set on the node the build pass recorded for it. A declaration the graph
53
+ // does not hold a node for, or whose type has no complete literal answer, is
54
+ // left alone rather than given a partial one.
55
+ func (g *Graph) putLiterals(checker *shimchecker.Checker, path string, statement *shimast.Node, kind NodeKind) {
56
+ symbol := statement.Symbol()
57
+ if symbol == nil || symbol.Name == "" {
58
+ return
59
+ }
60
+ node, ok := g.Nodes[nodeID(path, qualifiedName(symbol), kind)]
61
+ if !ok {
62
+ return
63
+ }
64
+ declared := shimchecker.Checker_getDeclaredTypeOfSymbol(checker, symbol)
65
+ if declared == nil {
66
+ return
67
+ }
68
+ if values, ok := literalValues(declared); ok {
69
+ node.Literals = values
70
+ }
71
+ if kind == NodeEnum {
72
+ node.EnumMembers = enumMembers(checker, statement)
73
+ }
74
+ }
75
+
76
+ // enumMembers pairs each member an enum declares with the value it carries.
77
+ //
78
+ // The names are the half a caller writes. `literals` answers which values the
79
+ // enum admits, the question a serializer asks, but the code says `Colors.Red`
80
+ // and never `"red"` — so an enum whose node the graph already held still sent a
81
+ // caller to the file to learn what to type (#738).
82
+ //
83
+ // The list comes from the declaration, not from the declared type's
84
+ // constituents, and that is not a detail. A type is a set, so the checker folds
85
+ // two members carrying one value into one constituent: `enum Dup { A = 'x', B =
86
+ // 'x' }` resolves to a single type, and reading names off it reports `A` and
87
+ // loses `B` — the same silent under-report as #732, out of the same instinct to
88
+ // take a declaration fact from a type. The value set really is one value, which
89
+ // is why `literals` is right to say `"x"` once; the member list is two members,
90
+ // and only the declaration says so.
91
+ //
92
+ // Each member's value still comes from the checker, per member, so an implicit
93
+ // `First` reports 0 with nothing reading the source. A member the checker
94
+ // cannot fold to a constant keeps its name and drops its value, because the
95
+ // name is the part this is for.
96
+ func enumMembers(checker *shimchecker.Checker, statement *shimast.Node) []EnumMember {
97
+ declaration := statement.AsEnumDeclaration()
98
+ if declaration == nil || declaration.Members == nil {
99
+ return nil
100
+ }
101
+ out := make([]EnumMember, 0, len(declaration.Members.Nodes))
102
+ for _, node := range declaration.Members.Nodes {
103
+ symbol := node.Symbol()
104
+ if symbol == nil || symbol.Name == "" {
105
+ continue
106
+ }
107
+ member := EnumMember{Name: symbol.Name}
108
+ if t := shimchecker.Checker_getTypeOfSymbol(checker, symbol); t != nil {
109
+ if value, ok := literalValue(t); ok {
110
+ member.Value = value
111
+ }
112
+ }
113
+ out = append(out, member)
114
+ }
115
+ if len(out) == 0 {
116
+ return nil
117
+ }
118
+ return out
119
+ }
120
+
121
+ // literalValues renders every constituent of t in TypeScript source form,
122
+ // reporting false unless all of them are enumerable.
123
+ //
124
+ // All-or-nothing is the contract. `type T = Kind | number` admits five literals
125
+ // and every other number besides; answering with the five would describe a type
126
+ // that does not exist, and the caller cannot tell that answer from a complete
127
+ // one. So a type whose members cannot all be named yields no list, and its
128
+ // signature carries the shape instead.
129
+ func literalValues(t *shimchecker.Type) ([]string, bool) {
130
+ // Types() panics on anything that is not a union, an intersection, or a
131
+ // template literal, so the flag test is what makes the union branch safe. A
132
+ // single-literal alias (`type One = 'a'`) is not a union and is its own only
133
+ // constituent.
134
+ constituents := []*shimchecker.Type{t}
135
+ if t.Flags()&shimchecker.TypeFlagsUnion != 0 {
136
+ constituents = t.Types()
137
+ }
138
+ values := make([]string, 0, len(constituents))
139
+ for _, constituent := range constituents {
140
+ value, ok := literalValue(constituent)
141
+ if !ok {
142
+ return nil, false
143
+ }
144
+ values = append(values, value)
145
+ }
146
+ if len(values) == 0 {
147
+ return nil, false
148
+ }
149
+ return values, true
150
+ }
151
+
152
+ // literalValue renders one constituent in TypeScript source form, reporting
153
+ // false when it names no single value a caller could write.
154
+ func literalValue(t *shimchecker.Type) (string, bool) {
155
+ flags := t.Flags()
156
+ switch {
157
+ case flags&shimchecker.TypeFlagsNull != 0:
158
+ return "null", true
159
+ case flags&shimchecker.TypeFlagsUndefined != 0:
160
+ return "undefined", true
161
+ case flags&shimchecker.TypeFlagsLiteral != 0:
162
+ // TypeFlagsLiteral covers an enum member too: EnumLiteral is always paired
163
+ // with StringLiteral or NumberLiteral on a member, and with Union on the
164
+ // enum type itself, which the union branch above has already opened.
165
+ value := t.AsLiteralType().Value()
166
+ if value == nil {
167
+ // A computed enum member the checker could not fold to a constant. It has
168
+ // a value at runtime that nothing here can name, so the enum has no
169
+ // complete answer.
170
+ return "", false
171
+ }
172
+ return shimchecker.ValueToString(value), true
173
+ }
174
+ return "", false
175
+ }
@@ -0,0 +1,74 @@
1
+ package graph
2
+
3
+ import (
4
+ "path/filepath"
5
+ "testing"
6
+
7
+ "github.com/samchon/ttsc/packages/ttsc/driver"
8
+ )
9
+
10
+ // TestLiteralsAreAbsentWhenTheTypeAdmitsMoreThanItCanName verifies the negative
11
+ // twin of every literal case: a type whose members cannot all be named reports
12
+ // none of them rather than the subset that can.
13
+ //
14
+ // This is what lets a present `literals` mean the whole type. `Widened` admits
15
+ // four literals and every other string besides, and a caller cannot tell a
16
+ // four-value answer for it from a four-value answer for a genuine four-member
17
+ // union — so it must get no answer, and read the signature instead. Each fixture
18
+ // here is one way a constituent stops being nameable: a primitive next to the
19
+ // literals, an unresolved type parameter, a plain primitive alias, and a
20
+ // computed enum member whose value the checker cannot fold to a constant.
21
+ //
22
+ // 1. Compile a fixture holding each non-enumerable shape.
23
+ // 2. Build the graph.
24
+ // 3. Assert none of them recorded a value set.
25
+ func TestLiteralsAreAbsentWhenTheTypeAdmitsMoreThanItCanName(t *testing.T) {
26
+ root := t.TempDir()
27
+ writeFile(t, filepath.Join(root, "tsconfig.json"), fixtureTSConfig)
28
+ writeFile(t, filepath.Join(root, "src", "main.ts"), `declare const compute: () => number;
29
+
30
+ export type Narrow = 'a' | 'b' | 'c' | 'd';
31
+ export type Widened = Narrow | string;
32
+ export type Generic<T> = T | 'a';
33
+ export type Primitive = string;
34
+
35
+ export enum Computed {
36
+ A = 'a',
37
+ B = compute(),
38
+ }
39
+ `)
40
+
41
+ prog, diags, err := driver.LoadProgram(root, "tsconfig.json", driver.LoadProgramOptions{})
42
+ if err != nil {
43
+ t.Fatal(err)
44
+ }
45
+ if len(diags) != 0 {
46
+ t.Fatalf("unexpected diagnostics: %v", diags)
47
+ }
48
+ defer func() { _ = prog.Close() }()
49
+
50
+ graph := Build(prog)
51
+ path := sourceFile(t, prog, "main.ts").FileName()
52
+
53
+ // The control: the shapes below differ from this one by exactly the property
54
+ // that makes them unnameable, so an empty result there is the rule acting and
55
+ // not the pass failing to run on this fixture at all.
56
+ if got := literalsOf(t, graph, nodeID(path, "Narrow", NodeTypeAlias)); len(got) != 4 {
57
+ t.Fatalf("control union did not report its four members: got %v", got)
58
+ }
59
+
60
+ for _, testCase := range []struct {
61
+ name string
62
+ kind NodeKind
63
+ why string
64
+ }{
65
+ {"Widened", NodeTypeAlias, "a union with `string` in it admits values no list can name"},
66
+ {"Generic", NodeTypeAlias, "an unresolved type parameter names no value"},
67
+ {"Primitive", NodeTypeAlias, "a primitive is not a value set"},
68
+ {"Computed", NodeEnum, "a computed enum member has a value nothing here can name"},
69
+ } {
70
+ if got := literalsOf(t, graph, nodeID(path, testCase.name, testCase.kind)); len(got) != 0 {
71
+ t.Fatalf("%s reported a partial value set %v: %s", testCase.name, got, testCase.why)
72
+ }
73
+ }
74
+ }
@@ -0,0 +1,79 @@
1
+ package graph
2
+
3
+ import (
4
+ "path/filepath"
5
+ "slices"
6
+ "testing"
7
+
8
+ "github.com/samchon/ttsc/packages/ttsc/driver"
9
+ )
10
+
11
+ // TestLiteralsCoverEnumMembersTheGraphHasNoNodesFor verifies that an enum
12
+ // reports its member values, string-valued, numeric, and implicitly numbered
13
+ // alike, however the declaration is laid out.
14
+ //
15
+ // An enum needed this more than a union did (#732). Its members are not nodes —
16
+ // the build pass records member nodes for classes and interfaces only — so
17
+ // nothing else in a details result carries them, and the old source-text scrape
18
+ // stopped at the line holding `{`, which is the first line of every enum written
19
+ // the ordinary way. A multi-line enum therefore reported no members at all,
20
+ // through `literals` or otherwise, and only a single-line one answered. The
21
+ // implicit fixture pins that the values come from the checker rather than the
22
+ // text: nothing in `Implicit`'s source spells out 0 and 1.
23
+ //
24
+ // 1. Compile a fixture with a multi-line string enum, a single-line one, and an
25
+ // enum whose members are implicitly numbered.
26
+ // 2. Build the graph.
27
+ // 3. Assert each reports its resolved member values, and that the enum still
28
+ // has no member nodes so `literals` is genuinely the only carrier.
29
+ func TestLiteralsCoverEnumMembersTheGraphHasNoNodesFor(t *testing.T) {
30
+ root := t.TempDir()
31
+ writeFile(t, filepath.Join(root, "tsconfig.json"), fixtureTSConfig)
32
+ writeFile(t, filepath.Join(root, "src", "main.ts"), `export enum Wrapped {
33
+ A = 'a',
34
+ B = 'b',
35
+ C = 'c',
36
+ }
37
+
38
+ export enum Flat { A = 'a', B = 'b', C = 'c' }
39
+
40
+ export enum Implicit {
41
+ First,
42
+ Second,
43
+ }
44
+ `)
45
+
46
+ prog, diags, err := driver.LoadProgram(root, "tsconfig.json", driver.LoadProgramOptions{})
47
+ if err != nil {
48
+ t.Fatal(err)
49
+ }
50
+ if len(diags) != 0 {
51
+ t.Fatalf("unexpected diagnostics: %v", diags)
52
+ }
53
+ defer func() { _ = prog.Close() }()
54
+
55
+ graph := Build(prog)
56
+ path := sourceFile(t, prog, "main.ts").FileName()
57
+
58
+ // The declaration whose members the old scrape could never reach.
59
+ wrapped := literalsOf(t, graph, nodeID(path, "Wrapped", NodeEnum))
60
+ if want := []string{`"a"`, `"b"`, `"c"`}; !slices.Equal(wrapped, want) {
61
+ t.Fatalf("multi-line enum under-reported its members: got %v, want %v", wrapped, want)
62
+ }
63
+ // Layout is not the fact; the single-line twin must agree exactly.
64
+ flat := literalsOf(t, graph, nodeID(path, "Flat", NodeEnum))
65
+ if !slices.Equal(flat, wrapped) {
66
+ t.Fatalf("enum layout changed the value set: wrapped %v, flat %v", wrapped, flat)
67
+ }
68
+ // Implicitly numbered members: the values exist only in the checker, never in
69
+ // the source text, so a scrape of any kind cannot produce this list.
70
+ implicit := literalsOf(t, graph, nodeID(path, "Implicit", NodeEnum))
71
+ if want := []string{"0", "1"}; !slices.Equal(implicit, want) {
72
+ t.Fatalf("implicitly numbered enum did not report its resolved values: got %v, want %v", implicit, want)
73
+ }
74
+ // The premise: no member node exists to carry these instead, so dropping
75
+ // `literals` for an enum would take its members out of the graph entirely.
76
+ if node, ok := graph.Nodes[nodeID(path, "Wrapped.A", NodeMethod)]; ok {
77
+ t.Fatalf("enum member unexpectedly modeled as a node (%v); literals may no longer be the only carrier", node.ID)
78
+ }
79
+ }
@@ -0,0 +1,84 @@
1
+ package graph
2
+
3
+ import (
4
+ "path/filepath"
5
+ "slices"
6
+ "testing"
7
+
8
+ "github.com/samchon/ttsc/packages/ttsc/driver"
9
+ )
10
+
11
+ // TestLiteralsDoNotDependOnDeclarationLineWrapping verifies that collectLiterals
12
+ // answers with the union's members whatever the declaration's layout: the same
13
+ // eight-member type written one member per line and written on one line both
14
+ // report all eight.
15
+ //
16
+ // This is the #732 regression. The value set used to be scraped out of the
17
+ // declaration's first four lines of source text and cut to six, so completeness
18
+ // was a function of line wrapping and of member count rather than of the type:
19
+ // the wrapped form reported the three members that fit in the snippet, and the
20
+ // flat form reported six of eight. Both cuts were silent. Eight members is the
21
+ // boundary that pins both at once — it is past the old six-member cap, so a
22
+ // reintroduced cap fails here on the flat twin instead of hiding behind a small
23
+ // fixture.
24
+ //
25
+ // 1. Compile a fixture with a wrapped union and a flat union of the same eight
26
+ // string literals.
27
+ // 2. Build the graph.
28
+ // 3. Assert both nodes carry all eight values, in source form, and that the two
29
+ // lists are identical.
30
+ func TestLiteralsDoNotDependOnDeclarationLineWrapping(t *testing.T) {
31
+ root := t.TempDir()
32
+ writeFile(t, filepath.Join(root, "tsconfig.json"), fixtureTSConfig)
33
+ writeFile(t, filepath.Join(root, "src", "main.ts"), `export type Wrapped =
34
+ | 'a' // a trailing comment the old snippet scrape also had to survive
35
+ | 'b'
36
+ | 'c'
37
+ | 'd'
38
+ | 'e'
39
+ | 'f'
40
+ | 'g'
41
+ | 'h';
42
+
43
+ export type Flat = 'a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'g' | 'h';
44
+ `)
45
+
46
+ prog, diags, err := driver.LoadProgram(root, "tsconfig.json", driver.LoadProgramOptions{})
47
+ if err != nil {
48
+ t.Fatal(err)
49
+ }
50
+ if len(diags) != 0 {
51
+ t.Fatalf("unexpected diagnostics: %v", diags)
52
+ }
53
+ defer func() { _ = prog.Close() }()
54
+
55
+ graph := Build(prog)
56
+ path := sourceFile(t, prog, "main.ts").FileName()
57
+
58
+ want := []string{`"a"`, `"b"`, `"c"`, `"d"`, `"e"`, `"f"`, `"g"`, `"h"`}
59
+ wrapped := literalsOf(t, graph, nodeID(path, "Wrapped", NodeTypeAlias))
60
+ flat := literalsOf(t, graph, nodeID(path, "Flat", NodeTypeAlias))
61
+
62
+ if !slices.Equal(wrapped, want) {
63
+ t.Fatalf("wrapped union under-reported its members: got %v, want %v", wrapped, want)
64
+ }
65
+ if !slices.Equal(flat, want) {
66
+ t.Fatalf("flat union under-reported its members: got %v, want %v", flat, want)
67
+ }
68
+ // The point of the fix stated directly: layout is not allowed to change the
69
+ // answer for one and the same type.
70
+ if !slices.Equal(wrapped, flat) {
71
+ t.Fatalf("line wrapping changed the value set: wrapped %v, flat %v", wrapped, flat)
72
+ }
73
+ }
74
+
75
+ // literalsOf returns the recorded value set of the node id names, failing the
76
+ // test when the graph holds no such node.
77
+ func literalsOf(t *testing.T, graph *Graph, id string) []string {
78
+ t.Helper()
79
+ node, ok := graph.Nodes[id]
80
+ if !ok {
81
+ t.Fatalf("graph has no node %q; nodes: %v", id, nodeIDSet(graph))
82
+ }
83
+ return node.Literals
84
+ }
@@ -0,0 +1,77 @@
1
+ package graph
2
+
3
+ import (
4
+ "path/filepath"
5
+ "slices"
6
+ "testing"
7
+
8
+ "github.com/samchon/ttsc/packages/ttsc/driver"
9
+ )
10
+
11
+ // TestLiteralsRenderEveryValueInTypescriptSourceForm verifies that each kind of
12
+ // literal a union can hold reaches the wire as the way it is written, and that
13
+ // the rendering is the checker's own rather than this package's.
14
+ //
15
+ // Source form is what makes the list readable as the type: it is the only
16
+ // rendering that tells `1` from `"1"` and `true` from `"true"`, which bare
17
+ // contents cannot, and the old string-only scrape could not report a numeric
18
+ // union at all. The escaping fixture is why the checker's `ValueToString` is
19
+ // used instead of a local formatter — a quote inside a value has to come back
20
+ // escaped the way TypeScript escapes it, and Go's own quoting is not that.
21
+ //
22
+ // 1. Compile a fixture with a string, numeric, boolean, bigint, nullable, and
23
+ // escaped-string union.
24
+ // 2. Build the graph.
25
+ // 3. Assert each value set renders in TypeScript source form.
26
+ func TestLiteralsRenderEveryValueInTypescriptSourceForm(t *testing.T) {
27
+ root := t.TempDir()
28
+ writeFile(t, filepath.Join(root, "tsconfig.json"), fixtureTSConfig)
29
+ writeFile(t, filepath.Join(root, "src", "main.ts"), `export type Strings = 'a' | 'b';
30
+ export type Numbers = 1 | 2 | 3;
31
+ export type Bools = true | false;
32
+ export type Bigints = 1n | 2n;
33
+ export type Nullable = 'a' | null;
34
+ export type Optional = 'a' | undefined;
35
+ export type Escaped = 'he said "hi"' | "it's" | 'tab\there';
36
+ export type Mixed = 'a' | 1 | true;
37
+ `)
38
+
39
+ prog, diags, err := driver.LoadProgram(root, "tsconfig.json", driver.LoadProgramOptions{})
40
+ if err != nil {
41
+ t.Fatal(err)
42
+ }
43
+ if len(diags) != 0 {
44
+ t.Fatalf("unexpected diagnostics: %v", diags)
45
+ }
46
+ defer func() { _ = prog.Close() }()
47
+
48
+ graph := Build(prog)
49
+ path := sourceFile(t, prog, "main.ts").FileName()
50
+
51
+ for _, testCase := range []struct {
52
+ name string
53
+ want []string
54
+ }{
55
+ // A string keeps its quotes, so the list reads as the type does.
56
+ {"Strings", []string{`"a"`, `"b"`}},
57
+ // Numbers were absent entirely under the string-only scrape.
58
+ {"Numbers", []string{"1", "2", "3"}},
59
+ {"Bools", []string{"false", "true"}},
60
+ {"Bigints", []string{"1n", "2n"}},
61
+ // A unit type that is not a literal still names one value a caller writes,
62
+ // so the set stays complete rather than dropping to nothing.
63
+ {"Nullable", []string{"null", `"a"`}},
64
+ {"Optional", []string{"undefined", `"a"`}},
65
+ // The checker's escaping, not Go's: an inner double quote is backslashed,
66
+ // an apostrophe is not re-quoted, and a tab stays an escape rather than a
67
+ // raw control character.
68
+ {"Escaped", []string{`"he said \"hi\""`, `"it's"`, `"tab\there"`}},
69
+ // Kinds mix in one union, and source form is what keeps them apart.
70
+ {"Mixed", []string{`"a"`, "1", "true"}},
71
+ } {
72
+ got := literalsOf(t, graph, nodeID(path, testCase.name, NodeTypeAlias))
73
+ if !slices.Equal(got, testCase.want) {
74
+ t.Fatalf("%s rendered %v, want %v", testCase.name, got, testCase.want)
75
+ }
76
+ }
77
+ }
@@ -0,0 +1,64 @@
1
+ package graph
2
+
3
+ import (
4
+ "path/filepath"
5
+ "slices"
6
+ "testing"
7
+
8
+ "github.com/samchon/ttsc/packages/ttsc/driver"
9
+ )
10
+
11
+ // TestLiteralsResolveThroughAliasIndirection verifies that a union assembled out
12
+ // of other unions reports every member it admits, including the ones no token of
13
+ // its own declaration names.
14
+ //
15
+ // This is the half of #732 that was not truncation but a wrong answer of the
16
+ // right shape. Reading `type Wide = Narrow | 'd'` off the source text finds one
17
+ // quoted token and reports `'d'`, a complete-looking three-member type reduced
18
+ // to one, with nothing marking the loss. Only the checker has followed Narrow.
19
+ // The nested case (`Wider = Wide | 'e'`) pins that the resolution is not one hop
20
+ // deep, and the checker also flattens and dedups, so `Duplicated` must report
21
+ // `'a'` once rather than twice.
22
+ //
23
+ // 1. Compile a fixture whose aliases build on each other, one of them
24
+ // re-listing a member an aliased union already has.
25
+ // 2. Build the graph.
26
+ // 3. Assert each alias reports the full set it admits, deduplicated.
27
+ func TestLiteralsResolveThroughAliasIndirection(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 type Narrow = 'a' | 'b' | 'c';
31
+ export type Wide = Narrow | 'd';
32
+ export type Wider = Wide | 'e';
33
+ export type Duplicated = Narrow | 'a';
34
+ `)
35
+
36
+ prog, diags, err := driver.LoadProgram(root, "tsconfig.json", driver.LoadProgramOptions{})
37
+ if err != nil {
38
+ t.Fatal(err)
39
+ }
40
+ if len(diags) != 0 {
41
+ t.Fatalf("unexpected diagnostics: %v", diags)
42
+ }
43
+ defer func() { _ = prog.Close() }()
44
+
45
+ graph := Build(prog)
46
+ path := sourceFile(t, prog, "main.ts").FileName()
47
+
48
+ // One hop: the three members reaching Wide through Narrow are Wide's too.
49
+ wide := literalsOf(t, graph, nodeID(path, "Wide", NodeTypeAlias))
50
+ if want := []string{`"a"`, `"b"`, `"c"`, `"d"`}; !slices.Equal(wide, want) {
51
+ t.Fatalf("alias indirection lost members: got %v, want %v", wide, want)
52
+ }
53
+ // Two hops: resolution is the type's, so depth costs nothing.
54
+ wider := literalsOf(t, graph, nodeID(path, "Wider", NodeTypeAlias))
55
+ if want := []string{`"a"`, `"b"`, `"c"`, `"d"`, `"e"`}; !slices.Equal(wider, want) {
56
+ t.Fatalf("nested alias indirection lost members: got %v, want %v", wider, want)
57
+ }
58
+ // A member named twice is one member; the checker has already deduped, so the
59
+ // list must not report `"a"` once per mention.
60
+ duplicated := literalsOf(t, graph, nodeID(path, "Duplicated", NodeTypeAlias))
61
+ if want := []string{`"a"`, `"b"`, `"c"`}; !slices.Equal(duplicated, want) {
62
+ t.Fatalf("a member named twice was reported twice: got %v, want %v", duplicated, want)
63
+ }
64
+ }
@@ -14,7 +14,7 @@ import (
14
14
  // field is added, removed, or given a new meaning, independently of the serve
15
15
  // envelope's protocol version: a one-shot `ttscgraph dump` written to a file has
16
16
  // a schema but never rode the protocol.
17
- const DumpSchemaVersion = 1
17
+ const DumpSchemaVersion = 3
18
18
 
19
19
  // The capabilities a snapshot can declare. Each names one class of evidence a
20
20
  // consumer may rely on when, and only when, the snapshot lists it.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ttsc",
3
- "version": "0.19.2",
3
+ "version": "0.19.3",
4
4
  "description": "General-purpose TypeScript-Go compiler, runtime, plugin host, and LSP host.",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",
@@ -35,13 +35,13 @@
35
35
  "typescript": "^7.0.2"
36
36
  },
37
37
  "optionalDependencies": {
38
- "@ttsc/linux-x64": "0.19.2",
39
- "@ttsc/linux-arm": "0.19.2",
40
- "@ttsc/linux-arm64": "0.19.2",
41
- "@ttsc/darwin-x64": "0.19.2",
42
- "@ttsc/win32-x64": "0.19.2",
43
- "@ttsc/win32-arm64": "0.19.2",
44
- "@ttsc/darwin-arm64": "0.19.2"
38
+ "@ttsc/linux-x64": "0.19.3",
39
+ "@ttsc/linux-arm64": "0.19.3",
40
+ "@ttsc/linux-arm": "0.19.3",
41
+ "@ttsc/darwin-arm64": "0.19.3",
42
+ "@ttsc/win32-x64": "0.19.3",
43
+ "@ttsc/darwin-x64": "0.19.3",
44
+ "@ttsc/win32-arm64": "0.19.3"
45
45
  },
46
46
  "sideEffects": false,
47
47
  "files": [
@@ -49,6 +49,19 @@ func Checker_getRegularTypeOfLiteralType(recv *innerchecker.Checker, t *innerche
49
49
  return checkerGetRegularTypeOfLiteralType(recv, t)
50
50
  }
51
51
 
52
+ // ValueToString renders a literal type's value in TypeScript source form: a
53
+ // string as a double-quoted, escaped literal, a number, boolean, or bigint as
54
+ // the way it is written. It is the checker's own renderer, so a consumer
55
+ // enumerating a literal union reports the values the way the compiler prints
56
+ // them, including numeric formatting and string escaping it should not re-derive.
57
+ //
58
+ // It panics on a value it does not handle — notably the nil a computed enum
59
+ // member carries — so a caller holding a `LiteralType.Value()` must reject nil
60
+ // before calling this.
61
+ func ValueToString(value any) string {
62
+ return innerchecker.ValueToString(value)
63
+ }
64
+
52
65
  // Checker_typeToStringFullyQualified formats a type with the same stable,
53
66
  // alias-aware flags TypeScript uses in diagnostics that name union members.
54
67
  // Keeping the flag bundle inside the shim avoids leaking checker-internal enum