typia 13.3.0 → 14.0.1

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 (84) hide show
  1. package/lib/internal/_createStandardSchema.d.ts +1 -2
  2. package/lib/internal/_createStandardSchema.js.map +1 -1
  3. package/lib/internal/_createStandardSchema.mjs.map +1 -1
  4. package/lib/internal/_randomPattern.js +20 -3
  5. package/lib/internal/_randomPattern.js.map +1 -1
  6. package/lib/internal/_randomPattern.mjs +5 -1
  7. package/lib/internal/_randomPattern.mjs.map +1 -1
  8. package/lib/internal/_randomStringLength.js +9 -2
  9. package/lib/internal/_randomStringLength.js.map +1 -1
  10. package/lib/internal/_randomStringLength.mjs +3 -1
  11. package/lib/internal/_randomStringLength.mjs.map +1 -1
  12. package/lib/module.d.ts +1 -2
  13. package/lib/module.js.map +1 -1
  14. package/lib/module.mjs.map +1 -1
  15. package/lib/re-exports.d.ts +1 -1
  16. package/lib/re-exports.js.map +1 -1
  17. package/lib/transform.mjs +1 -1
  18. package/native/cmd/ttsc-typia/create_assert_error_factory_arity_test.go +5 -2
  19. package/native/cmd/ttsc-typia/dependency_collector_virtual_scheme_test.go +52 -0
  20. package/native/cmd/ttsc-typia/dynamic_key_tags_transform_test.go +206 -0
  21. package/native/cmd/ttsc-typia/notation_kebab_case_transform_test.go +2 -2
  22. package/native/cmd/ttsc-typia/project_catalog_transform_coverage_test.go +84 -85
  23. package/native/cmd/ttsc-typia/project_dependencies_callee_argument_transform_test.go +177 -0
  24. package/native/cmd/ttsc-typia/project_dependencies_callee_barrel_transform_test.go +115 -0
  25. package/native/cmd/ttsc-typia/project_dependencies_callee_inference_transform_test.go +285 -0
  26. package/native/cmd/ttsc-typia/project_dependencies_callee_shape_transform_test.go +117 -0
  27. package/native/cmd/ttsc-typia/project_dependencies_callee_untransformed_barrel_transform_test.go +121 -0
  28. package/native/cmd/ttsc-typia/project_dependencies_complete_diagnostic_transform_test.go +116 -0
  29. package/native/cmd/ttsc-typia/project_dependencies_complete_envelope_transform_test.go +115 -0
  30. package/native/cmd/ttsc-typia/project_dependencies_complete_inferred_type_transform_test.go +127 -0
  31. package/native/cmd/ttsc-typia/project_dependencies_complete_replaced_library_transform_test.go +153 -0
  32. package/native/cmd/ttsc-typia/project_dependencies_complete_untouched_reprint_transform_test.go +155 -0
  33. package/native/cmd/ttsc-typia/project_dependencies_computed_key_transform_test.go +218 -0
  34. package/native/cmd/ttsc-typia/project_dependencies_custom_lib_name_transform_test.go +6 -5
  35. package/native/cmd/ttsc-typia/project_dependencies_enum_member_value_transform_test.go +131 -0
  36. package/native/cmd/ttsc-typia/project_dependencies_inferred_declaration_transform_test.go +159 -0
  37. package/native/cmd/ttsc-typia/project_dependencies_jsdoc_typedef_transform_test.go +134 -0
  38. package/native/cmd/ttsc-typia/project_dependencies_named_tuple_member_transform_test.go +123 -0
  39. package/native/cmd/ttsc-typia/project_dependencies_qualified_barrel_transform_test.go +141 -0
  40. package/native/cmd/ttsc-typia/project_dependencies_type_parameter_default_transform_test.go +127 -0
  41. package/native/cmd/ttsc-typia/template_literal_type_tags_transform_test.go +3 -1
  42. package/native/cmd/ttsc-typia/transform.go +195 -21
  43. package/native/cmd/ttsc-typia/transform_helpers_test.go +12 -0
  44. package/native/core/factories/MetadataCommentTagFactory.go +96 -27
  45. package/native/core/factories/metadata_comment_tag_factory_coverage_test.go +2 -2
  46. package/native/core/factories/metadata_comment_tag_factory_rejects_out_of_range_integer_test.go +124 -0
  47. package/native/core/programmers/AssertProgrammer.go +20 -6
  48. package/native/core/programmers/RandomProgrammer.go +78 -6
  49. package/native/core/programmers/ValidateProgrammer.go +21 -6
  50. package/native/core/programmers/helpers/RandomJoiner.go +8 -1
  51. package/native/core/programmers/iterate/check_dynamic_properties.go +177 -3
  52. package/native/core/programmers/iterate/check_object.go +12 -1
  53. package/native/core/schemas/metadata/MetadataDependency.go +613 -30
  54. package/native/transform/CallExpressionTransformer.go +16 -1
  55. package/package.json +4 -14
  56. package/src/internal/_createStandardSchema.ts +1 -2
  57. package/src/internal/_randomPattern.ts +18 -3
  58. package/src/internal/_randomStringLength.ts +9 -2
  59. package/src/module.ts +6 -2
  60. package/src/re-exports.ts +13 -0
  61. package/lib/executable/FileSystemIdentity.d.ts +0 -44
  62. package/lib/executable/FileSystemIdentity.js +0 -213
  63. package/lib/executable/FileSystemIdentity.js.map +0 -1
  64. package/lib/executable/FileSystemIdentity.mjs +0 -133
  65. package/lib/executable/FileSystemIdentity.mjs.map +0 -1
  66. package/lib/executable/TypiaGenerateWizard.d.ts +0 -9
  67. package/lib/executable/TypiaGenerateWizard.js +0 -852
  68. package/lib/executable/TypiaGenerateWizard.js.map +0 -1
  69. package/lib/executable/TypiaGenerateWizard.mjs +0 -618
  70. package/lib/executable/TypiaGenerateWizard.mjs.map +0 -1
  71. package/lib/executable/generate/ttsc.d.ts +0 -1
  72. package/lib/executable/generate/ttsc.js +0 -53
  73. package/lib/executable/generate/ttsc.js.map +0 -1
  74. package/lib/executable/generate/ttsc.mjs +0 -37
  75. package/lib/executable/generate/ttsc.mjs.map +0 -1
  76. package/lib/executable/typia.d.ts +0 -2
  77. package/lib/executable/typia.js +0 -84
  78. package/lib/executable/typia.js.map +0 -1
  79. package/lib/executable/typia.mjs +0 -60
  80. package/lib/executable/typia.mjs.map +0 -1
  81. package/src/executable/FileSystemIdentity.ts +0 -222
  82. package/src/executable/TypiaGenerateWizard.ts +0 -1133
  83. package/src/executable/generate/ttsc.ts +0 -70
  84. package/src/executable/typia.ts +0 -80
@@ -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
@@ -103,7 +103,7 @@ func runTransform(args []string) int {
103
103
  fmt.Fprintln(stderr, "ttsc-typia transform: --out requires --file")
104
104
  return 2
105
105
  }
106
- return runTransformProject(prog, cwd, typiaTransform, &transformDiags)
106
+ return runTransformProject(prog, cwd, typiaTransform, &transformDiags, &extras)
107
107
  }
108
108
 
109
109
  absFile := *file
@@ -190,11 +190,25 @@ func runTransformSingle(
190
190
  // dependencies[F]), so graph is added alongside dependencies, not a replacement.
191
191
  // Every section keys through driver.TransformOutputKey, so the consumer joins
192
192
  // them by key.
193
+ //
194
+ // A third section, `dependenciesComplete`, names the files whose `dependencies`
195
+ // entry is the whole story, and for those the consumer stops unioning: it
196
+ // derives dependencies[F] ∪ configs ∪ candidates and drops reach(edges, F) and
197
+ // globals. That is the difference between re-validating the reference closure
198
+ // of a file and re-validating the handful of declarations typia read, paid once
199
+ // per delivered module, and for the majority of a project's files -- the ones
200
+ // with no typia call at all -- between the closure and nothing.
201
+ //
202
+ // The declaration transfers responsibility, so it is made per file and only
203
+ // where the collector could bound the file's inputs; the dependency listener
204
+ // and the diagnostic report this function wires are what decide that
205
+ // (samchon/typia#2357). See transformDependencyCollector.Unbound.
193
206
  func runTransformProject(
194
207
  prog *driver.Program,
195
208
  cwd string,
196
209
  typiaTransform driver.PluginTransform,
197
210
  transformDiags *[]typiaTransformDiagnostic,
211
+ extras *nativecontext.ITypiaContext_Extras,
198
212
  ) int {
199
213
  out := transformProjectOutput{
200
214
  Diagnostics: []transformCompilerDiagnostic{},
@@ -209,8 +223,27 @@ func runTransformProject(
209
223
  sf := prog.SourceFile(fileName)
210
224
  return sf != nil && prog.TSProgram.IsLibFile(sf)
211
225
  })
212
- schemametadata.MetadataDependency_listen(prog.Checker, collector.Touch)
226
+ // Only this host declares completeness, which is why only this host asks the
227
+ // analysis to admit what it could not bound: every such admission costs one
228
+ // file its declaration and nothing else.
229
+ schemametadata.MetadataDependency_listen(prog.Checker, schemametadata.MetadataDependency_IListener{
230
+ File: collector.Touch,
231
+ Callee: collector.TouchCallee,
232
+ Unbounded: collector.Unbound,
233
+ })
213
234
  defer schemametadata.MetadataDependency_release(prog.Checker)
235
+ // A diagnostic means typia could not finish lowering a call in the file being
236
+ // transformed, so what the analysis consulted before giving up is not that
237
+ // file's whole input set: an edit to a declaration it never reached could make
238
+ // the same call succeed and publish different text. The build fails either
239
+ // way, but a false claim is still false, so withhold the declaration rather
240
+ // than reason about how far the analysis got. The transformer reads `extras`
241
+ // on every call, so wrapping it through the pointer reaches the loop below.
242
+ reportDiagnostic := extras.AddDiagnostic
243
+ extras.AddDiagnostic = func(diag *nativecontext.ITypiaDiagnostic) int {
244
+ collector.Unbound()
245
+ return reportDiagnostic(diag)
246
+ }
214
247
  for _, sf := range prog.SourceFiles() {
215
248
  if sf.IsDeclarationFile {
216
249
  continue
@@ -221,8 +254,16 @@ func runTransformProject(
221
254
  }
222
255
  collector.Begin(key)
223
256
  out.TypeScript[key] = transformFileToTypeScript(prog, typiaTransform, sf)
257
+ collector.End()
224
258
  }
225
259
  out.Dependencies = collector.ToJSON()
260
+ // The config chain is the one input that stays universal for a declared file,
261
+ // and it reaches the consumer through this same envelope's graph. Declaring
262
+ // completeness without a graph would leave nothing universal behind, so a
263
+ // tsconfig edit would stop invalidating the files the declaration narrowed.
264
+ if out.Graph != nil {
265
+ out.DependenciesComplete = collector.ToCompleteJSON(out.TypeScript)
266
+ }
226
267
  for _, diag := range *transformDiags {
227
268
  out.Diagnostics = append(out.Diagnostics, transformDiagnosticToCompilerDiagnostic(diag))
228
269
  }
@@ -359,24 +400,62 @@ type transformProjectOutput struct {
359
400
  // files are excluded (toolchain-versioned, not project inputs);
360
401
  // `node_modules` declaration files are included.
361
402
  Dependencies map[string][]string `json:"dependencies,omitempty"`
403
+ // DependenciesComplete lists the TypeScript keys whose Dependencies entry is
404
+ // exhaustive: nothing outside that entry, the file itself, and the universal
405
+ // config chain can change the file's transformed text. The consumer stops
406
+ // widening those files with the reference closure and the global-scope set.
407
+ //
408
+ // Unlisted is always the safe answer, so the list carries only what the
409
+ // collector could bound. An older ttsc ignores the field; 0.19.3 and newer
410
+ // narrow on it.
411
+ DependenciesComplete []string `json:"dependenciesComplete,omitempty"`
362
412
  }
363
413
 
364
414
  // transformDependencyCollector accumulates the consulted-declaration files the
365
415
  // metadata dependency listener reports while one project file is transformed,
366
- // attributing them to that file's envelope key.
416
+ // attributing them to that file's envelope key, and decides which of those keys
417
+ // may be declared complete.
367
418
  type transformDependencyCollector struct {
368
419
  cwd string
369
420
  current string
370
421
  files map[string]map[string]bool
422
+ // unbounded holds the keys withheld from the completeness declaration. It is
423
+ // a set of exclusions rather than a set of admissions because every path that
424
+ // learns a file cannot be bounded runs while that file is being transformed,
425
+ // whereas nothing ever proves the opposite: a file is declarable exactly when
426
+ // no such path fired for it.
427
+ unbounded map[string]bool
371
428
  // isLibraryFile reports the compiler's own classification of a file as a
372
429
  // default library. Classification must come from the program, never from a
373
430
  // filename pattern: a project's own ambient declaration file may legitimately
374
431
  // be named `lib.custom.d.ts`, and dropping it by basename silently loses
375
432
  // cache invalidation for the types it declares (samchon/typia#2108).
376
433
  isLibraryFile func(fileName string) bool
377
- // values memoizes fileName -> envelope value ("" for a dropped file): the
378
- // listener reports the same declaration files repeatedly across call sites.
379
- values map[string]string
434
+ // values memoizes fileName -> rendered envelope value: the listener reports
435
+ // the same declaration files repeatedly across call sites.
436
+ values map[string]transformDependencyValue
437
+ }
438
+
439
+ // transformDependencyBundledScheme is the prefix tsgo gives its embedded default
440
+ // libraries, and the one `driver.NewTransformGraph` drops from the host-owned
441
+ // reference graph.
442
+ const transformDependencyBundledScheme = "bundled:///"
443
+
444
+ // transformDependencyValue is one reported file rendered for the envelope: its
445
+ // key, or the reason it was dropped. The reasons differ in what they cost a
446
+ // completeness declaration, so they cannot both collapse to an empty value.
447
+ type transformDependencyValue struct {
448
+ // key is the envelope value, empty when the file was dropped.
449
+ key string
450
+ // withhold marks a file this envelope cannot report but the host-owned bound
451
+ // still carries, so narrowing to the reported list would lose it.
452
+ withhold bool
453
+ // libraryOnly marks a withhold whose only cause is the compiler classifying
454
+ // the file as a default library. Such a file can change a consulted type and
455
+ // so a generated validator, but never which declaration a call resolves to,
456
+ // which is why the identity channel drops it without withholding
457
+ // (samchon/typia#2361).
458
+ libraryOnly bool
380
459
  }
381
460
 
382
461
  func newTransformDependencyCollector(
@@ -387,7 +466,8 @@ func newTransformDependencyCollector(
387
466
  cwd: cwd,
388
467
  isLibraryFile: isLibraryFile,
389
468
  files: map[string]map[string]bool{},
390
- values: map[string]string{},
469
+ unbounded: map[string]bool{},
470
+ values: map[string]transformDependencyValue{},
391
471
  }
392
472
  }
393
473
 
@@ -396,11 +476,61 @@ func (collector *transformDependencyCollector) Begin(key string) {
396
476
  collector.current = key
397
477
  }
398
478
 
479
+ // End closes the attribution window Begin opened. Nothing consults the checker
480
+ // between two files today, so this changes no output; it is what keeps that
481
+ // true, because a touch that arrives outside a window is dropped rather than
482
+ // charged -- or worse, withheld -- against whichever file happened to run last.
483
+ func (collector *transformDependencyCollector) End() {
484
+ collector.current = ""
485
+ }
486
+
487
+ // Unbound withholds the completeness declaration from the file being
488
+ // transformed. The file keeps its reported dependencies and falls back to the
489
+ // host-owned reference-closure bound, which is what an unlisted file gets and
490
+ // is always sound. Its callers name the reasons: an input the analysis could
491
+ // only read from an inferred position, a consulted on-disk default library, and
492
+ // a call typia could not lower.
493
+ func (collector *transformDependencyCollector) Unbound() {
494
+ if collector.current == "" {
495
+ return
496
+ }
497
+ collector.unbounded[collector.current] = true
498
+ }
499
+
399
500
  // Touch records one consulted declaration file for the current key. Default
400
501
  // library files, virtual URI sources (e.g. tsgo's `bundled:///` libraries),
401
502
  // and the transformed file itself are dropped so the envelope carries only
402
503
  // actionable project inputs.
504
+ //
505
+ // A drop the host-owned bound does not share also withholds the current file's
506
+ // completeness declaration: the graph still carries that file, so a narrowed
507
+ // file would stop watching an input the default bound watches.
403
508
  func (collector *transformDependencyCollector) Touch(fileName string) {
509
+ collector.touch(fileName, true)
510
+ }
511
+
512
+ // TouchCallee records one file reached while deciding WHICH declaration a call
513
+ // resolves to, and does not let a default library withhold the current file.
514
+ //
515
+ // A default library is dropped from the reported list either way -- that
516
+ // contract is samchon/typia#2108's -- but the withholding beside it belongs to
517
+ // the type channel alone. Under `libReplacement` a replaced library can change
518
+ // a generated validator, so a file that consulted one has to give up its
519
+ // declaration. It can never change which declaration a call resolves to:
520
+ // callExpressionTransformer_targetModule recognizes typia's own calls by a path
521
+ // ending in `/typia/lib/<file>.d.ts` or `/typia/src/<file>.ts`, and a file the
522
+ // compiler classifies as a default library satisfies neither wherever it sits
523
+ // on disk. Withholding there cost a file its declaration for calling
524
+ // `table.get(...)`, and under `noembed` -- where every library is a real file --
525
+ // for calling anything at all (samchon/typia#2361).
526
+ //
527
+ // A virtual non-`bundled:///` scheme still withholds on both channels: that drop
528
+ // is about being unwatchable, not about being a library.
529
+ func (collector *transformDependencyCollector) TouchCallee(fileName string) {
530
+ collector.touch(fileName, false)
531
+ }
532
+
533
+ func (collector *transformDependencyCollector) touch(fileName string, library bool) {
404
534
  if collector.current == "" {
405
535
  return
406
536
  }
@@ -409,7 +539,10 @@ func (collector *transformDependencyCollector) Touch(fileName string) {
409
539
  value = collector.value(fileName)
410
540
  collector.values[fileName] = value
411
541
  }
412
- if value == "" || value == collector.current {
542
+ if value.withhold && (library || value.libraryOnly == false) {
543
+ collector.Unbound()
544
+ }
545
+ if value.key == "" || value.key == collector.current {
413
546
  return
414
547
  }
415
548
  set := collector.files[collector.current]
@@ -417,19 +550,36 @@ func (collector *transformDependencyCollector) Touch(fileName string) {
417
550
  set = map[string]bool{}
418
551
  collector.files[collector.current] = set
419
552
  }
420
- set[value] = true
553
+ set[value.key] = true
421
554
  }
422
555
 
423
- // value renders one reported file as its envelope value, or "" when the file
424
- // must be dropped: virtual URI sources (e.g. tsgo's `bundled:///` embedded
425
- // libraries) cannot be watched, and files the compiler classifies as default
426
- // libraries (on-disk in `noembed` or `libReplacement` configurations) are
427
- // toolchain inputs, not project inputs.
428
- func (collector *transformDependencyCollector) value(fileName string) string {
429
- if strings.Contains(fileName, "://") || collector.isLibraryFile(fileName) {
430
- return ""
431
- }
432
- return driver.TransformOutputKey(collector.cwd, fileName)
556
+ // value renders one reported file as its envelope value, or records why it must
557
+ // be dropped.
558
+ //
559
+ // tsgo's embedded libraries under `bundled:///` are nobody's input:
560
+ // `driver.NewTransformGraph` skips that exact prefix, so dropping them leaves
561
+ // the reported list and the host-owned bound in agreement and costs a
562
+ // completeness declaration nothing. Any other URI scheme is unwatchable for us
563
+ // but still reaches the graph, so it is dropped AND withheld -- the asymmetry
564
+ // only runs in the safe direction.
565
+ //
566
+ // A file the compiler classifies as a default library is a toolchain input
567
+ // rather than a project input, which is why the envelope has never reported one.
568
+ // Under `noembed` or `libReplacement` it is nevertheless a real path that
569
+ // `graph.globals` carries, so the default bound watches it while a narrowed file
570
+ // would not. Keep dropping it from the reported list -- that contract is
571
+ // #2108's -- and withhold the declaration from the file that consulted it.
572
+ func (collector *transformDependencyCollector) value(fileName string) transformDependencyValue {
573
+ if strings.HasPrefix(fileName, transformDependencyBundledScheme) {
574
+ return transformDependencyValue{}
575
+ }
576
+ if strings.Contains(fileName, "://") {
577
+ return transformDependencyValue{withhold: true}
578
+ }
579
+ if collector.isLibraryFile(fileName) {
580
+ return transformDependencyValue{withhold: true, libraryOnly: true}
581
+ }
582
+ return transformDependencyValue{key: driver.TransformOutputKey(collector.cwd, fileName)}
433
583
  }
434
584
 
435
585
  // ToJSON renders the collected sets as the envelope's `dependencies` map with
@@ -456,6 +606,30 @@ func (collector *transformDependencyCollector) ToJSON() map[string][]string {
456
606
  return output
457
607
  }
458
608
 
609
+ // ToCompleteJSON renders the envelope's `dependenciesComplete` list: every key
610
+ // the transform published except the ones the collector could not bound.
611
+ //
612
+ // The published map is the input rather than the entries of ToJSON. A file typia
613
+ // never touched reports no dependency at all, and that empty entry is the
614
+ // strongest claim in the envelope -- nothing outside the file can change a
615
+ // faithful re-print -- so reading the dependency map instead would drop the
616
+ // majority of the narrowing on the floor. Taking the same map the consumer keys
617
+ // on also makes the list a subset of it by construction.
618
+ func (collector *transformDependencyCollector) ToCompleteJSON(published map[string]string) []string {
619
+ output := make([]string, 0, len(published))
620
+ for key := range published {
621
+ if collector.unbounded[key] {
622
+ continue
623
+ }
624
+ output = append(output, key)
625
+ }
626
+ if len(output) == 0 {
627
+ return nil
628
+ }
629
+ sort.Strings(output)
630
+ return output
631
+ }
632
+
459
633
  type transformCompilerDiagnostic struct {
460
634
  File *string `json:"file"`
461
635
  Category string `json:"category"`
@@ -131,6 +131,7 @@ func ttscTypiaTestWriteCommonRuntimeStubs(t *testing.T, runtimeDir string) {
131
131
  "assert-guard-stub.cjs": ttscTypiaTestAssertGuardStub,
132
132
  "functional-error-stub.cjs": ttscTypiaTestFunctionalErrorStub,
133
133
  "access-expression-stub.cjs": ttscTypiaTestAccessExpressionStub,
134
+ "string-length-stub.cjs": ttscTypiaTestStringLengthStub,
134
135
  "notation-stub.cjs": ttscTypiaTestNotationStub,
135
136
  "json-stringify-array-stub.cjs": ttscTypiaTestJsonStringifyArrayStub,
136
137
  "json-stringify-property-stub.cjs": ttscTypiaTestJsonStringifyPropertyStub,
@@ -153,6 +154,7 @@ func ttscTypiaTestRewriteCommonJS(t *testing.T, js string) string {
153
154
  runtimeJS = strings.ReplaceAll(runtimeJS, `require("typia/lib/internal/_assertGuard")`, `require("./assert-guard-stub.cjs")`)
154
155
  runtimeJS = strings.ReplaceAll(runtimeJS, `require("typia/lib/internal/_functionalTypeGuardErrorFactory")`, `require("./functional-error-stub.cjs")`)
155
156
  runtimeJS = strings.ReplaceAll(runtimeJS, `require("typia/lib/internal/_accessExpressionAsString")`, `require("./access-expression-stub.cjs")`)
157
+ runtimeJS = strings.ReplaceAll(runtimeJS, `require("typia/lib/internal/_stringLength")`, `require("./string-length-stub.cjs")`)
156
158
  runtimeJS = strings.ReplaceAll(runtimeJS, `require("typia/lib/internal/_jsonStringifyArray")`, `require("./json-stringify-array-stub.cjs")`)
157
159
  runtimeJS = strings.ReplaceAll(runtimeJS, `require("typia/lib/internal/_jsonStringifyProperty")`, `require("./json-stringify-property-stub.cjs")`)
158
160
  runtimeJS = strings.ReplaceAll(runtimeJS, `require("typia/lib/internal/_jsonStringifyElement")`, `require("./json-stringify-element-stub.cjs")`)
@@ -290,6 +292,16 @@ const ttscTypiaTestRandomStringStub = `module.exports._randomString = (props) =>
290
292
  };
291
293
  `
292
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.
298
+ const ttscTypiaTestStringLengthStub = `module.exports._stringLength = (value) => {
299
+ let count = 0;
300
+ for (const _ of value) ++count;
301
+ return count;
302
+ };
303
+ `
304
+
293
305
  const ttscTypiaTestAccessExpressionStub = `const reserved = new Set([
294
306
  "break",
295
307
  "case",
@@ -196,6 +196,34 @@ func (metadataCommentTagFactoryNamespace) Get(props struct {
196
196
  return output
197
197
  }
198
198
 
199
+ // metadataCommentTagFactory_PARSER maps a JSDoc tag name to the tag records it
200
+ // produces. Each record's `Validate` is the runtime check the transform splices
201
+ // into the emitted validator, and it must spell the same check as the matching
202
+ // declaration in `packages/interface/src/tags` -- the two are one constraint.
203
+ //
204
+ // # WHEN A RECORD MAY NAME A RUNTIME HELPER
205
+ //
206
+ // A `Validate` may write `$importInternal("x")`, here or in the matching
207
+ // declaration, only under the rule below.
208
+ //
209
+ // That call makes the emitted validator `require("typia/lib/internal/_x")`. The
210
+ // type-tag half of each constraint is declared in `@typia/interface`, which
211
+ // `typia` depends on through a caret range, and a caret only floats upward. So
212
+ // inside one major an older `typia` installs a newer `@typia/interface` and
213
+ // emits an import its own runtime never published; that is #2330, and it is why
214
+ // #2336 and #2339 reverted these very templates to inline expressions.
215
+ //
216
+ // The rule: a template may name only a helper that every `typia` inside the
217
+ // caret range already ships. `isTypeInt8` and its siblings satisfy it --
218
+ // `@typia/interface` 13.0.0 named them and `typia` 13.0.0 shipped them. The
219
+ // four this file and `Type.ts` now name do not: `_stringLength`, `_isMultipleOf`
220
+ // (both first published in 13.1.19), `_isTypeInt64Bigint`, and
221
+ // `_isTypeUint64Bigint` (13.1.19). They are safe only because the release that
222
+ // carries them is a major, where the caret cannot reach a `typia` that predates
223
+ // them. A minor or patch release of this tree reopens #2330.
224
+ //
225
+ // Nothing here can enforce that; the release version is the maintainer's, and a
226
+ // campaign pull request never assigns one. This comment is the record.
199
227
  var metadataCommentTagFactory_PARSER = map[string]metadataCommentTagFactory_parser{
200
228
  "items": func(props struct {
201
229
  Report func(msg string) any
@@ -268,7 +296,13 @@ var metadataCommentTagFactory_PARSER = map[string]metadataCommentTagFactory_pars
268
296
  Report func(msg string) any
269
297
  Value string
270
298
  }) metadataCommentTagFactory_TagRecord {
271
- return metadataCommentTagFactory_numeric(props, "MultipleOf", "multipleOf", "$input % "+props.Value+" === 0", "$input % "+props.Value+"n === 0n")
299
+ // `@multipleOf` and `MultipleOf<N>` are two spellings of one constraint, so
300
+ // the number arm divides exact decimals through the same helper the type tag
301
+ // names. `%` would divide the binary doubles that are actually stored, which
302
+ // answers a different question than the `multipleOf` keyword this tag emits.
303
+ // The bigint arm needs no helper: a bigint remainder is already exact.
304
+ // `_isMultipleOf` first shipped in 13.1.19; see the doc comment above.
305
+ return metadataCommentTagFactory_numeric(props, "MultipleOf", "multipleOf", "$importInternal(\"_isMultipleOf\")($input, "+props.Value+")", "$input % "+props.Value+"n === 0n")
272
306
  },
273
307
  "format": func(props struct {
274
308
  Report func(msg string) any
@@ -291,9 +325,16 @@ var metadataCommentTagFactory_PARSER = map[string]metadataCommentTagFactory_pars
291
325
  Value string
292
326
  }) metadataCommentTagFactory_TagRecord {
293
327
  value := metadataCommentTagFactory_parse_number(props)
328
+ // `@length`, `@minLength`, and `@maxLength` measure through the same helper
329
+ // `MinLength<N>` and `MaxLength<N>` name, because the JSDoc spelling and the
330
+ // type-tag spelling of one constraint must never mean different things.
331
+ // `$input.length` counts UTF-16 code units, while the `minLength` and
332
+ // `maxLength` keywords these tags emit count characters, so one astral
333
+ // character would measure 2 against a schema that measures 1.
334
+ // `_stringLength` first shipped in 13.1.19; see the doc comment above.
294
335
  return metadataCommentTagFactory_TagRecord{"string": {
295
- {Name: "MinLength<" + props.Value + ">", Target: "string", Kind: "minLength", Value: value, Validate: props.Value + " <= $input.length", Exclusive: metadataCommentTagFactory_exclusive("minLength"), Schema: map[string]any{"minLength": value}},
296
- {Name: "MaxLength<" + props.Value + ">", Target: "string", Kind: "maxLength", Value: value, Validate: "$input.length <= " + props.Value, Exclusive: metadataCommentTagFactory_exclusive("maxLength"), Schema: map[string]any{"maxLength": value}},
336
+ {Name: "MinLength<" + props.Value + ">", Target: "string", Kind: "minLength", Value: value, Validate: props.Value + " <= $importInternal(\"_stringLength\")($input)", Exclusive: metadataCommentTagFactory_exclusive("minLength"), Schema: map[string]any{"minLength": value}},
337
+ {Name: "MaxLength<" + props.Value + ">", Target: "string", Kind: "maxLength", Value: value, Validate: "$importInternal(\"_stringLength\")($input) <= " + props.Value, Exclusive: metadataCommentTagFactory_exclusive("maxLength"), Schema: map[string]any{"maxLength": value}},
297
338
  }}
298
339
  },
299
340
  "minLength": func(props struct {
@@ -301,14 +342,14 @@ var metadataCommentTagFactory_PARSER = map[string]metadataCommentTagFactory_pars
301
342
  Value string
302
343
  }) metadataCommentTagFactory_TagRecord {
303
344
  value := metadataCommentTagFactory_parse_number(props)
304
- return metadataCommentTagFactory_TagRecord{"string": {{Name: "MinLength<" + props.Value + ">", Target: "string", Kind: "minLength", Value: value, Validate: props.Value + " <= $input.length", Exclusive: metadataCommentTagFactory_exclusive("minLength"), Schema: map[string]any{"minLength": value}}}}
345
+ return metadataCommentTagFactory_TagRecord{"string": {{Name: "MinLength<" + props.Value + ">", Target: "string", Kind: "minLength", Value: value, Validate: props.Value + " <= $importInternal(\"_stringLength\")($input)", Exclusive: metadataCommentTagFactory_exclusive("minLength"), Schema: map[string]any{"minLength": value}}}}
305
346
  },
306
347
  "maxLength": func(props struct {
307
348
  Report func(msg string) any
308
349
  Value string
309
350
  }) metadataCommentTagFactory_TagRecord {
310
351
  value := metadataCommentTagFactory_parse_number(props)
311
- return metadataCommentTagFactory_TagRecord{"string": {{Name: "MaxLength<" + props.Value + ">", Target: "string", Kind: "maxLength", Value: value, Validate: "$input.length <= " + props.Value, Exclusive: metadataCommentTagFactory_exclusive("maxLength"), Schema: map[string]any{"maxLength": value}}}}
352
+ return metadataCommentTagFactory_TagRecord{"string": {{Name: "MaxLength<" + props.Value + ">", Target: "string", Kind: "maxLength", Value: value, Validate: "$importInternal(\"_stringLength\")($input) <= " + props.Value, Exclusive: metadataCommentTagFactory_exclusive("maxLength"), Schema: map[string]any{"maxLength": value}}}}
312
353
  },
313
354
  }
314
355
 
@@ -407,28 +448,23 @@ func metadataCommentTagFactory_parse_type(props struct {
407
448
  "number": {{Name: "Type<" + strconv.Quote(value) + ">", Target: "number", Kind: "type", Value: value, Validate: validate, Exclusive: metadataCommentTagFactory_exclusive("type"), Schema: numberSchema}},
408
449
  }
409
450
  if value == "int64" || value == "uint64" {
410
- // Both checks stay inline, spelled exactly as `Type<"int64">` and
411
- // `Type<"uint64">` declare them, because `@type int64` and
412
- // `bigint & Type<"int64">` are two spellings of one constraint.
451
+ // `@type int64` and `bigint & Type<"int64">` are two spellings of one
452
+ // constraint, so both delegate to the helper that holds the exact inclusive
453
+ // bound. The bigint form used to emit `true` for int64 and a lower bound
454
+ // only for uint64, which certified any magnitude at all -- and
455
+ // `typia.protobuf.encode` then truncated the value to 64 bits, so a
456
+ // certified value came back different.
413
457
  //
414
- // That declaration must not name a runtime helper the way the `number` arms
415
- // above do. A `$importInternal(...)` in a tag template makes the emitted
416
- // validator import a `typia/lib/internal/*` module, and the declaration
417
- // lives in `@typia/interface`, which `typia` depends on through a caret
418
- // range that only floats upward -- so an older `typia` installs a newer
419
- // `@typia/interface` and emits an import its own runtime never shipped
420
- // (#2330). `isTypeInt64` and `isTypeUint64` are safe there because
421
- // `@typia/interface` 13.0.0 already named them and `typia` 13.0.0 already
422
- // shipped them; a helper introduced later is not, and inside one major
423
- // there is no range that prevents the pairing.
458
+ // The bound has to live in a helper rather than an inline literal here
459
+ // because it is only exact as a string: `BigInt(9223372036854775807)` rounds
460
+ // its `number` literal to 2**63 before BigInt ever parses it.
424
461
  //
425
- // The bound this leaves unenforced is real: int64 accepts any magnitude and
426
- // uint64 only rejects negatives. #2338 owns restoring it, and only a major
427
- // can carry it -- an inline exact comparison would name no helper, but
428
- // tightening an accepted range rejects data that passes today.
429
- bigintValidate := "BigInt(0) <= $input"
462
+ // See the `metadataCommentTagFactory_PARSER` doc comment: naming a helper
463
+ // here is safe only under a major bump, and `_isTypeInt64Bigint` /
464
+ // `_isTypeUint64Bigint` first shipped in 13.1.19.
465
+ bigintValidate := "$importInternal(\"isTypeUint64Bigint\")($input)"
430
466
  if value == "int64" {
431
- bigintValidate = "true"
467
+ bigintValidate = "$importInternal(\"isTypeInt64Bigint\")($input)"
432
468
  }
433
469
  record["bigint"] = []schemametadata.IMetadataTypeTag{
434
470
  {Name: "Type<" + strconv.Quote(value) + ">", Target: "bigint", Kind: "type", Value: value, Validate: bigintValidate, Exclusive: metadataCommentTagFactory_exclusive("type"), Schema: bigintSchema},
@@ -452,7 +488,7 @@ func metadataCommentTagFactory_numeric(props struct {
452
488
  "number": {{Name: name + "<" + props.Value + ">", Target: "number", Kind: kind, Value: number, Validate: numberValidate, Exclusive: exclusive, Schema: map[string]any{kind: number}}},
453
489
  }
454
490
  if integer != nil {
455
- record["bigint"] = []schemametadata.IMetadataTypeTag{{Name: name + "<" + props.Value + "n>", Target: "bigint", Kind: kind, Value: int64(*integer), Validate: bigintValidate, Exclusive: exclusive, Schema: map[string]any{kind: number}}}
491
+ record["bigint"] = []schemametadata.IMetadataTypeTag{{Name: name + "<" + props.Value + "n>", Target: "bigint", Kind: kind, Value: *integer, Validate: bigintValidate, Exclusive: exclusive, Schema: map[string]any{kind: number}}}
456
492
  }
457
493
  return record
458
494
  }
@@ -473,7 +509,7 @@ func metadataCommentTagFactory_parse_integer(props struct {
473
509
  Report func(msg string) any
474
510
  Unsigned bool
475
511
  Value string
476
- }) *int {
512
+ }) *int64 {
477
513
  parsedAny := metadataCommentTagFactory_parse_number(struct {
478
514
  Report func(msg string) any
479
515
  Value string
@@ -490,10 +526,43 @@ func metadataCommentTagFactory_parse_integer(props struct {
490
526
  props.Report("invalid unsigned integer")
491
527
  return nil
492
528
  }
493
- value := int(parsed)
529
+ // A float64 outside the destination range converts by an
530
+ // implementation-defined rule, not by wrapping: on amd64 both `1e19` and
531
+ // `-1e19` become -9223372036854775808, so a magnitude and its negation
532
+ // collapse to the same wrong number. Reject instead, because a tag whose value
533
+ // cannot be carried is a tag whose meaning is already lost.
534
+ if parsed < metadataCommentTagFactory_INT64_MINIMUM || parsed >= metadataCommentTagFactory_INT64_EXCLUSIVE_MAXIMUM {
535
+ props.Report("integer out of range")
536
+ return nil
537
+ }
538
+ value := int64(parsed)
494
539
  return &value
495
540
  }
496
541
 
542
+ // The int64 range as float64 bounds.
543
+ //
544
+ // int64 rather than int, because `int` is 32 bits on the 32-bit platforms ttsc
545
+ // publishes a binary for (`@ttsc/linux-arm`). Bounding by `int` would make the
546
+ // same `@minimum 3000000000` on a `bigint` compile on x64 and either be rejected
547
+ // or silently converted out of range on arm, and the value is already widened to
548
+ // int64 at its only numeric use. Bounding by int64 is what the tag means on
549
+ // every platform.
550
+ //
551
+ // The maximum is exclusive and spelled 2**63 rather than math.MaxInt64, because
552
+ // the inclusive form does not survive the comparison. An untyped constant
553
+ // 9223372036854775807.0 rounds to 2**63 once it meets a float64 operand, so
554
+ // `parsed > max` compares 2**63 against 2**63 and lets the one value through
555
+ // that int64 cannot hold. A strict `>=` against 2**63 is exact instead: no
556
+ // float64 lies between 2**63 - 1024 and 2**63, so rejecting from 2**63 up
557
+ // rejects exactly the magnitudes int64 cannot represent.
558
+ //
559
+ // The minimum needs no such care -- -2**63 is math.MinInt64 and is exactly
560
+ // representable as a float64.
561
+ const (
562
+ metadataCommentTagFactory_INT64_MINIMUM = -9223372036854775808.0
563
+ metadataCommentTagFactory_INT64_EXCLUSIVE_MAXIMUM = 9223372036854775808.0
564
+ )
565
+
497
566
  func metadataCommentTagFactory_includes(values []string, target string) bool {
498
567
  for _, value := range values {
499
568
  if value == target {
@@ -124,8 +124,8 @@ func TestMetadataCommentTagFactoryCoverage(t *testing.T) {
124
124
  if len(reports) != decimalReportCount || len(decimal["number"]) != 1 || len(decimal["bigint"]) != 0 {
125
125
  t.Fatalf("decimal multipleOf must remain a number-only JSDoc tag: %#v, reports=%#v", decimal, reports[decimalReportCount:])
126
126
  }
127
- if decimal["number"][0].Validate != "$input % 0.01 === 0" {
128
- t.Fatalf("decimal multipleOf must spell the same remainder check the MultipleOf type tag declares: %s", decimal["number"][0].Validate)
127
+ if decimal["number"][0].Validate != "$importInternal(\"_isMultipleOf\")($input, 0.01)" {
128
+ t.Fatalf("decimal multipleOf must use the shared exact helper: %s", decimal["number"][0].Validate)
129
129
  }
130
130
  invalidBefore := len(reports)
131
131
  _ = metadataCommentTagFactory_parse_number(struct {