@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fulcro/transform-core",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "Shared machinery behind the Fulcro compile time transformers. Installed for you; not meant to be depended on directly.",
5
5
  "keywords": [
6
6
  "typescript",