typia 13.2.0 → 14.0.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.
Files changed (135) hide show
  1. package/lib/http.d.ts +3 -3
  2. package/lib/http.js.map +1 -1
  3. package/lib/http.mjs.map +1 -1
  4. package/lib/internal/_assertGuard.js +13 -3
  5. package/lib/internal/_assertGuard.js.map +1 -1
  6. package/lib/internal/_assertGuard.mjs +4 -2
  7. package/lib/internal/_assertGuard.mjs.map +1 -1
  8. package/lib/internal/_isFormatIdnHostname.js +5 -1
  9. package/lib/internal/_isFormatIdnHostname.js.map +1 -1
  10. package/lib/internal/_isFormatIdnHostname.mjs +1 -1
  11. package/lib/internal/_isFormatIdnHostname.mjs.map +1 -1
  12. package/lib/internal/_isFormatTime.js +19 -2
  13. package/lib/internal/_isFormatTime.js.map +1 -1
  14. package/lib/internal/_isFormatTime.mjs +9 -2
  15. package/lib/internal/_isFormatTime.mjs.map +1 -1
  16. package/lib/internal/_randomArray.js +37 -4
  17. package/lib/internal/_randomArray.js.map +1 -1
  18. package/lib/internal/_randomArray.mjs +16 -3
  19. package/lib/internal/_randomArray.mjs.map +1 -1
  20. package/lib/internal/_randomFormatByte.js +15 -12
  21. package/lib/internal/_randomFormatByte.js.map +1 -1
  22. package/lib/internal/_randomFormatByte.mjs +11 -10
  23. package/lib/internal/_randomFormatByte.mjs.map +1 -1
  24. package/lib/internal/_randomFormatDatetime.mjs +1 -1
  25. package/lib/internal/_randomFormatHostname.d.ts +2 -2
  26. package/lib/internal/_randomFormatHostname.js +2 -2
  27. package/lib/internal/_randomFormatHostname.mjs +2 -2
  28. package/lib/internal/_randomFormatHostname.mjs.map +1 -1
  29. package/lib/internal/_randomFormatIdnHostname.js +9 -10
  30. package/lib/internal/_randomFormatIdnHostname.js.map +1 -1
  31. package/lib/internal/_randomFormatIdnHostname.mjs +3 -4
  32. package/lib/internal/_randomFormatIdnHostname.mjs.map +1 -1
  33. package/lib/internal/_randomFormatRegex.js +4 -4
  34. package/lib/internal/_randomFormatRegex.js.map +1 -1
  35. package/lib/internal/_randomFormatRegex.mjs +1 -2
  36. package/lib/internal/_randomFormatRegex.mjs.map +1 -1
  37. package/lib/internal/_randomFormatTime.js +15 -10
  38. package/lib/internal/_randomFormatTime.js.map +1 -1
  39. package/lib/internal/_randomFormatTime.mjs +6 -3
  40. package/lib/internal/_randomFormatTime.mjs.map +1 -1
  41. package/lib/internal/_randomPattern.js +20 -3
  42. package/lib/internal/_randomPattern.js.map +1 -1
  43. package/lib/internal/_randomPattern.mjs +5 -1
  44. package/lib/internal/_randomPattern.mjs.map +1 -1
  45. package/lib/internal/_randomStringLength.js +9 -2
  46. package/lib/internal/_randomStringLength.js.map +1 -1
  47. package/lib/internal/_randomStringLength.mjs +3 -1
  48. package/lib/internal/_randomStringLength.mjs.map +1 -1
  49. package/lib/internal/private/__randomComposition.d.ts +8 -0
  50. package/lib/internal/private/__randomComposition.js +21 -1
  51. package/lib/internal/private/__randomComposition.js.map +1 -1
  52. package/lib/internal/private/__randomComposition.mjs +18 -1
  53. package/lib/internal/private/__randomComposition.mjs.map +1 -1
  54. package/lib/json.d.ts +2 -2
  55. package/lib/json.js.map +1 -1
  56. package/lib/json.mjs.map +1 -1
  57. package/lib/module.d.ts +28 -13
  58. package/lib/module.js.map +1 -1
  59. package/lib/module.mjs.map +1 -1
  60. package/lib/notations.d.ts +4 -4
  61. package/lib/notations.js.map +1 -1
  62. package/lib/notations.mjs.map +1 -1
  63. package/lib/plain.d.ts +3 -3
  64. package/lib/plain.js.map +1 -1
  65. package/lib/plain.mjs.map +1 -1
  66. package/lib/protobuf.d.ts +2 -2
  67. package/lib/protobuf.js.map +1 -1
  68. package/lib/protobuf.mjs.map +1 -1
  69. package/lib/transform.mjs +1 -1
  70. package/native/cmd/ttsc-typia/assert_guard_factory_contract_test.go +17 -4
  71. package/native/cmd/ttsc-typia/create_assert_error_factory_arity_test.go +309 -0
  72. package/native/cmd/ttsc-typia/dynamic_key_tags_transform_test.go +206 -0
  73. package/native/cmd/ttsc-typia/local_function_named_like_typia_operation_transform_test.go +185 -0
  74. package/native/cmd/ttsc-typia/notation_kebab_case_transform_test.go +2 -2
  75. package/native/cmd/ttsc-typia/project_catalog_transform_coverage_test.go +84 -85
  76. package/native/cmd/ttsc-typia/recursive_conditional_alias_name_transform_test.go +307 -0
  77. package/native/cmd/ttsc-typia/shadowed_typia_module_declaration_diagnostic_test.go +152 -0
  78. package/native/cmd/ttsc-typia/transform.go +2 -2
  79. package/native/cmd/ttsc-typia/transform_helpers_test.go +96 -18
  80. package/native/core/factories/FormatCheatSheet.go +2 -2
  81. package/native/core/factories/MetadataCommentTagFactory.go +91 -7
  82. package/native/core/factories/factory_utility_coverage_test.go +896 -907
  83. package/native/core/factories/internal/metadata/MetadataHelper.go +5 -28
  84. package/native/core/factories/metadata_comment_tag_factory_rejects_out_of_range_integer_test.go +124 -0
  85. package/native/core/programmers/AssertProgrammer.go +20 -6
  86. package/native/core/programmers/RandomProgrammer.go +78 -3
  87. package/native/core/programmers/ValidateProgrammer.go +21 -6
  88. package/native/core/programmers/helpers/RandomJoiner.go +8 -1
  89. package/native/core/programmers/iterate/check_dynamic_properties.go +177 -3
  90. package/native/core/programmers/iterate/check_object.go +12 -1
  91. package/native/core/schemas/metadata/MetadataCollection.go +47 -5
  92. package/native/transform/CallExpressionTransformer.go +167 -0
  93. package/package.json +3 -12
  94. package/src/http.ts +18 -4
  95. package/src/internal/_assertGuard.ts +13 -2
  96. package/src/internal/_isFormatIdnHostname.ts +5 -1
  97. package/src/internal/_isFormatTime.ts +19 -2
  98. package/src/internal/_randomArray.ts +42 -6
  99. package/src/internal/_randomFormatByte.ts +17 -12
  100. package/src/internal/_randomFormatHostname.ts +2 -2
  101. package/src/internal/_randomFormatIdnHostname.ts +8 -9
  102. package/src/internal/_randomFormatRegex.ts +4 -6
  103. package/src/internal/_randomFormatTime.ts +15 -10
  104. package/src/internal/_randomPattern.ts +18 -3
  105. package/src/internal/_randomStringLength.ts +9 -2
  106. package/src/internal/private/__randomComposition.ts +24 -0
  107. package/src/json.ts +16 -4
  108. package/src/module.ts +56 -22
  109. package/src/notations.ts +16 -4
  110. package/src/plain.ts +12 -3
  111. package/src/protobuf.ts +16 -4
  112. package/lib/executable/FileSystemIdentity.d.ts +0 -44
  113. package/lib/executable/FileSystemIdentity.js +0 -213
  114. package/lib/executable/FileSystemIdentity.js.map +0 -1
  115. package/lib/executable/FileSystemIdentity.mjs +0 -133
  116. package/lib/executable/FileSystemIdentity.mjs.map +0 -1
  117. package/lib/executable/TypiaGenerateWizard.d.ts +0 -9
  118. package/lib/executable/TypiaGenerateWizard.js +0 -852
  119. package/lib/executable/TypiaGenerateWizard.js.map +0 -1
  120. package/lib/executable/TypiaGenerateWizard.mjs +0 -618
  121. package/lib/executable/TypiaGenerateWizard.mjs.map +0 -1
  122. package/lib/executable/generate/ttsc.d.ts +0 -1
  123. package/lib/executable/generate/ttsc.js +0 -53
  124. package/lib/executable/generate/ttsc.js.map +0 -1
  125. package/lib/executable/generate/ttsc.mjs +0 -37
  126. package/lib/executable/generate/ttsc.mjs.map +0 -1
  127. package/lib/executable/typia.d.ts +0 -2
  128. package/lib/executable/typia.js +0 -84
  129. package/lib/executable/typia.js.map +0 -1
  130. package/lib/executable/typia.mjs +0 -60
  131. package/lib/executable/typia.mjs.map +0 -1
  132. package/src/executable/FileSystemIdentity.ts +0 -222
  133. package/src/executable/TypiaGenerateWizard.ts +0 -1133
  134. package/src/executable/generate/ttsc.ts +0 -70
  135. package/src/executable/typia.ts +0 -80
@@ -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
+ `
@@ -20,8 +20,8 @@ import (
20
20
  nativetransform "github.com/samchon/typia/packages/typia/native/transform"
21
21
  )
22
22
 
23
- // runTransform is the source-to-source host the ttsc transform stage and the
24
- // `typia generate` wizard drive. Unlike `build` (which emits .js), it returns
23
+ // runTransform is the source-to-source host that drives the ttsc transform
24
+ // stage. Unlike `build` (which emits .js), it returns
25
25
  // transformed TypeScript: typia's per-file node transformer runs in an
26
26
  // EmitContext and the result SourceFile is printed back as TS (no JS script
27
27
  // transformers), so injected namespace imports stay as ES imports the caller
@@ -3,7 +3,6 @@ package main
3
3
  import (
4
4
  "bytes"
5
5
  "os"
6
- "os/exec"
7
6
  "path/filepath"
8
7
  "runtime"
9
8
  "sort"
@@ -11,16 +10,81 @@ import (
11
10
  "testing"
12
11
  )
13
12
 
13
+ // ttscTypiaTestTypecheck requires a fixture project to be free of TypeScript
14
+ // errors, which is what makes an `@ts-expect-error` in a fixture load-bearing.
15
+ //
16
+ // It runs the build command in no-emit mode with the rewrite disabled, so the
17
+ // only diagnostics it can report are the program's own. It used to shell out to
18
+ // `pnpm exec ttsc --noEmit -p tsconfig.json`, which reported nothing and exited
19
+ // 0 for a fixture under `.tmp-ttsc-typia-tests` — `const value: number = "bad"`
20
+ // passed — so every caller was asserting against an oracle that could not fail.
21
+ // `runBuild` is the same program these tests already drive, and
22
+ // build_reports_semantic_diagnostics_test.go pins that it surfaces semantic
23
+ // diagnostics as status 2.
14
24
  func ttscTypiaTestTypecheck(t *testing.T, project string) {
15
25
  t.Helper()
16
- pnpm, err := exec.LookPath("pnpm")
17
- if err != nil {
18
- t.Skip("pnpm executable not found")
26
+ out, errText, code := ttscTypiaTestCapture(func() int {
27
+ return runBuild([]string{
28
+ "--cwd", project,
29
+ "--tsconfig", "tsconfig.json",
30
+ "--noEmit",
31
+ "--rewrite-mode", "none",
32
+ })
33
+ })
34
+ if code != 0 {
35
+ t.Fatalf("fixture typecheck failed: code=%d\nstdout=%s\nstderr=%s", code, out, errText)
36
+ }
37
+ }
38
+
39
+ // ttscTypiaTestWriteFactoryStub installs a typia whose declaration file sits at
40
+ // the path the transformer's identity test accepts, so a fixture that also
41
+ // carries an ambient or augmenting `declare module "typia"` differs from a
42
+ // plain one only in that declaration.
43
+ func ttscTypiaTestWriteFactoryStub(t *testing.T, project string) {
44
+ t.Helper()
45
+ root := filepath.Join(project, "node_modules", "typia")
46
+ lib := filepath.Join(root, "lib")
47
+ if err := os.MkdirAll(lib, 0o755); err != nil {
48
+ t.Fatalf("mkdir typia stub: %v", err)
49
+ }
50
+ files := map[string]string{
51
+ "package.json": `{
52
+ "name": "typia",
53
+ "version": "0.0.0-test",
54
+ "main": "./lib/module.js",
55
+ "types": "./lib/module.d.ts",
56
+ "exports": {
57
+ ".": {
58
+ "types": "./lib/module.d.ts",
59
+ "default": "./lib/module.js"
60
+ },
61
+ "./lib/transform": "./lib/transform.js"
62
+ },
63
+ "ttsc": {
64
+ "plugin": { "transform": "typia/lib/transform" }
65
+ }
66
+ }
67
+ `,
68
+ filepath.Join("lib", "module.d.ts"): `declare namespace typia {
69
+ function createAssert<T>(): (input: unknown) => T;
70
+ function createIs<T>(): (input: unknown) => input is T;
71
+ }
72
+ declare const typia: {
73
+ createAssert: typeof typia.createAssert;
74
+ createIs: typeof typia.createIs;
75
+ };
76
+ export default typia;
77
+ export declare function createAssert<T>(): (input: unknown) => T;
78
+ export declare function createIs<T>(): (input: unknown) => input is T;
79
+ `,
80
+ filepath.Join("lib", "module.js"): "exports.createAssert = () => () => undefined;\nexports.createIs = () => () => false;\n",
81
+ filepath.Join("lib", "transform.js"): "module.exports = {};\n",
82
+ filepath.Join("lib", "transform.d.ts"): "export {};\n",
19
83
  }
20
- cmd := exec.Command(pnpm, "exec", "ttsc", "--noEmit", "-p", "tsconfig.json")
21
- cmd.Dir = project
22
- if output, err := cmd.CombinedOutput(); err != nil {
23
- t.Fatalf("fixture typecheck failed: %v\n%s", err, output)
84
+ for name, body := range files {
85
+ if err := os.WriteFile(filepath.Join(root, name), []byte(body), 0o644); err != nil {
86
+ t.Fatalf("write typia stub %s: %v", name, err)
87
+ }
24
88
  }
25
89
  }
26
90
 
@@ -61,14 +125,14 @@ func ttscTypiaTestCapture(run func() int) (string, string, int) {
61
125
  func ttscTypiaTestWriteCommonRuntimeStubs(t *testing.T, runtimeDir string) {
62
126
  t.Helper()
63
127
  files := map[string]string{
64
- "typia-stub.cjs": "module.exports = {};\n",
65
- "validate-report-stub.cjs": ttscTypiaTestValidateReportStub,
66
- "standard-schema-stub.cjs": ttscTypiaTestStandardSchemaStub,
67
- "assert-guard-stub.cjs": ttscTypiaTestAssertGuardStub,
68
- "functional-error-stub.cjs": ttscTypiaTestFunctionalErrorStub,
69
- "access-expression-stub.cjs": ttscTypiaTestAccessExpressionStub,
70
- "string-length-stub.cjs": ttscTypiaTestStringLengthStub,
71
- "notation-stub.cjs": ttscTypiaTestNotationStub,
128
+ "typia-stub.cjs": "module.exports = {};\n",
129
+ "validate-report-stub.cjs": ttscTypiaTestValidateReportStub,
130
+ "standard-schema-stub.cjs": ttscTypiaTestStandardSchemaStub,
131
+ "assert-guard-stub.cjs": ttscTypiaTestAssertGuardStub,
132
+ "functional-error-stub.cjs": ttscTypiaTestFunctionalErrorStub,
133
+ "access-expression-stub.cjs": ttscTypiaTestAccessExpressionStub,
134
+ "string-length-stub.cjs": ttscTypiaTestStringLengthStub,
135
+ "notation-stub.cjs": ttscTypiaTestNotationStub,
72
136
  "json-stringify-array-stub.cjs": ttscTypiaTestJsonStringifyArrayStub,
73
137
  "json-stringify-property-stub.cjs": ttscTypiaTestJsonStringifyPropertyStub,
74
138
  "json-stringify-element-stub.cjs": ttscTypiaTestJsonStringifyElementStub,
@@ -160,9 +224,20 @@ const ttscTypiaTestValidateReportStub = `module.exports._validateReport = (array
160
224
  const ttscTypiaTestStandardSchemaStub = `module.exports._createStandardSchema = (validate) => validate;
161
225
  `
162
226
 
227
+ // A double for packages/typia/src/internal/_assertGuard.ts that takes the same
228
+ // branches: `exceptionable === true` and a callable-factory requirement. The
229
+ // generated function's second parameter is caller-supplied, so a pointwise
230
+ // hand-off (`rows.map(assertUser)`) fills it with the element index; testing
231
+ // truthiness here would let this stub report `factory is not a function` for a
232
+ // case the shipped helper handles. The fallback error is deliberately not a
233
+ // TypeGuardError — a stub cannot import typia — so cases assert `props`
234
+ // members rather than the shipped message format.
163
235
  const ttscTypiaTestAssertGuardStub = `module.exports._assertGuard = (exceptionable, props, factory) => {
164
- if (exceptionable) {
165
- const error = factory ? factory(props) : Object.assign(new Error(props.expected), props);
236
+ if (exceptionable === true) {
237
+ const error =
238
+ typeof factory === "function"
239
+ ? factory(props)
240
+ : Object.assign(new Error(props.expected), props);
166
241
  throw error;
167
242
  }
168
243
  return false;
@@ -217,6 +292,9 @@ const ttscTypiaTestRandomStringStub = `module.exports._randomString = (props) =>
217
292
  };
218
293
  `
219
294
 
295
+ // A double for packages/typia/src/internal/_stringLength.ts: the string
296
+ // iterator walks code points, so a surrogate pair counts once, which is what
297
+ // `MinLength` and `MaxLength` now mean.
220
298
  const ttscTypiaTestStringLengthStub = `module.exports._stringLength = (value) => {
221
299
  let count = 0;
222
300
  for (const _ of value) ++count;
@@ -45,7 +45,7 @@ var FormatCheatSheet = map[string]string{
45
45
  "email": "/^[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/i.test($input)",
46
46
  "hostname": "/^(?=.{1,253}\\.?$)[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\\.[a-z0-9](?:[-0-9a-z]{0,61}[0-9a-z])?)*\\.?$/i.test($input)",
47
47
  "idn-email": "/^(([^<>()[\\]\\.,;:\\s@\\\"]+(\\.[^<>()[\\]\\.,;:\\s@\\\"]+)*)|(\\\".+\\\"))@(([^<>()[\\]\\.,;:\\s@\\\"]+\\.)+[^<>()[\\]\\.,;:\\s@\\\"]{2,})$/i.test($input)",
48
- "idn-hostname": "/^([a-z0-9\\u00a1-\\uffff0-9]+(-[a-z0-9\\u00a1-\\uffff0-9]+)*\\.)+[a-z\\u00a1-\\uffff]{2,}$/i.test($input)",
48
+ "idn-hostname": "/^(?=.{1,253}\\.?$)[a-z0-9\\u00a1-\\uffff](?:[a-z0-9\\u00a1-\\uffff-]{0,61}[a-z0-9\\u00a1-\\uffff])?(?:\\.[a-z0-9\\u00a1-\\uffff](?:[a-z0-9\\u00a1-\\uffff-]{0,61}[a-z0-9\\u00a1-\\uffff])?)*\\.?$/i.test($input)",
49
49
  "iri": `(() => !/[\u0000-\u0020\u007f-\u009f\ud800-\udfff"<>\\^\x60{|}]/u.test($input) && !/%(?![0-9A-Fa-f]{2})/u.test($input) && /^[A-Za-z][A-Za-z0-9+.-]*:/u.test($input))()`,
50
50
  "iri-reference": `(() => { if (/[\u0000-\u0020\u007f-\u009f\ud800-\udfff"<>\\^\x60{|}]/u.test($input) || /%(?![0-9A-Fa-f]{2})/u.test($input)) return false; const colon = $input.indexOf(":"); const boundary = $input.search(/[/?#]/u); return colon === -1 || (boundary !== -1 && boundary < colon) || /^[A-Za-z][A-Za-z0-9+.-]*:/u.test($input); })()`,
51
51
  "ipv4": "/^(?:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)\\.){3}(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)$/.test($input)",
@@ -56,7 +56,7 @@ var FormatCheatSheet = map[string]string{
56
56
  "url": "/^(?:https?|ftp):\\/\\/(?:[^\\s/?#@]+(?::[^\\s/?#@]*)?@)?(?:(?!(?:10|127)(?:\\.\\d{1,3}){3})(?!(?:169\\.254|192\\.168)(?:\\.\\d{1,3}){2})(?!172\\.(?:1[6-9]|2\\d|3[0-1])(?:\\.\\d{1,3}){2})(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}(?:\\.(?:[1-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))|(?=.{1,253}(?::\\d{2,5})?(?:\\/[^\\s]*)?$)(?:[a-z0-9\\u00a1-\\uffff](?:[-a-z0-9\\u00a1-\\uffff]{0,61}[a-z0-9\\u00a1-\\uffff])?\\.)+(?:[a-z\\u00a1-\\uffff](?:[-a-z0-9\\u00a1-\\uffff]{0,61}[a-z0-9\\u00a1-\\uffff])))(?::\\d{2,5})?(?:\\/[^\\s]*)?$/iu.test($input)",
57
57
  "date-time": `(() => { const match = /^([0-9]{4})-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])[Tt]((?:[01][0-9]|2[0-3])):([0-5][0-9]):([0-5][0-9]|60)(?:\.[0-9]+)?(?:[Zz]|([+-])((?:[01][0-9]|2[0-3])):([0-5][0-9]))$/.exec($input); if (match === null) return false; const year = Number(match[1]); const month = Number(match[2]); const day = Number(match[3]); if (day > [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month - 1] + (month === 2 && (year % 400 === 0 || (year % 4 === 0 && year % 100 !== 0)) ? 1 : 0)) return false; if (match[6] !== "60") return true; const instant = new Date(0); instant.setUTCFullYear(year, month - 1, day); instant.setUTCHours(Number(match[4]), Number(match[5]), 0, 0); const offset = match[7] === undefined ? 0 : (match[7] === "+" ? 1 : -1) * (Number(match[8]) * 60 + Number(match[9])); instant.setUTCMinutes(instant.getUTCMinutes() - offset); return instant.getUTCHours() === 23 && instant.getUTCMinutes() === 59 && ((instant.getUTCMonth() === 5 && instant.getUTCDate() === 30) || (instant.getUTCMonth() === 11 && instant.getUTCDate() === 31)); })()`,
58
58
  "date": `(() => { const match = /^([0-9]{4})-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$/.exec($input); if (match === null) return false; const year = Number(match[1]); const month = Number(match[2]); const day = Number(match[3]); return day <= [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month - 1] + (month === 2 && (year % 400 === 0 || (year % 4 === 0 && year % 100 !== 0)) ? 1 : 0); })()`,
59
- "time": "/^([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9](?:\\.[0-9]{1,9})?(Z|[+-]([01][0-9]|2[0-3]):[0-5][0-9])$/i.test($input)",
59
+ "time": `(() => { const match = /^([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9]|60)(?:\.[0-9]+)?(?:[Zz]|([+-])((?:[01][0-9]|2[0-3])):([0-5][0-9]))$/.exec($input); if (match === null) return false; if (match[3] !== "60") return true; const offset = match[4] === undefined ? 0 : (match[4] === "+" ? 1 : -1) * (Number(match[5]) * 60 + Number(match[6])); const minutes = Number(match[1]) * 60 + Number(match[2]) - offset; return ((minutes % 1440) + 1440) % 1440 === 1439; })()`,
60
60
  "duration": "/^P(?!$)((\\d+Y)?(\\d+M)?(\\d+D)?(T(?=\\d)(\\d+H)?(\\d+M)?(\\d+S)?)?|(\\d+W)?)$/.test($input)",
61
61
  "json-pointer": "/^(?:\\/(?:[^~/]|~0|~1)*)*$/.test($input)",
62
62
  "relative-json-pointer": "/^(?:0|[1-9][0-9]*)(?:#|(?:\\/(?:[^~/]|~0|~1)*)*)$/.test($input)",