typia 14.0.3 → 14.0.4

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.
@@ -0,0 +1,135 @@
1
+ package main
2
+
3
+ import (
4
+ "os"
5
+ "os/exec"
6
+ "path/filepath"
7
+ "strings"
8
+ "testing"
9
+ )
10
+
11
+ // TestBigintLiteralPrecisionTransform verifies a bigint literal reaches the
12
+ // emit exactly, at any magnitude.
13
+ //
14
+ // `ExpressionFactory.Bigint` spelled its argument as a bare number literal, so
15
+ // `9007199254740993n` was emitted as `BigInt(9007199254740993)` and JavaScript
16
+ // rounded that literal to 9007199254740992 before `BigInt` ever parsed it. The
17
+ // validator it built therefore accepted the wrong value: `is<9007199254740993n>`
18
+ // returned true for 9007199254740992n and false for the literal it was asked
19
+ // about. The repository already spells this hazard out for the two places that
20
+ // avoided it -- `numericRangeFactory_bigint_literal` and `_isTypeInt64Bigint` --
21
+ // and this factory is the one that did not.
22
+ //
23
+ // The factory feeds every bigint the transform emits, so the case sweeps the
24
+ // operations that reach it rather than `is` alone.
25
+ //
26
+ // 1. Build validators over bigint literals past 2**53 and at the int64 bounds.
27
+ // 2. Require the emitted code to pass the digits as a string, never as a
28
+ // number literal that rounds before `BigInt` sees it.
29
+ // 3. Execute them and assert each accepts its own literal and rejects the
30
+ // adjacent value a rounded literal would have collapsed onto.
31
+ func TestBigintLiteralPrecisionTransform(t *testing.T) {
32
+ node, err := exec.LookPath("node")
33
+ if err != nil {
34
+ t.Skip("node executable not found")
35
+ }
36
+ root := ttscTypiaTestRepoRoot(t)
37
+ base := filepath.Join(root, "packages", "typia", "native", ".tmp-ttsc-typia-tests")
38
+ if err := os.MkdirAll(base, 0o755); err != nil {
39
+ t.Fatalf("mkdir temp base: %v", err)
40
+ }
41
+ dir, err := os.MkdirTemp(base, "bigint-precision-")
42
+ if err != nil {
43
+ t.Fatalf("create temp fixture: %v", err)
44
+ }
45
+ t.Cleanup(func() { _ = os.RemoveAll(dir) })
46
+ src := filepath.Join(dir, "src")
47
+ if err := os.MkdirAll(src, 0o755); err != nil {
48
+ t.Fatalf("mkdir fixture src: %v", err)
49
+ }
50
+ if err := os.WriteFile(filepath.Join(dir, "tsconfig.json"), []byte(atomicIntersectionSchemaTSConfig), 0o644); err != nil {
51
+ t.Fatalf("write tsconfig: %v", err)
52
+ }
53
+ if err := os.WriteFile(filepath.Join(src, "main.ts"), []byte(bigintLiteralPrecisionSource), 0o644); err != nil {
54
+ t.Fatalf("write source: %v", err)
55
+ }
56
+ ttscTypiaTestTypecheck(t, dir)
57
+
58
+ out, errText, code := ttscTypiaTestCapture(func() int {
59
+ return runTransform([]string{
60
+ "--cwd", dir,
61
+ "--tsconfig", "tsconfig.json",
62
+ "--file", "src/main.ts",
63
+ "--output", "js",
64
+ })
65
+ })
66
+ if code != 0 {
67
+ t.Fatalf("bigint precision transform failed: code=%d stderr=\n%s", code, errText)
68
+ }
69
+ // A number-literal argument is the defect itself, so reject the spelling as
70
+ // well as the behavior: an emit that reads `BigInt(9007199254740993)` has
71
+ // already lost the value even where a later comparison happens to agree.
72
+ if strings.Contains(out, "BigInt(9007199254740993)") {
73
+ t.Fatalf("bigint literal must be passed as a string, not a number literal:\n%s", out)
74
+ }
75
+
76
+ runtimeDir := filepath.Join(dir, "runtime")
77
+ if err := os.MkdirAll(runtimeDir, 0o755); err != nil {
78
+ t.Fatalf("mkdir runtime dir: %v", err)
79
+ }
80
+ ttscTypiaTestWriteCommonRuntimeStubs(t, runtimeDir)
81
+ runtimeJS := ttscTypiaTestRewriteCommonJS(t, out)
82
+ if err := os.WriteFile(filepath.Join(runtimeDir, "main.cjs"), []byte(runtimeJS), 0o644); err != nil {
83
+ t.Fatalf("write runtime module: %v", err)
84
+ }
85
+ runner := filepath.Join(runtimeDir, "run.cjs")
86
+ if err := os.WriteFile(runner, []byte(bigintLiteralPrecisionRunner), 0o644); err != nil {
87
+ t.Fatalf("write runtime runner: %v", err)
88
+ }
89
+ cmd := exec.Command(node, runner)
90
+ cmd.Dir = runtimeDir
91
+ output, err := cmd.CombinedOutput()
92
+ if err != nil {
93
+ t.Fatalf("bigint precision runtime cases failed: %v\n%s", err, output)
94
+ }
95
+ }
96
+
97
+ const bigintLiteralPrecisionSource = `import typia from "typia";
98
+
99
+ // 2**53 + 1 is the smallest integer a double cannot represent; rounding it
100
+ // lands on 2**53, the value each validator below must reject.
101
+ export const isUnsafe = typia.createIs<9007199254740993n>();
102
+ export const isInt64Max = typia.createIs<9223372036854775807n>();
103
+ export const isInt64Min = typia.createIs<-9223372036854775808n>();
104
+ export const isUnion = typia.createIs<9007199254740993n | 9007199254740995n>();
105
+ export const isSafe = typia.createIs<2n>();
106
+ `
107
+
108
+ const bigintLiteralPrecisionRunner = `const mod = require("./main.cjs");
109
+
110
+ const check = (label, actual, expected) => {
111
+ if (actual !== expected) {
112
+ throw new Error(label + ": expected " + expected + ", got " + actual);
113
+ }
114
+ };
115
+
116
+ // Each literal is accepted, and the neighbor a rounded emit would have
117
+ // collapsed it onto is rejected.
118
+ check("isUnsafe(exact)", mod.isUnsafe(9007199254740993n), true);
119
+ check("isUnsafe(rounded)", mod.isUnsafe(9007199254740992n), false);
120
+ check("isInt64Max(exact)", mod.isInt64Max(9223372036854775807n), true);
121
+ check("isInt64Max(rounded)", mod.isInt64Max(9223372036854775808n), false);
122
+ check("isInt64Min(exact)", mod.isInt64Min(-9223372036854775808n), true);
123
+ check("isInt64Min(neighbor)", mod.isInt64Min(-9223372036854775807n), false);
124
+ check("isUnion(first)", mod.isUnion(9007199254740993n), true);
125
+ check("isUnion(second)", mod.isUnion(9007199254740995n), true);
126
+ check("isUnion(between)", mod.isUnion(9007199254740994n), false);
127
+ check("isUnion(rounded)", mod.isUnion(9007199254740992n), false);
128
+
129
+ // A magnitude a double does hold must keep working unchanged.
130
+ check("isSafe(exact)", mod.isSafe(2n), true);
131
+ check("isSafe(other)", mod.isSafe(3n), false);
132
+ check("isSafe(number)", mod.isSafe(2), false);
133
+
134
+ console.log("ok");
135
+ `
@@ -0,0 +1,148 @@
1
+ package main
2
+
3
+ import (
4
+ "os"
5
+ "os/exec"
6
+ "path/filepath"
7
+ "testing"
8
+ )
9
+
10
+ // TestReflectLiteralsBigintTransform verifies a bigint member comes back as a
11
+ // bigint, exactly.
12
+ //
13
+ // typescript-go hands a bigint literal back as a `jsnum.PseudoBigInt`, a struct
14
+ // in an internal package the shim does not re-export. Nothing downstream could
15
+ // name that type, so `LiteralFactory` reflected its fields and emitted
16
+ // `{ base10Value: "2", negative: false }` where `literals<2n>(): 2n[]` promises
17
+ // a bigint. Normalizing the constant to `*big.Int` at the metadata boundary
18
+ // gives every consumer a nameable exact value.
19
+ //
20
+ // Magnitude is the part a happy-path case would miss. A bigint exists precisely
21
+ // to hold what a `number` cannot, so the emit is only correct if it survives
22
+ // past 2**53.
23
+ //
24
+ // 1. Transform a module whose `reflect.literals` arguments span a bigint union,
25
+ // a bigint mixed with other literal kinds, and magnitudes on both sides of
26
+ // the double-precision limit including the int64 bounds.
27
+ // 2. Execute the emitted CommonJS.
28
+ // 3. Assert every member is `typeof "bigint"` and equal to the literal the
29
+ // source declared, digit for digit.
30
+ func TestReflectLiteralsBigintTransform(t *testing.T) {
31
+ node, err := exec.LookPath("node")
32
+ if err != nil {
33
+ t.Skip("node executable not found")
34
+ }
35
+ root := ttscTypiaTestRepoRoot(t)
36
+ base := filepath.Join(root, "packages", "typia", "native", ".tmp-ttsc-typia-tests")
37
+ if err := os.MkdirAll(base, 0o755); err != nil {
38
+ t.Fatalf("mkdir temp base: %v", err)
39
+ }
40
+ dir, err := os.MkdirTemp(base, "reflect-literals-bigint-")
41
+ if err != nil {
42
+ t.Fatalf("create temp fixture: %v", err)
43
+ }
44
+ t.Cleanup(func() { _ = os.RemoveAll(dir) })
45
+ src := filepath.Join(dir, "src")
46
+ if err := os.MkdirAll(src, 0o755); err != nil {
47
+ t.Fatalf("mkdir fixture src: %v", err)
48
+ }
49
+ if err := os.WriteFile(filepath.Join(dir, "tsconfig.json"), []byte(atomicIntersectionSchemaTSConfig), 0o644); err != nil {
50
+ t.Fatalf("write tsconfig: %v", err)
51
+ }
52
+ if err := os.WriteFile(filepath.Join(src, "main.ts"), []byte(reflectLiteralsBigintSource), 0o644); err != nil {
53
+ t.Fatalf("write source: %v", err)
54
+ }
55
+ ttscTypiaTestTypecheck(t, dir)
56
+
57
+ out, errText, code := ttscTypiaTestCapture(func() int {
58
+ return runTransform([]string{
59
+ "--cwd", dir,
60
+ "--tsconfig", "tsconfig.json",
61
+ "--file", "src/main.ts",
62
+ "--output", "js",
63
+ })
64
+ })
65
+ if code != 0 {
66
+ t.Fatalf("reflect.literals bigint transform failed: code=%d stderr=\n%s", code, errText)
67
+ }
68
+
69
+ runtimeDir := filepath.Join(dir, "runtime")
70
+ if err := os.MkdirAll(runtimeDir, 0o755); err != nil {
71
+ t.Fatalf("mkdir runtime dir: %v", err)
72
+ }
73
+ ttscTypiaTestWriteCommonRuntimeStubs(t, runtimeDir)
74
+ runtimeJS := ttscTypiaTestRewriteCommonJS(t, out)
75
+ if err := os.WriteFile(filepath.Join(runtimeDir, "main.cjs"), []byte(runtimeJS), 0o644); err != nil {
76
+ t.Fatalf("write runtime module: %v", err)
77
+ }
78
+ runner := filepath.Join(runtimeDir, "run.cjs")
79
+ if err := os.WriteFile(runner, []byte(reflectLiteralsBigintRunner), 0o644); err != nil {
80
+ t.Fatalf("write runtime runner: %v", err)
81
+ }
82
+ cmd := exec.Command(node, runner)
83
+ cmd.Dir = runtimeDir
84
+ output, err := cmd.CombinedOutput()
85
+ if err != nil {
86
+ t.Fatalf("reflect.literals bigint runtime cases failed: %v\n%s", err, output)
87
+ }
88
+ }
89
+
90
+ const reflectLiteralsBigintSource = `import typia from "typia";
91
+
92
+ export const small = typia.reflect.literals<1n | 2n>();
93
+ export const mixed = typia.reflect.literals<"A" | "B" | 1 | 2n>();
94
+ export const negative = typia.reflect.literals<-5n | 5n>();
95
+ export const zero = typia.reflect.literals<0n>();
96
+
97
+ // 2**53 + 1 is the smallest integer a double cannot hold, and the int64 bounds
98
+ // are where a rounded literal would land on the wrong side of the range.
99
+ export const unsafe = typia.reflect.literals<9007199254740993n>();
100
+ export const int64 = typia.reflect.literals<-9223372036854775808n | 9223372036854775807n>();
101
+ `
102
+
103
+ const reflectLiteralsBigintRunner = `const mod = require("./main.cjs");
104
+
105
+ const render = (value) =>
106
+ typeof value === "bigint" ? value.toString() + "n" : JSON.stringify(value);
107
+
108
+ const check = (label, actual, expected) => {
109
+ if (Array.isArray(actual) === false) {
110
+ throw new Error(label + ": expected an array, got " + render(actual));
111
+ }
112
+ if (actual.length !== expected.length) {
113
+ throw new Error(
114
+ label +
115
+ ": expected " +
116
+ expected.map(render).join(", ") +
117
+ ", got " +
118
+ actual.map(render).join(", "),
119
+ );
120
+ }
121
+ actual.forEach((item, index) => {
122
+ // typeof is asserted separately: a rounded BigInt(9007199254740993) is
123
+ // still a bigint, and an object literal is still deep-equal to nothing, so
124
+ // neither check alone catches both defects.
125
+ if (typeof item !== typeof expected[index]) {
126
+ throw new Error(
127
+ label + "[" + index + "]: expected typeof " + typeof expected[index] +
128
+ ", got " + typeof item + " (" + render(item) + ")",
129
+ );
130
+ }
131
+ if (item !== expected[index]) {
132
+ throw new Error(
133
+ label + "[" + index + "]: expected " + render(expected[index]) +
134
+ ", got " + render(item),
135
+ );
136
+ }
137
+ });
138
+ };
139
+
140
+ check("small", mod.small, [1n, 2n]);
141
+ check("mixed", mod.mixed, ["A", "B", 1, 2n]);
142
+ check("negative", mod.negative, [-5n, 5n]);
143
+ check("zero", mod.zero, [0n]);
144
+ check("unsafe", mod.unsafe, [9007199254740993n]);
145
+ check("int64", mod.int64, [-9223372036854775808n, 9223372036854775807n]);
146
+
147
+ console.log("ok");
148
+ `
@@ -7,24 +7,26 @@ import (
7
7
  "testing"
8
8
  )
9
9
 
10
- // TestReflectLiteralsNonLiteralRejectionTransform is the negative twin of the
11
- // empty-union admission fixed for issue #2377.
10
+ // TestReflectLiteralsNonLiteralRejectionTransform verifies every argument that
11
+ // names no listable literal is refused at compile time.
12
12
  //
13
- // Widening the admission predicate so that the empty union and the bare `null`
14
- // flag both pass must not widen it any further: every argument that carries a
15
- // member the emitter cannot render still has to abort the transform. The two
16
- // diagnostics stay distinguishable as well, because they say different things --
17
- // `NO` reports that nothing literal was found at all, while `ONLY` reports that
18
- // something literal was found next to something that is not. Collapsing them
19
- // would hide which half of a mixed argument is wrong.
13
+ // `reflect.literals` hands back the members of a union, so an argument it
14
+ // cannot enumerate has no answer to give. Refusing it is the point: emitting an
15
+ // empty array for `never`, or dropping the half it cannot render from a mixed
16
+ // argument, would hand the caller a list that silently disagrees with the type
17
+ // it was derived from (issue #2377).
20
18
  //
21
- // 1. Transform arguments holding no literal at all: a bare atomic, `any`, and
22
- // a tag-branded atomic, whose brand raises the member count without adding
23
- // anything the emitter can render.
24
- // 2. Transform arguments mixing a renderable member with a non-literal one:
25
- // `string | null`, the one-axis twin of the newly admitted `null`, and
26
- // `boolean | number`, where the renderable member is an atomic rather than
27
- // a constant.
19
+ // The two diagnostics stay distinguishable because they say different things:
20
+ // `NO` reports that nothing listable was found at all, while `ONLY` reports
21
+ // that something listable was found beside something that is not. Collapsing
22
+ // them would hide which half of a mixed argument is wrong.
23
+ //
24
+ // 1. Transform arguments naming no listable member: `never` and the bare
25
+ // `null` flag, which carry nothing, and `string`, `any`, and a tag-branded
26
+ // atomic, which carry a member that cannot be enumerated.
27
+ // 2. Transform arguments mixing a listable member with one that is not,
28
+ // including `boolean | number`, where the listable half is an atomic rather
29
+ // than a constant.
28
30
  // 3. Assert each fails, and with the diagnostic its composition calls for.
29
31
  func TestReflectLiteralsNonLiteralRejectionTransform(t *testing.T) {
30
32
  cases := []struct {
@@ -32,13 +34,17 @@ func TestReflectLiteralsNonLiteralRejectionTransform(t *testing.T) {
32
34
  Argument string
33
35
  Message string
34
36
  }{
37
+ {"never", "never", "no constant literal type found."},
38
+ {"never alias", "Empty", "no constant literal type found."},
39
+ {"exhaustive exclude", `Exclude<"a" | "b", "a" | "b">`, "no constant literal type found."},
40
+ {"bare null", "null", "no constant literal type found."},
35
41
  {"atomic", "string", "no constant literal type found."},
36
42
  {"any", "any", "no constant literal type found."},
37
- {"nullable atomic", "string | null", "only constant literal types are allowed."},
43
+ {"tag branded atomic", `string & tags.Format<"uuid">`, "no constant literal type found."},
44
+ {"nullable atomic", "string | null", "no constant literal type found."},
38
45
  {"literal beside atomic", `"a" | number`, "only constant literal types are allowed."},
39
46
  {"literal beside template", "`prefix${number}` | \"a\"", "only constant literal types are allowed."},
40
47
  {"renderable atomic beside atomic", "boolean | number", "only constant literal types are allowed."},
41
- {"tag branded atomic", `string & tags.Format<"uuid">`, "no constant literal type found."},
42
48
  }
43
49
  for _, tc := range cases {
44
50
  tc := tc
@@ -0,0 +1,173 @@
1
+ package main
2
+
3
+ import (
4
+ "os"
5
+ "os/exec"
6
+ "path/filepath"
7
+ "testing"
8
+ )
9
+
10
+ // TestReflectSchemaBigintTransform verifies the metadata a reflect operation
11
+ // hands back carries a bigint constant as a bigint.
12
+ //
13
+ // `IMetadataSchema.IValue<Atomic.Type>` declares `value: bigint` for a bigint
14
+ // constant, but this transformer lowered the whole metadata tree through
15
+ // `encoding/json` before writing it. JSON has no bigint, so that one member
16
+ // could not survive the trip: it arrived as whatever object its fields happened
17
+ // to spell. The trip was also redundant -- it marshaled structs into maps and
18
+ // lowercased each key's initial, which is what `LiteralFactory` already does
19
+ // when it reflects a struct -- so writing the tree directly both fixes the
20
+ // value and removes the step.
21
+ //
22
+ // `literals` and `schema` reach the emit by different routes, so a case on one
23
+ // says nothing about the other.
24
+ //
25
+ // 1. Reflect a bigint constant union through `schema` and through `schemas`.
26
+ // 2. Execute the emitted CommonJS.
27
+ // 3. Assert each reported value is `typeof "bigint"` and exact, and that the
28
+ // surrounding metadata still reports the constant and its neighbors.
29
+ func TestReflectSchemaBigintTransform(t *testing.T) {
30
+ node, err := exec.LookPath("node")
31
+ if err != nil {
32
+ t.Skip("node executable not found")
33
+ }
34
+ root := ttscTypiaTestRepoRoot(t)
35
+ base := filepath.Join(root, "packages", "typia", "native", ".tmp-ttsc-typia-tests")
36
+ if err := os.MkdirAll(base, 0o755); err != nil {
37
+ t.Fatalf("mkdir temp base: %v", err)
38
+ }
39
+ dir, err := os.MkdirTemp(base, "reflect-schema-bigint-")
40
+ if err != nil {
41
+ t.Fatalf("create temp fixture: %v", err)
42
+ }
43
+ t.Cleanup(func() { _ = os.RemoveAll(dir) })
44
+ src := filepath.Join(dir, "src")
45
+ if err := os.MkdirAll(src, 0o755); err != nil {
46
+ t.Fatalf("mkdir fixture src: %v", err)
47
+ }
48
+ if err := os.WriteFile(filepath.Join(dir, "tsconfig.json"), []byte(atomicIntersectionSchemaTSConfig), 0o644); err != nil {
49
+ t.Fatalf("write tsconfig: %v", err)
50
+ }
51
+ if err := os.WriteFile(filepath.Join(src, "main.ts"), []byte(reflectSchemaBigintSource), 0o644); err != nil {
52
+ t.Fatalf("write source: %v", err)
53
+ }
54
+ ttscTypiaTestTypecheck(t, dir)
55
+
56
+ out, errText, code := ttscTypiaTestCapture(func() int {
57
+ return runTransform([]string{
58
+ "--cwd", dir,
59
+ "--tsconfig", "tsconfig.json",
60
+ "--file", "src/main.ts",
61
+ "--output", "js",
62
+ })
63
+ })
64
+ if code != 0 {
65
+ t.Fatalf("reflect.schema bigint transform failed: code=%d stderr=\n%s", code, errText)
66
+ }
67
+
68
+ runtimeDir := filepath.Join(dir, "runtime")
69
+ if err := os.MkdirAll(runtimeDir, 0o755); err != nil {
70
+ t.Fatalf("mkdir runtime dir: %v", err)
71
+ }
72
+ ttscTypiaTestWriteCommonRuntimeStubs(t, runtimeDir)
73
+ runtimeJS := ttscTypiaTestRewriteCommonJS(t, out)
74
+ if err := os.WriteFile(filepath.Join(runtimeDir, "main.cjs"), []byte(runtimeJS), 0o644); err != nil {
75
+ t.Fatalf("write runtime module: %v", err)
76
+ }
77
+ runner := filepath.Join(runtimeDir, "run.cjs")
78
+ if err := os.WriteFile(runner, []byte(reflectSchemaBigintRunner), 0o644); err != nil {
79
+ t.Fatalf("write runtime runner: %v", err)
80
+ }
81
+ cmd := exec.Command(node, runner)
82
+ cmd.Dir = runtimeDir
83
+ output, err := cmd.CombinedOutput()
84
+ if err != nil {
85
+ t.Fatalf("reflect.schema bigint runtime cases failed: %v\n%s", err, output)
86
+ }
87
+ }
88
+
89
+ const reflectSchemaBigintSource = `import typia, { tags } from "typia";
90
+
91
+ export const unit = typia.reflect.schema<1n | 9007199254740993n>();
92
+ export const collection = typia.reflect.schemas<[2n, "a" | 3]>();
93
+ export const objects = typia.reflect.schema<{ big: 7n; name: string }>();
94
+
95
+ // A tag reaches a constant by matching the child's value against the merged
96
+ // parent's, so the constant value has to compare by value. The number and
97
+ // string constants are the controls that were never at risk.
98
+ export const tagged = typia.reflect.schema<(1n | 2n) & tags.Type<"int64">>();
99
+ export const taggedNumber = typia.reflect.schema<(1 | 2) & tags.Type<"uint32">>();
100
+ `
101
+
102
+ const reflectSchemaBigintRunner = `const mod = require("./main.cjs");
103
+
104
+ const render = (value) =>
105
+ typeof value === "bigint" ? value.toString() + "n" : JSON.stringify(value);
106
+
107
+ const constantsOf = (schema, type) => {
108
+ const constant = (schema.constants ?? []).find((c) => c.type === type);
109
+ return constant === undefined ? [] : constant.values.map((v) => v.value);
110
+ };
111
+
112
+ const check = (label, actual, expected) => {
113
+ if (actual.length !== expected.length) {
114
+ throw new Error(
115
+ label + ": expected " + expected.map(render).join(", ") +
116
+ ", got " + actual.map(render).join(", "),
117
+ );
118
+ }
119
+ actual.forEach((item, index) => {
120
+ if (typeof item !== typeof expected[index] || item !== expected[index]) {
121
+ throw new Error(
122
+ label + "[" + index + "]: expected " + render(expected[index]) +
123
+ " (" + typeof expected[index] + "), got " + render(item) +
124
+ " (" + typeof item + ")",
125
+ );
126
+ }
127
+ });
128
+ };
129
+
130
+ check("schema", constantsOf(mod.unit.schema, "bigint"), [1n, 9007199254740993n]);
131
+ check("schemas[0]", constantsOf(mod.collection.schemas[0], "bigint"), [2n]);
132
+
133
+ // A neighboring constant kind must keep its own representation.
134
+ check("schemas[1] string", constantsOf(mod.collection.schemas[1], "string"), ["a"]);
135
+ check("schemas[1] number", constantsOf(mod.collection.schemas[1], "number"), [3]);
136
+
137
+ // The surrounding tree still reports the object and its members.
138
+ const object = mod.objects.components.objects[0];
139
+ if (object === undefined || object.properties.length !== 2) {
140
+ throw new Error("object metadata was not emitted: " + JSON.stringify(mod.objects, (k, v) => typeof v === "bigint" ? v.toString() : v));
141
+ }
142
+ const big = object.properties.find(
143
+ (p) => constantsOf(p.key, "string")[0] === "big",
144
+ );
145
+ if (big === undefined) {
146
+ throw new Error("the 'big' property is missing from the object metadata");
147
+ }
148
+ check("object property", constantsOf(big.value, "bigint"), [7n]);
149
+
150
+ const tagNames = (schema, type) => {
151
+ const constant = (schema.constants ?? []).find((c) => c.type === type);
152
+ if (constant === undefined) {
153
+ throw new Error("no " + type + " constant was emitted");
154
+ }
155
+ return constant.values.map((v) =>
156
+ (v.tags ?? []).flat().map((t) => t.name).join(","),
157
+ );
158
+ };
159
+
160
+ // Every member keeps the tag the intersection put on it.
161
+ const expectTags = (label, actual, expected) => {
162
+ if (actual.length === 0 || actual.some((names) => names !== expected)) {
163
+ throw new Error(
164
+ label + ": expected every member tagged " + expected +
165
+ ", got " + JSON.stringify(actual),
166
+ );
167
+ }
168
+ };
169
+ expectTags("bigint constant", tagNames(mod.tagged.schema, "bigint"), 'Type<"int64">');
170
+ expectTags("number constant", tagNames(mod.taggedNumber.schema, "number"), 'Type<"uint32">');
171
+
172
+ console.log("ok");
173
+ `
@@ -66,6 +66,16 @@ func (expressionFactoryNamespace) Number(value any, emit ...*shimprinter.EmitCon
66
66
  return f.NewNumericLiteral(text, shimast.TokenFlagsNone)
67
67
  }
68
68
 
69
+ // Bigint emits `BigInt("<value>")` for a bigint literal.
70
+ //
71
+ // The argument has to stay a string literal. This used to spell the digits as a
72
+ // bare `number` literal, so `9007199254740993n` reached the emit as
73
+ // `BigInt(9007199254740993)` and JavaScript rounded the literal to
74
+ // 9007199254740992 before `BigInt` ever parsed it -- exactly the rounding
75
+ // `numericRangeFactory_bigint_literal` and `_isTypeInt64Bigint` already spell
76
+ // out and avoid. Every bigint an argument can carry is exact in base 10, and
77
+ // `BigInt` parses the digits of a string directly, so the string form is exact
78
+ // at any magnitude.
69
79
  func (expressionFactoryNamespace) Bigint(value any, emit ...*shimprinter.EmitContext) *shimast.Node {
70
80
  f := nativecontext.EmitFactoryOf(expressionFactory_factory, emit...)
71
81
  return f.NewCallExpression(
@@ -73,7 +83,7 @@ func (expressionFactoryNamespace) Bigint(value any, emit ...*shimprinter.EmitCon
73
83
  nil,
74
84
  nil,
75
85
  f.NewNodeList([]*shimast.Node{
76
- f.NewIdentifier(fmt.Sprint(value)),
86
+ f.NewStringLiteral(fmt.Sprint(value), shimast.TokenFlagsNone),
77
87
  }),
78
88
  shimast.NodeFlagsNone,
79
89
  )
@@ -11,6 +11,7 @@ import (
11
11
  shimast "github.com/microsoft/typescript-go/shim/ast"
12
12
  shimprinter "github.com/microsoft/typescript-go/shim/printer"
13
13
  nativecontext "github.com/samchon/typia/packages/typia/native/core/context"
14
+ schemametadata "github.com/samchon/typia/packages/typia/native/core/schemas/metadata"
14
15
  )
15
16
 
16
17
  type literalFactoryNamespace struct{}
@@ -93,6 +94,8 @@ func (literalFactoryNamespace) Write(input any, emit ...*shimprinter.EmitContext
93
94
  switch value := input.(type) {
94
95
  case bool:
95
96
  return literalFactory_writeBoolean(value, emit...)
97
+ case schemametadata.MetadataBigint:
98
+ return literalFactory_writeBigint(value, emit...)
96
99
  case *big.Int:
97
100
  return literalFactory_writeBigint(value, emit...)
98
101
  case int:
@@ -50,6 +50,9 @@ func Iterate_metadata_constant(props IMetadataIteratorProps) bool {
50
50
  typ = "number"
51
51
  } else if filter(nativechecker.TypeFlagsBigIntLiteral) {
52
52
  typ = "bigint"
53
+ // Normalize away the checker's unnameable `jsnum.PseudoBigInt`; see
54
+ // MetadataBigint for why the stand-in has to be a comparable value.
55
+ value = schemametadata.MetadataBigint{Text: fmt.Sprint(value)}
53
56
  }
54
57
  constant := iterate_metadata_constant_take(props.Metadata, typ)
55
58
  info := comment()
@@ -29,24 +29,12 @@ func (reflectLiteralsProgrammerNamespace) Write(props ReflectLiteralsProgrammer_
29
29
  Escape: true,
30
30
  Constant: true,
31
31
  Absorb: true,
32
- // Accept exactly what the emitter below can render: constant literals,
33
- // the `boolean` atomic (the `true | false` union), and `null`. `null` is
34
- // a `MetadataSchema` flag rather than a bucket, so `Size()` cannot see
35
- // it; weighing it on both sides keeps `length` meaning "renderable
36
- // members" against `size` meaning "members", which is what lets a
37
- // rejected argument carry the diagnostic its composition calls for
38
- // instead of collapsing onto `NO`.
39
- //
40
- // Equal counts with no member at all is an uninhabited argument: the
41
- // `never` keyword, an alias or `Exclude`/`Extract` that filters
42
- // everything away, or an intersection whose every distributed member
43
- // prunes to never. Its literal set is empty and
44
- // `literals<never>(): never[]` has exactly one inhabitant, so it emits
45
- // `[]` rather than a diagnostic (issue #2377). `undefined` and `void`
46
- // coalesce to the same empty metadata, but neither satisfies the public
47
- // `T extends Atomic.Type | null` bound, and typia refuses to transform
48
- // at all without `strictNullChecks` (`transform/transform.go`), so
49
- // neither reaches this predicate.
32
+ // Only a constant literal, the `boolean` atomic (which stands for the
33
+ // `true | false` constants), and `null` beside them are members this
34
+ // operation can hand back. Anything else -- an unconstrained atomic, a
35
+ // template, a bucket of any other kind -- is rejected, and so is an
36
+ // argument carrying no member at all: `never` has no literal to list, so
37
+ // it is a compile error rather than an empty array (issue #2377).
50
38
  Validate: func(next struct {
51
39
  Metadata *nativemetadata.MetadataSchema
52
40
  Explore nativefactories.MetadataFactory_IExplore
@@ -61,18 +49,13 @@ func (reflectLiteralsProgrammerNamespace) Write(props ReflectLiteralsProgrammer_
61
49
  length++
62
50
  }
63
51
  }
64
- size := next.Metadata.Size()
65
- if next.Metadata.Nullable {
66
- length++
67
- size++
68
- }
69
- if size == length {
70
- return []string{}
71
- }
72
52
  if length == 0 {
73
53
  return []string{string(ReflectLiteralsProgrammer_ErrorMessages_NO)}
74
54
  }
75
- return []string{string(ReflectLiteralsProgrammer_ErrorMessages_ONLY)}
55
+ if next.Metadata.Size() != length {
56
+ return []string{string(ReflectLiteralsProgrammer_ErrorMessages_ONLY)}
57
+ }
58
+ return []string{}
76
59
  },
77
60
  },
78
61
  Components: nativemetadata.NewMetadataCollection(),
@@ -0,0 +1,37 @@
1
+ package metadata
2
+
3
+ // MetadataBigint is the value of a `bigint` constant.
4
+ //
5
+ // typescript-go reports a bigint literal as a `jsnum.PseudoBigInt`, a struct in
6
+ // an internal package the shim does not re-export. Nothing downstream could
7
+ // name it, so consumers that had to render the value reflected its fields
8
+ // instead and emitted `{ base10Value: "2", negative: false }` where the caller
9
+ // declared `bigint`. This is the nameable stand-in.
10
+ //
11
+ // It is a comparable struct on purpose. Every other value a
12
+ // `MetadataConstantValue` carries -- `string`, `bool`, the number -- is
13
+ // comparable, and the factories compare those values with `==`; the
14
+ // intersection tag assigner is one such site. A pointer type such as
15
+ // `*math/big.Int` would compare identity there and, for bigints alone,
16
+ // silently drop whatever the comparison decides.
17
+ type MetadataBigint struct {
18
+ // Text is the exact value in base 10, prefixed with `-` when negative. A
19
+ // bigint exists to hold what a float64 cannot, so the digits are the only
20
+ // representation that stays exact at every magnitude.
21
+ Text string
22
+ }
23
+
24
+ // String renders the base-10 digits, which is what every consumer that lowers a
25
+ // bigint into emitted code reads through `fmt.Sprint`.
26
+ func (obj MetadataBigint) String() string {
27
+ return obj.Text
28
+ }
29
+
30
+ // MarshalJSON writes the digits unquoted, so metadata marshaled by a
31
+ // downstream tool carries the same JSON number a plain integer would.
32
+ func (obj MetadataBigint) MarshalJSON() ([]byte, error) {
33
+ if obj.Text == "" {
34
+ return []byte("0"), nil
35
+ }
36
+ return []byte(obj.Text), nil
37
+ }
@@ -0,0 +1,65 @@
1
+ package metadata
2
+
3
+ import (
4
+ "encoding/json"
5
+ "fmt"
6
+ "testing"
7
+ )
8
+
9
+ // TestMetadataBigintIsComparable pins the property the whole representation
10
+ // rests on: two independently built values of the same bigint are `==`.
11
+ //
12
+ // A `MetadataConstantValue.Value` is compared with `==` inside the factories --
13
+ // the intersection tag assigner matches a child's constant against the merged
14
+ // parent's that way -- and every other value it can hold (`string`, `bool`, the
15
+ // number) is a comparable value. A pointer stand-in such as `*math/big.Int`
16
+ // builds a fresh allocation per literal, so those comparisons would test
17
+ // identity and, for bigints alone, silently decide "not the same constant":
18
+ // `(1n | 2n) & tags.Type<"int64">` lost its tag that way. This is the guard,
19
+ // held one level below the transform so a future change to the representation
20
+ // fails here rather than as a dropped tag three packages away.
21
+ //
22
+ // 1. Build the same value twice, independently, and compare with `==`.
23
+ // 2. Compare a different value, and a negative against its positive.
24
+ // 3. Check the two renderings every consumer reads: `fmt.Sprint` for the emit
25
+ // and `encoding/json` for metadata a downstream tool marshals.
26
+ func TestMetadataBigintIsComparable(t *testing.T) {
27
+ const digits = "9007199254740993"
28
+ left := MetadataBigint{Text: digits}
29
+ right := MetadataBigint{Text: digits}
30
+ if left != right {
31
+ t.Fatalf("two values of the same bigint must be ==: %v vs %v", left, right)
32
+ }
33
+ var boxed any = left
34
+ if boxed != any(right) {
35
+ t.Fatalf("the same bigint must stay == once boxed in the `any` a constant value holds")
36
+ }
37
+ if left == (MetadataBigint{Text: "9007199254740992"}) {
38
+ t.Fatalf("distinct bigints must not compare equal")
39
+ }
40
+ if left == (MetadataBigint{Text: "-" + digits}) {
41
+ t.Fatalf("a negative must not compare equal to its positive")
42
+ }
43
+
44
+ if got := fmt.Sprint(left); got != digits {
45
+ t.Fatalf("fmt.Sprint must render the base-10 digits, got %q", got)
46
+ }
47
+ if got := fmt.Sprint(MetadataBigint{Text: "-5"}); got != "-5" {
48
+ t.Fatalf("a negative must keep its sign, got %q", got)
49
+ }
50
+
51
+ encoded, err := json.Marshal(left)
52
+ if err != nil {
53
+ t.Fatalf("marshal bigint: %v", err)
54
+ }
55
+ if string(encoded) != digits {
56
+ t.Fatalf("marshaled bigint must be the unquoted digits, got %s", encoded)
57
+ }
58
+ zero, err := json.Marshal(MetadataBigint{})
59
+ if err != nil {
60
+ t.Fatalf("marshal zero bigint: %v", err)
61
+ }
62
+ if string(zero) != "0" {
63
+ t.Fatalf("the zero value must marshal as 0, got %s", zero)
64
+ }
65
+ }
@@ -1,10 +1,7 @@
1
1
  package reflect
2
2
 
3
3
  import (
4
- "encoding/json"
5
4
  "strings"
6
- "unicode"
7
- "unicode/utf8"
8
5
 
9
6
  shimast "github.com/microsoft/typescript-go/shim/ast"
10
7
  shimprinter "github.com/microsoft/typescript-go/shim/printer"
@@ -67,50 +64,22 @@ func (reflectSchemaTransformerNamespace) Transform(props nativetransform.ITransf
67
64
  }, props.Context.Emit)
68
65
  }
69
66
 
67
+ // reflectTransformer_literal lowers a metadata tree into its object literal.
68
+ //
69
+ // This used to round-trip the tree through `encoding/json` and then lowercase
70
+ // each key's initial, which is exactly what `LiteralFactory` already does when
71
+ // it reflects a struct. The round trip was not merely redundant: JSON has no
72
+ // bigint, so a `bigint` constant -- the one value in the tree that is neither
73
+ // a string nor a JSON number -- could not survive it. It came back as the
74
+ // object its fields happened to spell, and `IMetadataSchema.IValue` declares
75
+ // `bigint` there.
76
+ //
77
+ // Writing the tree directly leaves every other member identical (diffed over a
78
+ // metadata tree spanning objects, optional and nullable members, tags, arrays,
79
+ // tuples, sets, maps, natives, functions, aliases, and unions: the bigint
80
+ // values were the only difference) and lets a bigint stay a bigint.
70
81
  func reflectTransformer_literal(value any, emit ...*shimprinter.EmitContext) *shimast.Node {
71
- return nativefactories.LiteralFactory.Write(reflectTransformer_toPrimitive(value), emit...)
72
- }
73
-
74
- func reflectTransformer_toPrimitive(value any) any {
75
- data, err := json.Marshal(value)
76
- if err != nil {
77
- return nil
78
- }
79
- var decoded any
80
- if err := json.Unmarshal(data, &decoded); err != nil {
81
- return nil
82
- }
83
- return reflectTransformer_lowerKeys(decoded)
84
- }
85
-
86
- func reflectTransformer_lowerKeys(value any) any {
87
- switch v := value.(type) {
88
- case map[string]any:
89
- output := map[string]any{}
90
- for key, elem := range v {
91
- output[reflectTransformer_lowerInitial(key)] = reflectTransformer_lowerKeys(elem)
92
- }
93
- return output
94
- case []any:
95
- output := make([]any, 0, len(v))
96
- for _, elem := range v {
97
- output = append(output, reflectTransformer_lowerKeys(elem))
98
- }
99
- return output
100
- default:
101
- return value
102
- }
103
- }
104
-
105
- func reflectTransformer_lowerInitial(str string) string {
106
- if str == "" {
107
- return str
108
- }
109
- r, size := utf8.DecodeRuneInString(str)
110
- if r == utf8.RuneError && size == 0 {
111
- return str
112
- }
113
- return string(unicode.ToLower(r)) + str[size:]
82
+ return nativefactories.LiteralFactory.Write(value, emit...)
114
83
  }
115
84
 
116
85
  func reflectTransformer_errors(errors []nativefactories.MetadataFactory_IError) []nativetransform.TransformerError_MetadataFactory_IError {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "typia",
3
- "version": "14.0.3",
3
+ "version": "14.0.4",
4
4
  "description": "Superfast runtime validators with only one line",
5
5
  "main": "lib/index.js",
6
6
  "exports": {
@@ -43,8 +43,8 @@
43
43
  "homepage": "https://typia.io",
44
44
  "dependencies": {
45
45
  "randexp": "^0.5.3",
46
- "@typia/interface": "^14.0.3",
47
- "@typia/utils": "^14.0.3"
46
+ "@typia/interface": "^14.0.4",
47
+ "@typia/utils": "^14.0.4"
48
48
  },
49
49
  "peerDependencies": {
50
50
  "ttsc": ">=0.19.2"
@@ -1,161 +0,0 @@
1
- package main
2
-
3
- import (
4
- "os"
5
- "os/exec"
6
- "path/filepath"
7
- "testing"
8
- )
9
-
10
- // TestReflectLiteralsEmptyUnionTransform pins issue #2377: the literal set of an
11
- // empty union is the empty array, not a transform error.
12
- //
13
- // The programmer admits a type argument by counting the members it can render,
14
- // and it renders three kinds -- constant literals, the `boolean` atomic, and
15
- // `null`. `null` is a `MetadataSchema` flag rather than a bucket, so it was
16
- // missing from the count, which made two admissible arguments look like "no
17
- // constant literal type found": `never`, whose union is empty on both sides of
18
- // the comparison, and `null`, which the public
19
- // `literals<T extends Atomic.Type | null>()` signature documents. `never` is
20
- // vacuously literal-only and `literals<never>(): never[]` has exactly one
21
- // inhabitant, so both must transform and run.
22
- //
23
- // 1. Transform a module whose `reflect.literals` arguments span every
24
- // uninhabited spelling a caller reaches -- the keyword, an alias, an
25
- // exhaustive `Exclude`, an empty `Extract`, a TypeScript-collapsed
26
- // intersection, and one typia prunes itself -- plus `null`, a fully mixed
27
- // literal union, and the plain `boolean` atomic.
28
- // 2. Execute the emitted CommonJS.
29
- // 3. Assert `never` yields `[]`, `null` yields `[null]`, and the arguments that
30
- // already worked keep their exact members and order, including the sort the
31
- // guide documents.
32
- func TestReflectLiteralsEmptyUnionTransform(t *testing.T) {
33
- node, err := exec.LookPath("node")
34
- if err != nil {
35
- t.Skip("node executable not found")
36
- }
37
- root := ttscTypiaTestRepoRoot(t)
38
- base := filepath.Join(root, "packages", "typia", "native", ".tmp-ttsc-typia-tests")
39
- if err := os.MkdirAll(base, 0o755); err != nil {
40
- t.Fatalf("mkdir temp base: %v", err)
41
- }
42
- dir, err := os.MkdirTemp(base, "reflect-literals-empty-")
43
- if err != nil {
44
- t.Fatalf("create temp fixture: %v", err)
45
- }
46
- t.Cleanup(func() { _ = os.RemoveAll(dir) })
47
- src := filepath.Join(dir, "src")
48
- if err := os.MkdirAll(src, 0o755); err != nil {
49
- t.Fatalf("mkdir fixture src: %v", err)
50
- }
51
- if err := os.WriteFile(filepath.Join(dir, "tsconfig.json"), []byte(atomicIntersectionSchemaTSConfig), 0o644); err != nil {
52
- t.Fatalf("write tsconfig: %v", err)
53
- }
54
- if err := os.WriteFile(filepath.Join(src, "main.ts"), []byte(reflectLiteralsEmptyUnionSource), 0o644); err != nil {
55
- t.Fatalf("write source: %v", err)
56
- }
57
- ttscTypiaTestTypecheck(t, dir)
58
-
59
- out, errText, code := ttscTypiaTestCapture(func() int {
60
- return runTransform([]string{
61
- "--cwd", dir,
62
- "--tsconfig", "tsconfig.json",
63
- "--file", "src/main.ts",
64
- "--output", "js",
65
- })
66
- })
67
- if code != 0 {
68
- t.Fatalf("reflect.literals empty union transform failed: code=%d stderr=\n%s", code, errText)
69
- }
70
-
71
- runtimeDir := filepath.Join(dir, "runtime")
72
- if err := os.MkdirAll(runtimeDir, 0o755); err != nil {
73
- t.Fatalf("mkdir runtime dir: %v", err)
74
- }
75
- ttscTypiaTestWriteCommonRuntimeStubs(t, runtimeDir)
76
- runtimeJS := ttscTypiaTestRewriteCommonJS(t, out)
77
- if err := os.WriteFile(filepath.Join(runtimeDir, "main.cjs"), []byte(runtimeJS), 0o644); err != nil {
78
- t.Fatalf("write runtime module: %v", err)
79
- }
80
- runner := filepath.Join(runtimeDir, "run.cjs")
81
- if err := os.WriteFile(runner, []byte(reflectLiteralsEmptyUnionRunner), 0o644); err != nil {
82
- t.Fatalf("write runtime runner: %v", err)
83
- }
84
- cmd := exec.Command(node, runner)
85
- cmd.Dir = runtimeDir
86
- output, err := cmd.CombinedOutput()
87
- if err != nil {
88
- t.Fatalf("reflect.literals empty union runtime cases failed: %v\n%s", err, output)
89
- }
90
- }
91
-
92
- const reflectLiteralsEmptyUnionSource = `import typia from "typia";
93
-
94
- type Color = "red" | "green" | "blue";
95
- type Empty = never;
96
-
97
- // The empty union: no member on either side of the admission comparison. Every
98
- // spelling a caller reaches it through has to transform, not only the keyword:
99
- // an alias hides it behind a name, and a conditional or an exhaustive Exclude
100
- // is how a generic caller produces one without writing "never" at all.
101
- export const empty = typia.reflect.literals<never>();
102
- export const emptyAlias = typia.reflect.literals<Empty>();
103
- export const emptyExclude = typia.reflect.literals<Exclude<Color, Color>>();
104
- export const emptyExtract = typia.reflect.literals<Extract<Color, "cyan">>();
105
-
106
- // Uninhabited without the keyword: TypeScript collapses one to never itself,
107
- // while the other stays an intersection whose every distributed member typia
108
- // prunes away. Both are empty on both sides of the comparison, so the rule the
109
- // keyword exercises has to cover them too.
110
- export const emptyCollapsed = typia.reflect.literals<string & number>();
111
- export const emptyPruned = typia.reflect.literals<(string | number) & { data: number }>();
112
-
113
- // A union whose only member is the nullable flag, which carries no bucket.
114
- export const onlyNull = typia.reflect.literals<null>();
115
-
116
- // Controls that already transformed: the flag must join the count without
117
- // displacing a constant, a boolean atomic, or their order.
118
- export const mixed = typia.reflect.literals<"a" | 1 | true | null>();
119
- export const booleanAtomic = typia.reflect.literals<boolean>();
120
- export const booleanAtomicNull = typia.reflect.literals<boolean | null>();
121
-
122
- // Declaration order deliberately disagrees with the emitted order: values of
123
- // one primitive type are sorted (iterate_metadata_sort), which is the behavior
124
- // the reflect.literals guide states.
125
- export const sorted = typia.reflect.literals<"b" | "a" | 10 | 2>();
126
- `
127
-
128
- const reflectLiteralsEmptyUnionRunner = `const mod = require("./main.cjs");
129
-
130
- const check = (label, actual, expected) => {
131
- if (Array.isArray(actual) === false) {
132
- throw new Error(label + ": expected an array, got " + JSON.stringify(actual));
133
- }
134
- if (
135
- actual.length !== expected.length ||
136
- actual.some((item, index) => item !== expected[index])
137
- ) {
138
- throw new Error(
139
- label +
140
- ": expected " +
141
- JSON.stringify(expected) +
142
- ", got " +
143
- JSON.stringify(actual),
144
- );
145
- }
146
- };
147
-
148
- check("never", mod.empty, []);
149
- check("never alias", mod.emptyAlias, []);
150
- check("exhaustive Exclude", mod.emptyExclude, []);
151
- check("empty Extract", mod.emptyExtract, []);
152
- check("collapsed intersection", mod.emptyCollapsed, []);
153
- check("pruned intersection", mod.emptyPruned, []);
154
- check("null", mod.onlyNull, [null]);
155
- check("mixed", mod.mixed, ["a", 1, true, null]);
156
- check("booleanAtomic", mod.booleanAtomic, [true, false]);
157
- check("booleanAtomicNull", mod.booleanAtomicNull, [true, false, null]);
158
- check("sorted", mod.sorted, ["a", "b", 2, 10]);
159
-
160
- console.log("ok");
161
- `