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.
- package/lib/internal/_randomPattern.js +20 -3
- package/lib/internal/_randomPattern.js.map +1 -1
- package/lib/internal/_randomPattern.mjs +5 -1
- package/lib/internal/_randomPattern.mjs.map +1 -1
- package/lib/internal/_randomStringLength.js +9 -2
- package/lib/internal/_randomStringLength.js.map +1 -1
- package/lib/internal/_randomStringLength.mjs +3 -1
- package/lib/internal/_randomStringLength.mjs.map +1 -1
- package/lib/transform.mjs +1 -1
- package/native/cmd/ttsc-typia/dynamic_key_tags_transform_test.go +206 -0
- package/native/cmd/ttsc-typia/notation_kebab_case_transform_test.go +2 -2
- package/native/cmd/ttsc-typia/project_catalog_transform_coverage_test.go +84 -85
- package/native/cmd/ttsc-typia/template_literal_type_tags_transform_test.go +3 -1
- package/native/cmd/ttsc-typia/transform.go +2 -2
- package/native/cmd/ttsc-typia/transform_helpers_test.go +12 -0
- package/native/core/factories/MetadataCommentTagFactory.go +96 -27
- package/native/core/factories/metadata_comment_tag_factory_coverage_test.go +2 -2
- package/native/core/factories/metadata_comment_tag_factory_rejects_out_of_range_integer_test.go +124 -0
- package/native/core/programmers/AssertProgrammer.go +20 -6
- package/native/core/programmers/RandomProgrammer.go +78 -6
- package/native/core/programmers/ValidateProgrammer.go +21 -6
- package/native/core/programmers/helpers/RandomJoiner.go +8 -1
- package/native/core/programmers/iterate/check_dynamic_properties.go +177 -3
- package/native/core/programmers/iterate/check_object.go +12 -1
- package/package.json +3 -12
- package/src/internal/_randomPattern.ts +18 -3
- package/src/internal/_randomStringLength.ts +9 -2
- package/lib/executable/FileSystemIdentity.d.ts +0 -44
- package/lib/executable/FileSystemIdentity.js +0 -213
- package/lib/executable/FileSystemIdentity.js.map +0 -1
- package/lib/executable/FileSystemIdentity.mjs +0 -133
- package/lib/executable/FileSystemIdentity.mjs.map +0 -1
- package/lib/executable/TypiaGenerateWizard.d.ts +0 -9
- package/lib/executable/TypiaGenerateWizard.js +0 -852
- package/lib/executable/TypiaGenerateWizard.js.map +0 -1
- package/lib/executable/TypiaGenerateWizard.mjs +0 -618
- package/lib/executable/TypiaGenerateWizard.mjs.map +0 -1
- package/lib/executable/generate/ttsc.d.ts +0 -1
- package/lib/executable/generate/ttsc.js +0 -53
- package/lib/executable/generate/ttsc.js.map +0 -1
- package/lib/executable/generate/ttsc.mjs +0 -37
- package/lib/executable/generate/ttsc.mjs.map +0 -1
- package/lib/executable/typia.d.ts +0 -2
- package/lib/executable/typia.js +0 -84
- package/lib/executable/typia.js.map +0 -1
- package/lib/executable/typia.mjs +0 -60
- package/lib/executable/typia.mjs.map +0 -1
- package/src/executable/FileSystemIdentity.ts +0 -222
- package/src/executable/TypiaGenerateWizard.ts +0 -1133
- package/src/executable/generate/ttsc.ts +0 -70
- 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
|
-
|
|
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
|
|
296
|
-
{Name: "MaxLength<" + props.Value + ">", Target: "string", Kind: "maxLength", Value: value, Validate: "$input
|
|
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
|
|
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
|
|
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
|
-
//
|
|
411
|
-
//
|
|
412
|
-
//
|
|
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
|
-
//
|
|
415
|
-
//
|
|
416
|
-
//
|
|
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
|
-
//
|
|
426
|
-
//
|
|
427
|
-
//
|
|
428
|
-
|
|
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 = "
|
|
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:
|
|
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
|
-
}) *
|
|
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
|
-
|
|
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
|
|
128
|
-
t.Fatalf("decimal multipleOf must
|
|
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 {
|
package/native/core/factories/metadata_comment_tag_factory_rejects_out_of_range_integer_test.go
ADDED
|
@@ -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:
|
|
69
|
-
Path:
|
|
70
|
-
Trace:
|
|
71
|
-
Numeric:
|
|
72
|
-
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:
|
|
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
|
|
1445
|
-
//
|
|
1446
|
-
//
|
|
1447
|
-
//
|
|
1448
|
-
//
|
|
1449
|
-
//
|
|
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:
|
|
59
|
-
Path:
|
|
60
|
-
Trace:
|
|
61
|
-
Numeric:
|
|
62
|
-
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:
|
|
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(
|
|
136
|
+
nativefactories.ExpressionFactory.Number(RandomJoiner_RECURSION_CUTOFF, props.Emit),
|
|
130
137
|
nil,
|
|
131
138
|
f.NewToken(shimast.KindGreaterThanEqualsToken),
|
|
132
139
|
f.NewIdentifier("_depth"),
|