@fougere/compiler 0.10.0-alpha.0 → 0.12.0-alpha.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/boot.d.ts +8 -3
- package/dist/boot.d.ts.map +1 -1
- package/dist/boot.js +14 -4
- package/dist/boot.js.map +1 -1
- package/dist/scan/Implemented.d.ts +14 -0
- package/dist/scan/Implemented.d.ts.map +1 -0
- package/dist/scan/Implemented.js +34 -0
- package/dist/scan/Implemented.js.map +1 -0
- package/dist/scan/TypeProgram.d.ts +21 -0
- package/dist/scan/TypeProgram.d.ts.map +1 -0
- package/dist/scan/TypeProgram.js +177 -0
- package/dist/scan/TypeProgram.js.map +1 -0
- package/dist/scan/emit.d.ts.map +1 -1
- package/dist/scan/emit.js +37 -28
- package/dist/scan/emit.js.map +1 -1
- package/dist/scan/handler-parser.d.ts +0 -3
- package/dist/scan/handler-parser.d.ts.map +1 -1
- package/dist/scan/handler-parser.js +111 -278
- package/dist/scan/handler-parser.js.map +1 -1
- package/dist/scan/refusals.d.ts +0 -7
- package/dist/scan/refusals.d.ts.map +1 -1
- package/dist/scan/refusals.js +11 -2
- package/dist/scan/refusals.js.map +1 -1
- package/dist/scan/scanner.d.ts.map +1 -1
- package/dist/scan/scanner.js +118 -82
- package/dist/scan/scanner.js.map +1 -1
- package/package.json +6 -6
- package/src/boot.ts +23 -7
- package/src/scan/Implemented.ts +41 -0
- package/src/scan/TypeProgram.ts +199 -0
- package/src/scan/emit.ts +32 -20
- package/src/scan/handler-parser.ts +122 -306
- package/src/scan/refusals.ts +13 -2
- package/src/scan/scanner.ts +158 -94
|
@@ -4,174 +4,10 @@ import { type ErrorCode } from '@fougere/core';
|
|
|
4
4
|
|
|
5
5
|
import type ts from '@typescript/typescript6';
|
|
6
6
|
import { refusalsIn } from './refusals.js';
|
|
7
|
+
import { loadTS, getTS, checkedSourceOf, sourceOf, findDefaultClass, projectOf } from './TypeProgram.js';
|
|
7
8
|
import { readFileSync, existsSync, statSync } from 'node:fs';
|
|
8
9
|
import { join, dirname, resolve as resolvePath } from 'node:path';
|
|
9
10
|
|
|
10
|
-
/** Lazy-loaded TypeScript module — avoids bundling the 9MB compiler. */
|
|
11
|
-
let _ts: typeof ts | undefined;
|
|
12
|
-
async function loadTS(): Promise<typeof ts> {
|
|
13
|
-
if (!_ts) _ts = (await import('@typescript/typescript6')).default;
|
|
14
|
-
return _ts;
|
|
15
|
-
}
|
|
16
|
-
function getTS(): typeof ts {
|
|
17
|
-
if (!_ts) throw new Error('TypeScript not loaded — call an async parse function first');
|
|
18
|
-
return _ts;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
interface TypeProject {
|
|
22
|
-
roots: Set<string>;
|
|
23
|
-
options: ts.CompilerOptions;
|
|
24
|
-
program: ts.Program;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/** One checked program per project/configuration during a scan. */
|
|
28
|
-
const typeProjects = new Map<string, TypeProject>();
|
|
29
|
-
const compilerProjects = new Map<string, { key: string; roots: string[]; options: ts.CompilerOptions }>();
|
|
30
|
-
|
|
31
|
-
/** What survives a run. */
|
|
32
|
-
const sourceFiles = new Map<string, { mtime: number; file: ts.SourceFile }>();
|
|
33
|
-
const retained = new Map<string, { host: ts.CompilerHost; program: ts.Program }>();
|
|
34
|
-
|
|
35
|
-
export function resetTypePrograms(): void {
|
|
36
|
-
typeProjects.clear();
|
|
37
|
-
compilerProjects.clear();
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function keptHost(key: string, options: ts.CompilerOptions): ts.CompilerHost {
|
|
41
|
-
const cached = retained.get(key);
|
|
42
|
-
if (cached) return cached.host;
|
|
43
|
-
const typescript = getTS();
|
|
44
|
-
const base = typescript.createCompilerHost(options);
|
|
45
|
-
const host: ts.CompilerHost = {
|
|
46
|
-
...base,
|
|
47
|
-
getSourceFile(fileName, languageVersion, onError, shouldCreate) {
|
|
48
|
-
const path = resolvePath(fileName);
|
|
49
|
-
const mtime = statSync(path, { throwIfNoEntry: false })?.mtimeMs ?? -1;
|
|
50
|
-
const cached = sourceFiles.get(path);
|
|
51
|
-
if (cached && cached.mtime === mtime) return cached.file;
|
|
52
|
-
const file = base.getSourceFile(fileName, languageVersion, onError, shouldCreate);
|
|
53
|
-
if (file && mtime >= 0) sourceFiles.set(path, { mtime, file });
|
|
54
|
-
return file;
|
|
55
|
-
},
|
|
56
|
-
};
|
|
57
|
-
retained.set(key, { host, program: undefined as unknown as ts.Program });
|
|
58
|
-
return host;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function builtProgram(key: string, roots: readonly string[], options: ts.CompilerOptions): ts.Program {
|
|
62
|
-
const typescript = getTS();
|
|
63
|
-
const host = keptHost(key, options);
|
|
64
|
-
const program = typescript.createProgram({
|
|
65
|
-
rootNames: [...roots], options, host, oldProgram: retained.get(key)?.program,
|
|
66
|
-
});
|
|
67
|
-
retained.set(key, { host, program });
|
|
68
|
-
return program;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function compilerProjectOf(filePath: string, projectRoot?: string): { key: string; roots: string[]; options: ts.CompilerOptions } {
|
|
72
|
-
const typescript = getTS();
|
|
73
|
-
const absolute = resolvePath(filePath);
|
|
74
|
-
const configPath = typescript.findConfigFile(dirname(absolute), typescript.sys.fileExists);
|
|
75
|
-
|
|
76
|
-
if (configPath) {
|
|
77
|
-
const key = `${configPath}:${projectRoot ?? ''}`;
|
|
78
|
-
const cached = compilerProjects.get(key);
|
|
79
|
-
if (cached) return cached;
|
|
80
|
-
|
|
81
|
-
const read = typescript.readConfigFile(configPath, typescript.sys.readFile);
|
|
82
|
-
if (read.error) throw new Error(typescript.flattenDiagnosticMessageText(read.error.messageText, '\n'));
|
|
83
|
-
const parsed = typescript.parseJsonConfigFileContent(read.config, typescript.sys, dirname(configPath));
|
|
84
|
-
const configured = {
|
|
85
|
-
key,
|
|
86
|
-
// Compiler options belong to the project; its entire include glob does not belong
|
|
87
|
-
// to this scan. Each declaration inspected below becomes a root and TypeScript
|
|
88
|
-
// follows its imports. Seeding the monorepo here made a one-file scan compile it all.
|
|
89
|
-
roots: [],
|
|
90
|
-
// Handlers may be authored or emitted as JavaScript. They still need to belong to
|
|
91
|
-
// the checked program so constructor parsing does not fail on the first cold scan.
|
|
92
|
-
options: { ...parsed.options, allowJs: true, noEmit: true },
|
|
93
|
-
};
|
|
94
|
-
compilerProjects.set(key, configured);
|
|
95
|
-
return configured;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
const key = projectRoot ?? dirname(absolute);
|
|
99
|
-
const cached = compilerProjects.get(key);
|
|
100
|
-
if (cached) return cached;
|
|
101
|
-
const configured = {
|
|
102
|
-
key,
|
|
103
|
-
roots: [],
|
|
104
|
-
options: {
|
|
105
|
-
target: typescript.ScriptTarget.ES2022,
|
|
106
|
-
module: typescript.ModuleKind.Node16,
|
|
107
|
-
moduleResolution: typescript.ModuleResolutionKind.Node16,
|
|
108
|
-
strict: true,
|
|
109
|
-
skipLibCheck: true,
|
|
110
|
-
allowJs: true,
|
|
111
|
-
noEmit: true,
|
|
112
|
-
},
|
|
113
|
-
};
|
|
114
|
-
compilerProjects.set(key, configured);
|
|
115
|
-
return configured;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/** Declare every file a run will read, so one program covers it. */
|
|
119
|
-
export async function seedTypeProgram(filePaths: readonly string[], projectRoot?: string): Promise<void> {
|
|
120
|
-
const typescript = await loadTS();
|
|
121
|
-
const grouped = new Map<string, { options: ts.CompilerOptions; paths: string[] }>();
|
|
122
|
-
|
|
123
|
-
for (const filePath of filePaths) {
|
|
124
|
-
const absolute = resolvePath(filePath);
|
|
125
|
-
const typescript = getTS();
|
|
126
|
-
const configured = compilerProjectOf(absolute, projectRoot);
|
|
127
|
-
const group = grouped.get(configured.key) ?? { options: configured.options, paths: [] };
|
|
128
|
-
group.paths.push(absolute);
|
|
129
|
-
grouped.set(configured.key, group);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
for (const [key, { options, paths }] of grouped) {
|
|
133
|
-
const roots = new Set(typeProjects.get(key)?.roots ?? []);
|
|
134
|
-
for (const path of paths) roots.add(path);
|
|
135
|
-
typeProjects.set(key, { roots, options, program: builtProgram(key, [...roots], options) });
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/** The program a file belongs to, built once and widened as more files are asked for. */
|
|
140
|
-
function projectOf(filePath: string, projectRoot?: string): { program: ts.Program; absolute: string } {
|
|
141
|
-
const absolute = resolvePath(filePath);
|
|
142
|
-
const configured = compilerProjectOf(absolute, projectRoot);
|
|
143
|
-
let project = typeProjects.get(configured.key);
|
|
144
|
-
|
|
145
|
-
if (!project) {
|
|
146
|
-
// `path.resolve` is variadic, so handing it directly to `map` also passed the
|
|
147
|
-
// index and the whole roots array as path segments. A fixture without a warm scan
|
|
148
|
-
// cache exposed that first-run-only failure.
|
|
149
|
-
const roots = new Set(configured.roots.map((root) => resolvePath(root)));
|
|
150
|
-
roots.add(absolute);
|
|
151
|
-
const program = builtProgram(configured.key, [...roots], configured.options);
|
|
152
|
-
project = { roots, options: configured.options, program };
|
|
153
|
-
typeProjects.set(configured.key, project);
|
|
154
|
-
} else if (!project.roots.has(absolute)) {
|
|
155
|
-
project.roots.add(absolute);
|
|
156
|
-
project.program = builtProgram(configured.key, [...project.roots], project.options);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
return { program: project.program, absolute };
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
function checkedSourceOf(filePath: string, projectRoot?: string): { source: ts.SourceFile; checker: ts.TypeChecker } {
|
|
163
|
-
const { program, absolute } = projectOf(filePath, projectRoot);
|
|
164
|
-
const source = program.getSourceFile(absolute);
|
|
165
|
-
if (!source) throw new Error(`TypeScript did not include '${absolute}' in its program.`);
|
|
166
|
-
|
|
167
|
-
return { source, checker: program.getTypeChecker() };
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/** A file, opened. Five places read and parsed one, each spelling the same two calls. */
|
|
171
|
-
function sourceOf(filePath: string): ts.SourceFile {
|
|
172
|
-
const ts = getTS();
|
|
173
|
-
return ts.createSourceFile(filePath, readFileSync(filePath, 'utf-8'), ts.ScriptTarget.Latest, true);
|
|
174
|
-
}
|
|
175
11
|
|
|
176
12
|
/** One declared parameter — a constructor's and a method's are read the same way. */
|
|
177
13
|
function parsedParam(param: ts.ParameterDeclaration, source: ts.SourceFile, checker?: ts.TypeChecker): Param {
|
|
@@ -194,75 +30,81 @@ function parseTypeNode(node: ts.TypeNode, source: ts.SourceFile, checker?: ts.Ty
|
|
|
194
30
|
const ts = getTS();
|
|
195
31
|
const raw = node.getText(source);
|
|
196
32
|
|
|
197
|
-
if (checker)
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
name: node.typeName.text,
|
|
206
|
-
generics: node.typeArguments?.map((arg) => parseTypeNode(arg, source, checker)) ?? [],
|
|
207
|
-
};
|
|
208
|
-
}
|
|
209
|
-
return parseCheckedType(checker.getTypeFromTypeNode(node), raw, checker);
|
|
33
|
+
if (checker) return announced(node, source, checker) ?? parseCheckedType(checker.getTypeFromTypeNode(node), raw, checker);
|
|
34
|
+
|
|
35
|
+
if (ts.isUnionTypeNode(node)) return fromUnion(node, source, raw);
|
|
36
|
+
if (ts.isTypeReferenceNode(node)) return fromReference(node, source, raw);
|
|
37
|
+
if (ts.isArrayTypeNode(node)) {
|
|
38
|
+
const inner = parseTypeNode(node.elementType, source);
|
|
39
|
+
|
|
40
|
+
return { ...inner, array: true, arrayDepth: (inner.arrayDepth ?? 0) + 1, raw };
|
|
210
41
|
}
|
|
211
42
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
t.kind === ts.SyntaxKind.UndefinedKeyword || t.kind === ts.SyntaxKind.VoidKeyword,
|
|
226
|
-
);
|
|
227
|
-
if (nonNull.length === 1) {
|
|
228
|
-
const inner = parseTypeNode(nonNull[0], source);
|
|
229
|
-
return { ...inner, nullable: nullable || inner.nullable, undefined: undefinable || inner.undefined, raw };
|
|
230
|
-
}
|
|
231
|
-
return { raw, name: raw, nullable, undefined: undefinable };
|
|
43
|
+
return fromKeyword(node, raw) ?? { raw, name: raw };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* `Fact<T>` and `Pipe<T>` are deliberately transparent in TypeScript (`= T`), because both
|
|
48
|
+
* receive the payload itself — one reads it, the other answers the value every reader then gets.
|
|
49
|
+
* The checker erases the marker, while the binding plan still needs it to tell either from an
|
|
50
|
+
* ordinary body.
|
|
51
|
+
*/
|
|
52
|
+
function announced(node: ts.TypeNode, source: ts.SourceFile, checker: ts.TypeChecker): TypeRef | undefined {
|
|
53
|
+
const ts = getTS();
|
|
54
|
+
if (!ts.isTypeReferenceNode(node) || !ts.isIdentifier(node.typeName) || !ANNOUNCED.has(node.typeName.text)) {
|
|
55
|
+
return undefined;
|
|
232
56
|
}
|
|
233
57
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
58
|
+
return {
|
|
59
|
+
raw: node.getText(source),
|
|
60
|
+
name: node.typeName.text,
|
|
61
|
+
generics: node.typeArguments?.map((arg) => parseTypeNode(arg, source, checker)) ?? [],
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** `T | null`, `T | undefined`, `T | void` — the absence is an axis, the rest is the type. */
|
|
66
|
+
function fromUnion(node: ts.UnionTypeNode, source: ts.SourceFile, raw: string): TypeRef {
|
|
67
|
+
const ts = getTS();
|
|
68
|
+
const isNull = (one: ts.TypeNode) =>
|
|
69
|
+
(ts.isLiteralTypeNode(one) && one.literal.kind === ts.SyntaxKind.NullKeyword)
|
|
70
|
+
|| one.kind === ts.SyntaxKind.NullKeyword;
|
|
71
|
+
const isAbsent = (one: ts.TypeNode) =>
|
|
72
|
+
one.kind === ts.SyntaxKind.UndefinedKeyword || one.kind === ts.SyntaxKind.VoidKeyword;
|
|
237
73
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
}
|
|
74
|
+
const nullable = node.types.some(isNull);
|
|
75
|
+
const undefinable = node.types.some(isAbsent);
|
|
76
|
+
const nonNull = node.types.filter((one) => !isNull(one) && !isAbsent(one));
|
|
242
77
|
|
|
243
|
-
|
|
244
|
-
if (typeName === 'Array' && node.typeArguments?.length === 1) {
|
|
245
|
-
const inner = parseTypeNode(node.typeArguments[0], source);
|
|
246
|
-
return { ...inner, array: true, arrayDepth: (inner.arrayDepth ?? 0) + 1, raw };
|
|
247
|
-
}
|
|
78
|
+
if (nonNull.length !== 1) return { raw, name: raw, nullable, undefined: undefinable };
|
|
248
79
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
}
|
|
80
|
+
const inner = parseTypeNode(nonNull[0]!, source);
|
|
81
|
+
|
|
82
|
+
return { ...inner, nullable: nullable || inner.nullable, undefined: undefinable || inner.undefined, raw };
|
|
83
|
+
}
|
|
254
84
|
|
|
255
|
-
|
|
256
|
-
|
|
85
|
+
/** `Promise<T>` and `Array<T>` are carriers; anything else keeps its name and its generics. */
|
|
86
|
+
function fromReference(node: ts.TypeReferenceNode, source: ts.SourceFile, raw: string): TypeRef {
|
|
87
|
+
const typeName = node.typeName.getText(source);
|
|
88
|
+
|
|
89
|
+
if (typeName === 'Promise' && node.typeArguments?.length === 1) {
|
|
90
|
+
return { ...parseTypeNode(node.typeArguments[0]!, source), promise: true, raw };
|
|
257
91
|
}
|
|
258
92
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
93
|
+
if (typeName === 'Array' && node.typeArguments?.length === 1) {
|
|
94
|
+
const inner = parseTypeNode(node.typeArguments[0]!, source);
|
|
95
|
+
|
|
262
96
|
return { ...inner, array: true, arrayDepth: (inner.arrayDepth ?? 0) + 1, raw };
|
|
263
97
|
}
|
|
264
98
|
|
|
265
|
-
|
|
99
|
+
if (node.typeArguments && node.typeArguments.length > 0) {
|
|
100
|
+
return { raw, name: typeName, generics: node.typeArguments.map((arg) => parseTypeNode(arg, source)) };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return { raw, name: typeName };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function fromKeyword(node: ts.TypeNode, raw: string): TypeRef | undefined {
|
|
107
|
+
const ts = getTS();
|
|
266
108
|
switch (node.kind) {
|
|
267
109
|
case ts.SyntaxKind.StringKeyword: return { raw, name: 'string' };
|
|
268
110
|
case ts.SyntaxKind.NumberKeyword: return { raw, name: 'number' };
|
|
@@ -272,15 +114,8 @@ function parseTypeNode(node: ts.TypeNode, source: ts.SourceFile, checker?: ts.Ty
|
|
|
272
114
|
case ts.SyntaxKind.NullKeyword: return { raw, name: 'null', nullable: true };
|
|
273
115
|
case ts.SyntaxKind.AnyKeyword: return { raw, name: 'any' };
|
|
274
116
|
case ts.SyntaxKind.UnknownKeyword: return { raw, name: 'unknown' };
|
|
117
|
+
default: return undefined;
|
|
275
118
|
}
|
|
276
|
-
|
|
277
|
-
// Object literal type: { title: string; limit: number }
|
|
278
|
-
if (ts.isTypeLiteralNode(node)) {
|
|
279
|
-
return { raw, name: raw };
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
// Fallback
|
|
283
|
-
return { raw, name: raw };
|
|
284
119
|
}
|
|
285
120
|
|
|
286
121
|
function meaningfulSymbolName(type: ts.Type, checker: ts.TypeChecker): string {
|
|
@@ -305,41 +140,10 @@ function parseCheckedType(type: ts.Type, raw: string, checker: ts.TypeChecker, d
|
|
|
305
140
|
const typescript = getTS();
|
|
306
141
|
if (depth > 12) return { raw, name: checker.typeToString(type) };
|
|
307
142
|
|
|
308
|
-
if (type.isUnion())
|
|
309
|
-
const nullable = type.types.some((member) => (member.flags & typescript.TypeFlags.Null) !== 0);
|
|
310
|
-
const undefinable = type.types.some((member) =>
|
|
311
|
-
(member.flags & (typescript.TypeFlags.Undefined | typescript.TypeFlags.Void)) !== 0,
|
|
312
|
-
);
|
|
313
|
-
const members = type.types.filter((member) =>
|
|
314
|
-
(member.flags & (typescript.TypeFlags.Null | typescript.TypeFlags.Undefined | typescript.TypeFlags.Void)) === 0,
|
|
315
|
-
);
|
|
316
|
-
// The checker represents the `boolean` keyword itself as `false | true`. Preserve
|
|
317
|
-
// the primitive vocabulary consumed by binding and presenter metadata.
|
|
318
|
-
if (
|
|
319
|
-
members.length > 0
|
|
320
|
-
&& members.every((member) => (member.flags & typescript.TypeFlags.BooleanLiteral) !== 0)
|
|
321
|
-
) {
|
|
322
|
-
return { raw, name: 'boolean', nullable, undefined: undefinable };
|
|
323
|
-
}
|
|
324
|
-
if (members.length === 1) {
|
|
325
|
-
const inner = parseCheckedType(members[0]!, raw, checker, depth + 1);
|
|
326
|
-
return {
|
|
327
|
-
...inner,
|
|
328
|
-
raw,
|
|
329
|
-
nullable: nullable || inner.nullable,
|
|
330
|
-
undefined: undefinable || inner.undefined,
|
|
331
|
-
};
|
|
332
|
-
}
|
|
333
|
-
return {
|
|
334
|
-
raw,
|
|
335
|
-
name: members.map((member) => meaningfulSymbolName(member, checker)).join(' | ') || raw,
|
|
336
|
-
nullable,
|
|
337
|
-
undefined: undefinable,
|
|
338
|
-
};
|
|
339
|
-
}
|
|
143
|
+
if (type.isUnion()) return fromCheckedUnion(type, raw, checker, depth);
|
|
340
144
|
|
|
341
|
-
// Present at runtime in the supported compiler versions, but intentionally omitted
|
|
342
|
-
//
|
|
145
|
+
// Present at runtime in the supported compiler versions, but intentionally omitted from
|
|
146
|
+
// TypeScript's public TypeChecker declaration.
|
|
343
147
|
const promised = (checker as ts.TypeChecker & {
|
|
344
148
|
getPromisedTypeOfPromise(candidate: ts.Type): ts.Type | undefined;
|
|
345
149
|
}).getPromisedTypeOfPromise(type);
|
|
@@ -348,24 +152,19 @@ function parseCheckedType(type: ts.Type, raw: string, checker: ts.TypeChecker, d
|
|
|
348
152
|
if (checker.isArrayType(type)) {
|
|
349
153
|
const [element] = checker.getTypeArguments(type as ts.TypeReference);
|
|
350
154
|
const inner = element ? parseCheckedType(element, raw, checker, depth + 1) : { raw, name: 'unknown' };
|
|
155
|
+
|
|
351
156
|
return { ...inner, raw, array: true, arrayDepth: (inner.arrayDepth ?? 0) + 1 };
|
|
352
157
|
}
|
|
353
158
|
|
|
354
|
-
|
|
355
|
-
if (
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
if ((type.flags & typescript.TypeFlags.Any) !== 0) return { raw, name: 'any' };
|
|
361
|
-
if ((type.flags & typescript.TypeFlags.Unknown) !== 0) return { raw, name: 'unknown' };
|
|
362
|
-
|
|
363
|
-
const reference = type as ts.TypeReference;
|
|
364
|
-
// A declared alias such as `Emit<PostPublished>` may reduce to a function or object
|
|
365
|
-
// type, so it is no longer a TypeReference. The checker keeps its arguments beside
|
|
366
|
-
// `aliasSymbol`; losing them turns DI keys into bare `Emit`/`Facade`.
|
|
159
|
+
const primitive = fromCheckedFlags(type.flags, raw);
|
|
160
|
+
if (primitive) return primitive;
|
|
161
|
+
|
|
162
|
+
// A declared alias such as `Emit<PostPublished>` may reduce to a function or object type, so it
|
|
163
|
+
// is no longer a TypeReference. The checker keeps its arguments beside `aliasSymbol`; losing
|
|
164
|
+
// them turns DI keys into bare `Emit`/`Facade`.
|
|
367
165
|
const args = (type as ts.Type & { aliasTypeArguments?: readonly ts.Type[] }).aliasTypeArguments
|
|
368
|
-
?? checker.getTypeArguments(
|
|
166
|
+
?? checker.getTypeArguments(type as ts.TypeReference);
|
|
167
|
+
|
|
369
168
|
return {
|
|
370
169
|
raw,
|
|
371
170
|
name: meaningfulSymbolName(type, checker),
|
|
@@ -373,6 +172,52 @@ function parseCheckedType(type: ts.Type, raw: string, checker: ts.TypeChecker, d
|
|
|
373
172
|
};
|
|
374
173
|
}
|
|
375
174
|
|
|
175
|
+
/** A union carries an absence on the side; what remains is the type. */
|
|
176
|
+
function fromCheckedUnion(type: ts.UnionType, raw: string, checker: ts.TypeChecker, depth: number): TypeRef {
|
|
177
|
+
const typescript = getTS();
|
|
178
|
+
const nullable = type.types.some((member) => (member.flags & typescript.TypeFlags.Null) !== 0);
|
|
179
|
+
const undefinable = type.types.some((member) =>
|
|
180
|
+
(member.flags & (typescript.TypeFlags.Undefined | typescript.TypeFlags.Void)) !== 0,
|
|
181
|
+
);
|
|
182
|
+
const members = type.types.filter((member) =>
|
|
183
|
+
(member.flags & (typescript.TypeFlags.Null | typescript.TypeFlags.Undefined | typescript.TypeFlags.Void)) === 0,
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
// The checker represents the `boolean` keyword itself as `false | true`. Preserve the
|
|
187
|
+
// primitive vocabulary consumed by binding and presenter metadata.
|
|
188
|
+
const allBooleans = members.length > 0
|
|
189
|
+
&& members.every((member) => (member.flags & typescript.TypeFlags.BooleanLiteral) !== 0);
|
|
190
|
+
if (allBooleans) return { raw, name: 'boolean', nullable, undefined: undefinable };
|
|
191
|
+
|
|
192
|
+
if (members.length === 1) {
|
|
193
|
+
const inner = parseCheckedType(members[0]!, raw, checker, depth + 1);
|
|
194
|
+
|
|
195
|
+
return { ...inner, raw, nullable: nullable || inner.nullable, undefined: undefinable || inner.undefined };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return {
|
|
199
|
+
raw,
|
|
200
|
+
name: members.map((member) => meaningfulSymbolName(member, checker)).join(' | ') || raw,
|
|
201
|
+
nullable,
|
|
202
|
+
undefined: undefinable,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** The keywords, read off the flags the checker already carries. */
|
|
207
|
+
function fromCheckedFlags(flags: ts.TypeFlags, raw: string): TypeRef | undefined {
|
|
208
|
+
const typescript = getTS();
|
|
209
|
+
if ((flags & typescript.TypeFlags.StringLike) !== 0) return { raw, name: 'string' };
|
|
210
|
+
if ((flags & typescript.TypeFlags.NumberLike) !== 0) return { raw, name: 'number' };
|
|
211
|
+
if ((flags & typescript.TypeFlags.BooleanLike) !== 0) return { raw, name: 'boolean' };
|
|
212
|
+
if ((flags & typescript.TypeFlags.Void) !== 0) return { raw, name: 'void', undefined: true };
|
|
213
|
+
if ((flags & typescript.TypeFlags.Undefined) !== 0) return { raw, name: 'undefined', undefined: true };
|
|
214
|
+
if ((flags & typescript.TypeFlags.Null) !== 0) return { raw, name: 'null', nullable: true };
|
|
215
|
+
if ((flags & typescript.TypeFlags.Any) !== 0) return { raw, name: 'any' };
|
|
216
|
+
if ((flags & typescript.TypeFlags.Unknown) !== 0) return { raw, name: 'unknown' };
|
|
217
|
+
|
|
218
|
+
return undefined;
|
|
219
|
+
}
|
|
220
|
+
|
|
376
221
|
// ── Module resolution ────────────────────────
|
|
377
222
|
|
|
378
223
|
const TS_KEYWORDS = new Set([
|
|
@@ -599,35 +444,6 @@ function parseInheritedMethods(
|
|
|
599
444
|
return [];
|
|
600
445
|
}
|
|
601
446
|
|
|
602
|
-
// ── Class finding ────────────────────────────
|
|
603
|
-
|
|
604
|
-
/** Find the default exported class in a source file. */
|
|
605
|
-
function findDefaultClass(source: ts.SourceFile): ts.ClassDeclaration | undefined {
|
|
606
|
-
const ts = getTS();
|
|
607
|
-
for (const stmt of source.statements) {
|
|
608
|
-
// export default class Foo { ... }
|
|
609
|
-
if (ts.isClassDeclaration(stmt) && stmt.modifiers?.some(
|
|
610
|
-
(m) => m.kind === ts.SyntaxKind.ExportKeyword,
|
|
611
|
-
) && stmt.modifiers?.some(
|
|
612
|
-
(m) => m.kind === ts.SyntaxKind.DefaultKeyword,
|
|
613
|
-
)) {
|
|
614
|
-
return stmt;
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
// export default Foo (separate statement) — find the class it points to
|
|
619
|
-
for (const stmt of source.statements) {
|
|
620
|
-
if (ts.isExportAssignment(stmt) && !stmt.isExportEquals && ts.isIdentifier(stmt.expression)) {
|
|
621
|
-
const name = stmt.expression.text;
|
|
622
|
-
for (const s of source.statements) {
|
|
623
|
-
if (ts.isClassDeclaration(s) && s.name?.text === name) return s;
|
|
624
|
-
}
|
|
625
|
-
}
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
return undefined;
|
|
629
|
-
}
|
|
630
|
-
|
|
631
447
|
/** What a handler file yielded — its methods, AND what the pass could not open. */
|
|
632
448
|
export interface HandlerParse {
|
|
633
449
|
methods: Signature[];
|
package/src/scan/refusals.ts
CHANGED
|
@@ -86,6 +86,17 @@ function codeIn(typescript: typeof ts, node: ts.NewExpression): ErrorCode | unde
|
|
|
86
86
|
* on `PostRepository`, which is the same node a refusal inside it was charged to. No name
|
|
87
87
|
* matching, and no guess about what a dependency holds.
|
|
88
88
|
*/
|
|
89
|
+
/** The set held under a key, created the first time something lands in it. */
|
|
90
|
+
function into<T>(held: Map<string, Set<T>>, key: string): Set<T> {
|
|
91
|
+
const already = held.get(key);
|
|
92
|
+
if (already) return already;
|
|
93
|
+
|
|
94
|
+
const made = new Set<T>();
|
|
95
|
+
held.set(key, made);
|
|
96
|
+
|
|
97
|
+
return made;
|
|
98
|
+
}
|
|
99
|
+
|
|
89
100
|
export function refusalsIn(typescript: typeof ts, program: ts.Program, isOperation: (name: string) => boolean): Refusals {
|
|
90
101
|
const checker = program.getTypeChecker();
|
|
91
102
|
const sites: Site[] = [];
|
|
@@ -106,7 +117,7 @@ export function refusalsIn(typescript: typeof ts, program: ts.Program, isOperati
|
|
|
106
117
|
const callee = calleeOf(typescript, checker, node);
|
|
107
118
|
const caller = callee ? holderOf(typescript, node) : undefined;
|
|
108
119
|
if (callee && caller && callee !== caller) {
|
|
109
|
-
(callers
|
|
120
|
+
into(callers, callee).add(caller);
|
|
110
121
|
}
|
|
111
122
|
}
|
|
112
123
|
|
|
@@ -154,7 +165,7 @@ function reached(
|
|
|
154
165
|
|
|
155
166
|
while (queue.length > 0) {
|
|
156
167
|
const at = queue.shift()!;
|
|
157
|
-
if (isOperation(at)) (found
|
|
168
|
+
if (isOperation(at)) into(found, at).add(site.code);
|
|
158
169
|
for (const caller of callers.get(at) ?? []) {
|
|
159
170
|
if (walked.has(caller)) continue;
|
|
160
171
|
walked.add(caller);
|