typia 13.3.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 (51) hide show
  1. package/lib/internal/_randomPattern.js +20 -3
  2. package/lib/internal/_randomPattern.js.map +1 -1
  3. package/lib/internal/_randomPattern.mjs +5 -1
  4. package/lib/internal/_randomPattern.mjs.map +1 -1
  5. package/lib/internal/_randomStringLength.js +9 -2
  6. package/lib/internal/_randomStringLength.js.map +1 -1
  7. package/lib/internal/_randomStringLength.mjs +3 -1
  8. package/lib/internal/_randomStringLength.mjs.map +1 -1
  9. package/lib/transform.mjs +1 -1
  10. package/native/cmd/ttsc-typia/dynamic_key_tags_transform_test.go +206 -0
  11. package/native/cmd/ttsc-typia/notation_kebab_case_transform_test.go +2 -2
  12. package/native/cmd/ttsc-typia/project_catalog_transform_coverage_test.go +84 -85
  13. package/native/cmd/ttsc-typia/template_literal_type_tags_transform_test.go +3 -1
  14. package/native/cmd/ttsc-typia/transform.go +2 -2
  15. package/native/cmd/ttsc-typia/transform_helpers_test.go +12 -0
  16. package/native/core/factories/MetadataCommentTagFactory.go +96 -27
  17. package/native/core/factories/metadata_comment_tag_factory_coverage_test.go +2 -2
  18. package/native/core/factories/metadata_comment_tag_factory_rejects_out_of_range_integer_test.go +124 -0
  19. package/native/core/programmers/AssertProgrammer.go +20 -6
  20. package/native/core/programmers/RandomProgrammer.go +78 -6
  21. package/native/core/programmers/ValidateProgrammer.go +21 -6
  22. package/native/core/programmers/helpers/RandomJoiner.go +8 -1
  23. package/native/core/programmers/iterate/check_dynamic_properties.go +177 -3
  24. package/native/core/programmers/iterate/check_object.go +12 -1
  25. package/package.json +3 -12
  26. package/src/internal/_randomPattern.ts +18 -3
  27. package/src/internal/_randomStringLength.ts +9 -2
  28. package/lib/executable/FileSystemIdentity.d.ts +0 -44
  29. package/lib/executable/FileSystemIdentity.js +0 -213
  30. package/lib/executable/FileSystemIdentity.js.map +0 -1
  31. package/lib/executable/FileSystemIdentity.mjs +0 -133
  32. package/lib/executable/FileSystemIdentity.mjs.map +0 -1
  33. package/lib/executable/TypiaGenerateWizard.d.ts +0 -9
  34. package/lib/executable/TypiaGenerateWizard.js +0 -852
  35. package/lib/executable/TypiaGenerateWizard.js.map +0 -1
  36. package/lib/executable/TypiaGenerateWizard.mjs +0 -618
  37. package/lib/executable/TypiaGenerateWizard.mjs.map +0 -1
  38. package/lib/executable/generate/ttsc.d.ts +0 -1
  39. package/lib/executable/generate/ttsc.js +0 -53
  40. package/lib/executable/generate/ttsc.js.map +0 -1
  41. package/lib/executable/generate/ttsc.mjs +0 -37
  42. package/lib/executable/generate/ttsc.mjs.map +0 -1
  43. package/lib/executable/typia.d.ts +0 -2
  44. package/lib/executable/typia.js +0 -84
  45. package/lib/executable/typia.js.map +0 -1
  46. package/lib/executable/typia.mjs +0 -60
  47. package/lib/executable/typia.mjs.map +0 -1
  48. package/src/executable/FileSystemIdentity.ts +0 -222
  49. package/src/executable/TypiaGenerateWizard.ts +0 -1133
  50. package/src/executable/generate/ttsc.ts +0 -70
  51. package/src/executable/typia.ts +0 -80
@@ -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 {
@@ -0,0 +1,124 @@
1
+ package factories
2
+
3
+ import "testing"
4
+
5
+ // TestMetadataCommentTagFactoryRejectsOutOfRangeInteger pins the boundary of the
6
+ // float64-to-int conversion behind every numeric JSDoc tag.
7
+ //
8
+ // `metadataCommentTagFactory_parse_integer` used to end in a bare `int(parsed)`.
9
+ // A float64 outside the destination range converts by an implementation-defined
10
+ // rule rather than by wrapping, so on amd64 both `1e19` and `-1e19` became
11
+ // -9223372036854775808 -- a magnitude and its negation collapsing to the same
12
+ // wrong number, silently, inside `@minItems` and `@maxItems`.
13
+ //
14
+ // The destination is int64, not int. `int` is 32 bits on the 32-bit platforms
15
+ // ttsc publishes a binary for (`@ttsc/linux-arm`), so bounding by `int` would
16
+ // leave the same defect at 2**31 there while closing it at 2**63 on x64 -- and
17
+ // would make `@minimum 3000000000` on a `bigint` mean one thing per platform.
18
+ // Case 4 is what holds the boundary platform-independent.
19
+ //
20
+ // The guard has to be exclusive at 2**63 rather than inclusive at
21
+ // math.MaxInt64. An untyped constant 9223372036854775807.0 rounds to 2**63 once
22
+ // it meets a float64 operand, so an inclusive comparison lets exactly the one
23
+ // value through that int64 cannot hold. This test is what keeps that distinction
24
+ // from being "simplified" back.
25
+ //
26
+ // 1. Reject magnitudes at and beyond 2**63 in both directions.
27
+ // 2. Accept the true minimum and the largest float64 below the maximum, and
28
+ // require the value carried through to be the value parsed.
29
+ // 3. Keep the existing non-integer and unsigned diagnostics untouched.
30
+ // 4. Require the accepted range to be int64's on every platform, not the
31
+ // compiling platform's `int`.
32
+ func TestMetadataCommentTagFactoryRejectsOutOfRangeInteger(t *testing.T) {
33
+ parse := func(value string, unsigned bool) (*int64, []string) {
34
+ messages := []string{}
35
+ report := func(msg string) any {
36
+ messages = append(messages, msg)
37
+ return nil
38
+ }
39
+ return metadataCommentTagFactory_parse_integer(struct {
40
+ Report func(msg string) any
41
+ Unsigned bool
42
+ Value string
43
+ }{Report: report, Unsigned: unsigned, Value: value}), messages
44
+ }
45
+
46
+ //----
47
+ // 1. out of range, both directions
48
+ //----
49
+ for _, value := range []string{
50
+ "9223372036854775808", // 2**63 exactly -- the value an inclusive guard let through
51
+ "10000000000000000000", // 1e19
52
+ "-10000000000000000000", // -1e19, which collapsed to the same int as +1e19
53
+ "1e30",
54
+ } {
55
+ parsed, messages := parse(value, false)
56
+ if parsed != nil {
57
+ t.Fatalf("%s must be rejected, got %d", value, *parsed)
58
+ }
59
+ if len(messages) == 0 {
60
+ t.Fatalf("%s must be reported, not dropped silently", value)
61
+ }
62
+ }
63
+
64
+ //----
65
+ // 2. in range, carried through unchanged
66
+ //----
67
+ for _, tuple := range []struct {
68
+ value string
69
+ expected int64
70
+ }{
71
+ {value: "0", expected: 0},
72
+ {value: "1", expected: 1},
73
+ {value: "-1", expected: -1},
74
+ {value: "-9223372036854775808", expected: -9223372036854775808}, // the true minimum
75
+ {value: "9223372036854774784", expected: 9223372036854774784}, // largest float64 below 2**63
76
+ } {
77
+ parsed, messages := parse(tuple.value, false)
78
+ if parsed == nil {
79
+ t.Fatalf("%s must be accepted, reports=%#v", tuple.value, messages)
80
+ }
81
+ if *parsed != tuple.expected {
82
+ t.Fatalf("%s must parse to %d, got %d", tuple.value, tuple.expected, *parsed)
83
+ }
84
+ if len(messages) != 0 {
85
+ t.Fatalf("%s must report nothing, got %#v", tuple.value, messages)
86
+ }
87
+ }
88
+
89
+ //----
90
+ // 3. the diagnostics that already existed
91
+ //----
92
+ if parsed, messages := parse("1.5", false); parsed != nil || len(messages) == 0 {
93
+ t.Fatalf("a non-integer must still be rejected: %#v %#v", parsed, messages)
94
+ }
95
+ if parsed, messages := parse("-1", true); parsed != nil || len(messages) == 0 {
96
+ t.Fatalf("a negative unsigned integer must still be rejected: %#v %#v", parsed, messages)
97
+ }
98
+
99
+ //----
100
+ // 4. the boundary is int64's on every platform, not the compiling one's int
101
+ //----
102
+ // These sit above int32 and below int64. A guard written against `int`
103
+ // accepts them here and converts them out of range on `@ttsc/linux-arm` --
104
+ // the same defect this test closes, moved to a platform the suite does not
105
+ // usually run on. `GOARCH=arm go vet ./core/factories/` is what executes this
106
+ // case for real; on a 64-bit host it is the declared type that carries it.
107
+ for _, tuple := range []struct {
108
+ value string
109
+ expected int64
110
+ }{
111
+ {value: "2147483648", expected: 2147483648}, // 2**31
112
+ {value: "-2147483649", expected: -2147483649}, // -2**31 - 1
113
+ {value: "4294967296", expected: 4294967296}, // 2**32, past uint32 too
114
+ {value: "3000000000", expected: 3000000000}, // a plausible @minimum on a bigint
115
+ } {
116
+ parsed, messages := parse(tuple.value, false)
117
+ if parsed == nil {
118
+ t.Fatalf("%s is inside int64 and must be accepted on every platform, reports=%#v", tuple.value, messages)
119
+ }
120
+ if *parsed != tuple.expected {
121
+ t.Fatalf("%s must parse to %d, got %d", tuple.value, tuple.expected, *parsed)
122
+ }
123
+ }
124
+ }
@@ -65,13 +65,13 @@ func (assertProgrammerNamespace) Decompose(props AssertProgrammer_DecomposeProps
65
65
  Type: props.Type,
66
66
  Name: props.Name,
67
67
  Config: nativeinternal.CheckerProgrammer_IConfig{
68
- Prefix: "_a",
69
- Path: true,
70
- Trace: true,
71
- Numeric: nativehelpers.OptionPredicator.Numeric(props.Context.Options),
72
- Equals: props.Config.Equals,
68
+ Prefix: "_a",
69
+ Path: true,
70
+ Trace: true,
71
+ Numeric: nativehelpers.OptionPredicator.Numeric(props.Context.Options),
72
+ Equals: props.Config.Equals,
73
73
  ObjectParents: props.Config.Equals == false,
74
- Atomist: assertProgrammer_atomist(props),
74
+ Atomist: assertProgrammer_atomist(props),
75
75
  Combiner: assertProgrammer_combiner(struct {
76
76
  Config AssertProgrammer_IConfig
77
77
  Context nativecontext.ITypiaContext
@@ -323,6 +323,20 @@ func assertProgrammer_assert_object(props assertProgrammer_assertObjectProps) *s
323
323
  Input: input,
324
324
  })
325
325
  },
326
+ InvalidKey: func(input *shimast.Expression, expected string, description *shimast.Expression) *shimast.Node {
327
+ return assertProgrammer_create_guard_call(assertProgrammer_createGuardCallProps{
328
+ Context: props.Context,
329
+ Functor: props.Functor,
330
+ Path: assertProgrammer_binary(
331
+ f.NewIdentifier("_path"),
332
+ shimast.KindPlusToken,
333
+ f.NewCallExpression(assertProgrammer_internal(props.Context, "accessExpressionAsString"), nil, nil, f.NewNodeList([]*shimast.Node{f.NewIdentifier("key")}), shimast.NodeFlagsNone),
334
+ props.Context.Emit,
335
+ ),
336
+ Expected: expected,
337
+ Input: input,
338
+ })
339
+ },
326
340
  Halt: func(expr *shimast.Expression) *shimast.Node {
327
341
  return assertProgrammer_or(
328
342
  assertProgrammer_equal(f.NewKeywordExpression(shimast.KindFalseKeyword), f.NewIdentifier("_exceptionable"), props.Context.Emit),
@@ -546,6 +546,26 @@ func randomProgrammer_decode(props randomProgrammer_decodeProps) *shimast.Node {
546
546
  if len(expressions) == 1 {
547
547
  return expressions[0]
548
548
  }
549
+ if escape := randomProgrammer_recursion_escape(props); escape != nil {
550
+ return nativefactories.ExpressionFactory.Conditional(
551
+ f.NewBinaryExpression(
552
+ nil,
553
+ nativefactories.ExpressionFactory.Number(nativehelpers.RandomJoiner_RECURSION_CUTOFF, props.Context.Emit),
554
+ nil,
555
+ f.NewToken(shimast.KindGreaterThanEqualsToken),
556
+ f.NewIdentifier("_depth"),
557
+ ),
558
+ randomProgrammer_decode_pick(props, expressions),
559
+ escape,
560
+ props.Context.Emit,
561
+ )
562
+ }
563
+ return randomProgrammer_decode_pick(props, expressions)
564
+ }
565
+
566
+ // randomProgrammer_decode_pick draws one of the candidate expressions uniformly.
567
+ func randomProgrammer_decode_pick(props randomProgrammer_decodeProps, expressions []*shimast.Node) *shimast.Node {
568
+ f := nativecontext.EmitFactoryOf(randomProgrammer_factory, props.Context.Emit)
549
569
  pickers := make([]*shimast.Node, 0, len(expressions))
550
570
  for _, expr := range expressions {
551
571
  value := expr
@@ -576,6 +596,50 @@ func randomProgrammer_decode(props randomProgrammer_decodeProps) *shimast.Node {
576
596
  )
577
597
  }
578
598
 
599
+ // randomProgrammer_recursion_escape reports the terminating candidate a
600
+ // recursive leaf must take once the depth cutoff is reached, or nil when the
601
+ // leaf has none to force.
602
+ //
603
+ // A recursive array already stops at the cutoff -- RandomJoiner emits
604
+ // `CUTOFF >= _depth ? … : []` around it. The nullable and optional escapes had
605
+ // no such guard: `null` and `undefined` were simply two more candidates in the
606
+ // uniform `_randomPick`, so the depth of a `self?: T` or `self: T | null` chain
607
+ // was geometric with no bound at all. Measured before this guard, 20,000 draws
608
+ // of `{ value: string; self?: Self }` reached depth 16, and exceeded 8 on 44 of
609
+ // them -- which is why a suite asserting a depth bound failed at random.
610
+ //
611
+ // The guard needs `_depth` in scope, so it applies only inside a generated
612
+ // function (`Explore.Function`) reached recursively (`Explore.Recursive`), and
613
+ // only where the leaf actually recurses through an object or tuple. A leaf that
614
+ // is merely nullable, with no recursive candidate, keeps drawing uniformly.
615
+ func randomProgrammer_recursion_escape(props randomProgrammer_decodeProps) *shimast.Node {
616
+ if props.Explore.Function == false || props.Explore.Recursive == false {
617
+ return nil
618
+ }
619
+ recursive := false
620
+ for _, object := range props.Metadata.Objects {
621
+ if object.Type != nil && object.Type.Recursive {
622
+ recursive = true
623
+ }
624
+ }
625
+ for _, tuple := range props.Metadata.Tuples {
626
+ if tuple.Type != nil && tuple.Type.Recursive {
627
+ recursive = true
628
+ }
629
+ }
630
+ if recursive == false {
631
+ return nil
632
+ }
633
+ f := nativecontext.EmitFactoryOf(randomProgrammer_factory, props.Context.Emit)
634
+ if props.Metadata.Nullable {
635
+ return f.NewKeywordExpression(shimast.KindNullKeyword)
636
+ }
637
+ if props.Metadata.IsRequired() == false {
638
+ return f.NewIdentifier("undefined")
639
+ }
640
+ return nil
641
+ }
642
+
579
643
  func randomProgrammer_decode_atomic(props randomProgrammer_decodeAtomicProps) []*shimast.Node {
580
644
  var schemaList []nativeiterate.JsonSchema
581
645
  if props.Atomic.Type == "boolean" {
@@ -1441,12 +1505,20 @@ func randomProgrammer_is_typed_array(name string) bool {
1441
1505
  // copy of the 64-bit bound that is deliberately inexact. They become `minimum` and
1442
1506
  // `maximum` comment tags on a number-typed JSON schema, and `number` cannot
1443
1507
  // represent 2**63 - 1 or 2**64 - 1 -- both arrive rounded up to the next power of
1444
- // two. That costs nothing here: the generator only picks a value from the range,
1445
- // and the TypedArray constructor wraps whatever it is handed. On the `number`
1446
- // path validation owns exactness instead, in _isTypeInt64 and _isTypeUint64. The
1447
- // `bigint` path owns none: Type<"int64"> declares no check and Type<"uint64">
1448
- // only a lower bound, so that neither declaration names a runtime helper an
1449
- // older `typia` cannot resolve (#2330). #2338 owns restoring the bound.
1508
+ // two. That costs nothing here, and not because the generator is careless: it
1509
+ // draws Math.floor(Math.random() * (maximum - minimum + 1)) + minimum, and that
1510
+ // span is 2**64 either way, where consecutive doubles are 2048 apart. So the
1511
+ // largest draw is 2**63 - 2048 for int64 and 2**64 - 2048 for uint64, both
1512
+ // inside the width. (Nearer the int64 maximum itself the spacing is 1024, which
1513
+ // is why the largest double below 2**63 is 2**63 - 1024; the span is what bounds
1514
+ // the draw.) The TypedArray constructor then wraps whatever it is handed anyway.
1515
+ //
1516
+ // Only the `bigint` path is exact, in _isTypeInt64Bigint and _isTypeUint64Bigint,
1517
+ // which take the true inclusive bound through a BigInt string a `number` literal
1518
+ // here could not spell. _isTypeInt64 and _isTypeUint64 on the `number` path
1519
+ // carry the same rounding this table does: their maxima are written 2**63 - 1
1520
+ // and 2**64 - 1, which are the powers of two as doubles, so each accepts one
1521
+ // value above its width -- the only float form that maximum has.
1450
1522
  func randomProgrammer_typed_array_range(name string) (string, string, string) {
1451
1523
  switch name {
1452
1524
  case "Uint8Array", "Uint8ClampedArray":
@@ -55,13 +55,13 @@ func (validateProgrammerNamespace) Decompose(props ValidateProgrammer_DecomposeP
55
55
  Type: props.Type,
56
56
  Name: props.Name,
57
57
  Config: nativeinternal.CheckerProgrammer_IConfig{
58
- Prefix: "_v",
59
- Path: true,
60
- Trace: true,
61
- Numeric: nativehelpers.OptionPredicator.Numeric(props.Context.Options),
62
- Equals: props.Config.Equals,
58
+ Prefix: "_v",
59
+ Path: true,
60
+ Trace: true,
61
+ Numeric: nativehelpers.OptionPredicator.Numeric(props.Context.Options),
62
+ Equals: props.Config.Equals,
63
63
  ObjectParents: props.Config.Equals == false,
64
- Atomist: validateProgrammer_atomist(props),
64
+ Atomist: validateProgrammer_atomist(props),
65
65
  Combiner: validateProgrammer_combine(struct {
66
66
  Config ValidateProgrammer_IConfig
67
67
  Context nativecontext.ITypiaContext
@@ -312,6 +312,21 @@ func validateProgrammer_validate_object(props validateProgrammer_validateObjectP
312
312
  Emit: props.Context.Emit,
313
313
  })
314
314
  },
315
+ InvalidKey: func(input *shimast.Expression, expected string, description *shimast.Expression) *shimast.Node {
316
+ return validateProgrammer_create_report_call(validateProgrammer_createReportCallProps{
317
+ Path: validateProgrammer_binary(
318
+ f.NewIdentifier("_path"),
319
+ shimast.KindPlusToken,
320
+ f.NewCallExpression(validateProgrammer_internal(props.Context, "accessExpressionAsString"), nil, nil, f.NewNodeList([]*shimast.Node{f.NewIdentifier("key")}), shimast.NodeFlagsNone),
321
+ props.Context.Emit,
322
+ ),
323
+ Expected: expected,
324
+ Value: input,
325
+ Description: description,
326
+ Functor: props.Functor,
327
+ Emit: props.Context.Emit,
328
+ })
329
+ },
315
330
  Halt: func(expr *shimast.Expression) *shimast.Node {
316
331
  return validateProgrammer_or(validateProgrammer_equal(f.NewKeywordExpression(shimast.KindFalseKeyword), f.NewIdentifier("_exceptionable"), props.Context.Emit), expr, props.Context.Emit)
317
332
  },
@@ -15,6 +15,13 @@ type randomJoinerNamespace struct{}
15
15
 
16
16
  var RandomJoiner = randomJoinerNamespace{}
17
17
 
18
+ // RandomJoiner_RECURSION_CUTOFF is the `_depth` past which a recursive draw
19
+ // stops descending: a recursive array empties, and a recursive leaf with a
20
+ // nullable or optional escape takes it. One constant because one number decides
21
+ // both -- a recursive value that stopped at two different depths depending on
22
+ // which escape it happened to own would make a depth bound unstatable.
23
+ const RandomJoiner_RECURSION_CUTOFF = 5
24
+
18
25
  type RandomJoiner_Decoder func(metadata *nativemetadata.MetadataSchema) *shimast.Node
19
26
 
20
27
  type RandomJoiner_ArrayProps struct {
@@ -126,7 +133,7 @@ func (randomJoinerNamespace) Array(props RandomJoiner_ArrayProps) *shimast.Node
126
133
  return nativefactories.ExpressionFactory.Conditional(
127
134
  f.NewBinaryExpression(
128
135
  nil,
129
- nativefactories.ExpressionFactory.Number(5, props.Emit),
136
+ nativefactories.ExpressionFactory.Number(RandomJoiner_RECURSION_CUTOFF, props.Emit),
130
137
  nil,
131
138
  f.NewToken(shimast.KindGreaterThanEqualsToken),
132
139
  f.NewIdentifier("_depth"),