@esportsplus/typescript 0.31.0 → 0.32.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/README.md +91 -0
- package/bin/tsc-lsp +3 -0
- package/build/cli/diagnostics.d.ts +5 -1
- package/build/cli/diagnostics.js +13 -8
- package/build/cli/tsc.d.ts +3 -1
- package/build/cli/tsc.js +77 -87
- package/build/compiler/coordinator.d.ts +1 -2
- package/build/compiler/coordinator.js +33 -22
- package/build/compiler/imports.d.ts +2 -0
- package/build/compiler/imports.js +9 -2
- package/build/compiler/language-service.d.ts +15 -4
- package/build/compiler/language-service.js +56 -24
- package/build/compiler/plugins/vite.js +7 -5
- package/build/compiler/sourcemap.d.ts +3 -1
- package/build/compiler/sourcemap.js +23 -5
- package/build/jsonc.d.ts +2 -0
- package/build/jsonc.js +85 -0
- package/build/lsp/bin.d.ts +1 -0
- package/build/lsp/bin.js +2 -0
- package/build/lsp/diagnostics.d.ts +10 -0
- package/build/lsp/diagnostics.js +69 -0
- package/build/lsp/index.d.ts +2 -0
- package/build/lsp/index.js +2 -0
- package/build/lsp/server.d.ts +4 -0
- package/build/lsp/server.js +130 -0
- package/build/lsp/workspace.d.ts +14 -0
- package/build/lsp/workspace.js +46 -0
- package/build/probe/adapter.d.ts +14 -0
- package/build/probe/adapter.js +42 -0
- package/build/probe/async/channel.d.ts +4 -0
- package/build/probe/async/channel.js +560 -0
- package/build/probe/async/value.d.ts +9 -0
- package/build/probe/async/value.js +16 -0
- package/build/probe/channels.d.ts +4 -0
- package/build/probe/channels.js +16 -0
- package/build/probe/exceptions/channel.d.ts +5 -0
- package/build/probe/exceptions/channel.js +669 -0
- package/build/probe/exceptions/jsdoc.d.ts +8 -0
- package/build/probe/exceptions/jsdoc.js +34 -0
- package/build/probe/exceptions/value.d.ts +39 -0
- package/build/probe/exceptions/value.js +158 -0
- package/build/probe/kernel/analyze.d.ts +8 -0
- package/build/probe/kernel/analyze.js +68 -0
- package/build/probe/kernel/ast.d.ts +11 -0
- package/build/probe/kernel/ast.js +49 -0
- package/build/probe/kernel/config.d.ts +5 -0
- package/build/probe/kernel/config.js +209 -0
- package/build/probe/kernel/fixpoint.d.ts +6 -0
- package/build/probe/kernel/fixpoint.js +206 -0
- package/build/probe/kernel/format.d.ts +4 -0
- package/build/probe/kernel/format.js +32 -0
- package/build/probe/kernel/graph.d.ts +4 -0
- package/build/probe/kernel/graph.js +507 -0
- package/build/probe/kernel/ids.d.ts +8 -0
- package/build/probe/kernel/ids.js +66 -0
- package/build/probe/kernel/program.d.ts +8 -0
- package/build/probe/kernel/program.js +13 -0
- package/build/probe/kernel/types.d.ts +134 -0
- package/build/probe/kernel/types.js +1 -0
- package/build/probe/overlay/base/async.jsonc +13 -0
- package/build/probe/overlay/base/exceptions.jsonc +54 -0
- package/build/probe/overlay/base/resources.jsonc +57 -0
- package/build/probe/overlay/load.d.ts +18 -0
- package/build/probe/overlay/load.js +302 -0
- package/build/probe/overlay/presets/express.jsonc +25 -0
- package/build/probe/overlay/presets/node.jsonc +17 -0
- package/build/probe/resources/channel.d.ts +4 -0
- package/build/probe/resources/channel.js +798 -0
- package/build/probe/resources/value.d.ts +9 -0
- package/build/probe/resources/value.js +36 -0
- package/build/tsconfig.d.ts +2 -0
- package/build/tsconfig.js +150 -0
- package/package.json +14 -8
- package/tsconfig.base.json +19 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import type * as ts from '../adapter.js';
|
|
2
|
+
export type FunctionLike = ts.FunctionDeclaration | ts.FunctionExpression | ts.ArrowFunction | ts.MethodDeclaration | ts.GetAccessorDeclaration | ts.SetAccessorDeclaration | ts.ConstructorDeclaration;
|
|
3
|
+
type FunctionInfo = {
|
|
4
|
+
readonly id: string;
|
|
5
|
+
readonly node: FunctionLike;
|
|
6
|
+
readonly sourceFile: ts.SourceFile;
|
|
7
|
+
readonly name: string;
|
|
8
|
+
readonly fileName: string;
|
|
9
|
+
readonly pos: number;
|
|
10
|
+
};
|
|
11
|
+
type Summary<V> = {
|
|
12
|
+
readonly value: V;
|
|
13
|
+
readonly fromCallbacks: ReadonlySet<number>;
|
|
14
|
+
};
|
|
15
|
+
export type Dispatch = 'optimist' | 'pessimist';
|
|
16
|
+
type CalleeResolution = {
|
|
17
|
+
readonly targets: ReadonlyArray<FunctionInfo>;
|
|
18
|
+
readonly overlay: OverlayLookup | undefined;
|
|
19
|
+
readonly functionArgs: ReadonlyMap<number, ReadonlyArray<FunctionInfo>>;
|
|
20
|
+
readonly unresolved: boolean;
|
|
21
|
+
};
|
|
22
|
+
type OverlayLookup = {
|
|
23
|
+
readonly pkg: string;
|
|
24
|
+
readonly symbol: string;
|
|
25
|
+
readonly entry: unknown;
|
|
26
|
+
};
|
|
27
|
+
type TransferContext<V> = {
|
|
28
|
+
readonly checker: ts.TypeChecker;
|
|
29
|
+
readonly fn: FunctionInfo;
|
|
30
|
+
readonly dispatch: Dispatch;
|
|
31
|
+
readonly channelConfig: unknown;
|
|
32
|
+
readonly sinks: ReadonlyArray<SinkConfig>;
|
|
33
|
+
summaryOf(fn: FunctionInfo): Summary<V>;
|
|
34
|
+
resolveCall(call: ts.CallExpression | ts.NewExpression): CalleeResolution;
|
|
35
|
+
resolveCallFor(channel: string, call: ts.CallExpression | ts.NewExpression): CalleeResolution;
|
|
36
|
+
peerSummaryValue(channel: string, fnId: string): unknown;
|
|
37
|
+
};
|
|
38
|
+
type DiagnoseContext<V> = {
|
|
39
|
+
readonly checker: ts.TypeChecker;
|
|
40
|
+
readonly fn: FunctionInfo;
|
|
41
|
+
readonly dispatch: Dispatch;
|
|
42
|
+
readonly channelConfig: unknown;
|
|
43
|
+
readonly sinks: ReadonlyArray<SinkConfig>;
|
|
44
|
+
readonly isBoundary: boolean;
|
|
45
|
+
summaryOf(fn: FunctionInfo): Summary<V>;
|
|
46
|
+
resolveCall(call: ts.CallExpression | ts.NewExpression): CalleeResolution;
|
|
47
|
+
resolveCallFor(channel: string, call: ts.CallExpression | ts.NewExpression): CalleeResolution;
|
|
48
|
+
peerSummaryValue(channel: string, fnId: string): unknown;
|
|
49
|
+
pathToBoundary(fn: FunctionInfo): ReadonlyArray<FunctionInfo>;
|
|
50
|
+
roots(): ReadonlyArray<FunctionInfo>;
|
|
51
|
+
};
|
|
52
|
+
type Channel<V> = {
|
|
53
|
+
readonly name: string;
|
|
54
|
+
readonly dependsOn?: ReadonlyArray<string>;
|
|
55
|
+
bottom(): V;
|
|
56
|
+
equals(a: V, b: V): boolean;
|
|
57
|
+
widen(prev: V, next: V, round: number): V;
|
|
58
|
+
transfer(ctx: TransferContext<V>): Summary<V>;
|
|
59
|
+
diagnose(ctx: DiagnoseContext<V>): ReadonlyArray<Diagnostic>;
|
|
60
|
+
};
|
|
61
|
+
type SourceLocation = {
|
|
62
|
+
readonly fileName: string;
|
|
63
|
+
readonly line: number;
|
|
64
|
+
readonly column: number;
|
|
65
|
+
readonly pos: number;
|
|
66
|
+
readonly end: number;
|
|
67
|
+
};
|
|
68
|
+
type DiagnosticRelated = {
|
|
69
|
+
readonly message: string;
|
|
70
|
+
readonly location: SourceLocation;
|
|
71
|
+
};
|
|
72
|
+
type DiagnosticEdit = {
|
|
73
|
+
readonly fileName: string;
|
|
74
|
+
readonly pos: number;
|
|
75
|
+
readonly end: number;
|
|
76
|
+
readonly newText: string;
|
|
77
|
+
};
|
|
78
|
+
type DiagnosticFix = {
|
|
79
|
+
readonly title: string;
|
|
80
|
+
readonly edits: ReadonlyArray<DiagnosticEdit>;
|
|
81
|
+
};
|
|
82
|
+
type Diagnostic = {
|
|
83
|
+
readonly channel: string;
|
|
84
|
+
readonly message: string;
|
|
85
|
+
readonly location: SourceLocation;
|
|
86
|
+
readonly related: ReadonlyArray<DiagnosticRelated>;
|
|
87
|
+
readonly fixes?: ReadonlyArray<DiagnosticFix>;
|
|
88
|
+
};
|
|
89
|
+
type HandlerBoundary = {
|
|
90
|
+
readonly callee: string;
|
|
91
|
+
readonly callbackArgs: ReadonlyArray<number>;
|
|
92
|
+
};
|
|
93
|
+
type SinkConfig = {
|
|
94
|
+
readonly callee: string;
|
|
95
|
+
readonly absorbs: ReadonlyArray<string> | undefined;
|
|
96
|
+
};
|
|
97
|
+
type ChannelConfig = {
|
|
98
|
+
readonly enabled: boolean;
|
|
99
|
+
readonly dispatch: Dispatch;
|
|
100
|
+
readonly options: unknown;
|
|
101
|
+
};
|
|
102
|
+
type AnalyzeConfig = {
|
|
103
|
+
readonly projectRoot: string;
|
|
104
|
+
readonly tsconfigPath: string;
|
|
105
|
+
readonly entryPoints: ReadonlyArray<string>;
|
|
106
|
+
readonly handlerBoundaries: ReadonlyArray<HandlerBoundary>;
|
|
107
|
+
readonly sinks: ReadonlyArray<SinkConfig>;
|
|
108
|
+
readonly presets: ReadonlyArray<string>;
|
|
109
|
+
readonly overlays: ReadonlyArray<string>;
|
|
110
|
+
readonly channels: Readonly<Record<string, ChannelConfig>>;
|
|
111
|
+
readonly failOnFindings: boolean;
|
|
112
|
+
readonly severity: 'error' | 'warning';
|
|
113
|
+
};
|
|
114
|
+
type OverlaySet = {
|
|
115
|
+
lookup(symbol: ts.Symbol, channel: string): OverlayLookup | undefined;
|
|
116
|
+
};
|
|
117
|
+
type CallGraph = {
|
|
118
|
+
reachedFunctions(): ReadonlyArray<FunctionInfo>;
|
|
119
|
+
boundaries(): ReadonlySet<string>;
|
|
120
|
+
calleesOf(fn: FunctionInfo): ReadonlyArray<FunctionInfo>;
|
|
121
|
+
resolveCall(call: ts.CallExpression | ts.NewExpression, channel: string): CalleeResolution;
|
|
122
|
+
resolveFunctionValue(expr: ts.Expression): ReadonlyArray<FunctionInfo>;
|
|
123
|
+
};
|
|
124
|
+
type SummaryStore<V> = {
|
|
125
|
+
get(id: string): Summary<V>;
|
|
126
|
+
};
|
|
127
|
+
type Analysis = {
|
|
128
|
+
readonly program: ts.Program;
|
|
129
|
+
readonly checker: ts.TypeChecker;
|
|
130
|
+
readonly config: AnalyzeConfig;
|
|
131
|
+
readonly overlays: OverlaySet;
|
|
132
|
+
readonly graph: CallGraph;
|
|
133
|
+
};
|
|
134
|
+
export { type Analysis, type AnalyzeConfig, type CallGraph, type CalleeResolution, type Channel, type ChannelConfig, type DiagnoseContext, type Diagnostic, type DiagnosticEdit, type DiagnosticFix, type DiagnosticRelated, type FunctionInfo, type HandlerBoundary, type OverlayLookup, type OverlaySet, type SinkConfig, type SourceLocation, type Summary, type SummaryStore, type TransferContext };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Base async-channel overlay bundle. The "async" section maps a lib symbol (see
|
|
2
|
+
// load.ts overlayKey) to an async entry. Cancellable leaves accept an
|
|
3
|
+
// `AbortSignal` and should be forwarded any signal the calling function holds:
|
|
4
|
+
// { "cancellable": true } - awaiting this without the held signal is a finding
|
|
5
|
+
{
|
|
6
|
+
"overlay": {
|
|
7
|
+
"async": {
|
|
8
|
+
"lib.dom": {
|
|
9
|
+
"fetch": { "cancellable": true }
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// Base exceptions-channel overlay. Top-level keys are lib sections; each section
|
|
2
|
+
// maps an overlay key (see load.ts overlayKey) to an exceptions entry:
|
|
3
|
+
// { "exceptions": ["SyntaxError"] } - the error types the leaf may throw
|
|
4
|
+
// { "exceptionsFromCallbacks": [0] } - inherits whatever the arg at that index throws
|
|
5
|
+
// { "exceptions": [] } - modeled as non-throwing (so it is not logged)
|
|
6
|
+
// This whole file is the "exceptions" channel section (channel is implicit here).
|
|
7
|
+
{
|
|
8
|
+
"lib.es5": {
|
|
9
|
+
"JSON.parse": { "exceptions": ["SyntaxError"] },
|
|
10
|
+
"JSON.stringify": { "exceptions": [] },
|
|
11
|
+
|
|
12
|
+
"decodeURI": { "exceptions": ["URIError"] },
|
|
13
|
+
"decodeURIComponent": { "exceptions": ["URIError"] },
|
|
14
|
+
"encodeURI": { "exceptions": ["URIError"] },
|
|
15
|
+
"encodeURIComponent": { "exceptions": ["URIError"] },
|
|
16
|
+
|
|
17
|
+
"RegExp": { "exceptions": ["SyntaxError"] },
|
|
18
|
+
|
|
19
|
+
"Number.parseInt": { "exceptions": [] },
|
|
20
|
+
"Number.parseFloat": { "exceptions": [] },
|
|
21
|
+
"parseInt": { "exceptions": [] },
|
|
22
|
+
"parseFloat": { "exceptions": [] },
|
|
23
|
+
|
|
24
|
+
"Array#map": { "exceptionsFromCallbacks": [0] },
|
|
25
|
+
"Array#forEach": { "exceptionsFromCallbacks": [0] },
|
|
26
|
+
"Array#filter": { "exceptionsFromCallbacks": [0] },
|
|
27
|
+
"Array#reduce": { "exceptionsFromCallbacks": [0] },
|
|
28
|
+
"Array#reduceRight": { "exceptionsFromCallbacks": [0] },
|
|
29
|
+
"Array#some": { "exceptionsFromCallbacks": [0] },
|
|
30
|
+
"Array#every": { "exceptionsFromCallbacks": [0] },
|
|
31
|
+
"Array#find": { "exceptionsFromCallbacks": [0] },
|
|
32
|
+
"Array#findIndex": { "exceptionsFromCallbacks": [0] },
|
|
33
|
+
"Array#sort": { "exceptionsFromCallbacks": [0] }
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
"lib.es2015": {
|
|
37
|
+
"Map#forEach": { "exceptionsFromCallbacks": [0] },
|
|
38
|
+
"Set#forEach": { "exceptionsFromCallbacks": [0] }
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
"lib.es2019": {
|
|
42
|
+
"Array#flatMap": { "exceptionsFromCallbacks": [0] }
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
"lib.es2020": {
|
|
46
|
+
"BigInt": { "exceptions": ["RangeError", "SyntaxError", "TypeError"] }
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
"lib.dom": {
|
|
50
|
+
"URL": { "exceptions": ["TypeError"] },
|
|
51
|
+
"URLSearchParams": { "exceptions": ["TypeError"] },
|
|
52
|
+
"structuredClone": { "exceptions": ["DataCloneError"] }
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// Base resources-channel overlay. A BUNDLE file: the `overlay.resources` section
|
|
2
|
+
// maps lib/module sections to an overlay key (see load.ts overlayKey) to a
|
|
3
|
+
// resources entry:
|
|
4
|
+
// { "acquires": "Interval", "releasedBy": "clearInterval" } - free-function release
|
|
5
|
+
// { "acquires": "FileHandle", "releasedBy": "#close" } - method release on the value
|
|
6
|
+
// { "acquires": "Listener", "releasedBy": "removeEventListener", "pairKey": [0, 1] }
|
|
7
|
+
// - receiver-keyed pair release
|
|
8
|
+
// { "acquires": "AbortController", "informational": true } - modeled, not obligation-bearing
|
|
9
|
+
{
|
|
10
|
+
"overlay": {
|
|
11
|
+
"resources": {
|
|
12
|
+
"lib.dom": {
|
|
13
|
+
"setInterval": { "acquires": "Interval", "releasedBy": "clearInterval" },
|
|
14
|
+
"setTimeout": { "acquires": "Timeout", "releasedBy": "clearTimeout" },
|
|
15
|
+
|
|
16
|
+
"EventTarget#addEventListener": { "acquires": "Listener", "releasedBy": "removeEventListener", "pairKey": [0, 1] },
|
|
17
|
+
"Element#addEventListener": { "acquires": "Listener", "releasedBy": "removeEventListener", "pairKey": [0, 1] },
|
|
18
|
+
"HTMLElement#addEventListener": { "acquires": "Listener", "releasedBy": "removeEventListener", "pairKey": [0, 1] },
|
|
19
|
+
"Window#addEventListener": { "acquires": "Listener", "releasedBy": "removeEventListener", "pairKey": [0, 1] },
|
|
20
|
+
"Document#addEventListener": { "acquires": "Listener", "releasedBy": "removeEventListener", "pairKey": [0, 1] },
|
|
21
|
+
"AbortSignal#addEventListener": { "acquires": "Listener", "releasedBy": "removeEventListener", "pairKey": [0, 1] },
|
|
22
|
+
|
|
23
|
+
"ResizeObserver": { "acquires": "ResizeObserver", "releasedBy": "#disconnect" },
|
|
24
|
+
"MutationObserver": { "acquires": "MutationObserver", "releasedBy": "#disconnect" },
|
|
25
|
+
"WebSocket": { "acquires": "WebSocket", "releasedBy": "#close" },
|
|
26
|
+
"AbortController": { "acquires": "AbortController", "informational": true }
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
"node:timers": {
|
|
30
|
+
"setInterval": { "acquires": "Interval", "releasedBy": "clearInterval" },
|
|
31
|
+
"setTimeout": { "acquires": "Timeout", "releasedBy": "clearTimeout" }
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
"node:fs/promises": {
|
|
35
|
+
"open": { "acquires": "FileHandle", "releasedBy": "#close" }
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
"node:fs": {
|
|
39
|
+
"createReadStream": { "acquires": "ReadStream", "releasedBy": "#destroy" },
|
|
40
|
+
"createWriteStream": { "acquires": "WriteStream", "releasedBy": "#destroy" }
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
"node:worker_threads": {
|
|
44
|
+
"Worker": { "acquires": "Worker", "releasedBy": "#terminate" }
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
"node:net": {
|
|
48
|
+
"Socket": { "acquires": "Socket", "releasedBy": "#destroy" },
|
|
49
|
+
"Server#listen": { "acquires": "Server", "releasedBy": "#close" }
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
"node:http": {
|
|
53
|
+
"Server#listen": { "acquires": "Server", "releasedBy": "#close" }
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { HandlerBoundary, OverlaySet, AnalyzeConfig } from '../kernel/types.js';
|
|
2
|
+
type SectionTable = Map<string, unknown>;
|
|
3
|
+
type ChannelTable = Map<string, SectionTable>;
|
|
4
|
+
type MergedOverlay = {
|
|
5
|
+
readonly channels: ReadonlyMap<string, ChannelTable>;
|
|
6
|
+
readonly boundaries: ReadonlyArray<HandlerBoundary>;
|
|
7
|
+
entry(channel: string, section: string, key: string): unknown;
|
|
8
|
+
};
|
|
9
|
+
type LoadedOverlays = OverlaySet & {
|
|
10
|
+
boundariesFromPresets(): ReadonlyArray<HandlerBoundary>;
|
|
11
|
+
};
|
|
12
|
+
declare function mergeOverlayData(files: ReadonlyArray<{
|
|
13
|
+
name: string;
|
|
14
|
+
text: string;
|
|
15
|
+
}>): MergedOverlay;
|
|
16
|
+
declare function overlayKey(parentName: string | undefined, memberName: string, isNamespace: boolean): string;
|
|
17
|
+
declare function loadOverlays(config: AnalyzeConfig): LoadedOverlays;
|
|
18
|
+
export { loadOverlays, mergeOverlayData, overlayKey, type LoadedOverlays, type MergedOverlay };
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import * as NodeFS from 'node:fs';
|
|
2
|
+
import * as NodePath from 'node:path';
|
|
3
|
+
import * as NodeURL from 'node:url';
|
|
4
|
+
import * as ts from '../adapter.js';
|
|
5
|
+
import { stripJsonc } from '../../jsonc.js';
|
|
6
|
+
const KNOWN_NAMESPACES = new Set([
|
|
7
|
+
'JSON',
|
|
8
|
+
'Math',
|
|
9
|
+
'Object',
|
|
10
|
+
'Reflect',
|
|
11
|
+
'Number',
|
|
12
|
+
'Console',
|
|
13
|
+
'console',
|
|
14
|
+
]);
|
|
15
|
+
function legacyStripJsonc(text) {
|
|
16
|
+
let out = '';
|
|
17
|
+
let i = 0;
|
|
18
|
+
const n = text.length;
|
|
19
|
+
let inString = false;
|
|
20
|
+
let quote = '';
|
|
21
|
+
while (i < n) {
|
|
22
|
+
const ch = text[i];
|
|
23
|
+
const next = i + 1 < n ? text[i + 1] : '';
|
|
24
|
+
if (inString) {
|
|
25
|
+
out += ch;
|
|
26
|
+
if (ch === '\\') {
|
|
27
|
+
out += next;
|
|
28
|
+
i += 2;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (ch === quote) {
|
|
32
|
+
inString = false;
|
|
33
|
+
}
|
|
34
|
+
i += 1;
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (ch === '"' || ch === "'") {
|
|
38
|
+
inString = true;
|
|
39
|
+
quote = ch;
|
|
40
|
+
out += ch;
|
|
41
|
+
i += 1;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (ch === '/' && next === '/') {
|
|
45
|
+
while (i < n && text[i] !== '\n') {
|
|
46
|
+
i += 1;
|
|
47
|
+
}
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (ch === '/' && next === '*') {
|
|
51
|
+
i += 2;
|
|
52
|
+
while (i < n && !(text[i] === '*' && text[i + 1] === '/')) {
|
|
53
|
+
i += 1;
|
|
54
|
+
}
|
|
55
|
+
i += 2;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
out += ch;
|
|
59
|
+
i += 1;
|
|
60
|
+
}
|
|
61
|
+
return out.replace(/,(\s*[}\]])/g, '$1');
|
|
62
|
+
}
|
|
63
|
+
void legacyStripJsonc;
|
|
64
|
+
function parseJsonc(name, text) {
|
|
65
|
+
let parsed;
|
|
66
|
+
try {
|
|
67
|
+
parsed = JSON.parse(stripJsonc(text));
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
throw new Error(`analyze overlay: invalid JSONC in ${name}: ${error.message}`, {
|
|
71
|
+
cause: error,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
if (typeof parsed !== 'object' ||
|
|
75
|
+
parsed === null ||
|
|
76
|
+
Array.isArray(parsed)) {
|
|
77
|
+
throw new Error(`analyze overlay: ${name} must be a JSON object`);
|
|
78
|
+
}
|
|
79
|
+
return parsed;
|
|
80
|
+
}
|
|
81
|
+
function isSectionMap(value) {
|
|
82
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
83
|
+
}
|
|
84
|
+
function mergeSections(into, sections) {
|
|
85
|
+
for (const [section, keys] of Object.entries(sections)) {
|
|
86
|
+
if (!isSectionMap(keys)) {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
let table = into.get(section);
|
|
90
|
+
if (!table) {
|
|
91
|
+
table = new Map();
|
|
92
|
+
into.set(section, table);
|
|
93
|
+
}
|
|
94
|
+
for (const [key, entry] of Object.entries(keys)) {
|
|
95
|
+
table.set(key, entry);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function readBoundaries(name, raw) {
|
|
100
|
+
if (raw === undefined) {
|
|
101
|
+
return [];
|
|
102
|
+
}
|
|
103
|
+
if (!Array.isArray(raw)) {
|
|
104
|
+
throw new Error(`analyze overlay: ${name} handlerBoundaries must be an array`);
|
|
105
|
+
}
|
|
106
|
+
return raw.map((item, index) => {
|
|
107
|
+
if (typeof item !== 'object' || item === null) {
|
|
108
|
+
throw new Error(`analyze overlay: ${name} handlerBoundaries[${index}] must be an object`);
|
|
109
|
+
}
|
|
110
|
+
const obj = item;
|
|
111
|
+
const callee = obj['callee'];
|
|
112
|
+
const callbackArgs = obj['callbackArgs'];
|
|
113
|
+
if (typeof callee !== 'string') {
|
|
114
|
+
throw new Error(`analyze overlay: ${name} handlerBoundaries[${index}].callee must be a string`);
|
|
115
|
+
}
|
|
116
|
+
if (!Array.isArray(callbackArgs) ||
|
|
117
|
+
callbackArgs.some((v) => typeof v !== 'number' || !Number.isInteger(v))) {
|
|
118
|
+
throw new Error(`analyze overlay: ${name} handlerBoundaries[${index}].callbackArgs must be an array of integers`);
|
|
119
|
+
}
|
|
120
|
+
return { callee, callbackArgs: callbackArgs };
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
function mergeOverlayData(files) {
|
|
124
|
+
return mergeParsedOverlayData(files.map((file) => ({
|
|
125
|
+
name: file.name,
|
|
126
|
+
root: parseJsonc(file.name, file.text),
|
|
127
|
+
})));
|
|
128
|
+
}
|
|
129
|
+
function mergeParsedOverlayData(files) {
|
|
130
|
+
const channels = new Map();
|
|
131
|
+
const boundaries = [];
|
|
132
|
+
const channelFor = (channel) => {
|
|
133
|
+
let table = channels.get(channel);
|
|
134
|
+
if (!table) {
|
|
135
|
+
table = new Map();
|
|
136
|
+
channels.set(channel, table);
|
|
137
|
+
}
|
|
138
|
+
return table;
|
|
139
|
+
};
|
|
140
|
+
for (const file of files) {
|
|
141
|
+
const root = file.root;
|
|
142
|
+
const isBundle = 'overlay' in root || 'handlerBoundaries' in root;
|
|
143
|
+
if (isBundle) {
|
|
144
|
+
boundaries.push(...readBoundaries(file.name, root['handlerBoundaries']));
|
|
145
|
+
const overlay = root['overlay'];
|
|
146
|
+
if (overlay !== undefined) {
|
|
147
|
+
if (!isSectionMap(overlay)) {
|
|
148
|
+
throw new Error(`analyze overlay: ${file.name} overlay must be an object`);
|
|
149
|
+
}
|
|
150
|
+
for (const [channel, sections] of Object.entries(overlay)) {
|
|
151
|
+
if (isSectionMap(sections)) {
|
|
152
|
+
mergeSections(channelFor(channel), sections);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
mergeSections(channelFor('exceptions'), root);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return {
|
|
162
|
+
channels,
|
|
163
|
+
boundaries,
|
|
164
|
+
entry(channel, section, key) {
|
|
165
|
+
return channels.get(channel)?.get(section)?.get(key);
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
function overlayKey(parentName, memberName, isNamespace) {
|
|
170
|
+
if (!parentName) {
|
|
171
|
+
return memberName;
|
|
172
|
+
}
|
|
173
|
+
return isNamespace
|
|
174
|
+
? `${parentName}.${memberName}`
|
|
175
|
+
: `${parentName}#${memberName}`;
|
|
176
|
+
}
|
|
177
|
+
function stripConstructor(name) {
|
|
178
|
+
return name.endsWith('Constructor')
|
|
179
|
+
? name.slice(0, -'Constructor'.length)
|
|
180
|
+
: name;
|
|
181
|
+
}
|
|
182
|
+
function declaredParentName(symbol) {
|
|
183
|
+
for (const decl of ts.symbolDeclarations(symbol)) {
|
|
184
|
+
const parent = decl.parent;
|
|
185
|
+
if (parent &&
|
|
186
|
+
(ts.isInterfaceDeclaration(parent) || ts.isClassDeclaration(parent))) {
|
|
187
|
+
return parent.name?.text;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return undefined;
|
|
191
|
+
}
|
|
192
|
+
function sourceHint(symbol) {
|
|
193
|
+
for (const decl of ts.symbolDeclarations(symbol)) {
|
|
194
|
+
const file = decl.getSourceFile().fileName;
|
|
195
|
+
const base = NodePath.basename(file);
|
|
196
|
+
const libMatch = /^(lib(?:\.[a-z0-9]+){1,})\.d\.ts$/.exec(base);
|
|
197
|
+
if (libMatch) {
|
|
198
|
+
const parts = libMatch[1].split('.');
|
|
199
|
+
return { kind: 'lib', section: parts.slice(0, 2).join('.') };
|
|
200
|
+
}
|
|
201
|
+
const normalized = file.replace(/\\/g, '/');
|
|
202
|
+
if (normalized.includes('@types/node/') ||
|
|
203
|
+
normalized.includes('/node_modules/node/')) {
|
|
204
|
+
return {
|
|
205
|
+
kind: 'node',
|
|
206
|
+
section: `node:${base.replace(/\.d\.ts$/, '')}`,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return { kind: 'other', section: undefined };
|
|
211
|
+
}
|
|
212
|
+
function keyFor(symbol) {
|
|
213
|
+
const parent = declaredParentName(symbol);
|
|
214
|
+
const stripped = parent ? stripConstructor(parent) : undefined;
|
|
215
|
+
const isNamespace = stripped ? KNOWN_NAMESPACES.has(stripped) : false;
|
|
216
|
+
return overlayKey(stripped, symbol.name, isNamespace);
|
|
217
|
+
}
|
|
218
|
+
function findInSections(channel, key, accept, preferred) {
|
|
219
|
+
if (preferred) {
|
|
220
|
+
const table = channel.get(preferred);
|
|
221
|
+
if (table && table.has(key)) {
|
|
222
|
+
return { pkg: preferred, symbol: key, entry: table.get(key) };
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
for (const [section, table] of channel) {
|
|
226
|
+
if (section === preferred || !accept(section)) {
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
if (table.has(key)) {
|
|
230
|
+
return { pkg: section, symbol: key, entry: table.get(key) };
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return undefined;
|
|
234
|
+
}
|
|
235
|
+
const HERE = NodePath.dirname(NodeURL.fileURLToPath(import.meta.url));
|
|
236
|
+
const shippedOverlayCache = new Map();
|
|
237
|
+
const userOverlayCache = new Map();
|
|
238
|
+
function readShippedOverlay(file) {
|
|
239
|
+
let cached = shippedOverlayCache.get(file);
|
|
240
|
+
if (!cached) {
|
|
241
|
+
cached = {
|
|
242
|
+
name: file,
|
|
243
|
+
root: parseJsonc(file, NodeFS.readFileSync(file, 'utf8')),
|
|
244
|
+
};
|
|
245
|
+
shippedOverlayCache.set(file, cached);
|
|
246
|
+
}
|
|
247
|
+
return cached;
|
|
248
|
+
}
|
|
249
|
+
function readUserOverlay(file) {
|
|
250
|
+
const mtimeMs = NodeFS.statSync(file).mtimeMs;
|
|
251
|
+
const cached = userOverlayCache.get(file);
|
|
252
|
+
if (!cached || cached.mtimeMs !== mtimeMs) {
|
|
253
|
+
const fresh = {
|
|
254
|
+
name: file,
|
|
255
|
+
root: parseJsonc(file, NodeFS.readFileSync(file, 'utf8')),
|
|
256
|
+
};
|
|
257
|
+
userOverlayCache.set(file, { mtimeMs, file: fresh });
|
|
258
|
+
return fresh;
|
|
259
|
+
}
|
|
260
|
+
return cached.file;
|
|
261
|
+
}
|
|
262
|
+
function loadOverlays(config) {
|
|
263
|
+
const files = [];
|
|
264
|
+
files.push(readShippedOverlay(NodePath.join(HERE, 'base', 'async.jsonc')));
|
|
265
|
+
files.push(readShippedOverlay(NodePath.join(HERE, 'base', 'exceptions.jsonc')));
|
|
266
|
+
files.push(readShippedOverlay(NodePath.join(HERE, 'base', 'resources.jsonc')));
|
|
267
|
+
for (const preset of config.presets) {
|
|
268
|
+
const presetPath = NodePath.join(HERE, 'presets', `${preset}.jsonc`);
|
|
269
|
+
if (!NodeFS.existsSync(presetPath)) {
|
|
270
|
+
throw new Error(`analyze overlay: unknown preset "${preset}" (no file at ${presetPath})`);
|
|
271
|
+
}
|
|
272
|
+
files.push(readShippedOverlay(presetPath));
|
|
273
|
+
}
|
|
274
|
+
for (const overlay of config.overlays) {
|
|
275
|
+
if (!NodeFS.existsSync(overlay)) {
|
|
276
|
+
throw new Error(`analyze overlay: overlay file not found: ${overlay}`);
|
|
277
|
+
}
|
|
278
|
+
files.push(readUserOverlay(overlay));
|
|
279
|
+
}
|
|
280
|
+
const merged = mergeParsedOverlayData(files);
|
|
281
|
+
return {
|
|
282
|
+
boundariesFromPresets() {
|
|
283
|
+
return merged.boundaries;
|
|
284
|
+
},
|
|
285
|
+
lookup(symbol, channel) {
|
|
286
|
+
const table = merged.channels.get(channel);
|
|
287
|
+
if (!table) {
|
|
288
|
+
return undefined;
|
|
289
|
+
}
|
|
290
|
+
const key = keyFor(symbol);
|
|
291
|
+
const hint = sourceHint(symbol);
|
|
292
|
+
if (hint.kind === 'lib') {
|
|
293
|
+
return findInSections(table, key, (s) => s.startsWith('lib.'), hint.section);
|
|
294
|
+
}
|
|
295
|
+
if (hint.kind === 'node') {
|
|
296
|
+
return findInSections(table, key, (s) => s.startsWith('node:'), hint.section);
|
|
297
|
+
}
|
|
298
|
+
return findInSections(table, key, (s) => !s.startsWith('lib.'), undefined);
|
|
299
|
+
},
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
export { loadOverlays, mergeOverlayData, overlayKey };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Express preset bundle. `handlerBoundaries` promote route callbacks to
|
|
2
|
+
// analysis boundaries (loadOverlays exposes them via boundariesFromPresets()).
|
|
3
|
+
// `overlay` follows the channel -> section -> key shape.
|
|
4
|
+
{
|
|
5
|
+
"handlerBoundaries": [
|
|
6
|
+
{ "callee": "express.Router#get", "callbackArgs": [1] },
|
|
7
|
+
{ "callee": "express.Router#post", "callbackArgs": [1] },
|
|
8
|
+
{ "callee": "express.Router#put", "callbackArgs": [1] },
|
|
9
|
+
{ "callee": "express.Router#patch", "callbackArgs": [1] },
|
|
10
|
+
{ "callee": "express.Router#delete", "callbackArgs": [1] },
|
|
11
|
+
{ "callee": "express.Router#all", "callbackArgs": [1] },
|
|
12
|
+
{ "callee": "express.Router#use", "callbackArgs": [0, 1] },
|
|
13
|
+
{ "callee": "express.Application#get", "callbackArgs": [1] },
|
|
14
|
+
{ "callee": "express.Application#post", "callbackArgs": [1] },
|
|
15
|
+
{ "callee": "express.Application#use", "callbackArgs": [0, 1] }
|
|
16
|
+
],
|
|
17
|
+
"overlay": {
|
|
18
|
+
"exceptions": {
|
|
19
|
+
"express": {
|
|
20
|
+
"Response#json": { "exceptions": ["TypeError"] },
|
|
21
|
+
"Response#sendFile": { "exceptions": ["TypeError"] }
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Node preset bundle. `overlay` is keyed by channel, then section (module),
|
|
2
|
+
// then overlay key. A couple of node:fs / node:path throwers prove the shape.
|
|
3
|
+
{
|
|
4
|
+
"overlay": {
|
|
5
|
+
"exceptions": {
|
|
6
|
+
"node:fs": {
|
|
7
|
+
"readFileSync": { "exceptions": ["Error"] },
|
|
8
|
+
"writeFileSync": { "exceptions": ["Error"] },
|
|
9
|
+
"mkdirSync": { "exceptions": ["Error"] }
|
|
10
|
+
},
|
|
11
|
+
"node:path": {
|
|
12
|
+
"relative": { "exceptions": ["TypeError"] },
|
|
13
|
+
"resolve": { "exceptions": ["TypeError"] }
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|