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
|
-
|
|
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.
|
|
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.
|
|
47
|
-
"@typia/utils": "^14.0.
|
|
46
|
+
"@typia/interface": "^14.0.3",
|
|
47
|
+
"@typia/utils": "^14.0.3"
|
|
48
48
|
},
|
|
49
49
|
"peerDependencies": {
|
|
50
50
|
"ttsc": ">=0.19.2"
|