typia 13.2.0 → 13.3.0
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.
- package/lib/http.d.ts +3 -3
- package/lib/http.js.map +1 -1
- package/lib/http.mjs.map +1 -1
- package/lib/internal/_assertGuard.js +13 -3
- package/lib/internal/_assertGuard.js.map +1 -1
- package/lib/internal/_assertGuard.mjs +4 -2
- package/lib/internal/_assertGuard.mjs.map +1 -1
- package/lib/internal/_isFormatIdnHostname.js +5 -1
- package/lib/internal/_isFormatIdnHostname.js.map +1 -1
- package/lib/internal/_isFormatIdnHostname.mjs +1 -1
- package/lib/internal/_isFormatIdnHostname.mjs.map +1 -1
- package/lib/internal/_isFormatTime.js +19 -2
- package/lib/internal/_isFormatTime.js.map +1 -1
- package/lib/internal/_isFormatTime.mjs +9 -2
- package/lib/internal/_isFormatTime.mjs.map +1 -1
- package/lib/internal/_randomArray.js +37 -4
- package/lib/internal/_randomArray.js.map +1 -1
- package/lib/internal/_randomArray.mjs +16 -3
- package/lib/internal/_randomArray.mjs.map +1 -1
- package/lib/internal/_randomFormatByte.js +15 -12
- package/lib/internal/_randomFormatByte.js.map +1 -1
- package/lib/internal/_randomFormatByte.mjs +11 -10
- package/lib/internal/_randomFormatByte.mjs.map +1 -1
- package/lib/internal/_randomFormatDatetime.mjs +1 -1
- package/lib/internal/_randomFormatHostname.d.ts +2 -2
- package/lib/internal/_randomFormatHostname.js +2 -2
- package/lib/internal/_randomFormatHostname.mjs +2 -2
- package/lib/internal/_randomFormatHostname.mjs.map +1 -1
- package/lib/internal/_randomFormatIdnHostname.js +9 -10
- package/lib/internal/_randomFormatIdnHostname.js.map +1 -1
- package/lib/internal/_randomFormatIdnHostname.mjs +3 -4
- package/lib/internal/_randomFormatIdnHostname.mjs.map +1 -1
- package/lib/internal/_randomFormatRegex.js +4 -4
- package/lib/internal/_randomFormatRegex.js.map +1 -1
- package/lib/internal/_randomFormatRegex.mjs +1 -2
- package/lib/internal/_randomFormatRegex.mjs.map +1 -1
- package/lib/internal/_randomFormatTime.js +15 -10
- package/lib/internal/_randomFormatTime.js.map +1 -1
- package/lib/internal/_randomFormatTime.mjs +6 -3
- package/lib/internal/_randomFormatTime.mjs.map +1 -1
- package/lib/internal/private/__randomComposition.d.ts +8 -0
- package/lib/internal/private/__randomComposition.js +21 -1
- package/lib/internal/private/__randomComposition.js.map +1 -1
- package/lib/internal/private/__randomComposition.mjs +18 -1
- package/lib/internal/private/__randomComposition.mjs.map +1 -1
- package/lib/json.d.ts +2 -2
- package/lib/json.js.map +1 -1
- package/lib/json.mjs.map +1 -1
- package/lib/module.d.ts +28 -13
- package/lib/module.js.map +1 -1
- package/lib/module.mjs.map +1 -1
- package/lib/notations.d.ts +4 -4
- package/lib/notations.js.map +1 -1
- package/lib/notations.mjs.map +1 -1
- package/lib/plain.d.ts +3 -3
- package/lib/plain.js.map +1 -1
- package/lib/plain.mjs.map +1 -1
- package/lib/protobuf.d.ts +2 -2
- package/lib/protobuf.js.map +1 -1
- package/lib/protobuf.mjs.map +1 -1
- package/native/cmd/ttsc-typia/assert_guard_factory_contract_test.go +17 -4
- package/native/cmd/ttsc-typia/create_assert_error_factory_arity_test.go +309 -0
- package/native/cmd/ttsc-typia/local_function_named_like_typia_operation_transform_test.go +185 -0
- package/native/cmd/ttsc-typia/recursive_conditional_alias_name_transform_test.go +307 -0
- package/native/cmd/ttsc-typia/shadowed_typia_module_declaration_diagnostic_test.go +152 -0
- package/native/cmd/ttsc-typia/template_literal_type_tags_transform_test.go +1 -3
- package/native/cmd/ttsc-typia/transform_helpers_test.go +92 -26
- package/native/core/factories/FormatCheatSheet.go +2 -2
- package/native/core/factories/MetadataCommentTagFactory.go +26 -11
- package/native/core/factories/factory_utility_coverage_test.go +896 -907
- package/native/core/factories/internal/metadata/MetadataHelper.go +5 -28
- package/native/core/factories/metadata_comment_tag_factory_coverage_test.go +2 -2
- package/native/core/programmers/RandomProgrammer.go +5 -2
- package/native/core/schemas/metadata/MetadataCollection.go +47 -5
- package/native/transform/CallExpressionTransformer.go +167 -0
- package/package.json +3 -3
- package/src/http.ts +18 -4
- package/src/internal/_assertGuard.ts +13 -2
- package/src/internal/_isFormatIdnHostname.ts +5 -1
- package/src/internal/_isFormatTime.ts +19 -2
- package/src/internal/_randomArray.ts +42 -6
- package/src/internal/_randomFormatByte.ts +17 -12
- package/src/internal/_randomFormatHostname.ts +2 -2
- package/src/internal/_randomFormatIdnHostname.ts +8 -9
- package/src/internal/_randomFormatRegex.ts +4 -6
- package/src/internal/_randomFormatTime.ts +15 -10
- package/src/internal/private/__randomComposition.ts +24 -0
- package/src/json.ts +16 -4
- package/src/module.ts +56 -22
- package/src/notations.ts +16 -4
- package/src/plain.ts +12 -3
- package/src/protobuf.ts +16 -4
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"encoding/json"
|
|
5
|
+
"os"
|
|
6
|
+
"path/filepath"
|
|
7
|
+
"strings"
|
|
8
|
+
"testing"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
// TestLocalFunctionNamedLikeTypiaOperationTransform verifies the shadowed-typia
|
|
12
|
+
// diagnostic does not fire on a name collision (#2328).
|
|
13
|
+
//
|
|
14
|
+
// The diagnostic that reports a redeclared typia call keys off the called name,
|
|
15
|
+
// and those names are ordinary words: a project may well own its own
|
|
16
|
+
// `createAssert`, import one from an unrelated package, or augment typia with a
|
|
17
|
+
// name that only exists under one of its namespaces. Reporting any of them
|
|
18
|
+
// would break builds that are correct today, so the name alone must never be
|
|
19
|
+
// enough — the callee has to reach typia through typia's own module specifier
|
|
20
|
+
// and spell the operation in the namespace it lives in.
|
|
21
|
+
//
|
|
22
|
+
// 1. Build a project that defines a local `createAssert`, imports a `createIs`
|
|
23
|
+
// from a relative module and a `createValidate` from a package whose name
|
|
24
|
+
// merely begins with "typia", and augments typia with a root `parse` — an
|
|
25
|
+
// operation name that exists only under a namespace.
|
|
26
|
+
// 2. Require a clean no-emit build, which proves every decoy really resolves,
|
|
27
|
+
// and a project transform with no diagnostic.
|
|
28
|
+
// 3. Require the one genuine `typia.createIs` call in the same file to still
|
|
29
|
+
// be rewritten, so silence is not the transform giving up on the file.
|
|
30
|
+
func TestLocalFunctionNamedLikeTypiaOperationTransform(t *testing.T) {
|
|
31
|
+
project := localTypiaNameCollisionProject(t)
|
|
32
|
+
|
|
33
|
+
// A no-emit build reports semantic diagnostics as well as typia's own, so a
|
|
34
|
+
// decoy that silently stopped resolving fails here instead of turning the
|
|
35
|
+
// rest of this test into a tautology.
|
|
36
|
+
out, errText, code := ttscTypiaTestCapture(func() int {
|
|
37
|
+
return runBuild([]string{
|
|
38
|
+
"--cwd", project,
|
|
39
|
+
"--tsconfig", "tsconfig.json",
|
|
40
|
+
"--noEmit",
|
|
41
|
+
})
|
|
42
|
+
})
|
|
43
|
+
if code != 0 {
|
|
44
|
+
t.Fatalf("name collision fixture is not clean, got %d\nstdout=%s\nstderr=%s", code, out, errText)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
out, errText, code = ttscTypiaTestCapture(func() int {
|
|
48
|
+
return runTransform([]string{
|
|
49
|
+
"--cwd", project,
|
|
50
|
+
"--tsconfig", "tsconfig.json",
|
|
51
|
+
})
|
|
52
|
+
})
|
|
53
|
+
if code != 0 {
|
|
54
|
+
t.Fatalf("name collisions must not fail the transform, got %d\nstdout=%s\nstderr=%s", code, out, errText)
|
|
55
|
+
}
|
|
56
|
+
var result transformProjectOutput
|
|
57
|
+
if err := json.Unmarshal([]byte(out), &result); err != nil {
|
|
58
|
+
t.Fatalf("decode transform output: %v\n%s", err, out)
|
|
59
|
+
}
|
|
60
|
+
if len(result.Diagnostics) != 0 {
|
|
61
|
+
t.Fatalf("name collisions produced diagnostics: %+v", result.Diagnostics)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
js, errText, code := ttscTypiaTestCapture(func() int {
|
|
65
|
+
return runTransform([]string{
|
|
66
|
+
"--cwd", project,
|
|
67
|
+
"--tsconfig", "tsconfig.json",
|
|
68
|
+
"--file", "src/main.ts",
|
|
69
|
+
"--output", "js",
|
|
70
|
+
})
|
|
71
|
+
})
|
|
72
|
+
if code != 0 {
|
|
73
|
+
t.Fatalf("name collision fixture failed to emit: code=%d stderr=\n%s", code, errText)
|
|
74
|
+
}
|
|
75
|
+
// The genuine call becomes an inline validator; the decoys stay ordinary
|
|
76
|
+
// calls. Type arguments are erased either way, so the emitted body is the
|
|
77
|
+
// only evidence that the transform ran on one and not the others.
|
|
78
|
+
if !strings.Contains(js, `"number" === typeof`) {
|
|
79
|
+
t.Fatalf("the genuine typia call did not emit its validator:\n%s", js)
|
|
80
|
+
}
|
|
81
|
+
for _, decoy := range []string{"createAssert()", "createIs)()", "createValidate)()", "parse)(\"{}\")"} {
|
|
82
|
+
if !strings.Contains(js, decoy) {
|
|
83
|
+
t.Fatalf("decoy call %q was rewritten or lost:\n%s", decoy, js)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
func localTypiaNameCollisionProject(t *testing.T) string {
|
|
89
|
+
t.Helper()
|
|
90
|
+
dir := t.TempDir()
|
|
91
|
+
src := filepath.Join(dir, "src")
|
|
92
|
+
if err := os.MkdirAll(src, 0o755); err != nil {
|
|
93
|
+
t.Fatalf("mkdir fixture src: %v", err)
|
|
94
|
+
}
|
|
95
|
+
if err := os.WriteFile(filepath.Join(dir, "tsconfig.json"), []byte(transformDiagnosticTSConfig), 0o644); err != nil {
|
|
96
|
+
t.Fatalf("write tsconfig: %v", err)
|
|
97
|
+
}
|
|
98
|
+
ttscTypiaTestWriteFactoryStub(t, dir)
|
|
99
|
+
localTypiaNameCollisionWriteNeighborStub(t, dir)
|
|
100
|
+
for name, body := range map[string]string{
|
|
101
|
+
"helpers.ts": localTypiaNameCollisionHelpers,
|
|
102
|
+
"augmentation.ts": localTypiaNameCollisionAugmentation,
|
|
103
|
+
"main.ts": localTypiaNameCollisionSource,
|
|
104
|
+
} {
|
|
105
|
+
if err := os.WriteFile(filepath.Join(src, name), []byte(body), 0o644); err != nil {
|
|
106
|
+
t.Fatalf("write fixture %s: %v", name, err)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return dir
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// localTypiaNameCollisionWriteNeighborStub installs a package whose name merely
|
|
113
|
+
// begins with "typia" and exports an operation name typia owns. It pins the
|
|
114
|
+
// one-past-boundary of the specifier test: the call reaches the specifier check
|
|
115
|
+
// under a name that matches, and only the specifier can reject it.
|
|
116
|
+
func localTypiaNameCollisionWriteNeighborStub(t *testing.T, project string) {
|
|
117
|
+
t.Helper()
|
|
118
|
+
root := filepath.Join(project, "node_modules", "typia-neighbor")
|
|
119
|
+
if err := os.MkdirAll(root, 0o755); err != nil {
|
|
120
|
+
t.Fatalf("mkdir typia-neighbor stub: %v", err)
|
|
121
|
+
}
|
|
122
|
+
files := map[string]string{
|
|
123
|
+
"package.json": `{
|
|
124
|
+
"name": "typia-neighbor",
|
|
125
|
+
"version": "0.0.0-test",
|
|
126
|
+
"main": "./index.js",
|
|
127
|
+
"types": "./index.d.ts",
|
|
128
|
+
"exports": {
|
|
129
|
+
".": {
|
|
130
|
+
"types": "./index.d.ts",
|
|
131
|
+
"default": "./index.js"
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
`,
|
|
136
|
+
"index.d.ts": "export declare function createValidate<T>(): (input: unknown) => T;\n",
|
|
137
|
+
"index.js": "exports.createValidate = () => (input) => input;\n",
|
|
138
|
+
}
|
|
139
|
+
for name, body := range files {
|
|
140
|
+
if err := os.WriteFile(filepath.Join(root, name), []byte(body), 0o644); err != nil {
|
|
141
|
+
t.Fatalf("write typia-neighbor stub %s: %v", name, err)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const localTypiaNameCollisionHelpers = `export const createIs =
|
|
147
|
+
<T,>() =>
|
|
148
|
+
(input: unknown): input is T =>
|
|
149
|
+
input !== undefined;
|
|
150
|
+
`
|
|
151
|
+
|
|
152
|
+
// A module augmentation that adds a root-level `parse` to typia. The leading
|
|
153
|
+
// `import "typia"` makes this file a module, so the block merges with the
|
|
154
|
+
// installed typia instead of replacing it. `parse` is an operation name under
|
|
155
|
+
// the `llm` namespace and nowhere else, so a gate that matched a flat union of
|
|
156
|
+
// every namespace's names would report this call — a build that works today.
|
|
157
|
+
const localTypiaNameCollisionAugmentation = `import "typia";
|
|
158
|
+
|
|
159
|
+
declare module "typia" {
|
|
160
|
+
export function parse(text: string): unknown;
|
|
161
|
+
}
|
|
162
|
+
`
|
|
163
|
+
|
|
164
|
+
const localTypiaNameCollisionSource = `import { createValidate } from "typia-neighbor";
|
|
165
|
+
import { parse } from "typia";
|
|
166
|
+
import typia from "typia";
|
|
167
|
+
|
|
168
|
+
import "./augmentation";
|
|
169
|
+
import { createIs } from "./helpers";
|
|
170
|
+
|
|
171
|
+
export interface User {
|
|
172
|
+
id: number;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const createAssert =
|
|
176
|
+
<T,>() =>
|
|
177
|
+
(input: unknown): T =>
|
|
178
|
+
input as T;
|
|
179
|
+
|
|
180
|
+
export const localAssert = createAssert<User>();
|
|
181
|
+
export const relativeIs = createIs<User>();
|
|
182
|
+
export const neighborValidate = createValidate<User>();
|
|
183
|
+
export const augmented = parse("{}");
|
|
184
|
+
export const genuineIs = typia.createIs<User>();
|
|
185
|
+
`
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"os"
|
|
5
|
+
"os/exec"
|
|
6
|
+
"path/filepath"
|
|
7
|
+
"strings"
|
|
8
|
+
"testing"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
// TestRecursiveConditionalAliasNameTransform pins the transform over a type
|
|
12
|
+
// whose resolved graph names itself (issue #2331).
|
|
13
|
+
//
|
|
14
|
+
// `Primitive<T>` instantiated at the self-recursive union `type Node = Date |
|
|
15
|
+
// Node[]` resolves to a union `X = string & Format<"date-time"> | X[]`, whose
|
|
16
|
+
// array member's sole type argument is `X` itself. The name builders walked
|
|
17
|
+
// union member -> array reference -> type argument and arrived back at the type
|
|
18
|
+
// whose name was still being composed, so they recursed until the plugin process
|
|
19
|
+
// died of a Go stack overflow: no diagnostic, no emit, and ttsc taken with it.
|
|
20
|
+
// `Primitive` is only one witness -- any self-recursive conditional alias
|
|
21
|
+
// produces the same graph -- so the fixture pins the bare `Rec<T>` form too.
|
|
22
|
+
//
|
|
23
|
+
// The names the fix produces have to stay usable, not merely terminate: the
|
|
24
|
+
// cycle's placeholder is the type's own symbol with its arguments unexpanded, so
|
|
25
|
+
// a JSON schema component keeps a short key instead of being named after a
|
|
26
|
+
// several-hundred-character structural dump of the elided rendering.
|
|
27
|
+
//
|
|
28
|
+
// 1. Transform is / assert / json.schema over `Primitive<Node>`, the bare
|
|
29
|
+
// recursive conditional alias, and the recursive-JsonValue object, beside
|
|
30
|
+
// controls (named recursive union, self-recursive array alias, recursive
|
|
31
|
+
// interface) that already transformed and must keep their names.
|
|
32
|
+
// 2. Require the emit to carry a recursive array validator rather than an
|
|
33
|
+
// inlined expansion.
|
|
34
|
+
// 3. Execute the emitted validators over positive, negative, nested and
|
|
35
|
+
// boundary values, and require the recursive schema to self-reference
|
|
36
|
+
// through a bounded component name.
|
|
37
|
+
func TestRecursiveConditionalAliasNameTransform(t *testing.T) {
|
|
38
|
+
project := recursiveConditionalAliasNameProject(t)
|
|
39
|
+
transform := func() (string, string, int) {
|
|
40
|
+
return ttscTypiaTestCapture(func() int {
|
|
41
|
+
return runTransform([]string{
|
|
42
|
+
"--cwd", project,
|
|
43
|
+
"--tsconfig", "tsconfig.json",
|
|
44
|
+
"--file", "src/main.ts",
|
|
45
|
+
"--output", "js",
|
|
46
|
+
})
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
out, errText, code := transform()
|
|
51
|
+
if code != 0 {
|
|
52
|
+
t.Fatalf("recursive conditional alias transform failed: code=%d stderr=\n%s", code, errText)
|
|
53
|
+
}
|
|
54
|
+
if strings.Contains(out, "_ia0") == false {
|
|
55
|
+
t.Fatalf("expected a recursive array validator in the emit, got:\n%s", out)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// The cycle guard is a map, and the name it settles on becomes a helper
|
|
59
|
+
// identifier and a schema component key. Map iteration order must not reach
|
|
60
|
+
// either, so the same source has to emit the same bytes.
|
|
61
|
+
repeat, repeatErr, repeatCode := transform()
|
|
62
|
+
if repeatCode != 0 {
|
|
63
|
+
t.Fatalf("recursive conditional alias repeat transform failed: code=%d stderr=\n%s", repeatCode, repeatErr)
|
|
64
|
+
}
|
|
65
|
+
if repeat != out {
|
|
66
|
+
t.Fatalf("repeat transform emitted different bytes:\nfirst:\n%s\nsecond:\n%s", out, repeat)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
recursiveConditionalAliasNameRunRuntimeCases(t, project, out)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
func recursiveConditionalAliasNameProject(t *testing.T) string {
|
|
73
|
+
t.Helper()
|
|
74
|
+
root := ttscTypiaTestRepoRoot(t)
|
|
75
|
+
base := filepath.Join(root, "packages", "typia", "native", ".tmp-ttsc-typia-tests")
|
|
76
|
+
if err := os.MkdirAll(base, 0o755); err != nil {
|
|
77
|
+
t.Fatalf("mkdir temp base: %v", err)
|
|
78
|
+
}
|
|
79
|
+
dir, err := os.MkdirTemp(base, "recursive-conditional-alias-")
|
|
80
|
+
if err != nil {
|
|
81
|
+
t.Fatalf("create temp fixture: %v", err)
|
|
82
|
+
}
|
|
83
|
+
t.Cleanup(func() {
|
|
84
|
+
_ = os.RemoveAll(dir)
|
|
85
|
+
})
|
|
86
|
+
src := filepath.Join(dir, "src")
|
|
87
|
+
if err := os.MkdirAll(src, 0o755); err != nil {
|
|
88
|
+
t.Fatalf("mkdir fixture src: %v", err)
|
|
89
|
+
}
|
|
90
|
+
if err := os.WriteFile(filepath.Join(dir, "tsconfig.json"), []byte(recursiveConditionalAliasNameTSConfig), 0o644); err != nil {
|
|
91
|
+
t.Fatalf("write tsconfig: %v", err)
|
|
92
|
+
}
|
|
93
|
+
if err := os.WriteFile(filepath.Join(src, "main.ts"), []byte(recursiveConditionalAliasNameSource), 0o644); err != nil {
|
|
94
|
+
t.Fatalf("write source: %v", err)
|
|
95
|
+
}
|
|
96
|
+
return dir
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
func recursiveConditionalAliasNameRunRuntimeCases(t *testing.T, project string, js string) {
|
|
100
|
+
t.Helper()
|
|
101
|
+
node, err := exec.LookPath("node")
|
|
102
|
+
if err != nil {
|
|
103
|
+
t.Skip("node executable not found")
|
|
104
|
+
}
|
|
105
|
+
runtimeDir := filepath.Join(project, "runtime")
|
|
106
|
+
if err := os.MkdirAll(runtimeDir, 0o755); err != nil {
|
|
107
|
+
t.Fatalf("mkdir runtime dir: %v", err)
|
|
108
|
+
}
|
|
109
|
+
ttscTypiaTestWriteCommonRuntimeStubs(t, runtimeDir)
|
|
110
|
+
if err := os.WriteFile(filepath.Join(runtimeDir, "is-format-date-time-stub.cjs"), []byte(recursiveConditionalAliasNameFormatStub), 0o644); err != nil {
|
|
111
|
+
t.Fatalf("write format stub: %v", err)
|
|
112
|
+
}
|
|
113
|
+
js = strings.ReplaceAll(js, `require("typia/lib/internal/_isFormatDateTime")`, `require("./is-format-date-time-stub.cjs")`)
|
|
114
|
+
runtimeJS := ttscTypiaTestRewriteCommonJS(t, js)
|
|
115
|
+
if err := os.WriteFile(filepath.Join(runtimeDir, "main.cjs"), []byte(runtimeJS), 0o644); err != nil {
|
|
116
|
+
t.Fatalf("write runtime module: %v", err)
|
|
117
|
+
}
|
|
118
|
+
runner := filepath.Join(runtimeDir, "run.cjs")
|
|
119
|
+
if err := os.WriteFile(runner, []byte(recursiveConditionalAliasNameRuntimeRunner), 0o644); err != nil {
|
|
120
|
+
t.Fatalf("write runtime runner: %v", err)
|
|
121
|
+
}
|
|
122
|
+
cmd := exec.Command(node, runner)
|
|
123
|
+
cmd.Dir = runtimeDir
|
|
124
|
+
output, err := cmd.CombinedOutput()
|
|
125
|
+
if err != nil {
|
|
126
|
+
t.Fatalf("recursive conditional alias runtime cases failed: %v\n%s", err, output)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const recursiveConditionalAliasNameTSConfig = `{
|
|
131
|
+
"compilerOptions": {
|
|
132
|
+
"target": "ES2022",
|
|
133
|
+
"module": "commonjs",
|
|
134
|
+
"moduleResolution": "bundler",
|
|
135
|
+
"ignoreDeprecations": "6.0",
|
|
136
|
+
"types": ["*"],
|
|
137
|
+
"esModuleInterop": true,
|
|
138
|
+
"strict": true,
|
|
139
|
+
"skipLibCheck": true
|
|
140
|
+
},
|
|
141
|
+
"include": ["src"]
|
|
142
|
+
}
|
|
143
|
+
`
|
|
144
|
+
|
|
145
|
+
// The shipped helper validates the RFC 3339 date-time shape; the runtime cases
|
|
146
|
+
// only need it to separate a date-time string from a plain one, and a stable
|
|
147
|
+
// predicate keeps a failure reproducible.
|
|
148
|
+
const recursiveConditionalAliasNameFormatStub = `module.exports._isFormatDateTime = (value) =>
|
|
149
|
+
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})$/.test(value);
|
|
150
|
+
`
|
|
151
|
+
|
|
152
|
+
const recursiveConditionalAliasNameSource = `import typia, { Primitive, tags } from "typia";
|
|
153
|
+
|
|
154
|
+
// 1. the reported case: Primitive over a self-recursive union carrying Date.
|
|
155
|
+
// Primitive rewrites the Date arm to a date-time string and keeps the array
|
|
156
|
+
// arm pointing back at the whole union.
|
|
157
|
+
type Node = Date | Node[];
|
|
158
|
+
export const isPrimitiveNode = typia.createIs<Primitive<Node>>();
|
|
159
|
+
export const assertPrimitiveNode = typia.createAssert<Primitive<Node>>();
|
|
160
|
+
export const schemaPrimitiveNode = typia.json.schema<Primitive<Node>>();
|
|
161
|
+
|
|
162
|
+
// 2. the same graph without Primitive: any self-recursive conditional alias
|
|
163
|
+
// instantiates to a union whose array member's type argument is that union
|
|
164
|
+
type Rec<T> = T extends Date
|
|
165
|
+
? string
|
|
166
|
+
: T extends (infer U)[]
|
|
167
|
+
? Rec<U>[]
|
|
168
|
+
: never;
|
|
169
|
+
export const isRec = typia.createIs<Rec<Node>>();
|
|
170
|
+
|
|
171
|
+
// 3. the second report: the recursion sits behind an object property
|
|
172
|
+
interface JsonObject {
|
|
173
|
+
[key: string]: JsonValue;
|
|
174
|
+
}
|
|
175
|
+
type JsonValue = string | number | boolean | null | JsonObject | JsonValue[];
|
|
176
|
+
interface Output {
|
|
177
|
+
at: Date;
|
|
178
|
+
payload: JsonValue;
|
|
179
|
+
}
|
|
180
|
+
export const isOutput = typia.createIs<Primitive<Output>>();
|
|
181
|
+
|
|
182
|
+
// controls: recursions that already named themselves through an alias or a
|
|
183
|
+
// declaration must keep transforming and must not take the cycle placeholder
|
|
184
|
+
type Named = (string & tags.Format<"date-time">) | Named[];
|
|
185
|
+
export const isNamed = typia.createIs<Named>();
|
|
186
|
+
export const schemaNamed = typia.json.schema<Named>();
|
|
187
|
+
|
|
188
|
+
type SelfArray = SelfArray[];
|
|
189
|
+
export const isSelfArray = typia.createIs<SelfArray>();
|
|
190
|
+
|
|
191
|
+
interface ICategory {
|
|
192
|
+
name: string;
|
|
193
|
+
children: ICategory[];
|
|
194
|
+
}
|
|
195
|
+
export const isCategory = typia.createIs<ICategory>();
|
|
196
|
+
export const schemaCategory = typia.json.schema<ICategory>();
|
|
197
|
+
`
|
|
198
|
+
|
|
199
|
+
const recursiveConditionalAliasNameRuntimeRunner = `const mod = require("./main.cjs");
|
|
200
|
+
|
|
201
|
+
let failures = 0;
|
|
202
|
+
const expect = (label, actual, expected) => {
|
|
203
|
+
if (actual !== expected) {
|
|
204
|
+
console.log("FAIL " + label + ": expected " + expected + ", got " + actual);
|
|
205
|
+
failures++;
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
const DATE = "2020-01-01T00:00:00.000Z";
|
|
210
|
+
|
|
211
|
+
// --- 1. Primitive<Node>: date-time string or (recursively) an array of those ---
|
|
212
|
+
expect("primitive top date-time", mod.isPrimitiveNode(DATE), true);
|
|
213
|
+
expect("primitive top plain string", mod.isPrimitiveNode("nope"), false);
|
|
214
|
+
expect("primitive top number", mod.isPrimitiveNode(1), false);
|
|
215
|
+
expect("primitive top null", mod.isPrimitiveNode(null), false);
|
|
216
|
+
expect("primitive top Date instance", mod.isPrimitiveNode(new Date(0)), false);
|
|
217
|
+
expect("primitive empty array", mod.isPrimitiveNode([]), true);
|
|
218
|
+
expect("primitive flat array", mod.isPrimitiveNode([DATE, DATE]), true);
|
|
219
|
+
expect("primitive flat array bad member", mod.isPrimitiveNode([DATE, "nope"]), false);
|
|
220
|
+
expect("primitive nested array", mod.isPrimitiveNode([[[DATE]], []]), true);
|
|
221
|
+
expect("primitive nested bad leaf", mod.isPrimitiveNode([[[1]]]), false);
|
|
222
|
+
expect("primitive nested Date instance", mod.isPrimitiveNode([[new Date(0)]]), false);
|
|
223
|
+
|
|
224
|
+
// depth boundary: the emitted validator recurses through one function, so a
|
|
225
|
+
// deep value must not need a deeper emit
|
|
226
|
+
let deep = [DATE];
|
|
227
|
+
for (let i = 0; i < 64; ++i) deep = [deep];
|
|
228
|
+
expect("primitive deeply nested", mod.isPrimitiveNode(deep), true);
|
|
229
|
+
let deepBad = [1];
|
|
230
|
+
for (let i = 0; i < 64; ++i) deepBad = [deepBad];
|
|
231
|
+
expect("primitive deeply nested bad leaf", mod.isPrimitiveNode(deepBad), false);
|
|
232
|
+
|
|
233
|
+
// the emitted recursive validator must not be fooled by a self-referencing value
|
|
234
|
+
const cyclic = [];
|
|
235
|
+
cyclic.push(cyclic);
|
|
236
|
+
expect("primitive cyclic value terminates", mod.isPrimitiveNode(cyclic), true);
|
|
237
|
+
|
|
238
|
+
// --- 1b. assert reports the arm, and passes what is valid ---
|
|
239
|
+
expect("assert passes", JSON.stringify(mod.assertPrimitiveNode([DATE])), JSON.stringify([DATE]));
|
|
240
|
+
let assertPath = "";
|
|
241
|
+
let assertExpected = "";
|
|
242
|
+
try {
|
|
243
|
+
mod.assertPrimitiveNode(["nope"]);
|
|
244
|
+
failures++;
|
|
245
|
+
console.log("FAIL assert should have thrown");
|
|
246
|
+
} catch (exp) {
|
|
247
|
+
assertPath = String(exp.path);
|
|
248
|
+
assertExpected = String(exp.expected);
|
|
249
|
+
}
|
|
250
|
+
expect("assert names the path", assertPath, "$input[0]");
|
|
251
|
+
// The expected string is the same name the fix has to keep readable: the arm
|
|
252
|
+
// itself, not a structural dump of the cycle it sits in.
|
|
253
|
+
expect("assert names the arm", assertExpected.includes('Format<"date-time">'), true);
|
|
254
|
+
expect("assert expectation is bounded", assertExpected.length <= 64, true);
|
|
255
|
+
|
|
256
|
+
// --- 2. the bare recursive conditional alias: string or (recursively) arrays ---
|
|
257
|
+
expect("rec top string", mod.isRec("anything"), true);
|
|
258
|
+
expect("rec top number", mod.isRec(1), false);
|
|
259
|
+
expect("rec nested array", mod.isRec([["a"], []]), true);
|
|
260
|
+
expect("rec nested bad leaf", mod.isRec([[1]]), false);
|
|
261
|
+
|
|
262
|
+
// --- 3. the recursion behind an object property ---
|
|
263
|
+
expect("output valid", mod.isOutput({ at: DATE, payload: { a: [1, "b", null, { c: true }] } }), true);
|
|
264
|
+
expect("output bad date", mod.isOutput({ at: "nope", payload: 1 }), false);
|
|
265
|
+
expect("output missing key", mod.isOutput({ payload: 1 }), false);
|
|
266
|
+
expect("output bad payload", mod.isOutput({ at: DATE, payload: undefined }), false);
|
|
267
|
+
|
|
268
|
+
// --- controls: the recursions that already worked ---
|
|
269
|
+
expect("named recursive union", mod.isNamed([[DATE]]), true);
|
|
270
|
+
expect("named recursive union bad leaf", mod.isNamed([["nope"]]), false);
|
|
271
|
+
expect("self array alias", mod.isSelfArray([[], [[]]]), true);
|
|
272
|
+
expect("self array alias bad leaf", mod.isSelfArray([1]), false);
|
|
273
|
+
expect("category valid", mod.isCategory({ name: "a", children: [{ name: "b", children: [] }] }), true);
|
|
274
|
+
expect("category bad child", mod.isCategory({ name: "a", children: [{ name: 1, children: [] }] }), false);
|
|
275
|
+
|
|
276
|
+
// --- the schema names the cycle without dumping its structural expansion ---
|
|
277
|
+
const unit = mod.schemaPrimitiveNode;
|
|
278
|
+
const keys = Object.keys(unit.components.schemas || {});
|
|
279
|
+
expect("schema has one component", keys.length, 1);
|
|
280
|
+
const key = keys[0];
|
|
281
|
+
if (key !== undefined) {
|
|
282
|
+
// The pre-fix walk could only have produced a name built from the checker's
|
|
283
|
+
// elided rendering, which runs to several hundred characters; the placeholder
|
|
284
|
+
// keeps it in the same range as an ordinary named component.
|
|
285
|
+
expect("component name is bounded", key.length <= 64, true);
|
|
286
|
+
expect("component is an array", unit.components.schemas[key].type, "array");
|
|
287
|
+
const items = unit.components.schemas[key].items;
|
|
288
|
+
const refs = (items.oneOf || []).filter((elem) => elem.$ref !== undefined);
|
|
289
|
+
expect("array items reference the component itself", refs.length === 1 && refs[0].$ref === "#/components/schemas/" + key, true);
|
|
290
|
+
expect("array items keep the date-time arm", (items.oneOf || []).some((elem) => elem.format === "date-time"), true);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// controls: a recursion reached through a declaration names every component it
|
|
294
|
+
// emits from that declaration, so the placeholder never applies to one
|
|
295
|
+
expect("category component name", Object.keys(mod.schemaCategory.components.schemas || {})[0], "ICategory");
|
|
296
|
+
const namedKeys = Object.keys(mod.schemaNamed.components.schemas || {});
|
|
297
|
+
expect(
|
|
298
|
+
"named recursion names every component from its declaration",
|
|
299
|
+
namedKeys.length !== 0 && namedKeys.every((elem) => elem.includes("Named")),
|
|
300
|
+
true,
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
if (failures > 0) {
|
|
304
|
+
throw new Error(failures + " assertion(s) failed");
|
|
305
|
+
}
|
|
306
|
+
console.log("all recursive-conditional-alias cases passed");
|
|
307
|
+
`
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"encoding/json"
|
|
5
|
+
"os"
|
|
6
|
+
"path/filepath"
|
|
7
|
+
"strings"
|
|
8
|
+
"testing"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
// TestShadowedTypiaModuleDeclarationDiagnostic verifies a redeclared typia call
|
|
12
|
+
// is reported instead of silently skipped (#2328).
|
|
13
|
+
//
|
|
14
|
+
// The transformer attributes a call by the file its resolved signature is
|
|
15
|
+
// declared in, which is what stops another package from claiming typia's
|
|
16
|
+
// transform. A project-local `declare module "typia"` moves that declaration out
|
|
17
|
+
// of typia's own files, so the identity test misses and the call used to pass
|
|
18
|
+
// through untransformed with no diagnostic — `ttsc.transform()` returned
|
|
19
|
+
// `type: "success"` with an empty diagnostic list, and the failure surfaced only
|
|
20
|
+
// at run time as `no transform has been configured`. The identity test stays;
|
|
21
|
+
// the silence does not.
|
|
22
|
+
//
|
|
23
|
+
// 1. Build a project with typia installed and an ambient `declare module
|
|
24
|
+
// "typia"` that redeclares `createAssert`, `json.createAssertParse`, and a
|
|
25
|
+
// `json.createAssert` that names no typia operation at all.
|
|
26
|
+
// 2. Run the transform over the project.
|
|
27
|
+
// 3. Require one diagnostic for each of the two real operations, at its call
|
|
28
|
+
// site, naming the operation as written and the shadowing file — and none
|
|
29
|
+
// for the third, whose spelling puts a root operation in a namespace.
|
|
30
|
+
func TestShadowedTypiaModuleDeclarationDiagnostic(t *testing.T) {
|
|
31
|
+
project := shadowedTypiaModuleProject(t)
|
|
32
|
+
out, errText, code := ttscTypiaTestCapture(func() int {
|
|
33
|
+
return runTransform([]string{
|
|
34
|
+
"--cwd", project,
|
|
35
|
+
"--tsconfig", "tsconfig.json",
|
|
36
|
+
})
|
|
37
|
+
})
|
|
38
|
+
if code != 3 {
|
|
39
|
+
t.Fatalf("shadowed typia declaration should fail with code 3, got %d\nstdout=%s\nstderr=%s", code, out, errText)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
var result transformProjectOutput
|
|
43
|
+
if err := json.Unmarshal([]byte(out), &result); err != nil {
|
|
44
|
+
t.Fatalf("decode transform output: %v\n%s", err, out)
|
|
45
|
+
}
|
|
46
|
+
// The root and namespaced spellings are looked up in different functor
|
|
47
|
+
// tables, so both have to be reported for the gate to be correct, and the
|
|
48
|
+
// code has to carry the namespace the user wrote.
|
|
49
|
+
expected := map[string]bool{
|
|
50
|
+
"typia.createAssert": false,
|
|
51
|
+
"typia.json.createAssertParse": false,
|
|
52
|
+
}
|
|
53
|
+
if len(result.Diagnostics) != len(expected) {
|
|
54
|
+
t.Fatalf("expected %d transform diagnostics, got %+v", len(expected), result.Diagnostics)
|
|
55
|
+
}
|
|
56
|
+
for _, diag := range result.Diagnostics {
|
|
57
|
+
seen, owned := expected[diag.Code]
|
|
58
|
+
if owned == false {
|
|
59
|
+
t.Fatalf("diagnostic code did not name a shadowed operation as written: %+v", diag)
|
|
60
|
+
}
|
|
61
|
+
if seen {
|
|
62
|
+
t.Fatalf("diagnostic code reported twice: %+v", diag)
|
|
63
|
+
}
|
|
64
|
+
expected[diag.Code] = true
|
|
65
|
+
if diag.File == nil || !strings.HasSuffix(filepath.ToSlash(*diag.File), "src/main.ts") {
|
|
66
|
+
t.Fatalf("diagnostic was not reported at the call site: %+v", diag)
|
|
67
|
+
}
|
|
68
|
+
if diag.Line != shadowedTypiaCallLines[diag.Code] {
|
|
69
|
+
t.Fatalf("diagnostic line mismatch for %s: %+v", diag.Code, diag)
|
|
70
|
+
}
|
|
71
|
+
if !strings.Contains(filepath.ToSlash(diag.MessageText), "src/typia.d.ts") {
|
|
72
|
+
t.Fatalf("diagnostic did not name the shadowing declaration: %+v", diag)
|
|
73
|
+
}
|
|
74
|
+
if !strings.Contains(diag.MessageText, "no transform was applied") {
|
|
75
|
+
t.Fatalf("diagnostic did not state the consequence: %+v", diag)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
for code, seen := range expected {
|
|
79
|
+
if seen == false {
|
|
80
|
+
t.Fatalf("no diagnostic reported %s", code)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// shadowedTypiaCallLines is the 1-based line of each shadowed call in
|
|
86
|
+
// shadowedTypiaCallSource, so a regression that reports the whole file or the
|
|
87
|
+
// wrong statement cannot pass on the file name alone.
|
|
88
|
+
var shadowedTypiaCallLines = map[string]int{
|
|
89
|
+
"typia.createAssert": 7,
|
|
90
|
+
"typia.json.createAssertParse": 8,
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
func shadowedTypiaModuleProject(t *testing.T) string {
|
|
94
|
+
t.Helper()
|
|
95
|
+
dir := t.TempDir()
|
|
96
|
+
src := filepath.Join(dir, "src")
|
|
97
|
+
if err := os.MkdirAll(src, 0o755); err != nil {
|
|
98
|
+
t.Fatalf("mkdir fixture src: %v", err)
|
|
99
|
+
}
|
|
100
|
+
if err := os.WriteFile(filepath.Join(dir, "tsconfig.json"), []byte(transformDiagnosticTSConfig), 0o644); err != nil {
|
|
101
|
+
t.Fatalf("write tsconfig: %v", err)
|
|
102
|
+
}
|
|
103
|
+
// typia is installed as well as shadowed, matching the reporter's project: an
|
|
104
|
+
// ambient module declaration outranks node resolution, and the fixture is
|
|
105
|
+
// only faithful if there is something for it to outrank.
|
|
106
|
+
ttscTypiaTestWriteFactoryStub(t, dir)
|
|
107
|
+
for name, body := range map[string]string{
|
|
108
|
+
"typia.d.ts": shadowedTypiaAmbientDeclaration,
|
|
109
|
+
"main.ts": shadowedTypiaCallSource,
|
|
110
|
+
} {
|
|
111
|
+
if err := os.WriteFile(filepath.Join(src, name), []byte(body), 0o644); err != nil {
|
|
112
|
+
t.Fatalf("write fixture %s: %v", name, err)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return dir
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const shadowedTypiaAmbientDeclaration = `declare module "typia" {
|
|
119
|
+
type ErrorFactory = (props: { method: string }) => Error;
|
|
120
|
+
export function createAssert<T>(
|
|
121
|
+
errorFactory?: ErrorFactory,
|
|
122
|
+
): (input: unknown, errorFactory?: ErrorFactory) => T;
|
|
123
|
+
export namespace json {
|
|
124
|
+
function createAssertParse<T>(
|
|
125
|
+
errorFactory?: ErrorFactory,
|
|
126
|
+
): (input: string, errorFactory?: ErrorFactory) => T;
|
|
127
|
+
function createAssert<T>(
|
|
128
|
+
errorFactory?: ErrorFactory,
|
|
129
|
+
): (input: unknown, errorFactory?: ErrorFactory) => T;
|
|
130
|
+
}
|
|
131
|
+
const typia: {
|
|
132
|
+
createAssert<T>(
|
|
133
|
+
errorFactory?: ErrorFactory,
|
|
134
|
+
): (input: unknown, errorFactory?: ErrorFactory) => T;
|
|
135
|
+
json: typeof json;
|
|
136
|
+
};
|
|
137
|
+
export default typia;
|
|
138
|
+
}
|
|
139
|
+
`
|
|
140
|
+
|
|
141
|
+
// The third call spells a root operation inside a namespace. typia has no
|
|
142
|
+
// `json.createAssert`, so nothing was shadowed and nothing may be reported.
|
|
143
|
+
const shadowedTypiaCallSource = `import typia from "typia";
|
|
144
|
+
|
|
145
|
+
export interface User {
|
|
146
|
+
id: number;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export const parseUser = typia.createAssert<User>();
|
|
150
|
+
export const parseUserJson = typia.json.createAssertParse<User>();
|
|
151
|
+
export const bogus = typia.json.createAssert<User>();
|
|
152
|
+
`
|
|
@@ -34,9 +34,7 @@ func TestTemplateLiteralTypeTagsTransform(t *testing.T) {
|
|
|
34
34
|
if code != 0 {
|
|
35
35
|
t.Fatalf("template literal tags transform failed: code=%d stderr=\n%s", code, errText)
|
|
36
36
|
}
|
|
37
|
-
if !strings.Contains(out,
|
|
38
|
-
!strings.Contains(out, "._stringLength(input) <= 30") ||
|
|
39
|
-
!strings.Contains(out, "RegExp(/^prefix(.*)postfix$/)") {
|
|
37
|
+
if !strings.Contains(out, "length <= 30") || !strings.Contains(out, "RegExp(/^prefix(.*)postfix$/)") {
|
|
40
38
|
t.Fatalf("emitted checker should combine the template pattern with its tags:\n%s", out)
|
|
41
39
|
}
|
|
42
40
|
templateLiteralTypeTagsRunRuntimeCases(t, project, out)
|