typia 13.1.19 → 13.2.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/executable/FileSystemIdentity.d.ts +23 -0
- package/lib/executable/FileSystemIdentity.js +30 -0
- package/lib/executable/FileSystemIdentity.js.map +1 -1
- package/lib/executable/FileSystemIdentity.mjs +4 -0
- package/lib/executable/FileSystemIdentity.mjs.map +1 -1
- package/lib/executable/TypiaGenerateWizard.js +15 -9
- package/lib/executable/TypiaGenerateWizard.js.map +1 -1
- package/lib/executable/TypiaGenerateWizard.mjs +11 -7
- package/lib/executable/TypiaGenerateWizard.mjs.map +1 -1
- package/lib/internal/_ProtobufReader.d.ts +18 -3
- package/lib/internal/_ProtobufReader.js +39 -11
- package/lib/internal/_ProtobufReader.js.map +1 -1
- package/lib/internal/_ProtobufReader.mjs +37 -10
- package/lib/internal/_ProtobufReader.mjs.map +1 -1
- package/lib/internal/_jsonStringifyArray.d.ts +1 -0
- package/lib/internal/_jsonStringifyArray.js +48 -0
- package/lib/internal/_jsonStringifyArray.js.map +1 -0
- package/lib/internal/_jsonStringifyArray.mjs +40 -0
- package/lib/internal/_jsonStringifyArray.mjs.map +1 -0
- package/lib/internal/_jsonStringifyElement.d.ts +1 -0
- package/lib/internal/_jsonStringifyElement.js +23 -0
- package/lib/internal/_jsonStringifyElement.js.map +1 -0
- package/lib/internal/_jsonStringifyElement.mjs +23 -0
- package/lib/internal/_jsonStringifyElement.mjs.map +1 -0
- package/lib/internal/_jsonStringifyProperty.d.ts +1 -0
- package/lib/internal/_jsonStringifyProperty.js +31 -0
- package/lib/internal/_jsonStringifyProperty.js.map +1 -0
- package/lib/internal/_jsonStringifyProperty.mjs +31 -0
- package/lib/internal/_jsonStringifyProperty.mjs.map +1 -0
- package/lib/internal/_llmApplicationFinalize.js +1 -1
- package/lib/internal/_llmApplicationFinalize.js.map +1 -1
- package/lib/internal/_llmApplicationFinalize.mjs +1 -1
- package/lib/internal/_llmApplicationFinalize.mjs.map +1 -1
- package/lib/internal/_randomFormatByte.js +24 -1
- package/lib/internal/_randomFormatByte.js.map +1 -1
- package/lib/internal/_randomFormatByte.mjs +20 -2
- package/lib/internal/_randomFormatByte.mjs.map +1 -1
- package/lib/internal/_randomFormatDate.d.ts +3 -6
- package/lib/internal/_randomFormatDate.js +2 -10
- package/lib/internal/_randomFormatDate.js.map +1 -1
- package/lib/internal/_randomFormatDate.mjs +2 -6
- package/lib/internal/_randomFormatDate.mjs.map +1 -1
- package/lib/internal/_randomFormatDatetime.d.ts +3 -6
- package/lib/internal/_randomFormatDatetime.js +25 -8
- package/lib/internal/_randomFormatDatetime.js.map +1 -1
- package/lib/internal/_randomFormatDatetime.mjs +17 -7
- package/lib/internal/_randomFormatDatetime.mjs.map +1 -1
- package/lib/internal/_randomFormatDuration.js +23 -15
- package/lib/internal/_randomFormatDuration.js.map +1 -1
- package/lib/internal/_randomFormatDuration.mjs +23 -15
- package/lib/internal/_randomFormatDuration.mjs.map +1 -1
- package/lib/internal/_randomFormatIpv4.js +31 -1
- package/lib/internal/_randomFormatIpv4.js.map +1 -1
- package/lib/internal/_randomFormatIpv4.mjs +23 -2
- package/lib/internal/_randomFormatIpv4.mjs.map +1 -1
- package/lib/internal/_randomFormatIpv6.js +57 -1
- package/lib/internal/_randomFormatIpv6.js.map +1 -1
- package/lib/internal/_randomFormatIpv6.mjs +45 -2
- package/lib/internal/_randomFormatIpv6.mjs.map +1 -1
- package/lib/internal/_randomFormatJsonPointer.js +14 -2
- package/lib/internal/_randomFormatJsonPointer.js.map +1 -1
- package/lib/internal/_randomFormatJsonPointer.mjs +13 -3
- package/lib/internal/_randomFormatJsonPointer.mjs.map +1 -1
- package/lib/internal/_randomFormatRegex.js +14 -1
- package/lib/internal/_randomFormatRegex.js.map +1 -1
- package/lib/internal/_randomFormatRegex.mjs +14 -2
- package/lib/internal/_randomFormatRegex.mjs.map +1 -1
- package/lib/internal/_randomFormatRelativeJsonPointer.js +19 -5
- package/lib/internal/_randomFormatRelativeJsonPointer.js.map +1 -1
- package/lib/internal/_randomFormatRelativeJsonPointer.mjs +21 -6
- package/lib/internal/_randomFormatRelativeJsonPointer.mjs.map +1 -1
- package/lib/internal/_randomFormatTime.js +33 -7
- package/lib/internal/_randomFormatTime.js.map +1 -1
- package/lib/internal/_randomFormatTime.mjs +37 -5
- package/lib/internal/_randomFormatTime.mjs.map +1 -1
- package/lib/internal/_randomPattern.js.map +1 -1
- package/lib/internal/_randomPattern.mjs.map +1 -1
- package/lib/internal/_randomStringLength.d.ts +41 -10
- package/lib/internal/_randomStringLength.js +49 -13
- package/lib/internal/_randomStringLength.js.map +1 -1
- package/lib/internal/_randomStringLength.mjs +48 -13
- package/lib/internal/_randomStringLength.mjs.map +1 -1
- package/lib/internal/private/__notationRename.js.map +1 -1
- package/lib/internal/private/__notationRename.mjs.map +1 -1
- package/lib/internal/private/__randomComposition.d.ts +23 -0
- package/lib/internal/private/__randomComposition.js +48 -0
- package/lib/internal/private/__randomComposition.js.map +1 -0
- package/lib/internal/private/__randomComposition.mjs +54 -0
- package/lib/internal/private/__randomComposition.mjs.map +1 -0
- package/lib/internal/private/__randomEpoch.d.ts +19 -0
- package/lib/internal/private/__randomEpoch.js +31 -0
- package/lib/internal/private/__randomEpoch.js.map +1 -0
- package/lib/internal/private/__randomEpoch.mjs +28 -0
- package/lib/internal/private/__randomEpoch.mjs.map +1 -0
- package/native/cmd/ttsc-typia/build.go +3 -0
- package/native/cmd/ttsc-typia/callable_interface_function_semantics_transform_test.go +331 -0
- package/native/cmd/ttsc-typia/callable_type_literal_boundaries_transform_test.go +1184 -0
- package/native/cmd/ttsc-typia/compare_functional_union_membership_transform_test.go +434 -0
- package/native/cmd/ttsc-typia/compare_template_literal_union_transform_test.go +241 -0
- package/native/cmd/ttsc-typia/default_library_spoof_native_identity_transform_test.go +216 -0
- package/native/cmd/ttsc-typia/json_schema_dynamic_key_determinism_transform_test.go +246 -0
- package/native/cmd/ttsc-typia/json_stringify_contextual_undefined_transform_test.go +556 -0
- package/native/cmd/ttsc-typia/lib_replacement_native_identity_transform_test.go +277 -0
- package/native/cmd/ttsc-typia/native_named_intersection_union_transform_test.go +390 -0
- package/native/cmd/ttsc-typia/node_buffer_native_exports_is_transform_test.go +526 -0
- package/native/cmd/ttsc-typia/package_declaration_native_identity_is_transform_test.go +293 -0
- package/native/cmd/ttsc-typia/protobuf_diagnostic_code_transform_test.go +167 -0
- package/native/cmd/ttsc-typia/symbol_index_domain_transform_test.go +316 -0
- package/native/cmd/ttsc-typia/transform.go +23 -0
- package/native/cmd/ttsc-typia/transform_helpers_test.go +77 -2
- package/native/cmd/ttsc-typia/user_global_native_identity_transform_test.go +484 -0
- package/native/core/factories/ProtobufFactory.go +6 -1
- package/native/core/factories/internal/metadata/MetadataHelper.go +185 -30
- package/native/core/factories/internal/metadata/emplace_metadata_object.go +25 -0
- package/native/core/factories/internal/metadata/iterate_metadata_function.go +1 -1
- package/native/core/factories/internal/metadata/iterate_metadata_intersection.go +103 -23
- package/native/core/factories/internal/metadata/iterate_metadata_map.go +3 -7
- package/native/core/factories/internal/metadata/iterate_metadata_native.go +32 -14
- package/native/core/factories/internal/metadata/iterate_metadata_set.go +3 -7
- package/native/core/programmers/compare/CompareEqualProgrammer.go +27 -13
- package/native/core/programmers/helpers/FunctionProgrammer.go +6 -13
- package/native/core/programmers/helpers/StringifyJoinder.go +129 -50
- package/native/core/programmers/helpers/joiner_branch_coverage_test.go +116 -120
- package/native/core/programmers/iterate/iterate_check_coverage_test.go +76 -158
- package/native/core/programmers/iterate/iterate_utility_coverage_test.go +231 -270
- package/native/core/programmers/iterate/json_schema_object.go +49 -7
- package/native/core/programmers/iterate/json_schema_pattern_property_order_test.go +44 -0
- package/native/core/programmers/iterate/openapi_v3_downgrader.go +34 -1
- package/native/core/programmers/iterate/template_runtime_pattern.go +11 -0
- package/native/core/programmers/json/JsonStringifyProgrammer.go +47 -6
- package/native/core/programmers/llm/LlmApplicationProgrammer.go +9 -1
- package/native/core/programmers/protobuf/ProtobufMessageProgrammer.go +1 -1
- package/native/core/schemas/metadata/MetadataCollection.go +0 -9
- package/native/core/schemas/metadata/MetadataDefaultLibrary.go +81 -0
- package/native/core/schemas/metadata/MetadataObjectType.go +7 -18
- package/native/go.mod +4 -4
- package/native/transform/features/protobuf/ProtobufMessageTransformer.go +1 -1
- package/package.json +3 -7
- package/src/executable/FileSystemIdentity.ts +33 -0
- package/src/executable/TypiaGenerateWizard.ts +18 -12
- package/src/internal/_ProtobufReader.ts +40 -11
- package/src/internal/_jsonStringifyArray.ts +50 -0
- package/src/internal/_jsonStringifyElement.ts +19 -0
- package/src/internal/_jsonStringifyProperty.ts +30 -0
- package/src/internal/_llmApplicationFinalize.ts +6 -1
- package/src/internal/_randomFormatByte.ts +30 -3
- package/src/internal/_randomFormatDate.ts +4 -20
- package/src/internal/_randomFormatDatetime.ts +34 -20
- package/src/internal/_randomFormatDuration.ts +19 -4
- package/src/internal/_randomFormatIpv4.ts +43 -3
- package/src/internal/_randomFormatIpv6.ts +67 -3
- package/src/internal/_randomFormatJsonPointer.ts +23 -3
- package/src/internal/_randomFormatRegex.ts +21 -3
- package/src/internal/_randomFormatRelativeJsonPointer.ts +23 -10
- package/src/internal/_randomFormatTime.ts +45 -12
- package/src/internal/_randomPattern.ts +4 -1
- package/src/internal/_randomStringLength.ts +61 -17
- package/src/internal/private/__notationRename.ts +1 -4
- package/src/internal/private/__randomComposition.ts +50 -0
- package/src/internal/private/__randomEpoch.ts +34 -0
- package/native/adapter/visit.go +0 -173
- package/native/adapter/visit_property_chain_edges_test.go +0 -129
- package/native/core/programmers/helpers/disable_function_programmer_declare.go +0 -10
- package/native/core/programmers/iterate/check_union_array_like.go +0 -422
- package/native/core/programmers/iterate/prune_object_properties.go +0 -105
- package/native/core/programmers/iterate/stringify_dynamic_properties.go +0 -219
- package/native/core/programmers/iterate/stringify_native.go +0 -14
- package/native/core/programmers/iterate/stringify_regular_properties.go +0 -112
- package/native/core/schemas/metadata/MetadataApplication.go +0 -41
|
@@ -0,0 +1,1184 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"fmt"
|
|
5
|
+
"os"
|
|
6
|
+
"os/exec"
|
|
7
|
+
"path/filepath"
|
|
8
|
+
"strings"
|
|
9
|
+
"testing"
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
// TestCallableTypeLiteralBoundariesTransform verifies that how a callable or
|
|
13
|
+
// constructable type is spelled cannot change what typia emits or how the
|
|
14
|
+
// emitted validator answers (#2238).
|
|
15
|
+
//
|
|
16
|
+
// #2250 gave call-only and construct-only `interface` declarations the function
|
|
17
|
+
// metadata semantics their mutually assignable function-type aliases already
|
|
18
|
+
// had. The same shape written as a type literal — `type X = { (v: number):
|
|
19
|
+
// string }` or the anonymous body at the call site — still reached structural
|
|
20
|
+
// object metadata, so a real function or class was rejected by the literal
|
|
21
|
+
// spelling and accepted by both the interface and the alias spelling. This
|
|
22
|
+
// regression pins the whole declaration-spelling axis rather than that one
|
|
23
|
+
// witness, because a fix that keyed on one node kind, one position, or one
|
|
24
|
+
// transform option would move a single cell and leave the class intact.
|
|
25
|
+
//
|
|
26
|
+
// The oracles are chosen so that no assertion reads its expectation from the
|
|
27
|
+
// emit under test. Mutual assignability is proven by the compiler: every
|
|
28
|
+
// spelling group compiles a bidirectional `Same<>` assertion, so "these must
|
|
29
|
+
// agree" is a TypeScript fact rather than a claim of this file. For member-free
|
|
30
|
+
// callable shapes the absolute answers come from typia's published function-type
|
|
31
|
+
// contract (default options skip a function slot, `functional: true` requires a
|
|
32
|
+
// real function). For member-carrying shapes the interface spelling is the
|
|
33
|
+
// oracle, because #2250 merged it as the reviewed boundary and #2238 declares
|
|
34
|
+
// hybrid callables an explicit negative boundary whose declared members must not
|
|
35
|
+
// be erased; the anchors below therefore also pin one value that the structural
|
|
36
|
+
// path must still accept, so a collapse of every hybrid answer to `false` cannot
|
|
37
|
+
// masquerade as parity.
|
|
38
|
+
//
|
|
39
|
+
// 1. Compile one project that spells fourteen callable shapes as an interface,
|
|
40
|
+
// as a named type literal, as an anonymous literal at the call site, and —
|
|
41
|
+
// where a plain function twin exists — as a function-type alias, each across
|
|
42
|
+
// six positions, both call forms, and both transform options.
|
|
43
|
+
// 2. Execute every spelling group against one shared value set and require the
|
|
44
|
+
// spellings to agree, then pin absolute anchors the parity assertions cannot
|
|
45
|
+
// see.
|
|
46
|
+
// 3. Repeat the call-only and construct-only pairs across local, re-exported,
|
|
47
|
+
// and ambient declaration provenance.
|
|
48
|
+
// 4. Require equivalent spellings to share one support-or-diagnostic contract,
|
|
49
|
+
// compiling each shape alone so a diagnostic count attributes to one pair.
|
|
50
|
+
func TestCallableTypeLiteralBoundariesTransform(t *testing.T) {
|
|
51
|
+
failures := []string{}
|
|
52
|
+
// Each matrix gets its own project so one transform never has to compile the
|
|
53
|
+
// other's fixture, and so a diagnostic can never be attributed to the wrong
|
|
54
|
+
// file.
|
|
55
|
+
spellingProject := callableTypeLiteralProject(t, "spelling", map[string]string{
|
|
56
|
+
"src/main.ts": callableTypeLiteralSpellingSource(),
|
|
57
|
+
})
|
|
58
|
+
provenanceProject := callableTypeLiteralProject(t, "provenance", map[string]string{
|
|
59
|
+
"src/main.ts": callableTypeLiteralProvenanceSource,
|
|
60
|
+
"src/reexport.ts": callableTypeLiteralReexportSource,
|
|
61
|
+
"src/ambient.d.ts": callableTypeLiteralAmbientSource,
|
|
62
|
+
})
|
|
63
|
+
// Both fixtures state their premise as compiled `Same<>` assertions, and the
|
|
64
|
+
// transform host reports typia's diagnostics rather than the checker's, so
|
|
65
|
+
// they are typechecked before anything is concluded from them.
|
|
66
|
+
ttscTypiaTestTypecheck(t, spellingProject)
|
|
67
|
+
ttscTypiaTestTypecheck(t, provenanceProject)
|
|
68
|
+
|
|
69
|
+
emits := map[string]string{}
|
|
70
|
+
for _, mode := range callableTypeLiteralModes() {
|
|
71
|
+
emits[mode.Name] = callableTypeLiteralTransform(t, spellingProject, "src/main.ts", mode.Functional)
|
|
72
|
+
}
|
|
73
|
+
output, err := callableTypeLiteralRun(
|
|
74
|
+
t,
|
|
75
|
+
spellingProject,
|
|
76
|
+
"spelling-runtime",
|
|
77
|
+
callableTypeLiteralSpellingRunner(),
|
|
78
|
+
map[string]string{
|
|
79
|
+
"default.cjs": ttscTypiaTestRewriteCommonJS(t, emits["default"]),
|
|
80
|
+
"functional.cjs": ttscTypiaTestRewriteCommonJS(t, emits["functional"]),
|
|
81
|
+
},
|
|
82
|
+
)
|
|
83
|
+
if err != nil {
|
|
84
|
+
failures = append(failures, fmt.Sprintf("spelling runtime matrix failed: %v\n%s", err, output))
|
|
85
|
+
}
|
|
86
|
+
for _, expected := range []string{
|
|
87
|
+
// 2 modes x 14 shapes x 6 positions x 2 forms x 12 values.
|
|
88
|
+
"RAN 4032 SPELLING GROUPS",
|
|
89
|
+
// 2 modes x 2 member-spelling pairs x 6 positions x 2 forms x 12 values x
|
|
90
|
+
// 3 declaration spellings.
|
|
91
|
+
"RAN 1728 MEMBER PAIRS",
|
|
92
|
+
// 43 anchor rows x 3 declaration spellings.
|
|
93
|
+
"RAN 129 ANCHORS",
|
|
94
|
+
// 4 optional-member states x 2 modes.
|
|
95
|
+
"RAN 8 OPTIONAL MEMBER ROWS",
|
|
96
|
+
} {
|
|
97
|
+
if !strings.Contains(output, expected) {
|
|
98
|
+
failures = append(failures, fmt.Sprintf("spelling runner did not report %q; got:\n%s", expected, output))
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
provenanceEmits := map[string]string{}
|
|
103
|
+
for _, mode := range callableTypeLiteralModes() {
|
|
104
|
+
provenanceEmits[mode.Name] = callableTypeLiteralTransform(t, provenanceProject, "src/main.ts", mode.Functional)
|
|
105
|
+
}
|
|
106
|
+
// The provenance fixture declares nothing but member-free call and construct
|
|
107
|
+
// signatures, so the failure is legible in its emit alone: TypeScript augments
|
|
108
|
+
// a callable object's apparent members with the global `Function` members, and
|
|
109
|
+
// these fragments appear exactly when a signature-only shape was expanded as a
|
|
110
|
+
// structural object instead. Reading the emit stops the runtime matrix from
|
|
111
|
+
// passing on a coincidence, and it is asserted here rather than on the
|
|
112
|
+
// spelling fixture because that fixture deliberately contains member-carrying
|
|
113
|
+
// shapes, for which the same fragments are correct.
|
|
114
|
+
for _, mode := range callableTypeLiteralModes() {
|
|
115
|
+
for _, fragment := range []string{"input.apply", "input.bind", "input.caller", "input.prototype"} {
|
|
116
|
+
if strings.Contains(provenanceEmits[mode.Name], fragment) {
|
|
117
|
+
failures = append(failures, fmt.Sprintf(
|
|
118
|
+
"%s provenance emit expanded a member-free callable through apparent Function member %q:\n%s",
|
|
119
|
+
mode.Name, fragment, provenanceEmits[mode.Name]))
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
provenanceOutput, provenanceErr := callableTypeLiteralRun(
|
|
124
|
+
t,
|
|
125
|
+
provenanceProject,
|
|
126
|
+
"provenance-runtime",
|
|
127
|
+
callableTypeLiteralProvenanceRunner,
|
|
128
|
+
map[string]string{
|
|
129
|
+
"default.cjs": ttscTypiaTestRewriteCommonJS(t, provenanceEmits["default"]),
|
|
130
|
+
"functional.cjs": ttscTypiaTestRewriteCommonJS(t, provenanceEmits["functional"]),
|
|
131
|
+
},
|
|
132
|
+
)
|
|
133
|
+
if provenanceErr != nil {
|
|
134
|
+
failures = append(failures, fmt.Sprintf("provenance runtime matrix failed: %v\n%s", provenanceErr, provenanceOutput))
|
|
135
|
+
}
|
|
136
|
+
// 3 provenances x 2 kinds x 3 spellings x 2 forms x 2 modes x 2 values.
|
|
137
|
+
if expected := "RAN 144 PROVENANCE CALLS"; !strings.Contains(provenanceOutput, expected) {
|
|
138
|
+
failures = append(failures, fmt.Sprintf("provenance runner did not report %q; got:\n%s", expected, provenanceOutput))
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
failures = append(failures, callableTypeLiteralConsumerFamilies(t)...)
|
|
142
|
+
failures = append(failures, callableTypeLiteralDiagnosticTwins(t)...)
|
|
143
|
+
|
|
144
|
+
if len(failures) != 0 {
|
|
145
|
+
t.Fatalf(
|
|
146
|
+
"callable declaration-spelling mismatches:\n%s\n\ndefault emit:\n%s\n\nfunctional emit:\n%s",
|
|
147
|
+
strings.Join(failures, "\n\n"),
|
|
148
|
+
emits["default"],
|
|
149
|
+
emits["functional"],
|
|
150
|
+
)
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
func callableTypeLiteralModes() []struct {
|
|
155
|
+
Name string
|
|
156
|
+
Functional bool
|
|
157
|
+
} {
|
|
158
|
+
return []struct {
|
|
159
|
+
Name string
|
|
160
|
+
Functional bool
|
|
161
|
+
}{
|
|
162
|
+
{Name: "default", Functional: false},
|
|
163
|
+
{Name: "functional", Functional: true},
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// callableTypeLiteralShape is one callable object shape under test. Body is the
|
|
168
|
+
// object-type body shared verbatim by the interface, named-literal, and inline
|
|
169
|
+
// spellings, which is the point of the fixture: only the declaration keyword
|
|
170
|
+
// differs between them.
|
|
171
|
+
type callableTypeLiteralShape struct {
|
|
172
|
+
Name string
|
|
173
|
+
Body string
|
|
174
|
+
// Alias is the plain function-type spelling of the same type, empty when the
|
|
175
|
+
// shape carries members and therefore has no function twin — its only alias
|
|
176
|
+
// spelling would be an intersection, which typia's intersection contract owns
|
|
177
|
+
// rather than this declaration-identity boundary.
|
|
178
|
+
Alias string
|
|
179
|
+
// IntersectionAlias is that intersection spelling. It is mutually assignable
|
|
180
|
+
// with the declaration spellings, so it is compiled and executed as evidence
|
|
181
|
+
// of where the declaration-identity contract legitimately hands over to the
|
|
182
|
+
// intersection contract, and never as a validator this change has to move.
|
|
183
|
+
IntersectionAlias string
|
|
184
|
+
// MemberTwin names the shape that is the same TypeScript type written with the
|
|
185
|
+
// other member spelling (`method(): void` against `method: () => void`).
|
|
186
|
+
MemberTwin string
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// callableTypeLiteralShapes lists every callable shape whose declaration
|
|
190
|
+
// spelling must not matter. The list is deliberately wider than the call-only
|
|
191
|
+
// and construct-only pair #2238 reproduces, because the members typia treats as
|
|
192
|
+
// unchecked are where the spellings historically disagreed, and because a fix
|
|
193
|
+
// that only looked at signature-only shapes has to be shown not to swallow the
|
|
194
|
+
// member-carrying ones.
|
|
195
|
+
func callableTypeLiteralShapes() []callableTypeLiteralShape {
|
|
196
|
+
call := "((value: number) => string)"
|
|
197
|
+
construct := "(new (value: number) => { value: number })"
|
|
198
|
+
return []callableTypeLiteralShape{
|
|
199
|
+
{Name: "PureCall", Body: "{ (value: number): string }", Alias: call},
|
|
200
|
+
{Name: "PureConstruct", Body: "{ new (value: number): { value: number } }", Alias: construct},
|
|
201
|
+
{Name: "Overload", Body: "{ (value: number): string; (value: string): number }",
|
|
202
|
+
IntersectionAlias: call + " & ((value: string) => number)"},
|
|
203
|
+
{Name: "ConstructOverload", Body: "{ new (value: number): { value: number }; new (value: string): { value: string } }",
|
|
204
|
+
IntersectionAlias: construct + " & (new (value: string) => { value: string })"},
|
|
205
|
+
{Name: "CallAndConstruct", Body: "{ (value: number): string; new (value: number): { value: number } }",
|
|
206
|
+
IntersectionAlias: call + " & " + construct},
|
|
207
|
+
{Name: "Method", Body: "{ (value: number): string; method(): void }", MemberTwin: "MethodProperty",
|
|
208
|
+
IntersectionAlias: call + " & { method(): void }"},
|
|
209
|
+
{Name: "MethodProperty", Body: "{ (value: number): string; method: () => void }", MemberTwin: "Method",
|
|
210
|
+
IntersectionAlias: call + " & { method: () => void }"},
|
|
211
|
+
{Name: "OptionalMethod", Body: "{ (value: number): string; method?(): void }", MemberTwin: "OptionalMethodProperty",
|
|
212
|
+
IntersectionAlias: call + " & { method?(): void }"},
|
|
213
|
+
{Name: "OptionalMethodProperty", Body: "{ (value: number): string; method?: () => void }", MemberTwin: "OptionalMethod",
|
|
214
|
+
IntersectionAlias: call + " & { method?: () => void }"},
|
|
215
|
+
{Name: "SymbolMember", Body: "{ (value: number): string; [callableBrand]: string }",
|
|
216
|
+
IntersectionAlias: call + " & { [callableBrand]: string }"},
|
|
217
|
+
{Name: "OptionalSymbolMember", Body: "{ (value: number): string; readonly [callableBrand]?: never }",
|
|
218
|
+
IntersectionAlias: call + " & { readonly [callableBrand]?: never }"},
|
|
219
|
+
{Name: "OptionalMember", Body: "{ (value: number): string; label?: string }",
|
|
220
|
+
IntersectionAlias: call + " & { label?: string }"},
|
|
221
|
+
{Name: "RequiredMember", Body: "{ (value: number): string; label: string }",
|
|
222
|
+
IntersectionAlias: call + " & { label: string }"},
|
|
223
|
+
{Name: "IndexSignature", Body: "{ (value: number): string; [key: string]: unknown }",
|
|
224
|
+
IntersectionAlias: call + " & { [key: string]: unknown }"},
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// callableTypeLiteralPositions places the shape where a validator can meet it.
|
|
229
|
+
// Top level and a nested property are the two the issue reproduces; the optional
|
|
230
|
+
// property, union arm, generic instantiation, and semantically empty
|
|
231
|
+
// intersection are the neighboring routes that reach the same classification
|
|
232
|
+
// through a different iterator, and each of them once produced a different
|
|
233
|
+
// answer for a function type than for an object type.
|
|
234
|
+
func callableTypeLiteralPositions() []struct {
|
|
235
|
+
Name string
|
|
236
|
+
Build func(target string) string
|
|
237
|
+
} {
|
|
238
|
+
return []struct {
|
|
239
|
+
Name string
|
|
240
|
+
Build func(target string) string
|
|
241
|
+
}{
|
|
242
|
+
{Name: "Top", Build: func(target string) string { return target }},
|
|
243
|
+
{Name: "Nested", Build: func(target string) string { return "{ fn: " + target + " }" }},
|
|
244
|
+
{Name: "OptionalProperty", Build: func(target string) string { return "{ fn?: " + target + " }" }},
|
|
245
|
+
{Name: "UnionArm", Build: func(target string) string { return "(" + target + `) | { kind: "data"; value: number }` }},
|
|
246
|
+
{Name: "Generic", Build: func(target string) string { return "CallableWrapper<" + target + ">" }},
|
|
247
|
+
{Name: "EmptyIntersection", Build: func(target string) string { return "(" + target + ") & Record<never, never>" }},
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// callableTypeLiteralSpellings names the declaration spellings of one shape.
|
|
252
|
+
// `Inline` has no declaration at all: the object-type body is written straight
|
|
253
|
+
// into the type argument, which is the only form that never reaches the alias
|
|
254
|
+
// iterator, so leaving it out would let a fix that only handled named aliases
|
|
255
|
+
// pass.
|
|
256
|
+
func callableTypeLiteralSpellings() []string {
|
|
257
|
+
return []string{"Interface", "Literal", "Inline", "Alias"}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
func callableTypeLiteralTarget(shape callableTypeLiteralShape, spelling string) string {
|
|
261
|
+
switch spelling {
|
|
262
|
+
case "Interface", "Literal":
|
|
263
|
+
return shape.Name + spelling
|
|
264
|
+
case "Inline":
|
|
265
|
+
return shape.Body
|
|
266
|
+
case "Alias":
|
|
267
|
+
return shape.Name + "Alias"
|
|
268
|
+
}
|
|
269
|
+
return ""
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
func callableTypeLiteralSpellingSource() string {
|
|
273
|
+
builder := &strings.Builder{}
|
|
274
|
+
builder.WriteString(`import typia from "typia";
|
|
275
|
+
|
|
276
|
+
declare const callableBrand: unique symbol;
|
|
277
|
+
type CallableWrapper<T> = { payload: T };
|
|
278
|
+
type Assert<T extends true> = T;
|
|
279
|
+
type Same<X, Y> = [X] extends [Y] ? ([Y] extends [X] ? true : false) : false;
|
|
280
|
+
|
|
281
|
+
`)
|
|
282
|
+
for _, shape := range callableTypeLiteralShapes() {
|
|
283
|
+
builder.WriteString("interface " + shape.Name + "Interface " + shape.Body + "\n")
|
|
284
|
+
builder.WriteString("type " + shape.Name + "Literal = " + shape.Body + ";\n")
|
|
285
|
+
if shape.Alias != "" {
|
|
286
|
+
builder.WriteString("type " + shape.Name + "Alias = " + shape.Alias + ";\n")
|
|
287
|
+
}
|
|
288
|
+
// The declaration axis, proven by the compiler rather than asserted here.
|
|
289
|
+
// These assertions are only load-bearing because the fixture is typechecked:
|
|
290
|
+
// the transform host reports typia's own diagnostics, not the checker's, so
|
|
291
|
+
// an unchecked fixture would let a false `Same<>` pass unnoticed and turn
|
|
292
|
+
// every parity claim below into a claim about two unrelated types.
|
|
293
|
+
builder.WriteString("type _" + shape.Name + "LiteralTwin = Assert<Same<" + shape.Name + "Interface, " + shape.Name + "Literal>>;\n")
|
|
294
|
+
builder.WriteString("type _" + shape.Name + "InlineTwin = Assert<Same<" + shape.Name + "Interface, " + shape.Body + ">>;\n")
|
|
295
|
+
if shape.Alias != "" {
|
|
296
|
+
builder.WriteString("type _" + shape.Name + "AliasTwin = Assert<Same<" + shape.Name + "Interface, " + shape.Name + "Alias>>;\n")
|
|
297
|
+
}
|
|
298
|
+
if shape.IntersectionAlias != "" {
|
|
299
|
+
// Type-level only: the intersection spelling is the declared hand-over
|
|
300
|
+
// point to typia's intersection contract, so it is proven equivalent here
|
|
301
|
+
// and exercised as a diagnostic, never as a validator.
|
|
302
|
+
builder.WriteString("type _" + shape.Name + "IntersectionTwin = Assert<Same<" + shape.Name + "Interface, " + shape.IntersectionAlias + ">>;\n")
|
|
303
|
+
}
|
|
304
|
+
if shape.MemberTwin != "" {
|
|
305
|
+
// The member axis: a method shorthand against a function-typed property.
|
|
306
|
+
builder.WriteString("type _" + shape.Name + "MemberTwin = Assert<Same<" + shape.Name + "Interface, " + shape.MemberTwin + "Interface>>;\n")
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
builder.WriteString("\n")
|
|
310
|
+
for _, shape := range callableTypeLiteralShapes() {
|
|
311
|
+
for _, spelling := range callableTypeLiteralSpellings() {
|
|
312
|
+
if spelling == "Alias" && shape.Alias == "" {
|
|
313
|
+
continue
|
|
314
|
+
}
|
|
315
|
+
for _, position := range callableTypeLiteralPositions() {
|
|
316
|
+
target := position.Build(callableTypeLiteralTarget(shape, spelling))
|
|
317
|
+
name := shape.Name + spelling + position.Name
|
|
318
|
+
builder.WriteString("export const direct" + name + " = (input: unknown): boolean => typia.is<" + target + ">(input);\n")
|
|
319
|
+
builder.WriteString("export const factory" + name + " = typia.createIs<" + target + ">();\n")
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
return builder.String()
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// callableTypeLiteralSpellingRunner builds the runner from the same tables the
|
|
327
|
+
// fixture is generated from, so a shape, position, or spelling added to one can
|
|
328
|
+
// never go unexercised by the other.
|
|
329
|
+
func callableTypeLiteralSpellingRunner() string {
|
|
330
|
+
shapes := []string{}
|
|
331
|
+
aliased := []string{}
|
|
332
|
+
pairs := []string{}
|
|
333
|
+
positions := []string{}
|
|
334
|
+
for _, shape := range callableTypeLiteralShapes() {
|
|
335
|
+
shapes = append(shapes, `"`+shape.Name+`"`)
|
|
336
|
+
if shape.Alias != "" {
|
|
337
|
+
aliased = append(aliased, `"`+shape.Name+`"`)
|
|
338
|
+
}
|
|
339
|
+
if shape.MemberTwin != "" && shape.Name < shape.MemberTwin {
|
|
340
|
+
pairs = append(pairs, `["`+shape.Name+`", "`+shape.MemberTwin+`"]`)
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
for _, position := range callableTypeLiteralPositions() {
|
|
344
|
+
positions = append(positions, `"`+position.Name+`"`)
|
|
345
|
+
}
|
|
346
|
+
runner := callableTypeLiteralSpellingRunnerTemplate
|
|
347
|
+
for _, replacement := range []struct {
|
|
348
|
+
Placeholder string
|
|
349
|
+
Value string
|
|
350
|
+
}{
|
|
351
|
+
{Placeholder: "__SHAPES__", Value: strings.Join(shapes, ", ")},
|
|
352
|
+
{Placeholder: "__ALIASED__", Value: strings.Join(aliased, ", ")},
|
|
353
|
+
{Placeholder: "__MEMBER_PAIRS__", Value: strings.Join(pairs, ", ")},
|
|
354
|
+
{Placeholder: "__POSITIONS__", Value: strings.Join(positions, ", ")},
|
|
355
|
+
} {
|
|
356
|
+
runner = strings.Replace(runner, replacement.Placeholder, replacement.Value, 1)
|
|
357
|
+
}
|
|
358
|
+
return runner
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
func callableTypeLiteralProject(t *testing.T, name string, files map[string]string) string {
|
|
362
|
+
t.Helper()
|
|
363
|
+
root := ttscTypiaTestRepoRoot(t)
|
|
364
|
+
base := filepath.Join(root, "packages", "typia", "native", ".tmp-ttsc-typia-tests")
|
|
365
|
+
if err := os.MkdirAll(base, 0o755); err != nil {
|
|
366
|
+
t.Fatalf("mkdir temp base: %v", err)
|
|
367
|
+
}
|
|
368
|
+
dir, err := os.MkdirTemp(base, "callable-type-literal-"+name+"-")
|
|
369
|
+
if err != nil {
|
|
370
|
+
t.Fatalf("create temp fixture: %v", err)
|
|
371
|
+
}
|
|
372
|
+
t.Cleanup(func() { _ = os.RemoveAll(dir) })
|
|
373
|
+
if err := os.MkdirAll(filepath.Join(dir, "src"), 0o755); err != nil {
|
|
374
|
+
t.Fatalf("mkdir fixture src: %v", err)
|
|
375
|
+
}
|
|
376
|
+
contents := map[string]string{"tsconfig.json": compareEqualCoverTSConfig}
|
|
377
|
+
for file, content := range files {
|
|
378
|
+
contents[file] = content
|
|
379
|
+
}
|
|
380
|
+
for file, content := range contents {
|
|
381
|
+
if err := os.WriteFile(filepath.Join(dir, filepath.FromSlash(file)), []byte(content), 0o644); err != nil {
|
|
382
|
+
t.Fatalf("write fixture file %s: %v", file, err)
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
return dir
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
func callableTypeLiteralTransform(t *testing.T, project string, file string, functional bool) string {
|
|
389
|
+
t.Helper()
|
|
390
|
+
option := ""
|
|
391
|
+
if functional {
|
|
392
|
+
option = `,"functional":true`
|
|
393
|
+
}
|
|
394
|
+
payload := `[{"config":{"transform":"typia/lib/transform"` + option + `},"name":"typia","stage":"transform"}]`
|
|
395
|
+
out, errText, code := ttscTypiaTestCapture(func() int {
|
|
396
|
+
return runTransform([]string{
|
|
397
|
+
"--cwd", project,
|
|
398
|
+
"--tsconfig", "tsconfig.json",
|
|
399
|
+
"--file", file,
|
|
400
|
+
"--output", "js",
|
|
401
|
+
"--plugins-json", payload,
|
|
402
|
+
})
|
|
403
|
+
})
|
|
404
|
+
if code != 0 {
|
|
405
|
+
t.Fatalf("callable declaration transform %s (functional=%t) failed: code=%d stdout=\n%s\nstderr=\n%s", file, functional, code, out, errText)
|
|
406
|
+
}
|
|
407
|
+
return out
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
func callableTypeLiteralRun(
|
|
411
|
+
t *testing.T,
|
|
412
|
+
project string,
|
|
413
|
+
name string,
|
|
414
|
+
runner string,
|
|
415
|
+
modules map[string]string,
|
|
416
|
+
) (string, error) {
|
|
417
|
+
t.Helper()
|
|
418
|
+
node, err := exec.LookPath("node")
|
|
419
|
+
if err != nil {
|
|
420
|
+
t.Skip("node executable not found")
|
|
421
|
+
return "", nil
|
|
422
|
+
}
|
|
423
|
+
runtimeDir := filepath.Join(project, name)
|
|
424
|
+
if err := os.MkdirAll(runtimeDir, 0o755); err != nil {
|
|
425
|
+
t.Fatalf("mkdir %s: %v", name, err)
|
|
426
|
+
}
|
|
427
|
+
ttscTypiaTestWriteCommonRuntimeStubs(t, runtimeDir)
|
|
428
|
+
files := map[string]string{"run.cjs": runner}
|
|
429
|
+
for file, content := range modules {
|
|
430
|
+
files[file] = content
|
|
431
|
+
}
|
|
432
|
+
for file, content := range files {
|
|
433
|
+
if err := os.WriteFile(filepath.Join(runtimeDir, file), []byte(content), 0o644); err != nil {
|
|
434
|
+
t.Fatalf("write runtime file %s: %v", file, err)
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
cmd := exec.Command(node, filepath.Join(runtimeDir, "run.cjs"))
|
|
438
|
+
cmd.Dir = runtimeDir
|
|
439
|
+
output, err := cmd.CombinedOutput()
|
|
440
|
+
return string(output), err
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// callableTypeLiteralDiagnosticTwins requires equivalent spellings to share one
|
|
444
|
+
// support-or-diagnostic contract, and pins where that contract legitimately
|
|
445
|
+
// stops being a declaration-identity question.
|
|
446
|
+
//
|
|
447
|
+
// Isolation is load-bearing. The transform reports diagnostics per file, so
|
|
448
|
+
// several shapes in one project collapse into a count that no longer says which
|
|
449
|
+
// pair produced which message, and a shape that stopped erroring cancels out a
|
|
450
|
+
// shape that started erroring twice. One project per shape per spelling keeps
|
|
451
|
+
// every count attributable and keeps a failing sibling from contaminating a
|
|
452
|
+
// spelling that must transform.
|
|
453
|
+
//
|
|
454
|
+
// The interface, named-literal, and inline spellings of one shape are the same
|
|
455
|
+
// declaration written three ways, so they must always agree. A member-carrying
|
|
456
|
+
// shape's only function-type spelling is an intersection, and typia rejects an
|
|
457
|
+
// intersection of runtime callability with a further obligation; that rejection
|
|
458
|
+
// belongs to typia's intersection contract, not to declaration identity, so it
|
|
459
|
+
// is recorded as the expected boundary rather than silently generalized.
|
|
460
|
+
func callableTypeLiteralDiagnosticTwins(t *testing.T) []string {
|
|
461
|
+
t.Helper()
|
|
462
|
+
failures := []string{}
|
|
463
|
+
for _, shape := range callableTypeLiteralShapes() {
|
|
464
|
+
observed := map[string]string{}
|
|
465
|
+
functionSpelling := shape.Alias
|
|
466
|
+
if functionSpelling == "" {
|
|
467
|
+
functionSpelling = shape.IntersectionAlias
|
|
468
|
+
}
|
|
469
|
+
for _, spelling := range callableTypeLiteralSpellings() {
|
|
470
|
+
declaration := ""
|
|
471
|
+
target := "Target"
|
|
472
|
+
switch spelling {
|
|
473
|
+
case "Interface":
|
|
474
|
+
declaration = "interface Target " + shape.Body + "\n"
|
|
475
|
+
case "Literal":
|
|
476
|
+
declaration = "type Target = " + shape.Body + ";\n"
|
|
477
|
+
case "Inline":
|
|
478
|
+
target = shape.Body
|
|
479
|
+
case "Alias":
|
|
480
|
+
declaration = "type Target = " + functionSpelling + ";\n"
|
|
481
|
+
}
|
|
482
|
+
// The equivalence of these spellings is established once, by typechecking
|
|
483
|
+
// the spelling fixture, rather than restated in every one of these
|
|
484
|
+
// single-shape projects.
|
|
485
|
+
source := "import typia from \"typia\";\n\ndeclare const callableBrand: unique symbol;\n" + declaration +
|
|
486
|
+
"export const direct = (input: unknown): boolean => typia.is<" + target + ">(input);\n" +
|
|
487
|
+
"export const factory = typia.createIs<" + target + ">();\n"
|
|
488
|
+
project := callableTypeLiteralProject(
|
|
489
|
+
t,
|
|
490
|
+
"twin-"+strings.ToLower(shape.Name)+"-"+strings.ToLower(spelling),
|
|
491
|
+
map[string]string{"src/main.ts": source},
|
|
492
|
+
)
|
|
493
|
+
_, errText, code := ttscTypiaTestCapture(func() int {
|
|
494
|
+
return runTransform([]string{
|
|
495
|
+
"--cwd", project,
|
|
496
|
+
"--tsconfig", "tsconfig.json",
|
|
497
|
+
"--file", "src/main.ts",
|
|
498
|
+
"--output", "js",
|
|
499
|
+
})
|
|
500
|
+
})
|
|
501
|
+
observed[spelling] = fmt.Sprintf("code=%d nonsensible=%d unsupported=%d",
|
|
502
|
+
code,
|
|
503
|
+
strings.Count(errText, "nonsensible intersection"),
|
|
504
|
+
strings.Count(errText, "does not support"),
|
|
505
|
+
)
|
|
506
|
+
}
|
|
507
|
+
for _, spelling := range []string{"Literal", "Inline"} {
|
|
508
|
+
if observed[spelling] != observed["Interface"] {
|
|
509
|
+
failures = append(failures, fmt.Sprintf(
|
|
510
|
+
"%s: %s spelling reported %s but the interface spelling reported %s",
|
|
511
|
+
shape.Name, spelling, observed[spelling], observed["Interface"]))
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
const supported = "code=0 nonsensible=0 unsupported=0"
|
|
515
|
+
if observed["Interface"] != supported {
|
|
516
|
+
failures = append(failures, fmt.Sprintf(
|
|
517
|
+
"%s: interface spelling reported %s; expected %s", shape.Name, observed["Interface"], supported))
|
|
518
|
+
}
|
|
519
|
+
// A member-free shape's function-type spelling is a plain function type, so
|
|
520
|
+
// it is inside this change's contract and must agree with the declaration
|
|
521
|
+
// spellings.
|
|
522
|
+
if shape.Alias != "" {
|
|
523
|
+
if observed["Alias"] != observed["Interface"] {
|
|
524
|
+
failures = append(failures, fmt.Sprintf(
|
|
525
|
+
"%s: alias spelling reported %s but the interface spelling reported %s",
|
|
526
|
+
shape.Name, observed["Alias"], observed["Interface"]))
|
|
527
|
+
}
|
|
528
|
+
continue
|
|
529
|
+
}
|
|
530
|
+
// Every function-type spelling must report what the declaration spellings
|
|
531
|
+
// report, including a member-carrying shape's, whose only such spelling is an
|
|
532
|
+
// intersection of a call signature with the rest.
|
|
533
|
+
//
|
|
534
|
+
// This row is what samchon/typia#2276 closed. Before it, six of the twelve
|
|
535
|
+
// shapes disagreed here — `{ method(): void }` was accepted while
|
|
536
|
+
// `{ method: () => void }` was refused, and `{ label?: string }` was accepted
|
|
537
|
+
// while `{ label: string }` was refused — because a call-signature-only arm
|
|
538
|
+
// was neither an object nor a phantom brand, so the intersection was called
|
|
539
|
+
// nonsensible while the interface spelling of the same type, which is one
|
|
540
|
+
// object carrying a call signature, was accepted.
|
|
541
|
+
if observed["Alias"] != observed["Interface"] {
|
|
542
|
+
failures = append(failures, fmt.Sprintf(
|
|
543
|
+
"%s: alias spelling reported %s but the interface spelling reported %s",
|
|
544
|
+
shape.Name, observed["Alias"], observed["Interface"]))
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
return failures
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// callableTypeLiteralConsumerFamilies checks the invariant where it actually
|
|
551
|
+
// reaches users. `metadata_get_function_node` feeds MetadataFactory.Analyze, and
|
|
552
|
+
// every typia operation reads the same metadata, so a declaration-spelling split
|
|
553
|
+
// is not an `is` bug: it changes what `random` generates, what `plain.clone`
|
|
554
|
+
// copies, what `json.schema` and `llm.application` publish, and which
|
|
555
|
+
// diagnostics `compare` and `protobuf` report.
|
|
556
|
+
//
|
|
557
|
+
// The three spellings are compiled in separate projects under one shared type
|
|
558
|
+
// name, so their emits must be byte-identical rather than merely equivalent.
|
|
559
|
+
// That is the strongest oracle available here: it cannot be satisfied by two
|
|
560
|
+
// different validators that happen to agree on the executed inputs, and it needs
|
|
561
|
+
// no expected-value table that could be written from the implementation's own
|
|
562
|
+
// output. The runtime rows then pin the answers the emit alone cannot: a
|
|
563
|
+
// function slot is dropped from JSON, cloned away, and ignored by equality.
|
|
564
|
+
func callableTypeLiteralConsumerFamilies(t *testing.T) []string {
|
|
565
|
+
t.Helper()
|
|
566
|
+
failures := []string{}
|
|
567
|
+
spellings := []struct {
|
|
568
|
+
Name string
|
|
569
|
+
Declaration string
|
|
570
|
+
}{
|
|
571
|
+
// One line each, so a diagnostic's reported position is comparable across
|
|
572
|
+
// the three projects rather than shifted by the declaration's own height.
|
|
573
|
+
{Name: "interface", Declaration: "interface Consumer { (input: { value: number }): { value: string } }\n"},
|
|
574
|
+
{Name: "literal", Declaration: "type Consumer = { (input: { value: number }): { value: string } };\n"},
|
|
575
|
+
{Name: "alias", Declaration: "type Consumer = (input: { value: number }) => { value: string };\n"},
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
emits := map[string]string{}
|
|
579
|
+
for _, spelling := range spellings {
|
|
580
|
+
project := callableTypeLiteralProject(t, "consumers-"+spelling.Name, map[string]string{
|
|
581
|
+
"src/main.ts": spelling.Declaration + callableTypeLiteralConsumerSource,
|
|
582
|
+
})
|
|
583
|
+
emits[spelling.Name] = callableTypeLiteralTransform(t, project, "src/main.ts", false)
|
|
584
|
+
}
|
|
585
|
+
for _, spelling := range []string{"literal", "alias"} {
|
|
586
|
+
if emits[spelling] != emits["interface"] {
|
|
587
|
+
failures = append(failures, fmt.Sprintf(
|
|
588
|
+
"consumer emits differ between the interface and %s spellings\n\ninterface:\n%s\n\n%s:\n%s",
|
|
589
|
+
spelling, emits["interface"], spelling, emits[spelling]))
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
runtimeProject := callableTypeLiteralProject(t, "consumers-runtime", nil)
|
|
594
|
+
modules := map[string]string{
|
|
595
|
+
"http-stub.cjs": callableTypeLiteralHTTPStub,
|
|
596
|
+
"llm-stub.cjs": callableTypeLiteralLLMStub,
|
|
597
|
+
}
|
|
598
|
+
for name, js := range emits {
|
|
599
|
+
modules[name+".cjs"] = ttscTypiaTestRewriteCommonJS(t, callableTypeLiteralStubInternals(js))
|
|
600
|
+
}
|
|
601
|
+
output, err := callableTypeLiteralRun(t, runtimeProject, "runtime", callableTypeLiteralConsumerRunner, modules)
|
|
602
|
+
if err != nil {
|
|
603
|
+
failures = append(failures, fmt.Sprintf("consumer-family runtime matrix failed: %v\n%s", err, output))
|
|
604
|
+
}
|
|
605
|
+
// 23 consumer families x 3 spellings, plus 6 anchor rows.
|
|
606
|
+
if expected := "RAN 69 CONSUMER FAMILIES 6 ANCHORS"; !strings.Contains(output, expected) {
|
|
607
|
+
failures = append(failures, fmt.Sprintf("consumer runner did not report %q; got:\n%s", expected, output))
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// The families whose contract is a rejection are checked by their exact
|
|
611
|
+
// diagnostic rather than by an exit code, and one isolated project per
|
|
612
|
+
// spelling keeps each message attributable.
|
|
613
|
+
rejections := map[string]string{}
|
|
614
|
+
for _, spelling := range spellings {
|
|
615
|
+
project := callableTypeLiteralProject(t, "consumer-rejections-"+spelling.Name, map[string]string{
|
|
616
|
+
"src/main.ts": spelling.Declaration + callableTypeLiteralConsumerRejectionSource,
|
|
617
|
+
})
|
|
618
|
+
_, errText, code := ttscTypiaTestCapture(func() int {
|
|
619
|
+
return runTransform([]string{
|
|
620
|
+
"--cwd", project,
|
|
621
|
+
"--tsconfig", "tsconfig.json",
|
|
622
|
+
"--file", "src/main.ts",
|
|
623
|
+
"--output", "js",
|
|
624
|
+
})
|
|
625
|
+
})
|
|
626
|
+
if code == 0 {
|
|
627
|
+
failures = append(failures, fmt.Sprintf(
|
|
628
|
+
"%s spelling: the unsupported-domain consumers transformed successfully", spelling.Name))
|
|
629
|
+
}
|
|
630
|
+
// Each spelling gets its own temporary project, so the only text that may
|
|
631
|
+
// legitimately differ between their diagnostics is that directory.
|
|
632
|
+
rejections[spelling.Name] = strings.ReplaceAll(
|
|
633
|
+
strings.ReplaceAll(errText, project, "<project>"),
|
|
634
|
+
strings.ReplaceAll(project, `\`, "/"), "<project>")
|
|
635
|
+
}
|
|
636
|
+
for _, spelling := range []string{"literal", "alias"} {
|
|
637
|
+
if rejections[spelling] != rejections["interface"] {
|
|
638
|
+
failures = append(failures, fmt.Sprintf(
|
|
639
|
+
"consumer diagnostics differ between the interface and %s spellings\n\ninterface:\n%s\n\n%s:\n%s",
|
|
640
|
+
spelling, rejections["interface"], spelling, rejections[spelling]))
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
return failures
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// callableTypeLiteralStubInternals redirects the shipped runtime helpers that
|
|
647
|
+
// only the consumer fixture pulls in. ttscTypiaTestRewriteCommonJS covers the
|
|
648
|
+
// shared ones and fails loudly on anything left unresolved, so these have to be
|
|
649
|
+
// replaced before it runs.
|
|
650
|
+
func callableTypeLiteralStubInternals(js string) string {
|
|
651
|
+
for _, replacement := range []struct {
|
|
652
|
+
Helper string
|
|
653
|
+
Stub string
|
|
654
|
+
}{
|
|
655
|
+
{Helper: "_httpFormDataReadString", Stub: "http-stub.cjs"},
|
|
656
|
+
{Helper: "_httpQueryReadString", Stub: "http-stub.cjs"},
|
|
657
|
+
{Helper: "_httpQueryParseURLSearchParams", Stub: "http-stub.cjs"},
|
|
658
|
+
{Helper: "_llmApplicationFinalize", Stub: "llm-stub.cjs"},
|
|
659
|
+
} {
|
|
660
|
+
js = strings.ReplaceAll(js, `require("typia/lib/internal/`+replacement.Helper+`")`, `require("./`+replacement.Stub+`")`)
|
|
661
|
+
}
|
|
662
|
+
return js
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
const callableTypeLiteralHTTPStub = `module.exports._httpFormDataReadString = (input) =>
|
|
666
|
+
input === null ? undefined : input === "null" ? null : input;
|
|
667
|
+
module.exports._httpQueryReadString = (input) =>
|
|
668
|
+
input === null ? undefined : input === "null" ? null : input;
|
|
669
|
+
module.exports._httpQueryParseURLSearchParams = (input) =>
|
|
670
|
+
typeof input === "string" ? new URLSearchParams(input) : input;
|
|
671
|
+
`
|
|
672
|
+
|
|
673
|
+
const callableTypeLiteralLLMStub = `module.exports._llmApplicationFinalize = (app, config) => ({
|
|
674
|
+
...app,
|
|
675
|
+
config: config ?? {},
|
|
676
|
+
functions: app.functions,
|
|
677
|
+
});
|
|
678
|
+
`
|
|
679
|
+
|
|
680
|
+
const callableTypeLiteralConsumerSource = `import typia from "typia";
|
|
681
|
+
|
|
682
|
+
interface ConsumerHolder {
|
|
683
|
+
fn: Consumer;
|
|
684
|
+
}
|
|
685
|
+
type ConsumerFunctional = (input: ConsumerHolder) => ConsumerHolder;
|
|
686
|
+
|
|
687
|
+
export const is = typia.createIs<ConsumerHolder>();
|
|
688
|
+
export const validate = typia.createValidate<ConsumerHolder>();
|
|
689
|
+
export const functional = typia.functional.isFunction<ConsumerFunctional>((input) => input);
|
|
690
|
+
export const random = typia.createRandom<ConsumerHolder>();
|
|
691
|
+
export const clone = typia.plain.createClone<ConsumerHolder>();
|
|
692
|
+
export const prune = typia.plain.createPrune<ConsumerHolder>();
|
|
693
|
+
export const classify = typia.plain.createClassify<ConsumerHolder>();
|
|
694
|
+
export const camel = typia.notations.createCamel<ConsumerHolder>();
|
|
695
|
+
export const pascal = typia.notations.createPascal<ConsumerHolder>();
|
|
696
|
+
export const equals = typia.compare.createEquals<ConsumerHolder>();
|
|
697
|
+
export const cover = typia.compare.createCover<ConsumerHolder>();
|
|
698
|
+
export const stringify = typia.json.createStringify<ConsumerHolder>();
|
|
699
|
+
export const jsonSchema = typia.json.schema<ConsumerHolder>();
|
|
700
|
+
export const jsonSchemas = typia.json.schemas<[ConsumerHolder]>();
|
|
701
|
+
export const jsonApplication = typia.json.application<ConsumerHolder>();
|
|
702
|
+
export const reflectSchema = typia.reflect.schema<ConsumerHolder>();
|
|
703
|
+
export const reflectSchemas = typia.reflect.schemas<[ConsumerHolder]>();
|
|
704
|
+
export const reflectName = typia.reflect.name<ConsumerHolder, true>();
|
|
705
|
+
export const llmSchema = typia.llm.schema<ConsumerHolder>({});
|
|
706
|
+
export const llmParameters = typia.llm.parameters<ConsumerHolder>();
|
|
707
|
+
export const httpFormData = typia.http.createFormData<ConsumerHolder>();
|
|
708
|
+
export const httpHeaders = typia.http.createHeaders<ConsumerHolder>();
|
|
709
|
+
export const httpQuery = typia.http.createQuery<ConsumerHolder>();
|
|
710
|
+
`
|
|
711
|
+
|
|
712
|
+
const callableTypeLiteralConsumerRejectionSource = `import typia from "typia";
|
|
713
|
+
|
|
714
|
+
interface RejectionHolder {
|
|
715
|
+
fn: Consumer;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
typia.compare.createEquals<Consumer>();
|
|
719
|
+
typia.protobuf.createEncode<RejectionHolder>();
|
|
720
|
+
typia.protobuf.createDecode<RejectionHolder>();
|
|
721
|
+
`
|
|
722
|
+
|
|
723
|
+
const callableTypeLiteralConsumerRunner = `const modules = {
|
|
724
|
+
interface: require("./interface.cjs"),
|
|
725
|
+
literal: require("./literal.cjs"),
|
|
726
|
+
alias: require("./alias.cjs"),
|
|
727
|
+
};
|
|
728
|
+
|
|
729
|
+
let families = 0;
|
|
730
|
+
let anchors = 0;
|
|
731
|
+
const failures = [];
|
|
732
|
+
const fn = (input) => ({ value: String(input.value) });
|
|
733
|
+
const other = (input) => ({ value: "other:" + input.value });
|
|
734
|
+
const holder = () => ({ fn });
|
|
735
|
+
|
|
736
|
+
// Each family is reduced to a JSON-comparable observation so three independent
|
|
737
|
+
// emits can be compared as behavior rather than as text.
|
|
738
|
+
const observations = {
|
|
739
|
+
is: (mod) => [mod.is(holder()), mod.is({}), mod.is({ fn: {} })],
|
|
740
|
+
validate: (mod) => [mod.validate(holder()).success, mod.validate({}).success],
|
|
741
|
+
functional: (mod) => [typeof mod.functional, Object.keys(mod.functional(holder()) || {})],
|
|
742
|
+
random: (mod) => Object.keys(mod.random()),
|
|
743
|
+
clone: (mod) => mod.clone(holder()),
|
|
744
|
+
prune: (mod) => { const value = holder(); mod.prune(value); return Object.keys(value); },
|
|
745
|
+
classify: (mod) => mod.classify(holder()),
|
|
746
|
+
camel: (mod) => mod.camel(holder()),
|
|
747
|
+
pascal: (mod) => mod.pascal(holder()),
|
|
748
|
+
equals: (mod) => [mod.equals(holder(), holder()), mod.equals({ fn }, { fn: other })],
|
|
749
|
+
cover: (mod) => [mod.cover(holder(), holder())],
|
|
750
|
+
stringify: (mod) => mod.stringify(holder()),
|
|
751
|
+
jsonSchema: (mod) => mod.jsonSchema,
|
|
752
|
+
jsonSchemas: (mod) => mod.jsonSchemas,
|
|
753
|
+
jsonApplication: (mod) => mod.jsonApplication,
|
|
754
|
+
reflectSchema: (mod) => mod.reflectSchema,
|
|
755
|
+
reflectSchemas: (mod) => mod.reflectSchemas,
|
|
756
|
+
reflectName: (mod) => mod.reflectName,
|
|
757
|
+
llmSchema: (mod) => mod.llmSchema,
|
|
758
|
+
llmParameters: (mod) => mod.llmParameters,
|
|
759
|
+
httpFormData: (mod) => {
|
|
760
|
+
const value = new FormData();
|
|
761
|
+
value.set("fn", "value");
|
|
762
|
+
return mod.httpFormData(value);
|
|
763
|
+
},
|
|
764
|
+
httpHeaders: (mod) => mod.httpHeaders(new Headers({ fn: "value" })),
|
|
765
|
+
httpQuery: (mod) => mod.httpQuery(new URLSearchParams({ fn: "value" })),
|
|
766
|
+
};
|
|
767
|
+
|
|
768
|
+
for (const [family, observe] of Object.entries(observations)) {
|
|
769
|
+
const answers = {};
|
|
770
|
+
for (const spelling of ["interface", "literal", "alias"]) {
|
|
771
|
+
families += 1;
|
|
772
|
+
try {
|
|
773
|
+
answers[spelling] = JSON.stringify(observe(modules[spelling]));
|
|
774
|
+
} catch (error) {
|
|
775
|
+
answers[spelling] = "threw " + String(error && error.message);
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
for (const spelling of ["literal", "alias"]) {
|
|
779
|
+
if (answers[spelling] !== answers.interface) {
|
|
780
|
+
failures.push(
|
|
781
|
+
"consumer " + family + ": interface=" + answers.interface + " " + spelling + "=" + answers[spelling]);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
// Parity cannot see a change that moves every spelling the same way, so these
|
|
787
|
+
// pin what a function slot means to the consumers that must act on it, taken
|
|
788
|
+
// from typia's published behavior for a function-typed member: it is not JSON
|
|
789
|
+
// data, it is not reconstructed by a plain copy, and it carries no identity that
|
|
790
|
+
// structural equality could compare.
|
|
791
|
+
const anchorRows = [
|
|
792
|
+
["is accepts a real function", (mod) => mod.is(holder()), true],
|
|
793
|
+
["stringify omits the function slot", (mod) => mod.stringify(holder()), "{}"],
|
|
794
|
+
["clone drops the function slot", (mod) => mod.clone(holder()).fn, undefined],
|
|
795
|
+
["classify drops the function slot", (mod) => mod.classify(holder()).fn, undefined],
|
|
796
|
+
["prune keeps the declared slot", (mod) => { const value = holder(); mod.prune(value); return value.fn; }, fn],
|
|
797
|
+
["equals ignores function identity", (mod) => mod.equals({ fn }, { fn: other }), true],
|
|
798
|
+
];
|
|
799
|
+
for (const [label, observe, expected] of anchorRows) {
|
|
800
|
+
anchors += 1;
|
|
801
|
+
const actual = observe(modules.interface);
|
|
802
|
+
if (actual !== expected) {
|
|
803
|
+
failures.push("consumer anchor " + label + ": expected " + expected + " but got " + actual);
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
console.log("RAN " + families + " CONSUMER FAMILIES " + anchors + " ANCHORS");
|
|
808
|
+
if (failures.length !== 0) throw new Error("MISMATCHES:\n" + failures.join("\n"));
|
|
809
|
+
`
|
|
810
|
+
|
|
811
|
+
const callableTypeLiteralReexportSource = `export type ReexportedCallLiteral = { (value: number): string };
|
|
812
|
+
export type ReexportedCallAlias = (value: number) => string;
|
|
813
|
+
export interface ReexportedCallInterface {
|
|
814
|
+
(value: number): string;
|
|
815
|
+
}
|
|
816
|
+
export type ReexportedConstructLiteral = { new (value: number): { value: number } };
|
|
817
|
+
export type ReexportedConstructAlias = new (value: number) => { value: number };
|
|
818
|
+
export interface ReexportedConstructInterface {
|
|
819
|
+
new (value: number): { value: number };
|
|
820
|
+
}
|
|
821
|
+
`
|
|
822
|
+
|
|
823
|
+
const callableTypeLiteralAmbientSource = `declare module "ambient-callable-declarations" {
|
|
824
|
+
export type AmbientCallLiteral = { (value: number): string };
|
|
825
|
+
export type AmbientCallAlias = (value: number) => string;
|
|
826
|
+
export interface AmbientCallInterface {
|
|
827
|
+
(value: number): string;
|
|
828
|
+
}
|
|
829
|
+
export type AmbientConstructLiteral = { new (value: number): { value: number } };
|
|
830
|
+
export type AmbientConstructAlias = new (value: number) => { value: number };
|
|
831
|
+
export interface AmbientConstructInterface {
|
|
832
|
+
new (value: number): { value: number };
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
`
|
|
836
|
+
|
|
837
|
+
const callableTypeLiteralProvenanceSource = `import typia from "typia";
|
|
838
|
+
import type {
|
|
839
|
+
ReexportedCallAlias,
|
|
840
|
+
ReexportedCallInterface,
|
|
841
|
+
ReexportedCallLiteral,
|
|
842
|
+
ReexportedConstructAlias,
|
|
843
|
+
ReexportedConstructInterface,
|
|
844
|
+
ReexportedConstructLiteral,
|
|
845
|
+
} from "./reexport";
|
|
846
|
+
import type {
|
|
847
|
+
AmbientCallAlias,
|
|
848
|
+
AmbientCallInterface,
|
|
849
|
+
AmbientCallLiteral,
|
|
850
|
+
AmbientConstructAlias,
|
|
851
|
+
AmbientConstructInterface,
|
|
852
|
+
AmbientConstructLiteral,
|
|
853
|
+
} from "ambient-callable-declarations";
|
|
854
|
+
|
|
855
|
+
type LocalCallLiteral = { (value: number): string };
|
|
856
|
+
type LocalCallAlias = (value: number) => string;
|
|
857
|
+
interface LocalCallInterface {
|
|
858
|
+
(value: number): string;
|
|
859
|
+
}
|
|
860
|
+
type LocalConstructLiteral = { new (value: number): { value: number } };
|
|
861
|
+
type LocalConstructAlias = new (value: number) => { value: number };
|
|
862
|
+
interface LocalConstructInterface {
|
|
863
|
+
new (value: number): { value: number };
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
type Assert<T extends true> = T;
|
|
867
|
+
type Same<X, Y> = [X] extends [Y] ? ([Y] extends [X] ? true : false) : false;
|
|
868
|
+
type _LocalCall = Assert<Same<LocalCallLiteral, LocalCallAlias>>;
|
|
869
|
+
type _LocalCallInterface = Assert<Same<LocalCallLiteral, LocalCallInterface>>;
|
|
870
|
+
type _LocalConstruct = Assert<Same<LocalConstructLiteral, LocalConstructAlias>>;
|
|
871
|
+
type _LocalConstructInterface = Assert<Same<LocalConstructLiteral, LocalConstructInterface>>;
|
|
872
|
+
type _ReexportedCall = Assert<Same<ReexportedCallLiteral, ReexportedCallAlias>>;
|
|
873
|
+
type _ReexportedCallInterface = Assert<Same<ReexportedCallLiteral, ReexportedCallInterface>>;
|
|
874
|
+
type _ReexportedConstruct = Assert<Same<ReexportedConstructLiteral, ReexportedConstructAlias>>;
|
|
875
|
+
type _ReexportedConstructInterface = Assert<Same<ReexportedConstructLiteral, ReexportedConstructInterface>>;
|
|
876
|
+
type _AmbientCall = Assert<Same<AmbientCallLiteral, AmbientCallAlias>>;
|
|
877
|
+
type _AmbientCallInterface = Assert<Same<AmbientCallLiteral, AmbientCallInterface>>;
|
|
878
|
+
type _AmbientConstruct = Assert<Same<AmbientConstructLiteral, AmbientConstructAlias>>;
|
|
879
|
+
type _AmbientConstructInterface = Assert<Same<AmbientConstructLiteral, AmbientConstructInterface>>;
|
|
880
|
+
|
|
881
|
+
export const directLocalCallLiteral = (input: unknown): boolean => typia.is<LocalCallLiteral>(input);
|
|
882
|
+
export const factoryLocalCallLiteral = typia.createIs<LocalCallLiteral>();
|
|
883
|
+
export const directLocalCallAlias = (input: unknown): boolean => typia.is<LocalCallAlias>(input);
|
|
884
|
+
export const factoryLocalCallAlias = typia.createIs<LocalCallAlias>();
|
|
885
|
+
export const directLocalCallInterface = (input: unknown): boolean => typia.is<LocalCallInterface>(input);
|
|
886
|
+
export const factoryLocalCallInterface = typia.createIs<LocalCallInterface>();
|
|
887
|
+
export const directLocalConstructLiteral = (input: unknown): boolean => typia.is<LocalConstructLiteral>(input);
|
|
888
|
+
export const factoryLocalConstructLiteral = typia.createIs<LocalConstructLiteral>();
|
|
889
|
+
export const directLocalConstructAlias = (input: unknown): boolean => typia.is<LocalConstructAlias>(input);
|
|
890
|
+
export const factoryLocalConstructAlias = typia.createIs<LocalConstructAlias>();
|
|
891
|
+
export const directLocalConstructInterface = (input: unknown): boolean => typia.is<LocalConstructInterface>(input);
|
|
892
|
+
export const factoryLocalConstructInterface = typia.createIs<LocalConstructInterface>();
|
|
893
|
+
|
|
894
|
+
export const directReexportedCallLiteral = (input: unknown): boolean => typia.is<ReexportedCallLiteral>(input);
|
|
895
|
+
export const factoryReexportedCallLiteral = typia.createIs<ReexportedCallLiteral>();
|
|
896
|
+
export const directReexportedCallAlias = (input: unknown): boolean => typia.is<ReexportedCallAlias>(input);
|
|
897
|
+
export const factoryReexportedCallAlias = typia.createIs<ReexportedCallAlias>();
|
|
898
|
+
export const directReexportedCallInterface = (input: unknown): boolean => typia.is<ReexportedCallInterface>(input);
|
|
899
|
+
export const factoryReexportedCallInterface = typia.createIs<ReexportedCallInterface>();
|
|
900
|
+
export const directReexportedConstructLiteral = (input: unknown): boolean => typia.is<ReexportedConstructLiteral>(input);
|
|
901
|
+
export const factoryReexportedConstructLiteral = typia.createIs<ReexportedConstructLiteral>();
|
|
902
|
+
export const directReexportedConstructAlias = (input: unknown): boolean => typia.is<ReexportedConstructAlias>(input);
|
|
903
|
+
export const factoryReexportedConstructAlias = typia.createIs<ReexportedConstructAlias>();
|
|
904
|
+
export const directReexportedConstructInterface = (input: unknown): boolean => typia.is<ReexportedConstructInterface>(input);
|
|
905
|
+
export const factoryReexportedConstructInterface = typia.createIs<ReexportedConstructInterface>();
|
|
906
|
+
|
|
907
|
+
export const directAmbientCallLiteral = (input: unknown): boolean => typia.is<AmbientCallLiteral>(input);
|
|
908
|
+
export const factoryAmbientCallLiteral = typia.createIs<AmbientCallLiteral>();
|
|
909
|
+
export const directAmbientCallAlias = (input: unknown): boolean => typia.is<AmbientCallAlias>(input);
|
|
910
|
+
export const factoryAmbientCallAlias = typia.createIs<AmbientCallAlias>();
|
|
911
|
+
export const directAmbientCallInterface = (input: unknown): boolean => typia.is<AmbientCallInterface>(input);
|
|
912
|
+
export const factoryAmbientCallInterface = typia.createIs<AmbientCallInterface>();
|
|
913
|
+
export const directAmbientConstructLiteral = (input: unknown): boolean => typia.is<AmbientConstructLiteral>(input);
|
|
914
|
+
export const factoryAmbientConstructLiteral = typia.createIs<AmbientConstructLiteral>();
|
|
915
|
+
export const directAmbientConstructAlias = (input: unknown): boolean => typia.is<AmbientConstructAlias>(input);
|
|
916
|
+
export const factoryAmbientConstructAlias = typia.createIs<AmbientConstructAlias>();
|
|
917
|
+
export const directAmbientConstructInterface = (input: unknown): boolean => typia.is<AmbientConstructInterface>(input);
|
|
918
|
+
export const factoryAmbientConstructInterface = typia.createIs<AmbientConstructInterface>();
|
|
919
|
+
`
|
|
920
|
+
|
|
921
|
+
const callableTypeLiteralProvenanceRunner = `const modules = {
|
|
922
|
+
default: require("./default.cjs"),
|
|
923
|
+
functional: require("./functional.cjs"),
|
|
924
|
+
};
|
|
925
|
+
|
|
926
|
+
let calls = 0;
|
|
927
|
+
const failures = [];
|
|
928
|
+
const callable = (value) => String(value);
|
|
929
|
+
class Constructable { constructor(value) { this.value = value; } }
|
|
930
|
+
|
|
931
|
+
// A declaration's provenance is not part of its type, so the four-cell function
|
|
932
|
+
// contract has to hold identically for a local declaration, one imported from a
|
|
933
|
+
// sibling module, and one from an ambient module declaration.
|
|
934
|
+
const expectations = {
|
|
935
|
+
default: { real: true, placeholder: true },
|
|
936
|
+
functional: { real: true, placeholder: false },
|
|
937
|
+
};
|
|
938
|
+
for (const provenance of ["Local", "Reexported", "Ambient"]) {
|
|
939
|
+
for (const kind of ["Call", "Construct"]) {
|
|
940
|
+
const real = kind === "Call" ? callable : Constructable;
|
|
941
|
+
for (const spelling of ["Literal", "Alias", "Interface"]) {
|
|
942
|
+
for (const form of ["direct", "factory"]) {
|
|
943
|
+
const name = form + provenance + kind + spelling;
|
|
944
|
+
for (const mode of ["default", "functional"]) {
|
|
945
|
+
for (const [label, value] of [["real", real], ["placeholder", {}]]) {
|
|
946
|
+
calls += 1;
|
|
947
|
+
const validator = modules[mode][name];
|
|
948
|
+
if (typeof validator !== "function") {
|
|
949
|
+
failures.push("missing export " + mode + " " + name);
|
|
950
|
+
continue;
|
|
951
|
+
}
|
|
952
|
+
const actual = validator(value);
|
|
953
|
+
const expected = expectations[mode][label];
|
|
954
|
+
if (actual !== expected) {
|
|
955
|
+
failures.push(mode + " " + name + " " + label + ": expected " + expected + " but got " + actual);
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
console.log("RAN " + calls + " PROVENANCE CALLS");
|
|
965
|
+
if (failures.length !== 0) throw new Error("MISMATCHES:\n" + failures.join("\n"));
|
|
966
|
+
`
|
|
967
|
+
|
|
968
|
+
const callableTypeLiteralSpellingRunnerTemplate = `const modules = {
|
|
969
|
+
default: require("./default.cjs"),
|
|
970
|
+
functional: require("./functional.cjs"),
|
|
971
|
+
};
|
|
972
|
+
|
|
973
|
+
let groups = 0;
|
|
974
|
+
let memberPairs = 0;
|
|
975
|
+
let anchors = 0;
|
|
976
|
+
let optionalRows = 0;
|
|
977
|
+
const failures = [];
|
|
978
|
+
|
|
979
|
+
// A missing export means the fixture and this runner disagree about the matrix.
|
|
980
|
+
// Reporting it by name keeps that from surfacing as an unattributable TypeError
|
|
981
|
+
// halfway through the run.
|
|
982
|
+
const call = (mode, name, input) => {
|
|
983
|
+
const validator = modules[mode][name];
|
|
984
|
+
if (typeof validator !== "function") {
|
|
985
|
+
failures.push("missing export " + mode + " " + name);
|
|
986
|
+
return "MISSING";
|
|
987
|
+
}
|
|
988
|
+
return validator(input);
|
|
989
|
+
};
|
|
990
|
+
|
|
991
|
+
const callable = (value) => String(value);
|
|
992
|
+
class Constructable { constructor(value) { this.value = value; } }
|
|
993
|
+
const methoded = Object.assign((value) => String(value), { method: () => undefined });
|
|
994
|
+
const labeled = Object.assign((value) => String(value), { label: "present" });
|
|
995
|
+
const mislabeled = Object.assign((value) => String(value), { label: 123 });
|
|
996
|
+
const indexed = Object.assign((value) => String(value), { extra: 1 });
|
|
997
|
+
// An object carrying every apparent member TypeScript gives a callable object
|
|
998
|
+
// type — the global Function interface's apply/call/bind/toString/prototype/
|
|
999
|
+
// length/arguments/caller plus name — beside the members each shape declares.
|
|
1000
|
+
// It is the one value the structural path a member-carrying shape must keep is
|
|
1001
|
+
// supposed to accept, so without it a regression that answered false everywhere
|
|
1002
|
+
// would satisfy every parity assertion below.
|
|
1003
|
+
const apparent = {
|
|
1004
|
+
apply: () => undefined,
|
|
1005
|
+
call: () => undefined,
|
|
1006
|
+
bind: () => undefined,
|
|
1007
|
+
toString: () => "",
|
|
1008
|
+
length: 0,
|
|
1009
|
+
name: "",
|
|
1010
|
+
prototype: {},
|
|
1011
|
+
arguments: null,
|
|
1012
|
+
caller: () => undefined,
|
|
1013
|
+
label: "present",
|
|
1014
|
+
method: () => undefined,
|
|
1015
|
+
extra: 1,
|
|
1016
|
+
};
|
|
1017
|
+
|
|
1018
|
+
const shapes = [__SHAPES__];
|
|
1019
|
+
const aliasedShapes = new Set([__ALIASED__]);
|
|
1020
|
+
// Each pair differs only in whether its member is written as a method shorthand
|
|
1021
|
+
// or as a function-typed property. TypeScript calls them the same type, so every
|
|
1022
|
+
// validator built from either must answer identically.
|
|
1023
|
+
const memberSpellingPairs = [__MEMBER_PAIRS__];
|
|
1024
|
+
const positions = [__POSITIONS__];
|
|
1025
|
+
const values = [
|
|
1026
|
+
["callable", callable],
|
|
1027
|
+
["constructable", Constructable],
|
|
1028
|
+
["methoded", methoded],
|
|
1029
|
+
["labeled", labeled],
|
|
1030
|
+
["mislabeled", mislabeled],
|
|
1031
|
+
["indexed", indexed],
|
|
1032
|
+
["apparent", apparent],
|
|
1033
|
+
["placeholder", {}],
|
|
1034
|
+
["dataOnly", { label: "present" }],
|
|
1035
|
+
["badDataOnly", { label: 123 }],
|
|
1036
|
+
["unionData", { kind: "data", value: 1 }],
|
|
1037
|
+
["badUnionData", { kind: "data", value: "bad" }],
|
|
1038
|
+
];
|
|
1039
|
+
|
|
1040
|
+
const wrap = (position, value) =>
|
|
1041
|
+
position === "Nested" || position === "OptionalProperty"
|
|
1042
|
+
? { fn: value }
|
|
1043
|
+
: position === "Generic"
|
|
1044
|
+
? { payload: value }
|
|
1045
|
+
: value;
|
|
1046
|
+
|
|
1047
|
+
for (const mode of ["default", "functional"]) {
|
|
1048
|
+
for (const position of positions) {
|
|
1049
|
+
for (const form of ["direct", "factory"]) {
|
|
1050
|
+
for (const [label, value] of values) {
|
|
1051
|
+
const input = wrap(position, value);
|
|
1052
|
+
for (const shape of shapes) {
|
|
1053
|
+
groups += 1;
|
|
1054
|
+
const spellings = ["Interface", "Literal", "Inline"];
|
|
1055
|
+
if (aliasedShapes.has(shape)) spellings.push("Alias");
|
|
1056
|
+
const answers = spellings.map((spelling) => call(mode, form + shape + spelling + position, input));
|
|
1057
|
+
for (let i = 1; i < answers.length; ++i) {
|
|
1058
|
+
if (answers[i] !== answers[0]) {
|
|
1059
|
+
failures.push(
|
|
1060
|
+
"spelling " + mode + " " + form + shape + position + " " + label +
|
|
1061
|
+
": " + spellings[0] + "=" + answers[0] + " " + spellings[i] + "=" + answers[i]);
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
for (const [method, property] of memberSpellingPairs) {
|
|
1066
|
+
for (const spelling of ["Interface", "Literal", "Inline"]) {
|
|
1067
|
+
memberPairs += 1;
|
|
1068
|
+
const left = call(mode, form + method + spelling + position, input);
|
|
1069
|
+
const right = call(mode, form + property + spelling + position, input);
|
|
1070
|
+
if (left !== right) {
|
|
1071
|
+
failures.push(
|
|
1072
|
+
"member " + spelling + " " + mode + " " + form + method + "/" + property + position + " " + label +
|
|
1073
|
+
": method=" + left + " property=" + right);
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
// Parity cannot see a change that moves every spelling the same way, so these
|
|
1083
|
+
// anchors pin the answers themselves. A member-free callable is a function type,
|
|
1084
|
+
// which typia documents as a slot default options skip and functional options
|
|
1085
|
+
// require a real function for. A member-carrying callable keeps the structural
|
|
1086
|
+
// object shape #2250 merged and #2238 declares an explicit negative boundary:
|
|
1087
|
+
// its guard opens with a typeof "object" test, so a real function is rejected,
|
|
1088
|
+
// and it still requires the members it declares, so a bare {} is rejected too.
|
|
1089
|
+
const anchorRows = [
|
|
1090
|
+
["default", "directPureCall", "Top", callable, true],
|
|
1091
|
+
["functional", "directPureCall", "Top", callable, true],
|
|
1092
|
+
["default", "directPureCall", "Top", {}, true],
|
|
1093
|
+
["functional", "directPureCall", "Top", {}, false],
|
|
1094
|
+
["default", "factoryPureCall", "Top", callable, true],
|
|
1095
|
+
["functional", "factoryPureCall", "Top", {}, false],
|
|
1096
|
+
["default", "directPureConstruct", "Top", Constructable, true],
|
|
1097
|
+
["functional", "directPureConstruct", "Top", Constructable, true],
|
|
1098
|
+
["default", "directPureConstruct", "Top", {}, true],
|
|
1099
|
+
["functional", "directPureConstruct", "Top", {}, false],
|
|
1100
|
+
["default", "directOverload", "Top", callable, true],
|
|
1101
|
+
["functional", "directOverload", "Top", callable, true],
|
|
1102
|
+
["functional", "directOverload", "Top", {}, false],
|
|
1103
|
+
["default", "directConstructOverload", "Top", Constructable, true],
|
|
1104
|
+
["functional", "directConstructOverload", "Top", {}, false],
|
|
1105
|
+
["default", "directCallAndConstruct", "Top", callable, true],
|
|
1106
|
+
["functional", "directCallAndConstruct", "Top", callable, true],
|
|
1107
|
+
["functional", "directCallAndConstruct", "Top", {}, false],
|
|
1108
|
+
["default", "directPureCall", "Nested", { fn: callable }, true],
|
|
1109
|
+
["functional", "directPureCall", "Nested", { fn: callable }, true],
|
|
1110
|
+
["functional", "directPureCall", "Nested", { fn: {} }, false],
|
|
1111
|
+
["default", "directPureCall", "EmptyIntersection", callable, true],
|
|
1112
|
+
["functional", "directPureCall", "EmptyIntersection", callable, true],
|
|
1113
|
+
["functional", "directPureCall", "EmptyIntersection", {}, false],
|
|
1114
|
+
["default", "directPureCall", "Generic", { payload: callable }, true],
|
|
1115
|
+
["functional", "directPureCall", "Generic", { payload: {} }, false],
|
|
1116
|
+
["default", "directPureCall", "UnionArm", callable, true],
|
|
1117
|
+
["default", "directPureCall", "UnionArm", { kind: "data", value: 1 }, true],
|
|
1118
|
+
["default", "directPureCall", "UnionArm", { kind: "data", value: "bad" }, false],
|
|
1119
|
+
["functional", "directPureCall", "UnionArm", callable, true],
|
|
1120
|
+
["functional", "directPureCall", "UnionArm", { kind: "data", value: 1 }, true],
|
|
1121
|
+
["functional", "directPureCall", "UnionArm", { kind: "data", value: "bad" }, false],
|
|
1122
|
+
|
|
1123
|
+
["default", "directRequiredMember", "Top", labeled, false],
|
|
1124
|
+
["functional", "directRequiredMember", "Top", labeled, false],
|
|
1125
|
+
["default", "directRequiredMember", "Top", {}, false],
|
|
1126
|
+
["default", "directRequiredMember", "Top", apparent, true],
|
|
1127
|
+
["functional", "directRequiredMember", "Top", apparent, true],
|
|
1128
|
+
["default", "directIndexSignature", "Top", indexed, false],
|
|
1129
|
+
["default", "directIndexSignature", "Top", apparent, true],
|
|
1130
|
+
["default", "directMethod", "Top", methoded, false],
|
|
1131
|
+
["default", "directMethod", "Top", apparent, true],
|
|
1132
|
+
["default", "directMethodProperty", "Top", methoded, false],
|
|
1133
|
+
["default", "directMethodProperty", "Top", apparent, true],
|
|
1134
|
+
];
|
|
1135
|
+
for (const [mode, prefix, position, value, expected] of anchorRows) {
|
|
1136
|
+
for (const spelling of ["Interface", "Literal", "Inline"]) {
|
|
1137
|
+
anchors += 1;
|
|
1138
|
+
const name = prefix + spelling + position;
|
|
1139
|
+
const actual = call(mode, name, value);
|
|
1140
|
+
if (actual !== expected) {
|
|
1141
|
+
failures.push("anchor " + mode + " " + name + ": expected " + expected + " but got " + actual);
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
// The optional data member is its own boundary: a callable shape whose only
|
|
1147
|
+
// member may be absent is the shape most likely to be mistaken for a member-free
|
|
1148
|
+
// one, so a fix that widened the signature-only rule too far would erase the
|
|
1149
|
+
// member and start accepting a bare function. Present-valid, omitted,
|
|
1150
|
+
// present-invalid, and data-only are the four states that distinguish "the
|
|
1151
|
+
// member is still checked" from "the member is gone", and the interface spelling
|
|
1152
|
+
// is the oracle #2250 merged for them.
|
|
1153
|
+
for (const [label, value] of [
|
|
1154
|
+
["present-valid", labeled],
|
|
1155
|
+
["omitted", callable],
|
|
1156
|
+
["present-invalid", mislabeled],
|
|
1157
|
+
["data-only", { label: "present" }],
|
|
1158
|
+
]) {
|
|
1159
|
+
for (const mode of ["default", "functional"]) {
|
|
1160
|
+
optionalRows += 1;
|
|
1161
|
+
const oracle = call(mode, "directOptionalMemberInterfaceTop", value);
|
|
1162
|
+
for (const spelling of ["Literal", "Inline"]) {
|
|
1163
|
+
const actual = call(mode, "directOptionalMember" + spelling + "Top", value);
|
|
1164
|
+
if (actual !== oracle) {
|
|
1165
|
+
failures.push(
|
|
1166
|
+
"optional member " + mode + " " + spelling + " " + label +
|
|
1167
|
+
": expected the interface oracle " + oracle + " but got " + actual);
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
// A callable value that omits the optional member must not be accepted while
|
|
1171
|
+
// the declared member is still part of the type; if it were, the member had
|
|
1172
|
+
// been erased.
|
|
1173
|
+
if (label === "omitted" && oracle !== false) {
|
|
1174
|
+
failures.push("optional member " + mode + " omitted: interface oracle accepted a bare function");
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
console.log("RAN " + groups + " SPELLING GROUPS");
|
|
1180
|
+
console.log("RAN " + memberPairs + " MEMBER PAIRS");
|
|
1181
|
+
console.log("RAN " + anchors + " ANCHORS");
|
|
1182
|
+
console.log("RAN " + optionalRows + " OPTIONAL MEMBER ROWS");
|
|
1183
|
+
if (failures.length !== 0) throw new Error("MISMATCHES:\n" + failures.join("\n"));
|
|
1184
|
+
`
|