@esportsplus/typescript 0.32.0 → 0.33.1
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/README.md +34 -49
- package/build/cli/tsc.d.ts +3 -2
- package/build/cli/tsc.js +15 -97
- package/build/{probe → guard}/adapter.d.ts +1 -2
- package/build/{probe → guard}/adapter.js +1 -4
- package/build/{probe → guard}/async/channel.d.ts +1 -1
- package/build/{probe → guard}/async/channel.js +11 -191
- package/build/{probe → guard}/async/value.d.ts +1 -2
- package/build/{probe → guard}/async/value.js +1 -4
- package/build/guard/channels.d.ts +4 -0
- package/build/guard/channels.js +13 -0
- package/build/{probe → guard}/exceptions/channel.d.ts +3 -2
- package/build/{probe → guard}/exceptions/channel.js +167 -107
- package/build/guard/exceptions/derive.d.ts +49 -0
- package/build/guard/exceptions/derive.js +478 -0
- package/build/guard/exceptions/resolve-js.d.ts +2 -0
- package/build/guard/exceptions/resolve-js.js +26 -0
- package/build/{probe → guard}/exceptions/value.d.ts +3 -2
- package/build/{probe → guard}/exceptions/value.js +12 -4
- package/build/{probe → guard}/kernel/analyze.d.ts +2 -1
- package/build/guard/kernel/analyze.js +84 -0
- package/build/guard/kernel/ast.d.ts +13 -0
- package/build/{probe → guard}/kernel/ast.js +22 -15
- package/build/guard/kernel/config.js +93 -0
- package/build/guard/kernel/fixpoint.d.ts +6 -0
- package/build/{probe → guard}/kernel/fixpoint.js +8 -48
- package/build/guard/kernel/graph.d.ts +4 -0
- package/build/guard/kernel/graph.js +268 -0
- package/build/{probe → guard}/kernel/ids.d.ts +2 -3
- package/build/{probe → guard}/kernel/ids.js +1 -6
- package/build/{probe → guard}/kernel/types.d.ts +3 -32
- package/build/{probe → guard}/overlay/base/async.jsonc +1 -1
- package/build/{probe → guard}/overlay/base/exceptions.jsonc +16 -3
- package/build/{probe → guard}/overlay/base/resources.jsonc +7 -8
- package/build/{probe → guard}/overlay/load.d.ts +3 -7
- package/build/{probe → guard}/overlay/load.js +37 -120
- package/build/{probe → guard}/resources/channel.d.ts +1 -1
- package/build/{probe → guard}/resources/channel.js +81 -159
- package/build/{probe → guard}/resources/value.d.ts +1 -2
- package/build/{probe → guard}/resources/value.js +1 -14
- package/build/lsp/diagnostics.d.ts +2 -2
- package/build/lsp/diagnostics.js +2 -2
- package/build/lsp/server.js +41 -3
- package/build/lsp/workspace.d.ts +6 -3
- package/build/lsp/workspace.js +19 -5
- package/build/tsconfig.d.ts +2 -1
- package/build/tsconfig.js +44 -102
- package/package.json +2 -2
- package/tsconfig.base.json +12 -19
- package/tsconfig.package.json +6 -1
- package/build/probe/channels.d.ts +0 -4
- package/build/probe/channels.js +0 -16
- package/build/probe/kernel/analyze.js +0 -68
- package/build/probe/kernel/ast.d.ts +0 -11
- package/build/probe/kernel/config.js +0 -209
- package/build/probe/kernel/fixpoint.d.ts +0 -6
- package/build/probe/kernel/graph.d.ts +0 -4
- package/build/probe/kernel/graph.js +0 -507
- package/build/probe/overlay/presets/express.jsonc +0 -25
- package/build/probe/overlay/presets/node.jsonc +0 -17
- /package/build/{probe → guard}/exceptions/jsdoc.d.ts +0 -0
- /package/build/{probe → guard}/exceptions/jsdoc.js +0 -0
- /package/build/{probe → guard}/kernel/config.d.ts +0 -0
- /package/build/{probe → guard}/kernel/format.d.ts +0 -0
- /package/build/{probe → guard}/kernel/format.js +0 -0
- /package/build/{probe → guard}/kernel/program.d.ts +0 -0
- /package/build/{probe → guard}/kernel/program.js +0 -0
- /package/build/{probe → guard}/kernel/types.js +0 -0
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
import { dirname } from 'node:path';
|
|
2
|
+
import * as NodeFS from 'node:fs';
|
|
3
|
+
import * as ts from '../adapter.js';
|
|
4
|
+
import { API } from 'typescript/unstable/sync';
|
|
5
|
+
import { bodyOf, hasModifier, isAwaited, unwrap } from '../kernel/ast.js';
|
|
6
|
+
import { isFunctionLike } from '../kernel/ids.js';
|
|
7
|
+
import { resolveBackingJs } from './resolve-js.js';
|
|
8
|
+
import { ORIGIN_CAP, originOf } from './value.js';
|
|
9
|
+
const GLOBAL_ERROR_CLASSES = new Set([
|
|
10
|
+
'AggregateError',
|
|
11
|
+
'Error',
|
|
12
|
+
'EvalError',
|
|
13
|
+
'RangeError',
|
|
14
|
+
'ReferenceError',
|
|
15
|
+
'SyntaxError',
|
|
16
|
+
'TypeError',
|
|
17
|
+
'URIError',
|
|
18
|
+
]);
|
|
19
|
+
const MAX_AVG_LINE = 300;
|
|
20
|
+
const MAX_FILE_BYTES = 512 * 1024;
|
|
21
|
+
const MAX_DEPTH = 6;
|
|
22
|
+
function createThrowIndex() {
|
|
23
|
+
return {
|
|
24
|
+
api: null,
|
|
25
|
+
contents: new Map(),
|
|
26
|
+
nextProject: 0,
|
|
27
|
+
parsed: new Map(),
|
|
28
|
+
resolved: new Map(),
|
|
29
|
+
snapshots: [],
|
|
30
|
+
summaries: new Map(),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function disposeThrowIndex(index) {
|
|
34
|
+
for (let snapshot of index.snapshots) {
|
|
35
|
+
try {
|
|
36
|
+
if (!snapshot.isDisposed()) {
|
|
37
|
+
snapshot.dispose();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
index.api?.close();
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
}
|
|
48
|
+
index.api = null;
|
|
49
|
+
index.contents.clear();
|
|
50
|
+
index.parsed.clear();
|
|
51
|
+
index.resolved.clear();
|
|
52
|
+
index.snapshots.length = 0;
|
|
53
|
+
index.summaries.clear();
|
|
54
|
+
}
|
|
55
|
+
function isMinified(text) {
|
|
56
|
+
let lines = 1;
|
|
57
|
+
for (let i = 0, n = text.length; i < n; i++) {
|
|
58
|
+
if (text[i] === '\n') {
|
|
59
|
+
lines += 1;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return text.length / lines > MAX_AVG_LINE;
|
|
63
|
+
}
|
|
64
|
+
function normalize(fileName) {
|
|
65
|
+
return fileName.replace(/\\/g, '/');
|
|
66
|
+
}
|
|
67
|
+
function overlayFileSystem(contents) {
|
|
68
|
+
return {
|
|
69
|
+
fileExists: (fileName) => contents.has(normalize(fileName)) || undefined,
|
|
70
|
+
getAccessibleEntries: (directoryName) => {
|
|
71
|
+
let directory = normalize(directoryName), files = [];
|
|
72
|
+
for (let fileName of contents.keys()) {
|
|
73
|
+
let slash = fileName.lastIndexOf('/');
|
|
74
|
+
if (fileName.slice(0, slash) === directory) {
|
|
75
|
+
files.push(fileName.slice(slash + 1));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return files.length > 0 ? { directories: [], files } : undefined;
|
|
79
|
+
},
|
|
80
|
+
readFile: (fileName) => contents.get(normalize(fileName)),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
function parseText(index, text) {
|
|
84
|
+
try {
|
|
85
|
+
if (!index.api) {
|
|
86
|
+
index.api = new API({ cwd: '/guard-scan', fs: overlayFileSystem(index.contents) });
|
|
87
|
+
}
|
|
88
|
+
let directory = `/guard-scan/${index.nextProject++}`, config = `${directory}/tsconfig.json`, file = `${directory}/scan.js`;
|
|
89
|
+
index.contents.set(file, text);
|
|
90
|
+
index.contents.set(config, JSON.stringify({
|
|
91
|
+
compilerOptions: {
|
|
92
|
+
allowJs: true,
|
|
93
|
+
checkJs: false,
|
|
94
|
+
module: 'commonjs',
|
|
95
|
+
noEmit: true,
|
|
96
|
+
target: 'esnext',
|
|
97
|
+
},
|
|
98
|
+
files: ['scan.js'],
|
|
99
|
+
}));
|
|
100
|
+
let snapshot = index.api.updateSnapshot({ openProjects: [config] });
|
|
101
|
+
index.snapshots.push(snapshot);
|
|
102
|
+
return (snapshot
|
|
103
|
+
.getProject(config)
|
|
104
|
+
?.program.getSourceFile(file) ?? null);
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function parseFile(index, realpath) {
|
|
111
|
+
let stat;
|
|
112
|
+
try {
|
|
113
|
+
stat = NodeFS.statSync(realpath);
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
const cached = index.parsed.get(realpath);
|
|
119
|
+
if (cached && cached.mtimeMs === stat.mtimeMs && cached.size === stat.size) {
|
|
120
|
+
return cached;
|
|
121
|
+
}
|
|
122
|
+
if (cached) {
|
|
123
|
+
index.summaries.clear();
|
|
124
|
+
}
|
|
125
|
+
let text = '';
|
|
126
|
+
try {
|
|
127
|
+
text = stat.size <= MAX_FILE_BYTES ? NodeFS.readFileSync(realpath, 'utf8') : '';
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
text = '';
|
|
131
|
+
}
|
|
132
|
+
const sourceFile = text.length > 0 && !isMinified(text) ? parseText(index, text) : null;
|
|
133
|
+
const parsed = {
|
|
134
|
+
fileIndex: sourceFile ? indexFile(sourceFile) : null,
|
|
135
|
+
mtimeMs: stat.mtimeMs,
|
|
136
|
+
size: stat.size,
|
|
137
|
+
sourceFile,
|
|
138
|
+
};
|
|
139
|
+
index.parsed.set(realpath, parsed);
|
|
140
|
+
return parsed;
|
|
141
|
+
}
|
|
142
|
+
function classifyThrow(expr) {
|
|
143
|
+
if (!expr) {
|
|
144
|
+
return 'Error';
|
|
145
|
+
}
|
|
146
|
+
const value = unwrap(expr);
|
|
147
|
+
if (ts.isNewExpression(value) &&
|
|
148
|
+
ts.isIdentifier(value.expression) &&
|
|
149
|
+
GLOBAL_ERROR_CLASSES.has(value.expression.text)) {
|
|
150
|
+
return value.expression.text;
|
|
151
|
+
}
|
|
152
|
+
return 'Error';
|
|
153
|
+
}
|
|
154
|
+
function initializerFn(node) {
|
|
155
|
+
const initializer = node.initializer;
|
|
156
|
+
return initializer && isFunctionLike(initializer) ? initializer : undefined;
|
|
157
|
+
}
|
|
158
|
+
function requireSpecifier(expr) {
|
|
159
|
+
if (!expr || !ts.isCallExpression(expr) || !ts.isIdentifier(expr.expression) || expr.expression.text !== 'require') {
|
|
160
|
+
return undefined;
|
|
161
|
+
}
|
|
162
|
+
const arg = expr.arguments[0];
|
|
163
|
+
return arg && ts.isStringLiteral(arg) ? arg.text : undefined;
|
|
164
|
+
}
|
|
165
|
+
function targetOf(expr, fns, classes) {
|
|
166
|
+
const value = unwrap(expr);
|
|
167
|
+
if (isFunctionLike(value)) {
|
|
168
|
+
return value;
|
|
169
|
+
}
|
|
170
|
+
if (ts.isIdentifier(value)) {
|
|
171
|
+
return fns.get(value.text) ?? classes.get(value.text);
|
|
172
|
+
}
|
|
173
|
+
return undefined;
|
|
174
|
+
}
|
|
175
|
+
function collectDeclarations(sourceFile, classes, fns, imports, namespaces) {
|
|
176
|
+
for (const statement of sourceFile.statements) {
|
|
177
|
+
if (ts.isFunctionDeclaration(statement) && statement.name) {
|
|
178
|
+
fns.set(statement.name.text, statement);
|
|
179
|
+
}
|
|
180
|
+
else if (ts.isClassDeclaration(statement) && statement.name) {
|
|
181
|
+
classes.set(statement.name.text, statement);
|
|
182
|
+
}
|
|
183
|
+
else if (ts.isVariableStatement(statement)) {
|
|
184
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
185
|
+
const fn = initializerFn(declaration);
|
|
186
|
+
if (fn && ts.isIdentifier(declaration.name)) {
|
|
187
|
+
fns.set(declaration.name.text, fn);
|
|
188
|
+
}
|
|
189
|
+
const specifier = requireSpecifier(declaration.initializer);
|
|
190
|
+
if (specifier && ts.isIdentifier(declaration.name)) {
|
|
191
|
+
namespaces.set(declaration.name.text, specifier);
|
|
192
|
+
}
|
|
193
|
+
else if (specifier && ts.isObjectBindingPattern(declaration.name)) {
|
|
194
|
+
for (const element of declaration.name.elements) {
|
|
195
|
+
if (ts.isBindingElement(element) && element.name && ts.isIdentifier(element.name)) {
|
|
196
|
+
const property = element.propertyName;
|
|
197
|
+
imports.set(element.name.text, {
|
|
198
|
+
exportName: property && ts.isIdentifier(property) ? property.text : element.name.text,
|
|
199
|
+
specifier,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
else if (ts.isImportDeclaration(statement) && ts.isStringLiteral(statement.moduleSpecifier) && statement.importClause) {
|
|
207
|
+
const specifier = statement.moduleSpecifier.text;
|
|
208
|
+
const clause = statement.importClause;
|
|
209
|
+
if (clause.name) {
|
|
210
|
+
imports.set(clause.name.text, { exportName: 'default', specifier });
|
|
211
|
+
}
|
|
212
|
+
if (clause.namedBindings && ts.isNamespaceImport(clause.namedBindings)) {
|
|
213
|
+
namespaces.set(clause.namedBindings.name.text, specifier);
|
|
214
|
+
}
|
|
215
|
+
else if (clause.namedBindings && ts.isNamedImports(clause.namedBindings)) {
|
|
216
|
+
for (const element of clause.namedBindings.elements) {
|
|
217
|
+
imports.set(element.name.text, { exportName: (element.propertyName ?? element.name).text, specifier });
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
function collectExports(sourceFile, classes, fns, exportsByName, reexports) {
|
|
224
|
+
let reexportAll;
|
|
225
|
+
const add = (name, expr) => {
|
|
226
|
+
const target = targetOf(expr, fns, classes);
|
|
227
|
+
if (target) {
|
|
228
|
+
exportsByName.set(name, target);
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
for (const statement of sourceFile.statements) {
|
|
232
|
+
const exported = hasModifier(statement, ts.SyntaxKind.ExportKeyword);
|
|
233
|
+
const defaultExport = hasModifier(statement, ts.SyntaxKind.DefaultKeyword);
|
|
234
|
+
if (ts.isFunctionDeclaration(statement) && exported) {
|
|
235
|
+
exportsByName.set(defaultExport ? 'default' : statement.name?.text ?? 'default', statement);
|
|
236
|
+
}
|
|
237
|
+
else if (ts.isClassDeclaration(statement) && exported) {
|
|
238
|
+
exportsByName.set(defaultExport ? 'default' : statement.name?.text ?? 'default', statement);
|
|
239
|
+
}
|
|
240
|
+
else if (ts.isVariableStatement(statement) && exported) {
|
|
241
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
242
|
+
if (ts.isIdentifier(declaration.name)) {
|
|
243
|
+
add(declaration.name.text, (declaration.initializer ?? declaration.name));
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
else if (ts.isExportAssignment(statement) && !statement.isExportEquals) {
|
|
248
|
+
add('default', statement.expression);
|
|
249
|
+
}
|
|
250
|
+
else if (ts.isExportDeclaration(statement) && statement.moduleSpecifier && ts.isStringLiteral(statement.moduleSpecifier)) {
|
|
251
|
+
const specifier = statement.moduleSpecifier.text;
|
|
252
|
+
if (!statement.exportClause) {
|
|
253
|
+
reexportAll = specifier;
|
|
254
|
+
}
|
|
255
|
+
else if (ts.isNamedExports(statement.exportClause)) {
|
|
256
|
+
for (const element of statement.exportClause.elements) {
|
|
257
|
+
reexports.set(element.name.text, { exportName: (element.propertyName ?? element.name).text, specifier });
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
else if (ts.isExpressionStatement(statement) && ts.isBinaryExpression(statement.expression) && statement.expression.operatorToken.kind === ts.SyntaxKind.EqualsToken) {
|
|
262
|
+
reexportAll = collectCjsExport(statement.expression, add) ?? reexportAll;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return reexportAll;
|
|
266
|
+
}
|
|
267
|
+
function collectCjsExport(assign, add) {
|
|
268
|
+
const left = assign.left;
|
|
269
|
+
if (!ts.isPropertyAccessExpression(left)) {
|
|
270
|
+
return undefined;
|
|
271
|
+
}
|
|
272
|
+
if (ts.isIdentifier(left.expression) && left.expression.text === 'exports') {
|
|
273
|
+
add(left.name.text, assign.right);
|
|
274
|
+
return undefined;
|
|
275
|
+
}
|
|
276
|
+
if (ts.isPropertyAccessExpression(left.expression) && ts.isIdentifier(left.expression.expression) && left.expression.expression.text === 'module' && left.expression.name.text === 'exports') {
|
|
277
|
+
add(left.name.text, assign.right);
|
|
278
|
+
return undefined;
|
|
279
|
+
}
|
|
280
|
+
if (!ts.isIdentifier(left.expression) || left.expression.text !== 'module' || left.name.text !== 'exports') {
|
|
281
|
+
return undefined;
|
|
282
|
+
}
|
|
283
|
+
const required = requireSpecifier(assign.right);
|
|
284
|
+
if (required) {
|
|
285
|
+
return required;
|
|
286
|
+
}
|
|
287
|
+
const value = unwrap(assign.right);
|
|
288
|
+
if (!ts.isObjectLiteralExpression(value)) {
|
|
289
|
+
add('default', assign.right);
|
|
290
|
+
return undefined;
|
|
291
|
+
}
|
|
292
|
+
for (const property of value.properties) {
|
|
293
|
+
if (ts.isPropertyAssignment(property) && (ts.isIdentifier(property.name) || ts.isStringLiteral(property.name))) {
|
|
294
|
+
add(property.name.text, property.initializer);
|
|
295
|
+
}
|
|
296
|
+
else if (ts.isShorthandPropertyAssignment(property) && ts.isIdentifier(property.name)) {
|
|
297
|
+
add(property.name.text, property.name);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return undefined;
|
|
301
|
+
}
|
|
302
|
+
function indexFile(sourceFile) {
|
|
303
|
+
const classes = new Map();
|
|
304
|
+
const exportsByName = new Map();
|
|
305
|
+
const fns = new Map();
|
|
306
|
+
const imports = new Map();
|
|
307
|
+
const namespaces = new Map();
|
|
308
|
+
const reexports = new Map();
|
|
309
|
+
collectDeclarations(sourceFile, classes, fns, imports, namespaces);
|
|
310
|
+
const reexportAll = collectExports(sourceFile, classes, fns, exportsByName, reexports);
|
|
311
|
+
return { classes, exportsByName, fns, imports, namespaces, reexportAll, reexports };
|
|
312
|
+
}
|
|
313
|
+
function locateExport(index, realpath, exportName, seen = new Set()) {
|
|
314
|
+
const key = `${realpath}\0${exportName}`;
|
|
315
|
+
if (seen.has(key)) {
|
|
316
|
+
return undefined;
|
|
317
|
+
}
|
|
318
|
+
const parsed = parseFile(index, realpath);
|
|
319
|
+
if (!parsed?.sourceFile || !parsed.fileIndex) {
|
|
320
|
+
return undefined;
|
|
321
|
+
}
|
|
322
|
+
const target = parsed.fileIndex.exportsByName.get(exportName);
|
|
323
|
+
if (target) {
|
|
324
|
+
return { fileIndex: parsed.fileIndex, realpath, sourceFile: parsed.sourceFile, target };
|
|
325
|
+
}
|
|
326
|
+
const redirect = parsed.fileIndex.reexports.get(exportName) ??
|
|
327
|
+
(parsed.fileIndex.reexportAll ? { exportName, specifier: parsed.fileIndex.reexportAll } : undefined);
|
|
328
|
+
if (!redirect) {
|
|
329
|
+
return undefined;
|
|
330
|
+
}
|
|
331
|
+
const next = resolveBackingJs(realpath, redirect.specifier);
|
|
332
|
+
return next ? locateExport(index, next, redirect.exportName, new Set(seen).add(key)) : undefined;
|
|
333
|
+
}
|
|
334
|
+
function findMethod(sourceFile, classNode, memberName) {
|
|
335
|
+
const members = classNode.members ?? [];
|
|
336
|
+
for (const member of members) {
|
|
337
|
+
if (ts.isMethodDeclaration(member) && ts.isIdentifier(member.name) && member.name.text === memberName) {
|
|
338
|
+
return member;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
const name = classNode.name;
|
|
342
|
+
if (!name || !ts.isIdentifier(name)) {
|
|
343
|
+
return undefined;
|
|
344
|
+
}
|
|
345
|
+
for (const statement of sourceFile.statements) {
|
|
346
|
+
if (!ts.isExpressionStatement(statement) || !ts.isBinaryExpression(statement.expression) || statement.expression.operatorToken.kind !== ts.SyntaxKind.EqualsToken) {
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
const left = statement.expression.left;
|
|
350
|
+
if (ts.isPropertyAccessExpression(left) &&
|
|
351
|
+
ts.isPropertyAccessExpression(left.expression) &&
|
|
352
|
+
ts.isIdentifier(left.expression.expression) &&
|
|
353
|
+
left.expression.expression.text === name.text &&
|
|
354
|
+
left.expression.name.text === 'prototype' &&
|
|
355
|
+
left.name.text === memberName) {
|
|
356
|
+
const target = unwrap(statement.expression.right);
|
|
357
|
+
if (isFunctionLike(target)) {
|
|
358
|
+
return target;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
return undefined;
|
|
363
|
+
}
|
|
364
|
+
function importedTarget(index, realpath, fileIndex, call) {
|
|
365
|
+
if (ts.isIdentifier(call.expression)) {
|
|
366
|
+
const binding = fileIndex.imports.get(call.expression.text);
|
|
367
|
+
if (!binding) {
|
|
368
|
+
return undefined;
|
|
369
|
+
}
|
|
370
|
+
const next = resolveBackingJs(realpath, binding.specifier);
|
|
371
|
+
return next ? locateExport(index, next, binding.exportName) : undefined;
|
|
372
|
+
}
|
|
373
|
+
if (ts.isPropertyAccessExpression(call.expression) && ts.isIdentifier(call.expression.expression)) {
|
|
374
|
+
const specifier = fileIndex.namespaces.get(call.expression.expression.text);
|
|
375
|
+
if (!specifier) {
|
|
376
|
+
return undefined;
|
|
377
|
+
}
|
|
378
|
+
const next = resolveBackingJs(realpath, specifier);
|
|
379
|
+
return next ? locateExport(index, next, call.expression.name.text) : undefined;
|
|
380
|
+
}
|
|
381
|
+
return undefined;
|
|
382
|
+
}
|
|
383
|
+
function mergeClasses(into, from) {
|
|
384
|
+
for (const [name, origins] of from) {
|
|
385
|
+
const existing = into.get(name);
|
|
386
|
+
if (!existing) {
|
|
387
|
+
into.set(name, origins.slice(0, ORIGIN_CAP));
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
for (const origin of origins) {
|
|
391
|
+
if (existing.length < ORIGIN_CAP && !existing.some((candidate) => candidate.pos === origin.pos && candidate.fileName === origin.fileName)) {
|
|
392
|
+
existing.push(origin);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
function computeEscaping(index, realpath, sourceFile, fileIndex, functionNode, depth, chain) {
|
|
398
|
+
const classes = new Map();
|
|
399
|
+
const body = bodyOf(functionNode);
|
|
400
|
+
if (!body || depth <= 0 || chain.has(functionNode)) {
|
|
401
|
+
return classes;
|
|
402
|
+
}
|
|
403
|
+
const nextChain = new Set(chain).add(functionNode);
|
|
404
|
+
const visit = (node) => {
|
|
405
|
+
if (node !== functionNode && isFunctionLike(node)) {
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
if (ts.isThrowStatement(node)) {
|
|
409
|
+
const name = classifyThrow(node.expression);
|
|
410
|
+
const origins = classes.get(name) ?? [];
|
|
411
|
+
if (origins.length < ORIGIN_CAP) {
|
|
412
|
+
origins.push(originOf(node, sourceFile, realpath));
|
|
413
|
+
}
|
|
414
|
+
classes.set(name, origins);
|
|
415
|
+
}
|
|
416
|
+
else if (ts.isCallExpression(node)) {
|
|
417
|
+
let target = ts.isIdentifier(node.expression) ? fileIndex.fns.get(node.expression.text) : undefined;
|
|
418
|
+
let targetPath = realpath;
|
|
419
|
+
let targetSource = sourceFile;
|
|
420
|
+
let targetIndex = fileIndex;
|
|
421
|
+
if (!target) {
|
|
422
|
+
const imported = importedTarget(index, realpath, fileIndex, node);
|
|
423
|
+
if (imported) {
|
|
424
|
+
target = imported.target;
|
|
425
|
+
targetPath = imported.realpath;
|
|
426
|
+
targetSource = imported.sourceFile;
|
|
427
|
+
targetIndex = imported.fileIndex;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
if (target && !(hasModifier(target, ts.SyntaxKind.AsyncKeyword) && !isAwaited(node))) {
|
|
431
|
+
mergeClasses(classes, computeEscaping(index, targetPath, targetSource, targetIndex, target, depth - 1, nextChain));
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
ts.forEachChild(node, visit);
|
|
435
|
+
};
|
|
436
|
+
ts.forEachChild(body, visit);
|
|
437
|
+
return classes;
|
|
438
|
+
}
|
|
439
|
+
function computeSummary(index, realpath, exportName, memberName) {
|
|
440
|
+
const located = locateExport(index, realpath, exportName);
|
|
441
|
+
if (!located) {
|
|
442
|
+
return null;
|
|
443
|
+
}
|
|
444
|
+
let target = located.target;
|
|
445
|
+
if (memberName) {
|
|
446
|
+
const method = findMethod(located.sourceFile, target, memberName);
|
|
447
|
+
if (!method) {
|
|
448
|
+
return null;
|
|
449
|
+
}
|
|
450
|
+
target = method;
|
|
451
|
+
}
|
|
452
|
+
return {
|
|
453
|
+
async: hasModifier(target, ts.SyntaxKind.AsyncKeyword),
|
|
454
|
+
classes: computeEscaping(index, located.realpath, located.sourceFile, located.fileIndex, target, MAX_DEPTH, new Set()),
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
function deriveThrows(index, params) {
|
|
458
|
+
let resolution = `${dirname(params.importerFileName)}\0${params.specifier}`, realpath = index.resolved.get(resolution);
|
|
459
|
+
if (!index.resolved.has(resolution)) {
|
|
460
|
+
realpath = resolveBackingJs(params.importerFileName, params.specifier);
|
|
461
|
+
index.resolved.set(resolution, realpath);
|
|
462
|
+
}
|
|
463
|
+
if (!realpath) {
|
|
464
|
+
return undefined;
|
|
465
|
+
}
|
|
466
|
+
parseFile(index, realpath);
|
|
467
|
+
const key = `${realpath}\0${params.exportName}\0${params.memberName ?? ''}`;
|
|
468
|
+
let cached = index.summaries.get(key);
|
|
469
|
+
if (!index.summaries.has(key)) {
|
|
470
|
+
cached = computeSummary(index, realpath, params.exportName, params.memberName);
|
|
471
|
+
index.summaries.set(key, cached);
|
|
472
|
+
}
|
|
473
|
+
if (!cached || (cached.async && !params.awaited) || cached.classes.size === 0) {
|
|
474
|
+
return undefined;
|
|
475
|
+
}
|
|
476
|
+
return { classes: cached.classes };
|
|
477
|
+
}
|
|
478
|
+
export { createThrowIndex, deriveThrows, disposeThrowIndex };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import * as NodeFS from 'node:fs';
|
|
2
|
+
import * as NodePath from 'node:path';
|
|
3
|
+
import { createRequire, isBuiltin } from 'node:module';
|
|
4
|
+
const SCANNABLE = new Set(['.cjs', '.js', '.mjs']);
|
|
5
|
+
function resolveBackingJs(importerFileName, specifier) {
|
|
6
|
+
if (isBuiltin(specifier)) {
|
|
7
|
+
return undefined;
|
|
8
|
+
}
|
|
9
|
+
let resolved;
|
|
10
|
+
try {
|
|
11
|
+
resolved = createRequire(importerFileName).resolve(specifier);
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
if (!SCANNABLE.has(NodePath.extname(resolved).toLowerCase())) {
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
return NodeFS.realpathSync(resolved);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return resolved;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export { resolveBackingJs };
|
|
@@ -13,6 +13,7 @@ type ExceptionsValue = {
|
|
|
13
13
|
readonly origins: ReadonlyMap<string, ReadonlyArray<ExceptionOrigin>>;
|
|
14
14
|
};
|
|
15
15
|
declare const TOP_KEY = "\0top";
|
|
16
|
+
declare const ORIGIN_CAP = 4;
|
|
16
17
|
declare function bottom(): ExceptionsValue;
|
|
17
18
|
declare function top(): ExceptionsValue;
|
|
18
19
|
declare function single(key: string, display: string): ExceptionsValue;
|
|
@@ -24,7 +25,7 @@ declare function widen(prev: ExceptionsValue, next: ExceptionsValue, round: numb
|
|
|
24
25
|
declare function subtract(a: ExceptionsValue, keys: ReadonlySet<string>): ExceptionsValue;
|
|
25
26
|
declare function isEmpty(v: ExceptionsValue): boolean;
|
|
26
27
|
declare function render(v: ExceptionsValue): string;
|
|
27
|
-
declare function
|
|
28
|
+
declare function originOf(node: ts.Node, sourceFile: ts.SourceFile, fileName?: string): ExceptionOrigin;
|
|
28
29
|
declare function refOfType(checker: ts.TypeChecker, t: ts.Type): {
|
|
29
30
|
top: true;
|
|
30
31
|
} | {
|
|
@@ -36,4 +37,4 @@ declare function typeRef(checker: ts.TypeChecker, type: ts.Type): ReadonlyArray<
|
|
|
36
37
|
key: string;
|
|
37
38
|
display: string;
|
|
38
39
|
}>;
|
|
39
|
-
export { TOP_KEY, bottom,
|
|
40
|
+
export { ORIGIN_CAP, TOP_KEY, bottom, equals, isEmpty, join, originOf, originsOf, refOfType, render, single, subtract, top, type ExceptionOrigin, type ExceptionsValue, typeRef, widen, withOrigin };
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import * as ts from '../adapter.js';
|
|
2
|
+
import { constituents } from '../kernel/ast.js';
|
|
3
|
+
import { locationOf } from '../kernel/ids.js';
|
|
2
4
|
const TOP_KEY = '\u0000top';
|
|
3
5
|
const TOP_DISPLAY = 'an unknown error';
|
|
4
6
|
const WIDEN_CAP = 8;
|
|
@@ -122,8 +124,14 @@ function render(v) {
|
|
|
122
124
|
return TOP_DISPLAY;
|
|
123
125
|
return Array.from(v.types.values()).sort().join(' | ');
|
|
124
126
|
}
|
|
125
|
-
function
|
|
126
|
-
|
|
127
|
+
function originOf(node, sourceFile, fileName = sourceFile.fileName) {
|
|
128
|
+
const location = locationOf(node, sourceFile);
|
|
129
|
+
const text = node.getText(sourceFile).replace(/\s+/g, ' ').trim();
|
|
130
|
+
return {
|
|
131
|
+
...location,
|
|
132
|
+
fileName,
|
|
133
|
+
text: text.length > 120 ? `${text.slice(0, 117)}…` : text,
|
|
134
|
+
};
|
|
127
135
|
}
|
|
128
136
|
function basename(p) {
|
|
129
137
|
const i = Math.max(p.lastIndexOf('/'), p.lastIndexOf('\\'));
|
|
@@ -146,7 +154,7 @@ function refOfType(checker, t) {
|
|
|
146
154
|
}
|
|
147
155
|
function typeRef(checker, type) {
|
|
148
156
|
const out = [];
|
|
149
|
-
for (const t of
|
|
157
|
+
for (const t of constituents(type)) {
|
|
150
158
|
const r = refOfType(checker, t);
|
|
151
159
|
if (r.top)
|
|
152
160
|
out.push({ key: TOP_KEY, display: TOP_DISPLAY });
|
|
@@ -155,4 +163,4 @@ function typeRef(checker, type) {
|
|
|
155
163
|
}
|
|
156
164
|
return out;
|
|
157
165
|
}
|
|
158
|
-
export { TOP_KEY, bottom,
|
|
166
|
+
export { ORIGIN_CAP, TOP_KEY, bottom, equals, isEmpty, join, originOf, originsOf, refOfType, render, single, subtract, top, typeRef, widen, withOrigin };
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import * as ts from '../adapter.js';
|
|
2
2
|
import type { Diagnostic, AnalyzeConfig } from './types.js';
|
|
3
|
+
import type { ThrowIndex } from '../exceptions/derive.js';
|
|
3
4
|
type AnalyzeResult = {
|
|
4
5
|
readonly diagnostics: ReadonlyArray<Diagnostic>;
|
|
5
6
|
};
|
|
6
|
-
declare function analyzeProgram(program: ts.Program, checker: ts.TypeChecker, config: AnalyzeConfig): AnalyzeResult;
|
|
7
|
+
declare function analyzeProgram(program: ts.Program, checker: ts.TypeChecker, config: AnalyzeConfig, throwIndex?: ThrowIndex): AnalyzeResult;
|
|
7
8
|
declare function analyze(config: AnalyzeConfig): AnalyzeResult;
|
|
8
9
|
export { analyze, analyzeProgram, type AnalyzeResult };
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { channelFor } from '../channels.js';
|
|
2
|
+
import { buildCallGraph } from './graph.js';
|
|
3
|
+
import { buildProgram } from './program.js';
|
|
4
|
+
import { createThrowIndex, disposeThrowIndex } from '../exceptions/derive.js';
|
|
5
|
+
import { runChannel } from './fixpoint.js';
|
|
6
|
+
import { loadOverlays } from '../overlay/load.js';
|
|
7
|
+
function analyzeProgram(program, checker, config, throwIndex) {
|
|
8
|
+
const index = throwIndex ?? createThrowIndex();
|
|
9
|
+
try {
|
|
10
|
+
return runAnalysis(program, checker, config, index);
|
|
11
|
+
}
|
|
12
|
+
finally {
|
|
13
|
+
if (!throwIndex) {
|
|
14
|
+
disposeThrowIndex(index);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function runAnalysis(program, checker, config, throwIndex) {
|
|
19
|
+
const overlays = loadOverlays();
|
|
20
|
+
const graph = buildCallGraph(program, checker, overlays);
|
|
21
|
+
const analysis = { checker, graph };
|
|
22
|
+
const built = new Map();
|
|
23
|
+
const instance = (name) => {
|
|
24
|
+
const cached = built.get(name);
|
|
25
|
+
if (cached) {
|
|
26
|
+
return cached;
|
|
27
|
+
}
|
|
28
|
+
const channelConfig = config.channels[name];
|
|
29
|
+
const channel = channelConfig
|
|
30
|
+
? channelFor(name, throwIndex)
|
|
31
|
+
: undefined;
|
|
32
|
+
if (!channelConfig || !channel) {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
const entry = [channelConfig, channel];
|
|
36
|
+
built.set(name, entry);
|
|
37
|
+
return entry;
|
|
38
|
+
};
|
|
39
|
+
const enabled = new Set();
|
|
40
|
+
for (const [name, channelConfig] of Object.entries(config.channels)) {
|
|
41
|
+
if (channelConfig.severity !== 'off') {
|
|
42
|
+
enabled.add(name);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const runnable = new Set();
|
|
46
|
+
const include = (name, stack) => {
|
|
47
|
+
if (runnable.has(name) || stack.has(name)) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const inst = instance(name);
|
|
51
|
+
if (!inst) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const next = new Set(stack).add(name);
|
|
55
|
+
for (const dep of inst[1].dependsOn ?? []) {
|
|
56
|
+
include(dep, next);
|
|
57
|
+
}
|
|
58
|
+
runnable.add(name);
|
|
59
|
+
};
|
|
60
|
+
for (const name of enabled) {
|
|
61
|
+
include(name, new Set());
|
|
62
|
+
}
|
|
63
|
+
const diagnostics = [];
|
|
64
|
+
const peers = new Map();
|
|
65
|
+
for (const name of runnable) {
|
|
66
|
+
const [channelConfig, channel] = instance(name);
|
|
67
|
+
const result = runChannel(analysis, channel, channelConfig.options, peers);
|
|
68
|
+
peers.set(name, result.store);
|
|
69
|
+
if (enabled.has(name)) {
|
|
70
|
+
diagnostics.push(...result.diagnostics);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return { diagnostics };
|
|
74
|
+
}
|
|
75
|
+
function analyze(config) {
|
|
76
|
+
const built = buildProgram(config.tsconfigPath);
|
|
77
|
+
try {
|
|
78
|
+
return analyzeProgram(built.program, built.checker, config);
|
|
79
|
+
}
|
|
80
|
+
finally {
|
|
81
|
+
built.dispose();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
export { analyze, analyzeProgram };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import * as ts from '../adapter.js';
|
|
2
|
+
import type { FunctionLike } from './types.js';
|
|
3
|
+
type UnwrapOptions = {
|
|
4
|
+
assertions?: boolean;
|
|
5
|
+
awaits?: boolean;
|
|
6
|
+
};
|
|
7
|
+
declare const bodyOf: (node: ts.Node) => ts.Node | undefined;
|
|
8
|
+
declare const constituents: (type: ts.Type) => ReadonlyArray<ts.Type>;
|
|
9
|
+
declare const hasModifier: (node: ts.Node, kind: ts.SyntaxKind) => boolean;
|
|
10
|
+
declare const isAwaited: (call: ts.CallExpression) => boolean;
|
|
11
|
+
declare const paramSymbols: (checker: ts.TypeChecker, fn: FunctionLike) => Map<ts.Symbol, number>;
|
|
12
|
+
declare const unwrap: (expr: ts.Expression, options?: UnwrapOptions) => ts.Expression;
|
|
13
|
+
export { bodyOf, constituents, hasModifier, isAwaited, paramSymbols, unwrap };
|