typia 14.0.2 → 14.0.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.
@@ -0,0 +1,161 @@
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
+ `
@@ -0,0 +1,92 @@
1
+ package main
2
+
3
+ import (
4
+ "os"
5
+ "path/filepath"
6
+ "strings"
7
+ "testing"
8
+ )
9
+
10
+ // TestReflectLiteralsNonLiteralRejectionTransform is the negative twin of the
11
+ // empty-union admission fixed for issue #2377.
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.
20
+ //
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.
28
+ // 3. Assert each fails, and with the diagnostic its composition calls for.
29
+ func TestReflectLiteralsNonLiteralRejectionTransform(t *testing.T) {
30
+ cases := []struct {
31
+ Name string
32
+ Argument string
33
+ Message string
34
+ }{
35
+ {"atomic", "string", "no constant literal type found."},
36
+ {"any", "any", "no constant literal type found."},
37
+ {"nullable atomic", "string | null", "only constant literal types are allowed."},
38
+ {"literal beside atomic", `"a" | number`, "only constant literal types are allowed."},
39
+ {"literal beside template", "`prefix${number}` | \"a\"", "only constant literal types are allowed."},
40
+ {"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
+ }
43
+ for _, tc := range cases {
44
+ tc := tc
45
+ t.Run(tc.Name, func(t *testing.T) {
46
+ project := reflectLiteralsRejectionProject(t, tc.Argument)
47
+ out, errText, code := ttscTypiaTestCapture(func() int {
48
+ return runTransform([]string{
49
+ "--cwd", project,
50
+ "--tsconfig", "tsconfig.json",
51
+ "--file", "src/main.ts",
52
+ "--output", "js",
53
+ })
54
+ })
55
+ if code == 0 {
56
+ t.Fatalf("reflect.literals<%s> transformed successfully, want rejection:\n%s", tc.Argument, out)
57
+ }
58
+ if !strings.Contains(errText, "typia transform error") {
59
+ t.Fatalf("reflect.literals<%s> diagnostics missing:\nstdout=%s\nstderr=%s", tc.Argument, out, errText)
60
+ }
61
+ if !strings.Contains(errText, tc.Message) {
62
+ t.Fatalf("reflect.literals<%s> reported the wrong reason, want %q:\n%s", tc.Argument, tc.Message, errText)
63
+ }
64
+ })
65
+ }
66
+ }
67
+
68
+ func reflectLiteralsRejectionProject(t *testing.T, argument string) string {
69
+ t.Helper()
70
+ root := ttscTypiaTestRepoRoot(t)
71
+ base := filepath.Join(root, "packages", "typia", "native", ".tmp-ttsc-typia-tests")
72
+ if err := os.MkdirAll(base, 0o755); err != nil {
73
+ t.Fatalf("mkdir temp base: %v", err)
74
+ }
75
+ dir, err := os.MkdirTemp(base, "reflect-literals-reject-")
76
+ if err != nil {
77
+ t.Fatalf("create temp fixture: %v", err)
78
+ }
79
+ t.Cleanup(func() { _ = os.RemoveAll(dir) })
80
+ src := filepath.Join(dir, "src")
81
+ if err := os.MkdirAll(src, 0o755); err != nil {
82
+ t.Fatalf("mkdir fixture src: %v", err)
83
+ }
84
+ if err := os.WriteFile(filepath.Join(dir, "tsconfig.json"), []byte(atomicIntersectionSchemaTSConfig), 0o644); err != nil {
85
+ t.Fatalf("write tsconfig: %v", err)
86
+ }
87
+ source := "import typia, { tags } from \"typia\";\n\nexport const values = typia.reflect.literals<" + argument + ">();\n"
88
+ if err := os.WriteFile(filepath.Join(src, "main.ts"), []byte(source), 0o644); err != nil {
89
+ t.Fatalf("write source: %v", err)
90
+ }
91
+ return dir
92
+ }
@@ -29,6 +29,24 @@ 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
50
  Validate: func(next struct {
33
51
  Metadata *nativemetadata.MetadataSchema
34
52
  Explore nativefactories.MetadataFactory_IExplore
@@ -43,13 +61,18 @@ func (reflectLiteralsProgrammerNamespace) Write(props ReflectLiteralsProgrammer_
43
61
  length++
44
62
  }
45
63
  }
64
+ size := next.Metadata.Size()
65
+ if next.Metadata.Nullable {
66
+ length++
67
+ size++
68
+ }
69
+ if size == length {
70
+ return []string{}
71
+ }
46
72
  if length == 0 {
47
73
  return []string{string(ReflectLiteralsProgrammer_ErrorMessages_NO)}
48
74
  }
49
- if next.Metadata.Size() != length {
50
- return []string{string(ReflectLiteralsProgrammer_ErrorMessages_ONLY)}
51
- }
52
- return []string{}
75
+ return []string{string(ReflectLiteralsProgrammer_ErrorMessages_ONLY)}
53
76
  },
54
77
  },
55
78
  Components: nativemetadata.NewMetadataCollection(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "typia",
3
- "version": "14.0.2",
3
+ "version": "14.0.3",
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.2",
47
- "@typia/utils": "^14.0.2"
46
+ "@typia/interface": "^14.0.3",
47
+ "@typia/utils": "^14.0.3"
48
48
  },
49
49
  "peerDependencies": {
50
50
  "ttsc": ">=0.19.2"