@fulcro/transform-core 0.7.0 → 0.8.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/dist/index.d.ts +1 -0
- package/dist/index.js +3 -1
- package/dist/structural/explain/index.d.ts +14 -0
- package/dist/structural/explain/index.js +287 -0
- package/dist/structural/index.d.ts +88 -0
- package/dist/structural/index.js +498 -0
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -18,4 +18,5 @@
|
|
|
18
18
|
*/
|
|
19
19
|
export { createFileTransformer, type FileTransformer, type TransformCoreOptions, } from './program/index.js';
|
|
20
20
|
export { type CallForm, type CallRewriter, IDENTIFIER_PATTERN, isOwnedCall, isTupleType, type RewriteContext, utilityModuleSegment, } from './shared/index.js';
|
|
21
|
+
export { buildStructuralTest, type StructuralOptions, type TypeTest, } from './structural/index.js';
|
|
21
22
|
export { createTransformer, type TransformerFactory, type TransformerOptions, } from './transformer/index.js';
|
package/dist/index.js
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* other.
|
|
19
19
|
*/
|
|
20
20
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
-
exports.createTransformer = exports.utilityModuleSegment = exports.isTupleType = exports.isOwnedCall = exports.IDENTIFIER_PATTERN = exports.createFileTransformer = void 0;
|
|
21
|
+
exports.createTransformer = exports.buildStructuralTest = exports.utilityModuleSegment = exports.isTupleType = exports.isOwnedCall = exports.IDENTIFIER_PATTERN = exports.createFileTransformer = void 0;
|
|
22
22
|
var program_1 = require("./program/index.js");
|
|
23
23
|
Object.defineProperty(exports, "createFileTransformer", { enumerable: true, get: function () { return program_1.createFileTransformer; } });
|
|
24
24
|
var shared_1 = require("./shared/index.js");
|
|
@@ -26,5 +26,7 @@ Object.defineProperty(exports, "IDENTIFIER_PATTERN", { enumerable: true, get: fu
|
|
|
26
26
|
Object.defineProperty(exports, "isOwnedCall", { enumerable: true, get: function () { return shared_1.isOwnedCall; } });
|
|
27
27
|
Object.defineProperty(exports, "isTupleType", { enumerable: true, get: function () { return shared_1.isTupleType; } });
|
|
28
28
|
Object.defineProperty(exports, "utilityModuleSegment", { enumerable: true, get: function () { return shared_1.utilityModuleSegment; } });
|
|
29
|
+
var structural_1 = require("./structural/index.js");
|
|
30
|
+
Object.defineProperty(exports, "buildStructuralTest", { enumerable: true, get: function () { return structural_1.buildStructuralTest; } });
|
|
29
31
|
var transformer_1 = require("./transformer/index.js");
|
|
30
32
|
Object.defineProperty(exports, "createTransformer", { enumerable: true, get: function () { return transformer_1.createTransformer; } });
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import typescript from 'typescript';
|
|
2
|
+
/**
|
|
3
|
+
* Builds the walker that names where a value stopped matching a type.
|
|
4
|
+
*
|
|
5
|
+
* @param type Type the call asked for.
|
|
6
|
+
* @param written The type as the consumer wrote it.
|
|
7
|
+
* @param at Node the call sits at, for scope.
|
|
8
|
+
* @param checker Checker of the program being compiled.
|
|
9
|
+
* @param factory Node factory of the current transformation.
|
|
10
|
+
* @param fallback Builds the yes-or-no check for a type, for what this cannot
|
|
11
|
+
* describe precisely.
|
|
12
|
+
* @returns The arrow function, or `null` when nothing useful can be said.
|
|
13
|
+
*/
|
|
14
|
+
export declare const buildExplainer: (type: typescript.Type, written: string, at: typescript.Node, checker: typescript.TypeChecker, factory: typescript.NodeFactory, fallback: (type: typescript.Type, value: typescript.Expression) => typescript.Expression | null) => typescript.Expression | null;
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.buildExplainer = void 0;
|
|
7
|
+
const typescript_1 = __importDefault(require("typescript"));
|
|
8
|
+
/**
|
|
9
|
+
* Writing out *where* a value stopped matching a type.
|
|
10
|
+
*
|
|
11
|
+
* The check beside this one answers yes or no, which is all a filter needs. An
|
|
12
|
+
* `as<Order>(payload)` that refuses needs more than that: told only "not an
|
|
13
|
+
* Order" about a record with forty fields, you are no better off than before
|
|
14
|
+
* the check existed.
|
|
15
|
+
*
|
|
16
|
+
* So this emits a second walker that returns the first path that failed, and
|
|
17
|
+
* what was there instead:
|
|
18
|
+
*
|
|
19
|
+
* ```text
|
|
20
|
+
* customer.email: expected string, got number
|
|
21
|
+
* items[3].quantity: expected number, got undefined
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* It runs **only after the fast check has already refused**, so nothing on the
|
|
25
|
+
* happy path pays for it. That is also why it can be written the slow, obvious
|
|
26
|
+
* way — as statements that walk and return early — rather than as one fused
|
|
27
|
+
* expression.
|
|
28
|
+
*
|
|
29
|
+
* Where a type is more than this can describe precisely — an intersection, a
|
|
30
|
+
* tuple, a union of object shapes — it falls back to naming the type that was
|
|
31
|
+
* expected at that path. Vague beats wrong: a path is a promise about where the
|
|
32
|
+
* problem is, and inventing one would send someone to the wrong field.
|
|
33
|
+
*/
|
|
34
|
+
/** How deep the walker descends before falling back to naming the type. */
|
|
35
|
+
const MAX_DEPTH = 8;
|
|
36
|
+
/**
|
|
37
|
+
* Tells whether a flag is present on a type.
|
|
38
|
+
*
|
|
39
|
+
* @param type Type being inspected.
|
|
40
|
+
* @param flag Flag looked for.
|
|
41
|
+
* @returns `true` when the type carries it.
|
|
42
|
+
*/
|
|
43
|
+
const has = (type, flag) => (type.flags & flag) !== 0;
|
|
44
|
+
/**
|
|
45
|
+
* Builds the expression naming what a value actually is, at runtime.
|
|
46
|
+
*
|
|
47
|
+
* `typeof` with `null` corrected, which is the one answer it gives that would
|
|
48
|
+
* mislead someone reading the message.
|
|
49
|
+
*
|
|
50
|
+
* @param factory Node factory.
|
|
51
|
+
* @param value Expression being described.
|
|
52
|
+
* @returns An expression producing the description.
|
|
53
|
+
*/
|
|
54
|
+
const describe = (factory, value) => factory.createConditionalExpression(factory.createStrictEquality(value, factory.createNull()), undefined, factory.createStringLiteral('null'), undefined, factory.createTypeOfExpression(value));
|
|
55
|
+
/**
|
|
56
|
+
* Builds `return "<path>: expected <expected>, got " + typeof value`.
|
|
57
|
+
*
|
|
58
|
+
* @param factory Node factory.
|
|
59
|
+
* @param path Path of the value inside the whole, as an expression so that an
|
|
60
|
+
* array index can be part of it.
|
|
61
|
+
* @param expected Name of the type that was wanted.
|
|
62
|
+
* @param value Expression that failed.
|
|
63
|
+
* @returns The return statement.
|
|
64
|
+
*/
|
|
65
|
+
const complain = (factory, path, expected, value) => {
|
|
66
|
+
// At the root there is no path to name, and prefixing an empty one leaves a
|
|
67
|
+
// stray colon in front of the message.
|
|
68
|
+
const atRoot = typescript_1.default.isStringLiteral(path) && path.text === '';
|
|
69
|
+
const prefix = atRoot
|
|
70
|
+
? factory.createStringLiteral(`expected ${expected}, got `)
|
|
71
|
+
: factory.createAdd(path, factory.createStringLiteral(`: expected ${expected}, got `));
|
|
72
|
+
return factory.createReturnStatement(factory.createAdd(prefix, describe(factory, value)));
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* Reads a property off an expression, quoting the key when it has to.
|
|
76
|
+
*
|
|
77
|
+
* @param factory Node factory.
|
|
78
|
+
* @param value Expression carrying the property.
|
|
79
|
+
* @param name Name of the property.
|
|
80
|
+
* @returns The access expression.
|
|
81
|
+
*/
|
|
82
|
+
const propertyOf = (factory, value, name) => /^[A-Za-z_$][\w$]*$/.test(name)
|
|
83
|
+
? factory.createPropertyAccessExpression(value, name)
|
|
84
|
+
: factory.createElementAccessExpression(value, factory.createStringLiteral(name));
|
|
85
|
+
/**
|
|
86
|
+
* Extends a path expression with a property name.
|
|
87
|
+
*
|
|
88
|
+
* @param factory Node factory.
|
|
89
|
+
* @param path Path so far.
|
|
90
|
+
* @param name Property being descended into.
|
|
91
|
+
* @returns The extended path.
|
|
92
|
+
*/
|
|
93
|
+
const pathTo = (factory, path, name) => typescript_1.default.isStringLiteral(path) && path.text === ''
|
|
94
|
+
? factory.createStringLiteral(name)
|
|
95
|
+
: factory.createAdd(path, factory.createStringLiteral(`.${name}`));
|
|
96
|
+
/**
|
|
97
|
+
* Writes the statements that look for a failure of one type at one path.
|
|
98
|
+
*
|
|
99
|
+
* @param type Type being checked for.
|
|
100
|
+
* @param value Expression the check applies to.
|
|
101
|
+
* @param path Path of that expression inside the whole.
|
|
102
|
+
* @param written The type as the consumer wrote it.
|
|
103
|
+
* @param walk State of the generation.
|
|
104
|
+
* @param depth How deep into the type this is.
|
|
105
|
+
* @returns Statements returning a description, or `null` when nothing can be
|
|
106
|
+
* said precisely enough to be worth saying.
|
|
107
|
+
*/
|
|
108
|
+
const explainFor = (type, value, path, written, walk, depth) => {
|
|
109
|
+
const { checker, factory } = walk;
|
|
110
|
+
if (depth > MAX_DEPTH)
|
|
111
|
+
return vague(type, value, path, written, walk);
|
|
112
|
+
// Nothing to report: these accept anything.
|
|
113
|
+
if (has(type, typescript_1.default.TypeFlags.Any | typescript_1.default.TypeFlags.Unknown)) {
|
|
114
|
+
return [];
|
|
115
|
+
}
|
|
116
|
+
const primitive = primitiveNameOf(type);
|
|
117
|
+
if (primitive !== null) {
|
|
118
|
+
return [
|
|
119
|
+
factory.createIfStatement(factory.createStrictInequality(factory.createTypeOfExpression(value), factory.createStringLiteral(primitive)), factory.createBlock([complain(factory, path, primitive, value)], true)),
|
|
120
|
+
];
|
|
121
|
+
}
|
|
122
|
+
if (has(type, typescript_1.default.TypeFlags.Object)) {
|
|
123
|
+
if (checker.isArrayType(type)) {
|
|
124
|
+
return explainArray(type, value, path, written, walk, depth);
|
|
125
|
+
}
|
|
126
|
+
// A tuple, a function, a class: all described by name rather than walked.
|
|
127
|
+
// A class is checked with `instanceof` and there is no sub-path to point
|
|
128
|
+
// at; a tuple would need its own index handling for little gain.
|
|
129
|
+
if (checker.isTupleType(type)) {
|
|
130
|
+
return vague(type, value, path, written, walk);
|
|
131
|
+
}
|
|
132
|
+
const symbol = type.getSymbol();
|
|
133
|
+
if (symbol !== undefined &&
|
|
134
|
+
(symbol.flags & typescript_1.default.SymbolFlags.Class) !== 0) {
|
|
135
|
+
return vague(type, value, path, written, walk);
|
|
136
|
+
}
|
|
137
|
+
if (type.getCallSignatures().length > 0) {
|
|
138
|
+
return vague(type, value, path, written, walk);
|
|
139
|
+
}
|
|
140
|
+
return explainObject(type, value, path, written, walk, depth);
|
|
141
|
+
}
|
|
142
|
+
// Unions, intersections, literals and everything else: the yes-or-no check
|
|
143
|
+
// is exact, so it decides, and the message names the type rather than
|
|
144
|
+
// guessing at which member was meant.
|
|
145
|
+
return vague(type, value, path, written, walk);
|
|
146
|
+
};
|
|
147
|
+
/**
|
|
148
|
+
* The primitive flags that have a `typeof` name, and that name.
|
|
149
|
+
*/
|
|
150
|
+
const PRIMITIVE_NAMES = [
|
|
151
|
+
[typescript_1.default.TypeFlags.StringLike, 'string'],
|
|
152
|
+
[typescript_1.default.TypeFlags.NumberLike, 'number'],
|
|
153
|
+
[typescript_1.default.TypeFlags.BigIntLike, 'bigint'],
|
|
154
|
+
[typescript_1.default.TypeFlags.BooleanLike, 'boolean'],
|
|
155
|
+
[typescript_1.default.TypeFlags.ESSymbolLike, 'symbol'],
|
|
156
|
+
];
|
|
157
|
+
/**
|
|
158
|
+
* Names the `typeof` group a type belongs to, when it belongs to one.
|
|
159
|
+
*
|
|
160
|
+
* Literal types are excluded on purpose: `'dark'` is a string as far as
|
|
161
|
+
* `typeof` goes, and reporting it as one would be a description that passes
|
|
162
|
+
* while the value is still wrong.
|
|
163
|
+
*
|
|
164
|
+
* @param type Type being classified.
|
|
165
|
+
* @returns The `typeof` name, or `null`.
|
|
166
|
+
*/
|
|
167
|
+
const primitiveNameOf = (type) => {
|
|
168
|
+
if (type.isLiteral() || has(type, typescript_1.default.TypeFlags.BooleanLiteral)) {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
if (type.isUnion())
|
|
172
|
+
return null;
|
|
173
|
+
for (const [flag, name] of PRIMITIVE_NAMES) {
|
|
174
|
+
if (has(type, flag))
|
|
175
|
+
return name;
|
|
176
|
+
}
|
|
177
|
+
return null;
|
|
178
|
+
};
|
|
179
|
+
/**
|
|
180
|
+
* Falls back to naming the expected type at a path.
|
|
181
|
+
*
|
|
182
|
+
* @param type Type that was wanted.
|
|
183
|
+
* @param value Expression that failed.
|
|
184
|
+
* @param path Path of that expression.
|
|
185
|
+
* @param written The type as written.
|
|
186
|
+
* @param walk State of the generation.
|
|
187
|
+
* @returns The statements, or `null` when even the yes-or-no check is missing.
|
|
188
|
+
*/
|
|
189
|
+
const vague = (type, value, path, written, walk) => {
|
|
190
|
+
const check = walk.fallback(type, value);
|
|
191
|
+
if (check === null)
|
|
192
|
+
return null;
|
|
193
|
+
return [
|
|
194
|
+
walk.factory.createIfStatement(walk.factory.createPrefixUnaryExpression(typescript_1.default.SyntaxKind.ExclamationToken, walk.factory.createParenthesizedExpression(check)), walk.factory.createBlock([complain(walk.factory, path, written, value)], true)),
|
|
195
|
+
];
|
|
196
|
+
};
|
|
197
|
+
/**
|
|
198
|
+
* Writes the statements describing an array that does not match.
|
|
199
|
+
*
|
|
200
|
+
* @param type Array type being checked for.
|
|
201
|
+
* @param value Expression the check applies to.
|
|
202
|
+
* @param path Path of that expression.
|
|
203
|
+
* @param written The type as written.
|
|
204
|
+
* @param walk State of the generation.
|
|
205
|
+
* @param depth How deep into the type this is.
|
|
206
|
+
* @returns The statements, or `null`.
|
|
207
|
+
*/
|
|
208
|
+
const explainArray = (type, value, path, written, walk, depth) => {
|
|
209
|
+
const { checker, factory } = walk;
|
|
210
|
+
const [element] = checker.getTypeArguments(type);
|
|
211
|
+
if (element === undefined)
|
|
212
|
+
return vague(type, value, path, written, walk);
|
|
213
|
+
const isArray = factory.createIfStatement(factory.createPrefixUnaryExpression(typescript_1.default.SyntaxKind.ExclamationToken, factory.createCallExpression(factory.createPropertyAccessExpression(factory.createIdentifier('Array'), 'isArray'), undefined, [value])), factory.createBlock([complain(factory, path, 'an array', value)], true));
|
|
214
|
+
const index = `i${walk.counter.value++}`;
|
|
215
|
+
const elementPath = factory.createAdd(factory.createAdd(path, factory.createStringLiteral('[')), factory.createAdd(factory.createIdentifier(index), factory.createStringLiteral(']')));
|
|
216
|
+
const inner = explainFor(element, factory.createElementAccessExpression(value, factory.createIdentifier(index)), elementPath, checker.typeToString(element), walk, depth + 1);
|
|
217
|
+
if (inner === null)
|
|
218
|
+
return [isArray];
|
|
219
|
+
// A plain loop rather than `every`, so the index is in hand for the path and
|
|
220
|
+
// the walk can return out of the middle of it.
|
|
221
|
+
return [
|
|
222
|
+
isArray,
|
|
223
|
+
factory.createForStatement(factory.createVariableDeclarationList([
|
|
224
|
+
factory.createVariableDeclaration(index, undefined, undefined, factory.createNumericLiteral(0)),
|
|
225
|
+
], typescript_1.default.NodeFlags.Let), factory.createLessThan(factory.createIdentifier(index), factory.createPropertyAccessExpression(value, 'length')), factory.createPostfixIncrement(factory.createIdentifier(index)), factory.createBlock(inner, true)),
|
|
226
|
+
];
|
|
227
|
+
};
|
|
228
|
+
/**
|
|
229
|
+
* Writes the statements describing an object that does not match.
|
|
230
|
+
*
|
|
231
|
+
* @param type Object type being checked for.
|
|
232
|
+
* @param value Expression the check applies to.
|
|
233
|
+
* @param path Path of that expression.
|
|
234
|
+
* @param written The type as written.
|
|
235
|
+
* @param walk State of the generation.
|
|
236
|
+
* @param depth How deep into the type this is.
|
|
237
|
+
* @returns The statements, or `null`.
|
|
238
|
+
*/
|
|
239
|
+
const explainObject = (type, value, path, written, walk, depth) => {
|
|
240
|
+
const { checker, factory } = walk;
|
|
241
|
+
const properties = type.getProperties();
|
|
242
|
+
if (properties.length === 0)
|
|
243
|
+
return vague(type, value, path, written, walk);
|
|
244
|
+
// Ruled out before any property is read, or the walk would throw while
|
|
245
|
+
// trying to explain rather than returning the explanation.
|
|
246
|
+
const statements = [
|
|
247
|
+
factory.createIfStatement(factory.createLogicalOr(factory.createStrictEquality(value, factory.createNull()), factory.createStrictInequality(factory.createTypeOfExpression(value), factory.createStringLiteral('object'))), factory.createBlock([complain(factory, path, written, value)], true)),
|
|
248
|
+
];
|
|
249
|
+
for (const property of properties) {
|
|
250
|
+
const name = property.getName();
|
|
251
|
+
if (name.startsWith('__@'))
|
|
252
|
+
continue;
|
|
253
|
+
const propertyType = checker.getTypeOfSymbolAtLocation(property, walk.at);
|
|
254
|
+
const access = propertyOf(factory, value, name);
|
|
255
|
+
const inner = explainFor(propertyType, access, pathTo(factory, path, name), checker.typeToString(propertyType), walk, depth + 1);
|
|
256
|
+
if (inner === null || inner.length === 0)
|
|
257
|
+
continue;
|
|
258
|
+
const optional = (property.flags & typescript_1.default.SymbolFlags.Optional) !== 0;
|
|
259
|
+
// An absent optional property is not a failure, so the whole check for it
|
|
260
|
+
// is skipped rather than being made to pass.
|
|
261
|
+
statements.push(optional
|
|
262
|
+
? factory.createIfStatement(factory.createStrictInequality(access, factory.createIdentifier('undefined')), factory.createBlock(inner, true))
|
|
263
|
+
: factory.createBlock(inner, true));
|
|
264
|
+
}
|
|
265
|
+
return statements;
|
|
266
|
+
};
|
|
267
|
+
/**
|
|
268
|
+
* Builds the walker that names where a value stopped matching a type.
|
|
269
|
+
*
|
|
270
|
+
* @param type Type the call asked for.
|
|
271
|
+
* @param written The type as the consumer wrote it.
|
|
272
|
+
* @param at Node the call sits at, for scope.
|
|
273
|
+
* @param checker Checker of the program being compiled.
|
|
274
|
+
* @param factory Node factory of the current transformation.
|
|
275
|
+
* @param fallback Builds the yes-or-no check for a type, for what this cannot
|
|
276
|
+
* describe precisely.
|
|
277
|
+
* @returns The arrow function, or `null` when nothing useful can be said.
|
|
278
|
+
*/
|
|
279
|
+
const buildExplainer = (type, written, at, checker, factory, fallback) => {
|
|
280
|
+
const walk = { checker, factory, at, fallback, counter: { value: 0 } };
|
|
281
|
+
const parameter = 'v';
|
|
282
|
+
const body = explainFor(type, factory.createIdentifier(parameter), factory.createStringLiteral(''), written, walk, 0);
|
|
283
|
+
if (body === null || body.length === 0)
|
|
284
|
+
return null;
|
|
285
|
+
return factory.createArrowFunction(undefined, undefined, [factory.createParameterDeclaration(undefined, undefined, parameter)], undefined, factory.createToken(typescript_1.default.SyntaxKind.EqualsGreaterThanToken), factory.createBlock([...body, factory.createReturnStatement(factory.createNull())], true));
|
|
286
|
+
};
|
|
287
|
+
exports.buildExplainer = buildExplainer;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import typescript from 'typescript';
|
|
2
|
+
import { RewriteContext } from '../shared/index.js';
|
|
3
|
+
/**
|
|
4
|
+
* Writing out the checks a type implies.
|
|
5
|
+
*
|
|
6
|
+
* A type with no single runtime token — an interface, an object literal type, a
|
|
7
|
+
* union of them — still has a *shape*, and the checker knows it completely
|
|
8
|
+
* while the compiler is running. So rather than looking for something to point
|
|
9
|
+
* at, this writes the test out: the properties, their types, their optionality,
|
|
10
|
+
* all the way down.
|
|
11
|
+
*
|
|
12
|
+
* ```ts
|
|
13
|
+
* // interface Account { id: number; tags: string[] }
|
|
14
|
+
* values.ofType<Account>();
|
|
15
|
+
*
|
|
16
|
+
* // becomes, near enough:
|
|
17
|
+
* values.ofType({
|
|
18
|
+
* name: 'Account',
|
|
19
|
+
* matches: (v) =>
|
|
20
|
+
* v !== null &&
|
|
21
|
+
* typeof v === 'object' &&
|
|
22
|
+
* typeof v.id === 'number' &&
|
|
23
|
+
* Array.isArray(v.tags) &&
|
|
24
|
+
* v.tags.every((e) => typeof e === 'string'),
|
|
25
|
+
* });
|
|
26
|
+
* ```
|
|
27
|
+
*
|
|
28
|
+
* **The governing rule is that it refuses whatever it cannot prove.** A test
|
|
29
|
+
* that answers `true` for something that is not an `Account` is worse than no
|
|
30
|
+
* test at all: it is false confidence at exactly the boundary where the data is
|
|
31
|
+
* least trustworthy. So every construct this does not fully understand returns
|
|
32
|
+
* `null` from here, and the call is left unresolved for the runtime to reject
|
|
33
|
+
* out loud. There is no partial or optimistic check anywhere in this file.
|
|
34
|
+
*/
|
|
35
|
+
/**
|
|
36
|
+
* A test deciding whether a value is of some type, by looking at its shape.
|
|
37
|
+
*
|
|
38
|
+
* What this module emits, and what the runtime halves of `ofType`, `cast`, `is`
|
|
39
|
+
* and `as` receive. Declared here because more than one library consumes it;
|
|
40
|
+
* each also re-exports a structurally identical one of its own, so nothing a
|
|
41
|
+
* consumer imports depends on this package.
|
|
42
|
+
*
|
|
43
|
+
* @template R Type a passing value is taken to be.
|
|
44
|
+
*/
|
|
45
|
+
export interface TypeTest<R> {
|
|
46
|
+
/**
|
|
47
|
+
* Decides whether a value is an `R`.
|
|
48
|
+
*
|
|
49
|
+
* @param value Value being tested.
|
|
50
|
+
* @returns `true` when the value is of that type.
|
|
51
|
+
*/
|
|
52
|
+
readonly matches: (value: unknown) => boolean;
|
|
53
|
+
/**
|
|
54
|
+
* Names where a value stopped being an `R`.
|
|
55
|
+
*
|
|
56
|
+
* Emitted only where the answer is worth the code — a failing `as<T>()`
|
|
57
|
+
* needs to say which field was wrong, while a filter only needs yes or no.
|
|
58
|
+
* Runs after `matches` has already refused, so the happy path never pays
|
|
59
|
+
* for it.
|
|
60
|
+
*
|
|
61
|
+
* @param value Value that failed.
|
|
62
|
+
* @returns The path and reason, or `null` if it cannot say.
|
|
63
|
+
*/
|
|
64
|
+
readonly explain?: (value: unknown) => string | null;
|
|
65
|
+
/** Name of the type, as it was written. */
|
|
66
|
+
readonly name?: string;
|
|
67
|
+
}
|
|
68
|
+
/** What a caller wants emitted beyond the yes-or-no check. */
|
|
69
|
+
export interface StructuralOptions {
|
|
70
|
+
/**
|
|
71
|
+
* Also emit a walker naming where a value stopped matching.
|
|
72
|
+
*
|
|
73
|
+
* Worth it where a refusal has to be actionable — a failing `as<T>()` — and
|
|
74
|
+
* not where the answer is only ever used as a filter.
|
|
75
|
+
*/
|
|
76
|
+
readonly explain?: boolean;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Builds the shape test for a type, when one can be proven.
|
|
80
|
+
*
|
|
81
|
+
* @param type Type the call asked for.
|
|
82
|
+
* @param written The type as the consumer wrote it, for error messages.
|
|
83
|
+
* @param at Node the call sits at, for scope.
|
|
84
|
+
* @param context Compilation in progress.
|
|
85
|
+
* @returns The object literal to pass at runtime, or `null` when the type
|
|
86
|
+
* cannot be checked honestly.
|
|
87
|
+
*/
|
|
88
|
+
export declare const buildStructuralTest: (type: typescript.Type, written: string, at: typescript.Node, context: RewriteContext, options?: StructuralOptions) => typescript.Expression | null;
|
|
@@ -0,0 +1,498 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.buildStructuralTest = void 0;
|
|
7
|
+
const typescript_1 = __importDefault(require("typescript"));
|
|
8
|
+
const explain_1 = require("./explain/index.js");
|
|
9
|
+
/** How deep a type may nest before this gives up rather than grinding on. */
|
|
10
|
+
const MAX_DEPTH = 12;
|
|
11
|
+
/**
|
|
12
|
+
* Tells whether a flag is present on a type.
|
|
13
|
+
*
|
|
14
|
+
* @param type Type being inspected.
|
|
15
|
+
* @param flag Flag looked for.
|
|
16
|
+
* @returns `true` when the type carries it.
|
|
17
|
+
*/
|
|
18
|
+
const has = (type, flag) => (type.flags & flag) !== 0;
|
|
19
|
+
/**
|
|
20
|
+
* Builds `typeof value === name`.
|
|
21
|
+
*
|
|
22
|
+
* @param factory Node factory.
|
|
23
|
+
* @param value Expression being tested.
|
|
24
|
+
* @param name Expected `typeof` answer.
|
|
25
|
+
* @returns The comparison.
|
|
26
|
+
*/
|
|
27
|
+
const typeofIs = (factory, value, name) => factory.createStrictEquality(factory.createTypeOfExpression(value), factory.createStringLiteral(name));
|
|
28
|
+
/**
|
|
29
|
+
* Joins checks with `&&`, or answers `true` when there are none.
|
|
30
|
+
*
|
|
31
|
+
* @param factory Node factory.
|
|
32
|
+
* @param checks Checks to combine.
|
|
33
|
+
* @returns The combined expression.
|
|
34
|
+
*/
|
|
35
|
+
const all = (factory, checks) => checks.length === 0
|
|
36
|
+
? factory.createTrue()
|
|
37
|
+
: checks.reduce((left, right) => factory.createLogicalAnd(left, right));
|
|
38
|
+
/**
|
|
39
|
+
* Joins checks with `||`, or answers `false` when there are none.
|
|
40
|
+
*
|
|
41
|
+
* @param factory Node factory.
|
|
42
|
+
* @param checks Checks to combine.
|
|
43
|
+
* @returns The combined expression.
|
|
44
|
+
*/
|
|
45
|
+
const any = (factory, checks) => checks.length === 0
|
|
46
|
+
? factory.createFalse()
|
|
47
|
+
: checks.reduce((left, right) => factory.createLogicalOr(left, right));
|
|
48
|
+
/**
|
|
49
|
+
* Reads a property off an expression, quoting the key when it has to.
|
|
50
|
+
*
|
|
51
|
+
* @param factory Node factory.
|
|
52
|
+
* @param value Expression carrying the property.
|
|
53
|
+
* @param name Name of the property.
|
|
54
|
+
* @returns The access expression.
|
|
55
|
+
*/
|
|
56
|
+
const propertyOf = (factory, value, name) => /^[A-Za-z_$][\w$]*$/.test(name)
|
|
57
|
+
? factory.createPropertyAccessExpression(value, name)
|
|
58
|
+
: factory.createElementAccessExpression(value, factory.createStringLiteral(name));
|
|
59
|
+
/**
|
|
60
|
+
* Names a global constructor that is safe to test with `instanceof`.
|
|
61
|
+
*
|
|
62
|
+
* Only the ones every runtime has. A user class goes through the scope check
|
|
63
|
+
* instead, since it may not be in scope as a value at the call site.
|
|
64
|
+
*/
|
|
65
|
+
const GLOBAL_CLASSES = new Set([
|
|
66
|
+
'Date',
|
|
67
|
+
'RegExp',
|
|
68
|
+
'Map',
|
|
69
|
+
'Set',
|
|
70
|
+
'WeakMap',
|
|
71
|
+
'WeakSet',
|
|
72
|
+
'Promise',
|
|
73
|
+
'Error',
|
|
74
|
+
'ArrayBuffer',
|
|
75
|
+
'DataView',
|
|
76
|
+
'Uint8Array',
|
|
77
|
+
'Int8Array',
|
|
78
|
+
'Uint16Array',
|
|
79
|
+
'Int16Array',
|
|
80
|
+
'Uint32Array',
|
|
81
|
+
'Int32Array',
|
|
82
|
+
'Float32Array',
|
|
83
|
+
'Float64Array',
|
|
84
|
+
'BigInt64Array',
|
|
85
|
+
'BigUint64Array',
|
|
86
|
+
]);
|
|
87
|
+
/**
|
|
88
|
+
* Tells whether a type already admits `undefined` on its own.
|
|
89
|
+
*
|
|
90
|
+
* An optional property normally carries it in its declared type, and the check
|
|
91
|
+
* written for that union tests it. Knowing that saves wrapping the result in
|
|
92
|
+
* the same test a second time.
|
|
93
|
+
*
|
|
94
|
+
* @param type Type being inspected.
|
|
95
|
+
* @returns `true` when `undefined` is one of its members.
|
|
96
|
+
*/
|
|
97
|
+
const admitsUndefined = (type) => has(type, typescript_1.default.TypeFlags.Undefined) ||
|
|
98
|
+
(type.isUnion() &&
|
|
99
|
+
type.types.some((member) => has(member, typescript_1.default.TypeFlags.Undefined)));
|
|
100
|
+
/**
|
|
101
|
+
* Lists the types a type is built out of, for the cycle search.
|
|
102
|
+
*
|
|
103
|
+
* Deliberately stops where the writer stops: a class is tested with
|
|
104
|
+
* `instanceof` and a function by `typeof`, so neither is descended into and
|
|
105
|
+
* neither can put a type in a cycle it does not really have.
|
|
106
|
+
*
|
|
107
|
+
* @param type Type being taken apart.
|
|
108
|
+
* @param checker Checker of the program being compiled.
|
|
109
|
+
* @param at Node the lookups happen from.
|
|
110
|
+
* @returns The types it refers to.
|
|
111
|
+
*/
|
|
112
|
+
const partsOf = (type, checker, at) => {
|
|
113
|
+
if (type.isUnionOrIntersection())
|
|
114
|
+
return type.types;
|
|
115
|
+
if (!has(type, typescript_1.default.TypeFlags.Object))
|
|
116
|
+
return [];
|
|
117
|
+
if (checker.isArrayType(type) || checker.isTupleType(type)) {
|
|
118
|
+
return checker.getTypeArguments(type);
|
|
119
|
+
}
|
|
120
|
+
const symbol = type.getSymbol();
|
|
121
|
+
if (symbol !== undefined &&
|
|
122
|
+
((symbol.flags & typescript_1.default.SymbolFlags.Class) !== 0 ||
|
|
123
|
+
GLOBAL_CLASSES.has(symbol.getName()))) {
|
|
124
|
+
return [];
|
|
125
|
+
}
|
|
126
|
+
if (type.getCallSignatures().length > 0 ||
|
|
127
|
+
type.getConstructSignatures().length > 0) {
|
|
128
|
+
return [];
|
|
129
|
+
}
|
|
130
|
+
return type
|
|
131
|
+
.getProperties()
|
|
132
|
+
.map((property) => checker.getTypeOfSymbolAtLocation(property, at));
|
|
133
|
+
};
|
|
134
|
+
/**
|
|
135
|
+
* Finds the types that take part in a cycle.
|
|
136
|
+
*
|
|
137
|
+
* Run before a single node is built, because whether a type needs to become a
|
|
138
|
+
* function is a property of the whole graph and cannot be decided from inside
|
|
139
|
+
* a depth-first walk of it: by the time a cycle is met, the type that closes it
|
|
140
|
+
* has already been written out inline.
|
|
141
|
+
*
|
|
142
|
+
* @param root Type the call asked for.
|
|
143
|
+
* @param checker Checker of the program being compiled.
|
|
144
|
+
* @param at Node the lookups happen from.
|
|
145
|
+
* @returns Every type that refers back to itself, directly or through others.
|
|
146
|
+
*/
|
|
147
|
+
const findRecursive = (root, checker, at) => {
|
|
148
|
+
const recursive = new Set();
|
|
149
|
+
const finished = new Set();
|
|
150
|
+
const path = new Set();
|
|
151
|
+
const visit = (type) => {
|
|
152
|
+
// Met again on the way down: this is the type the cycle closes on.
|
|
153
|
+
if (path.has(type)) {
|
|
154
|
+
recursive.add(type);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (finished.has(type))
|
|
158
|
+
return;
|
|
159
|
+
path.add(type);
|
|
160
|
+
for (const part of partsOf(type, checker, at))
|
|
161
|
+
visit(part);
|
|
162
|
+
path.delete(type);
|
|
163
|
+
finished.add(type);
|
|
164
|
+
};
|
|
165
|
+
visit(root);
|
|
166
|
+
return recursive;
|
|
167
|
+
};
|
|
168
|
+
/**
|
|
169
|
+
* Tells whether a name is usable as a value where the call sits.
|
|
170
|
+
*
|
|
171
|
+
* A class imported with `import type` is erased before the emitted code runs,
|
|
172
|
+
* so naming it in a runtime position would compile and then fail.
|
|
173
|
+
*
|
|
174
|
+
* @param name Name being looked up.
|
|
175
|
+
* @param generation State of the generation.
|
|
176
|
+
* @returns `true` when the name survives into the emitted code as a value.
|
|
177
|
+
*/
|
|
178
|
+
const isValueInScope = (name, generation) => {
|
|
179
|
+
if (GLOBAL_CLASSES.has(name))
|
|
180
|
+
return true;
|
|
181
|
+
const resolved = generation.context.checker.resolveName(name, generation.at, typescript_1.default.SymbolFlags.Value, false);
|
|
182
|
+
if (resolved === undefined)
|
|
183
|
+
return false;
|
|
184
|
+
return !(resolved.declarations ?? []).some((declaration) => {
|
|
185
|
+
if (typescript_1.default.isImportSpecifier(declaration)) {
|
|
186
|
+
return (declaration.isTypeOnly || declaration.parent.parent.isTypeOnly === true);
|
|
187
|
+
}
|
|
188
|
+
if (typescript_1.default.isImportClause(declaration))
|
|
189
|
+
return declaration.isTypeOnly;
|
|
190
|
+
return false;
|
|
191
|
+
});
|
|
192
|
+
};
|
|
193
|
+
/**
|
|
194
|
+
* Writes the check for one type, applied to one expression.
|
|
195
|
+
*
|
|
196
|
+
* @param type Type being checked for.
|
|
197
|
+
* @param value Expression the check is applied to.
|
|
198
|
+
* @param generation State of the generation.
|
|
199
|
+
* @param depth How deep into the type this is.
|
|
200
|
+
* @returns The check, or `null` when the type cannot be proven at runtime.
|
|
201
|
+
*/
|
|
202
|
+
const checkFor = (type, value, generation, depth) => {
|
|
203
|
+
const { checker, factory } = generation.context;
|
|
204
|
+
if (depth > MAX_DEPTH)
|
|
205
|
+
return null;
|
|
206
|
+
// Accepts everything, honestly: there is nothing to check, and saying so is
|
|
207
|
+
// not the same as failing to check.
|
|
208
|
+
if (has(type, typescript_1.default.TypeFlags.Any | typescript_1.default.TypeFlags.Unknown)) {
|
|
209
|
+
return factory.createTrue();
|
|
210
|
+
}
|
|
211
|
+
if (has(type, typescript_1.default.TypeFlags.Never))
|
|
212
|
+
return factory.createFalse();
|
|
213
|
+
if (has(type, typescript_1.default.TypeFlags.Null)) {
|
|
214
|
+
return factory.createStrictEquality(value, factory.createNull());
|
|
215
|
+
}
|
|
216
|
+
if (has(type, typescript_1.default.TypeFlags.Undefined | typescript_1.default.TypeFlags.Void)) {
|
|
217
|
+
return factory.createStrictEquality(value, factory.createIdentifier('undefined'));
|
|
218
|
+
}
|
|
219
|
+
// Literals before the primitive groups they belong to, or `'dark'` would be
|
|
220
|
+
// checked as any old string.
|
|
221
|
+
if (type.isStringLiteral()) {
|
|
222
|
+
return factory.createStrictEquality(value, factory.createStringLiteral(type.value));
|
|
223
|
+
}
|
|
224
|
+
if (type.isNumberLiteral()) {
|
|
225
|
+
return factory.createStrictEquality(value, factory.createNumericLiteral(type.value));
|
|
226
|
+
}
|
|
227
|
+
if (has(type, typescript_1.default.TypeFlags.BooleanLiteral)) {
|
|
228
|
+
const written = checker.typeToString(type);
|
|
229
|
+
return factory.createStrictEquality(value, written === 'true' ? factory.createTrue() : factory.createFalse());
|
|
230
|
+
}
|
|
231
|
+
// A union takes in enums too, which the checker presents as one.
|
|
232
|
+
if (type.isUnion()) {
|
|
233
|
+
const members = type.types.map((member) => checkFor(member, value, generation, depth + 1));
|
|
234
|
+
if (members.some((member) => member === null))
|
|
235
|
+
return null;
|
|
236
|
+
return factory.createParenthesizedExpression(any(factory, members));
|
|
237
|
+
}
|
|
238
|
+
if (type.isIntersection()) {
|
|
239
|
+
const members = type.types.map((member) => checkFor(member, value, generation, depth + 1));
|
|
240
|
+
if (members.some((member) => member === null))
|
|
241
|
+
return null;
|
|
242
|
+
return factory.createParenthesizedExpression(all(factory, members));
|
|
243
|
+
}
|
|
244
|
+
if (has(type, typescript_1.default.TypeFlags.StringLike)) {
|
|
245
|
+
return typeofIs(factory, value, 'string');
|
|
246
|
+
}
|
|
247
|
+
if (has(type, typescript_1.default.TypeFlags.NumberLike)) {
|
|
248
|
+
return typeofIs(factory, value, 'number');
|
|
249
|
+
}
|
|
250
|
+
if (has(type, typescript_1.default.TypeFlags.BigIntLike)) {
|
|
251
|
+
return typeofIs(factory, value, 'bigint');
|
|
252
|
+
}
|
|
253
|
+
if (has(type, typescript_1.default.TypeFlags.BooleanLike)) {
|
|
254
|
+
return typeofIs(factory, value, 'boolean');
|
|
255
|
+
}
|
|
256
|
+
if (has(type, typescript_1.default.TypeFlags.ESSymbolLike)) {
|
|
257
|
+
return typeofIs(factory, value, 'symbol');
|
|
258
|
+
}
|
|
259
|
+
if (!has(type, typescript_1.default.TypeFlags.Object))
|
|
260
|
+
return null;
|
|
261
|
+
// A type that refers back to itself. It becomes a function so that the
|
|
262
|
+
// reference has a name to call, and every mention of it — the first one
|
|
263
|
+
// included — becomes a call to that name.
|
|
264
|
+
if (generation.recursive.has(type)) {
|
|
265
|
+
return checkThroughFunction(type, value, generation, depth);
|
|
266
|
+
}
|
|
267
|
+
// Belt and braces. The cycle search above should have caught anything that
|
|
268
|
+
// could re-enter, so reaching here means it missed one, and writing it out
|
|
269
|
+
// would not terminate.
|
|
270
|
+
if (generation.open.has(type))
|
|
271
|
+
return null;
|
|
272
|
+
return checkForObject(type, value, generation, depth);
|
|
273
|
+
};
|
|
274
|
+
/**
|
|
275
|
+
* Writes a recursive type as a named function, and refers to it by name.
|
|
276
|
+
*
|
|
277
|
+
* The name is registered **before** the body is written, which is the whole
|
|
278
|
+
* trick: the reference that closes the cycle is met while the body is still
|
|
279
|
+
* being built, and it needs something to call.
|
|
280
|
+
*
|
|
281
|
+
* @param type Type being checked for.
|
|
282
|
+
* @param value Expression the check is applied to.
|
|
283
|
+
* @param generation State of the generation.
|
|
284
|
+
* @param depth How deep into the type this is.
|
|
285
|
+
* @returns A call to the function, or `null` when the body cannot be written.
|
|
286
|
+
*/
|
|
287
|
+
const checkThroughFunction = (type, value, generation, depth) => {
|
|
288
|
+
const { factory } = generation.context;
|
|
289
|
+
const existing = generation.declared.get(type);
|
|
290
|
+
if (existing !== undefined) {
|
|
291
|
+
return factory.createCallExpression(factory.createIdentifier(existing.name), undefined, [value]);
|
|
292
|
+
}
|
|
293
|
+
const declaration = {
|
|
294
|
+
name: `check${generation.counter.value++}`,
|
|
295
|
+
};
|
|
296
|
+
generation.declared.set(type, declaration);
|
|
297
|
+
// Written against its own parameter rather than against the caller's
|
|
298
|
+
// expression, because the same body serves every call site.
|
|
299
|
+
const parameter = `r${generation.counter.value++}`;
|
|
300
|
+
const body = checkForObject(type, factory.createIdentifier(parameter), generation,
|
|
301
|
+
// Reset: the depth cap is there to stop an unbounded walk, and a
|
|
302
|
+
// recursive type is bounded by the function call rather than by how deep
|
|
303
|
+
// the writer went to reach it.
|
|
304
|
+
0);
|
|
305
|
+
if (body === null) {
|
|
306
|
+
// Nothing can be emitted, so the half-registered name must go with it.
|
|
307
|
+
generation.declared.delete(type);
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
declaration.body = factory.createArrowFunction(undefined, undefined, [factory.createParameterDeclaration(undefined, undefined, parameter)], undefined, factory.createToken(typescript_1.default.SyntaxKind.EqualsGreaterThanToken), body);
|
|
311
|
+
return factory.createCallExpression(factory.createIdentifier(declaration.name), undefined, [value]);
|
|
312
|
+
};
|
|
313
|
+
/**
|
|
314
|
+
* Writes the check for an object type: array, tuple, class, or plain shape.
|
|
315
|
+
*
|
|
316
|
+
* @param type Type being checked for.
|
|
317
|
+
* @param value Expression the check is applied to.
|
|
318
|
+
* @param generation State of the generation.
|
|
319
|
+
* @param depth How deep into the type this is.
|
|
320
|
+
* @returns The check, or `null` when the type cannot be proven at runtime.
|
|
321
|
+
*/
|
|
322
|
+
const checkForObject = (type, value, generation, depth) => {
|
|
323
|
+
const { checker, factory } = generation.context;
|
|
324
|
+
generation.open.add(type);
|
|
325
|
+
try {
|
|
326
|
+
if (checker.isArrayType(type)) {
|
|
327
|
+
const [element] = checker.getTypeArguments(type);
|
|
328
|
+
if (element === undefined)
|
|
329
|
+
return null;
|
|
330
|
+
const parameter = `e${generation.counter.value++}`;
|
|
331
|
+
const inner = checkFor(element, factory.createIdentifier(parameter), generation, depth + 1);
|
|
332
|
+
if (inner === null)
|
|
333
|
+
return null;
|
|
334
|
+
// Every element, not a sample: a check that looked at the first one
|
|
335
|
+
// would be exactly the optimistic half-answer this file refuses to
|
|
336
|
+
// produce. What that costs is the caller's to know, and documented.
|
|
337
|
+
return factory.createLogicalAnd(factory.createCallExpression(factory.createPropertyAccessExpression(factory.createIdentifier('Array'), 'isArray'), undefined, [value]), factory.createCallExpression(factory.createPropertyAccessExpression(value, 'every'), undefined, [
|
|
338
|
+
factory.createArrowFunction(undefined, undefined, [
|
|
339
|
+
factory.createParameterDeclaration(undefined, undefined, parameter),
|
|
340
|
+
], undefined, factory.createToken(typescript_1.default.SyntaxKind.EqualsGreaterThanToken), inner),
|
|
341
|
+
]));
|
|
342
|
+
}
|
|
343
|
+
if (checker.isTupleType(type)) {
|
|
344
|
+
const reference = type;
|
|
345
|
+
const target = reference.target;
|
|
346
|
+
// A rest or optional element makes the length variable, and a length
|
|
347
|
+
// this cannot pin down is one it will not guess at.
|
|
348
|
+
if (target.hasRestElement || target.minLength !== target.fixedLength) {
|
|
349
|
+
return null;
|
|
350
|
+
}
|
|
351
|
+
const elements = checker.getTypeArguments(reference);
|
|
352
|
+
const checks = [
|
|
353
|
+
factory.createCallExpression(factory.createPropertyAccessExpression(factory.createIdentifier('Array'), 'isArray'), undefined, [value]),
|
|
354
|
+
factory.createStrictEquality(factory.createPropertyAccessExpression(value, 'length'), factory.createNumericLiteral(elements.length)),
|
|
355
|
+
];
|
|
356
|
+
for (const [index, element] of elements.entries()) {
|
|
357
|
+
const inner = checkFor(element, factory.createElementAccessExpression(value, factory.createNumericLiteral(index)), generation, depth + 1);
|
|
358
|
+
if (inner === null)
|
|
359
|
+
return null;
|
|
360
|
+
checks.push(inner);
|
|
361
|
+
}
|
|
362
|
+
return factory.createParenthesizedExpression(all(factory, checks));
|
|
363
|
+
}
|
|
364
|
+
const symbol = type.getSymbol();
|
|
365
|
+
// A class, or one of the built-in types that behaves like one.
|
|
366
|
+
//
|
|
367
|
+
// The built-ins do not carry `SymbolFlags.Class`: the standard library
|
|
368
|
+
// declares `interface Date` beside a separate `declare var Date`, so the
|
|
369
|
+
// flag check alone misses them — and then the shape path below writes out
|
|
370
|
+
// a `typeof` check for all fifty methods of `Date`, including its symbol
|
|
371
|
+
// keyed member, which cannot be named in source at all. Matching the
|
|
372
|
+
// known names first is what keeps that from being emitted.
|
|
373
|
+
const isClassLike = symbol !== undefined &&
|
|
374
|
+
((symbol.flags & typescript_1.default.SymbolFlags.Class) !== 0 ||
|
|
375
|
+
GLOBAL_CLASSES.has(symbol.getName()));
|
|
376
|
+
if (isClassLike && symbol !== undefined) {
|
|
377
|
+
const name = symbol.getName();
|
|
378
|
+
return isValueInScope(name, generation)
|
|
379
|
+
? factory.createBinaryExpression(value, factory.createToken(typescript_1.default.SyntaxKind.InstanceOfKeyword), factory.createIdentifier(name))
|
|
380
|
+
: null;
|
|
381
|
+
}
|
|
382
|
+
// A function type. Only that it is callable can be checked — no runtime
|
|
383
|
+
// sees a signature — and the shape below would be wrong for one.
|
|
384
|
+
if (type.getCallSignatures().length > 0 ||
|
|
385
|
+
type.getConstructSignatures().length > 0) {
|
|
386
|
+
return typeofIs(factory, value, 'function');
|
|
387
|
+
}
|
|
388
|
+
// An index signature means arbitrary keys, which this does not attempt.
|
|
389
|
+
if (checker.getIndexInfosOfType(type).length > 0)
|
|
390
|
+
return null;
|
|
391
|
+
const properties = type.getProperties();
|
|
392
|
+
// An object type with nothing in it accepts any object, and a check that
|
|
393
|
+
// only says "is an object" is not what the caller asked about.
|
|
394
|
+
if (properties.length === 0)
|
|
395
|
+
return null;
|
|
396
|
+
const checks = [
|
|
397
|
+
factory.createStrictInequality(value, factory.createNull()),
|
|
398
|
+
typeofIs(factory, value, 'object'),
|
|
399
|
+
];
|
|
400
|
+
for (const property of properties) {
|
|
401
|
+
const name = property.getName();
|
|
402
|
+
// A member keyed by a symbol. The compiler spells these `__@name@id`,
|
|
403
|
+
// which is not a property name any source can write, so there is no
|
|
404
|
+
// honest check to emit for one.
|
|
405
|
+
if (name.startsWith('__@'))
|
|
406
|
+
return null;
|
|
407
|
+
const propertyType = checker.getTypeOfSymbolAtLocation(property, generation.at);
|
|
408
|
+
const optional = (property.flags & typescript_1.default.SymbolFlags.Optional) !== 0;
|
|
409
|
+
const access = propertyOf(factory, value, name);
|
|
410
|
+
const inner = checkFor(propertyType, access, generation, depth + 1);
|
|
411
|
+
if (inner === null)
|
|
412
|
+
return null;
|
|
413
|
+
// Absent and present-but-undefined are the same here, which is what
|
|
414
|
+
// reading a missing property gives back anyway. Extra properties are
|
|
415
|
+
// never rejected: structural typing allows them, and rejecting them
|
|
416
|
+
// would make this disagree with the compiler that produced it.
|
|
417
|
+
//
|
|
418
|
+
// Only wrapped when the property's own type does not already admit
|
|
419
|
+
// `undefined`, which it normally does — otherwise the emitted check
|
|
420
|
+
// asks the same question twice.
|
|
421
|
+
const needsWrapping = optional && !admitsUndefined(propertyType);
|
|
422
|
+
checks.push(needsWrapping
|
|
423
|
+
? factory.createParenthesizedExpression(factory.createLogicalOr(factory.createStrictEquality(access, factory.createIdentifier('undefined')), inner))
|
|
424
|
+
: inner);
|
|
425
|
+
}
|
|
426
|
+
return factory.createParenthesizedExpression(all(factory, checks));
|
|
427
|
+
}
|
|
428
|
+
finally {
|
|
429
|
+
generation.open.delete(type);
|
|
430
|
+
}
|
|
431
|
+
};
|
|
432
|
+
/**
|
|
433
|
+
* Builds the shape test for a type, when one can be proven.
|
|
434
|
+
*
|
|
435
|
+
* @param type Type the call asked for.
|
|
436
|
+
* @param written The type as the consumer wrote it, for error messages.
|
|
437
|
+
* @param at Node the call sits at, for scope.
|
|
438
|
+
* @param context Compilation in progress.
|
|
439
|
+
* @returns The object literal to pass at runtime, or `null` when the type
|
|
440
|
+
* cannot be checked honestly.
|
|
441
|
+
*/
|
|
442
|
+
const buildStructuralTest = (type, written, at, context, options = {}) => {
|
|
443
|
+
const { factory } = context;
|
|
444
|
+
const generation = {
|
|
445
|
+
context,
|
|
446
|
+
at,
|
|
447
|
+
open: new Set(),
|
|
448
|
+
recursive: findRecursive(type, context.checker, at),
|
|
449
|
+
declared: new Map(),
|
|
450
|
+
counter: { value: 0 },
|
|
451
|
+
};
|
|
452
|
+
const parameter = 'v';
|
|
453
|
+
const body = checkFor(type, factory.createIdentifier(parameter), generation, 0);
|
|
454
|
+
if (body === null)
|
|
455
|
+
return null;
|
|
456
|
+
// Built only where it is asked for. A filter needs yes or no, and emitting a
|
|
457
|
+
// second walker at every one of those call sites would double the code for
|
|
458
|
+
// a message nobody reads.
|
|
459
|
+
const explainer = options.explain === true
|
|
460
|
+
? (0, explain_1.buildExplainer)(type, written, at, context.checker, factory,
|
|
461
|
+
// Handed the same yes-or-no builder the fast check uses, so the
|
|
462
|
+
// two can never disagree about what counts as a match. A separate
|
|
463
|
+
// generation, since the walker and the check number their own
|
|
464
|
+
// helpers independently.
|
|
465
|
+
(inner, value) => checkFor(inner, value, {
|
|
466
|
+
context,
|
|
467
|
+
at,
|
|
468
|
+
open: new Set(),
|
|
469
|
+
recursive: generation.recursive,
|
|
470
|
+
declared: new Map(),
|
|
471
|
+
counter: generation.counter,
|
|
472
|
+
}, 0))
|
|
473
|
+
: null;
|
|
474
|
+
const test = factory.createObjectLiteralExpression([
|
|
475
|
+
factory.createPropertyAssignment('name', factory.createStringLiteral(written)),
|
|
476
|
+
...(explainer === null
|
|
477
|
+
? []
|
|
478
|
+
: [factory.createPropertyAssignment('explain', explainer)]),
|
|
479
|
+
factory.createPropertyAssignment('matches', factory.createArrowFunction(undefined, undefined, [factory.createParameterDeclaration(undefined, undefined, parameter)], undefined, factory.createToken(typescript_1.default.SyntaxKind.EqualsGreaterThanToken), body)),
|
|
480
|
+
], false);
|
|
481
|
+
if (generation.declared.size === 0)
|
|
482
|
+
return test;
|
|
483
|
+
// Recursive types need somewhere to live that is evaluated once rather than
|
|
484
|
+
// per element, and that does not add a name to the surrounding scope. The
|
|
485
|
+
// functions are hoisted into a block that runs where the call sits and hands
|
|
486
|
+
// back the test itself.
|
|
487
|
+
const statements = [];
|
|
488
|
+
for (const declaration of generation.declared.values()) {
|
|
489
|
+
if (declaration.body === undefined)
|
|
490
|
+
continue;
|
|
491
|
+
statements.push(factory.createVariableStatement(undefined, factory.createVariableDeclarationList([
|
|
492
|
+
factory.createVariableDeclaration(declaration.name, undefined, undefined, declaration.body),
|
|
493
|
+
], typescript_1.default.NodeFlags.Const)));
|
|
494
|
+
}
|
|
495
|
+
statements.push(factory.createReturnStatement(test));
|
|
496
|
+
return factory.createCallExpression(factory.createParenthesizedExpression(factory.createArrowFunction(undefined, undefined, [], undefined, factory.createToken(typescript_1.default.SyntaxKind.EqualsGreaterThanToken), factory.createBlock(statements, true))), undefined, []);
|
|
497
|
+
};
|
|
498
|
+
exports.buildStructuralTest = buildStructuralTest;
|
package/package.json
CHANGED