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.
Files changed (92) 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/private/__randomComposition.d.ts +8 -0
  42. package/lib/internal/private/__randomComposition.js +21 -1
  43. package/lib/internal/private/__randomComposition.js.map +1 -1
  44. package/lib/internal/private/__randomComposition.mjs +18 -1
  45. package/lib/internal/private/__randomComposition.mjs.map +1 -1
  46. package/lib/json.d.ts +2 -2
  47. package/lib/json.js.map +1 -1
  48. package/lib/json.mjs.map +1 -1
  49. package/lib/module.d.ts +28 -13
  50. package/lib/module.js.map +1 -1
  51. package/lib/module.mjs.map +1 -1
  52. package/lib/notations.d.ts +4 -4
  53. package/lib/notations.js.map +1 -1
  54. package/lib/notations.mjs.map +1 -1
  55. package/lib/plain.d.ts +3 -3
  56. package/lib/plain.js.map +1 -1
  57. package/lib/plain.mjs.map +1 -1
  58. package/lib/protobuf.d.ts +2 -2
  59. package/lib/protobuf.js.map +1 -1
  60. package/lib/protobuf.mjs.map +1 -1
  61. package/native/cmd/ttsc-typia/assert_guard_factory_contract_test.go +17 -4
  62. package/native/cmd/ttsc-typia/create_assert_error_factory_arity_test.go +309 -0
  63. package/native/cmd/ttsc-typia/local_function_named_like_typia_operation_transform_test.go +185 -0
  64. package/native/cmd/ttsc-typia/recursive_conditional_alias_name_transform_test.go +307 -0
  65. package/native/cmd/ttsc-typia/shadowed_typia_module_declaration_diagnostic_test.go +152 -0
  66. package/native/cmd/ttsc-typia/template_literal_type_tags_transform_test.go +1 -3
  67. package/native/cmd/ttsc-typia/transform_helpers_test.go +92 -26
  68. package/native/core/factories/FormatCheatSheet.go +2 -2
  69. package/native/core/factories/MetadataCommentTagFactory.go +26 -11
  70. package/native/core/factories/factory_utility_coverage_test.go +896 -907
  71. package/native/core/factories/internal/metadata/MetadataHelper.go +5 -28
  72. package/native/core/factories/metadata_comment_tag_factory_coverage_test.go +2 -2
  73. package/native/core/programmers/RandomProgrammer.go +5 -2
  74. package/native/core/schemas/metadata/MetadataCollection.go +47 -5
  75. package/native/transform/CallExpressionTransformer.go +167 -0
  76. package/package.json +3 -3
  77. package/src/http.ts +18 -4
  78. package/src/internal/_assertGuard.ts +13 -2
  79. package/src/internal/_isFormatIdnHostname.ts +5 -1
  80. package/src/internal/_isFormatTime.ts +19 -2
  81. package/src/internal/_randomArray.ts +42 -6
  82. package/src/internal/_randomFormatByte.ts +17 -12
  83. package/src/internal/_randomFormatHostname.ts +2 -2
  84. package/src/internal/_randomFormatIdnHostname.ts +8 -9
  85. package/src/internal/_randomFormatRegex.ts +4 -6
  86. package/src/internal/_randomFormatTime.ts +15 -10
  87. package/src/internal/private/__randomComposition.ts +24 -0
  88. package/src/json.ts +16 -4
  89. package/src/module.ts +56 -22
  90. package/src/notations.ts +16 -4
  91. package/src/plain.ts +12 -3
  92. 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, `require("typia/lib/internal/_stringLength")`) ||
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)