@cloudcare/hvigor-ohos-plugin 0.1.0 → 0.1.2-alpha.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/CHANGELOG.md +14 -2
- package/README.md +32 -17
- package/hvigorCompile/ft-cold-launch-transform.ts +595 -192
- package/hvigorCompile/ft-console-log-transform.ts +72 -176
- package/hvigorCompile/ft-generated-names.ts +19 -0
- package/hvigorCompile/ft-lexical-scope.ts +65 -0
- package/hvigorCompile/ft-session-replay-transform.ts +363 -0
- package/hvigorCompile/ft-source-transform-host.ts +135 -0
- package/hvigorCompile/ft-transform-context.ts +36 -0
- package/hvigorCompile/ft-web-replay-transform.ts +51 -0
- package/index.js +7 -7
- package/package.json +13 -3
|
@@ -7,28 +7,12 @@
|
|
|
7
7
|
* never changed.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
};
|
|
14
|
-
};
|
|
10
|
+
import { hasRuntimeDependency, SourceReader } from './ft-transform-context';
|
|
11
|
+
import { createLexicalResolver } from './ft-lexical-scope';
|
|
12
|
+
import { allocateName, collectIdentifierNames } from './ft-generated-names';
|
|
15
13
|
|
|
16
14
|
type TypeScriptApi = any;
|
|
17
15
|
|
|
18
|
-
interface CompilePluginContext {
|
|
19
|
-
share: {
|
|
20
|
-
projectConfig: {
|
|
21
|
-
etsLoaderPath: string;
|
|
22
|
-
};
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
interface SharedSourceTransformRegistry {
|
|
27
|
-
originalReadFile: (fileName: string, options?: any) => any;
|
|
28
|
-
buffer: any;
|
|
29
|
-
transforms: Map<string, (fileName: string, code: string) => string>;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
16
|
interface TransformCandidate {
|
|
33
17
|
start: number;
|
|
34
18
|
end: number;
|
|
@@ -37,11 +21,14 @@ interface TransformCandidate {
|
|
|
37
21
|
level: string;
|
|
38
22
|
source: 'console' | 'hilog';
|
|
39
23
|
callArguments: SourceRange[];
|
|
24
|
+
needsStatementSeparator: boolean;
|
|
25
|
+
generatedNames?: string[];
|
|
40
26
|
}
|
|
41
27
|
|
|
42
28
|
interface SourceRange {
|
|
43
29
|
start: number;
|
|
44
30
|
end: number;
|
|
31
|
+
spread: boolean;
|
|
45
32
|
}
|
|
46
33
|
|
|
47
34
|
const HILOG_MODULES: string[] = ['@ohos.hilog', '@kit.PerformanceAnalysisKit'];
|
|
@@ -52,10 +39,6 @@ const BRIDGE_IMPORT = '@guancecloud/ft_sdk/Index';
|
|
|
52
39
|
// incremental/daemon build. Keep a marker in the in-memory result so a later
|
|
53
40
|
// reader does not wrap the original console/HiLog call a second time.
|
|
54
41
|
const TRANSFORM_MARKER = '/* guance-console-log-transform:instrumented */';
|
|
55
|
-
const SOURCE_TRANSFORM_REGISTRY_KEY = '__guanceHvigorSourceTransformRegistry';
|
|
56
|
-
const TRANSFORM_ID = 'guance-console-log-transform';
|
|
57
|
-
|
|
58
|
-
let registeredFileSystem: any = null;
|
|
59
42
|
|
|
60
43
|
export function isApplicationArkTsSource(id: string): boolean {
|
|
61
44
|
const normalizedId = id.replace(/\\/g, '/');
|
|
@@ -69,77 +52,58 @@ function visit(ts: TypeScriptApi, node: any, visitor: (current: any) => void): v
|
|
|
69
52
|
ts.forEachChild(node, (child: any) => visit(ts, child, visitor));
|
|
70
53
|
}
|
|
71
54
|
|
|
72
|
-
function
|
|
73
|
-
const
|
|
74
|
-
visit(ts, sourceFile, (node: any) => {
|
|
75
|
-
if (ts.isIdentifier(node)) {
|
|
76
|
-
identifiers.add(node.text);
|
|
77
|
-
}
|
|
78
|
-
});
|
|
79
|
-
return identifiers;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function isNameDeclaredOutsideImports(ts: TypeScriptApi, sourceFile: any, name: string): boolean {
|
|
83
|
-
let declared = false;
|
|
84
|
-
visit(ts, sourceFile, (node: any) => {
|
|
85
|
-
if (declared || ts.isImportDeclaration(node)) {
|
|
86
|
-
return;
|
|
87
|
-
}
|
|
88
|
-
if (ts.isVariableDeclaration(node) || ts.isFunctionDeclaration(node) || ts.isClassDeclaration(node) ||
|
|
89
|
-
ts.isParameter(node) || ts.isEnumDeclaration(node) || ts.isInterfaceDeclaration(node) ||
|
|
90
|
-
ts.isTypeAliasDeclaration(node)) {
|
|
91
|
-
const declarationName = node.name;
|
|
92
|
-
if (declarationName && ts.isIdentifier(declarationName) && declarationName.text === name) {
|
|
93
|
-
declared = true;
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
});
|
|
97
|
-
return declared;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function collectHiLogBindings(ts: TypeScriptApi, sourceFile: any): Set<string> {
|
|
101
|
-
const bindings = new Set<string>();
|
|
55
|
+
function collectHiLogBindings(ts: TypeScriptApi, sourceFile: any): Map<string, any> {
|
|
56
|
+
const bindings = new Map<string, any>();
|
|
102
57
|
sourceFile.statements.forEach((statement: any) => {
|
|
103
58
|
if (!ts.isImportDeclaration(statement) || !statement.moduleSpecifier ||
|
|
104
59
|
!ts.isStringLiteral(statement.moduleSpecifier) ||
|
|
105
|
-
!HILOG_MODULES.includes(statement.moduleSpecifier.text) || !statement.importClause) {
|
|
60
|
+
!HILOG_MODULES.includes(statement.moduleSpecifier.text) || !statement.importClause || statement.importClause.isTypeOnly) {
|
|
106
61
|
return;
|
|
107
62
|
}
|
|
108
63
|
|
|
109
64
|
const importClause = statement.importClause;
|
|
110
65
|
if (importClause.name) {
|
|
111
|
-
bindings.
|
|
66
|
+
bindings.set(importClause.name.text, importClause);
|
|
112
67
|
}
|
|
113
68
|
if (!importClause.namedBindings) {
|
|
114
69
|
return;
|
|
115
70
|
}
|
|
116
71
|
if (ts.isNamespaceImport(importClause.namedBindings)) {
|
|
117
|
-
bindings.
|
|
72
|
+
bindings.set(importClause.namedBindings.name.text, importClause.namedBindings);
|
|
118
73
|
return;
|
|
119
74
|
}
|
|
120
75
|
importClause.namedBindings.elements.forEach((element: any) => {
|
|
121
76
|
const importedName = element.propertyName ? element.propertyName.text : element.name.text;
|
|
122
|
-
if (importedName === 'hilog') {
|
|
123
|
-
bindings.
|
|
77
|
+
if (importedName === 'hilog' && !element.isTypeOnly) {
|
|
78
|
+
bindings.set(element.name.text, element);
|
|
124
79
|
}
|
|
125
80
|
});
|
|
126
81
|
});
|
|
127
82
|
return bindings;
|
|
128
83
|
}
|
|
129
84
|
|
|
85
|
+
function needsStatementSeparator(ts: TypeScriptApi, node: any): boolean {
|
|
86
|
+
const statement = node.parent;
|
|
87
|
+
if (!ts.isExpressionStatement(statement)) {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
const container = statement.parent;
|
|
91
|
+
// Only statement lists accept a leading empty statement. In an unbraced
|
|
92
|
+
// if/loop body it would replace the body; inside an expression it is invalid.
|
|
93
|
+
return ts.isSourceFile(container) || ts.isBlock(container) || ts.isModuleBlock(container) ||
|
|
94
|
+
ts.isCaseClause(container) || ts.isDefaultClause(container);
|
|
95
|
+
}
|
|
96
|
+
|
|
130
97
|
function collectCandidates(ts: TypeScriptApi, sourceFile: any, code: string): TransformCandidate[] {
|
|
131
98
|
const candidates: TransformCandidate[] = [];
|
|
132
99
|
const hiLogBindings = collectHiLogBindings(ts, sourceFile);
|
|
133
|
-
const
|
|
134
|
-
const shadowedHiLogBindings = new Set<string>();
|
|
135
|
-
hiLogBindings.forEach((binding: string) => {
|
|
136
|
-
if (isNameDeclaredOutsideImports(ts, sourceFile, binding)) {
|
|
137
|
-
shadowedHiLogBindings.add(binding);
|
|
138
|
-
}
|
|
139
|
-
});
|
|
100
|
+
const resolve = createLexicalResolver(ts, sourceFile);
|
|
140
101
|
|
|
141
102
|
visit(ts, sourceFile, (node: any) => {
|
|
142
|
-
|
|
103
|
+
// Retain optional calls verbatim so their receiver/method short-circuit also
|
|
104
|
+
// controls argument evaluation. Nested ordinary calls remain eligible.
|
|
105
|
+
if (!ts.isCallExpression(node) || !ts.isPropertyAccessExpression(node.expression) ||
|
|
106
|
+
(ts.isOptionalChain && ts.isOptionalChain(node))) {
|
|
143
107
|
return;
|
|
144
108
|
}
|
|
145
109
|
const delegate = node.expression.expression;
|
|
@@ -157,29 +121,27 @@ function collectCandidates(ts: TypeScriptApi, sourceFile: any, code: string): Tr
|
|
|
157
121
|
|
|
158
122
|
const callArguments: SourceRange[] = node.arguments.map((argument: any) => ({
|
|
159
123
|
start: argument.getStart(sourceFile),
|
|
160
|
-
end: argument.end
|
|
124
|
+
end: argument.end,
|
|
125
|
+
spread: ts.isSpreadElement(argument)
|
|
161
126
|
}));
|
|
162
|
-
|
|
163
|
-
|
|
127
|
+
const statementSeparator = needsStatementSeparator(ts, node);
|
|
128
|
+
if (delegateName === 'console' && !resolve(delegate, delegateName) && CONSOLE_LEVELS.includes(level)) {
|
|
129
|
+
candidates.push({ start, end, calleeEnd, delegate: delegateName, level, source: 'console', callArguments,
|
|
130
|
+
needsStatementSeparator: statementSeparator });
|
|
164
131
|
return;
|
|
165
132
|
}
|
|
166
|
-
if (hiLogBindings.has(delegateName) &&
|
|
167
|
-
HILOG_LEVELS.includes(level) && callArguments.length >= 3) {
|
|
168
|
-
candidates.push({ start, end, calleeEnd, delegate: delegateName, level, source: 'hilog', callArguments
|
|
133
|
+
if (hiLogBindings.has(delegateName) && resolve(delegate, delegateName) === hiLogBindings.get(delegateName) &&
|
|
134
|
+
HILOG_LEVELS.includes(level) && (callArguments.length >= 3 || callArguments.some(argument => argument.spread))) {
|
|
135
|
+
candidates.push({ start, end, calleeEnd, delegate: delegateName, level, source: 'hilog', callArguments,
|
|
136
|
+
needsStatementSeparator: statementSeparator });
|
|
169
137
|
}
|
|
170
138
|
});
|
|
171
139
|
return candidates;
|
|
172
140
|
}
|
|
173
141
|
|
|
174
|
-
function
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
let suffix = 1;
|
|
178
|
-
while (identifiers.has(bridgeName)) {
|
|
179
|
-
bridgeName = `${baseName}${suffix}`;
|
|
180
|
-
suffix++;
|
|
181
|
-
}
|
|
182
|
-
return bridgeName;
|
|
142
|
+
function allocateCandidateNames(candidate: TransformCandidate, identifiers: Set<string>): string[] {
|
|
143
|
+
const prefix = candidate.source === 'console' ? '__ftConsole' : '__ftHiLog';
|
|
144
|
+
return ['Call', 'Args', 'Values'].map(suffix => allocateName(identifiers, prefix + suffix));
|
|
183
145
|
}
|
|
184
146
|
|
|
185
147
|
function isNestedIn(candidate: TransformCandidate, parent: TransformCandidate): boolean {
|
|
@@ -211,42 +173,31 @@ function renderCandidate(code: string, candidates: TransformCandidate[], bridgeN
|
|
|
211
173
|
candidate: TransformCandidate): string {
|
|
212
174
|
const renderedArguments = candidate.callArguments.map((argument: SourceRange) =>
|
|
213
175
|
renderCandidates(code, candidates, bridgeName, argument.start, argument.end));
|
|
176
|
+
const statementSeparator = candidate.needsStatementSeparator ? ';' : '';
|
|
177
|
+
const [original, args, values] = candidate.generatedNames!;
|
|
178
|
+
const valueType = '(Object | null | undefined)';
|
|
179
|
+
const originalType = candidate.source === 'console' ? `(...${args}: ${valueType}[]) => void` :
|
|
180
|
+
`(domain: number, tag: string, format: string, ...${values}: ${valueType}[]) => void`;
|
|
181
|
+
let body: string;
|
|
214
182
|
if (candidate.source === 'console') {
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
const
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
183
|
+
body = `${bridgeName}.console('${candidate.level}', (): void => ${original}(...${args}), ` +
|
|
184
|
+
`...(${args} as Object[]))`;
|
|
185
|
+
} else {
|
|
186
|
+
const header = `${args}[0] as number, ${args}[1] as string, ${args}[2] as string`;
|
|
187
|
+
body = `{ const ${values}: ${valueType}[] = ${args}.slice(3); ` +
|
|
188
|
+
`${bridgeName}.hilog('${candidate.level}', ${header}, (): void => ${original}(${header}, ...${values}), ` +
|
|
189
|
+
`...(${values} as Object[])); }`;
|
|
222
190
|
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
}
|
|
234
|
-
return `__ftHiLogArg${index - 3}`;
|
|
235
|
-
});
|
|
236
|
-
const parameterDeclarations = argumentNames.map((name: string, index: number) => {
|
|
237
|
-
if (index === 0) {
|
|
238
|
-
return `${name}: number`;
|
|
239
|
-
}
|
|
240
|
-
if (index === 1 || index === 2) {
|
|
241
|
-
return `${name}: string`;
|
|
242
|
-
}
|
|
243
|
-
return `${name}: Object`;
|
|
244
|
-
}).join(', ');
|
|
245
|
-
const originalCall = `${candidate.delegate}.${candidate.level}(${argumentNames.join(', ')})`;
|
|
246
|
-
const captureArguments = argumentNames.length > 3 ? `, ${argumentNames.slice(3).join(', ')}` : '';
|
|
247
|
-
return `((${parameterDeclarations}): void => ${bridgeName}.hilog('${candidate.level}', ` +
|
|
248
|
-
`${argumentNames[0]}, ${argumentNames[1]}, ${argumentNames[2]}, (): void => ${originalCall}` +
|
|
249
|
-
`${captureArguments}))(${renderedArguments.join(', ')})`;
|
|
191
|
+
// Resolve and bind the original method before evaluating its arguments.
|
|
192
|
+
// Casting only the capture array keeps compatibility with older runtime HARs;
|
|
193
|
+
// null/undefined values themselves are forwarded unchanged.
|
|
194
|
+
// HiLog is a platform namespace, not an ArkTS object receiver. Its exported
|
|
195
|
+
// functions are static; console methods retain their receiver through bind.
|
|
196
|
+
const callee = `${candidate.delegate}.${candidate.level}`;
|
|
197
|
+
const captured = candidate.source === 'console' ? `${callee}.bind(${candidate.delegate})` : callee;
|
|
198
|
+
const argumentsText = renderedArguments.length > 0 ? ', ' + renderedArguments.join(', ') : '';
|
|
199
|
+
return `${statementSeparator}((${original}: ${originalType}, ...${args}: ${valueType}[]): void => ${body})` +
|
|
200
|
+
`(${captured} as ${originalType}${argumentsText})`;
|
|
250
201
|
}
|
|
251
202
|
|
|
252
203
|
function findImportInsertionOffset(ts: TypeScriptApi, sourceFile: any): number {
|
|
@@ -259,8 +210,9 @@ function findImportInsertionOffset(ts: TypeScriptApi, sourceFile: any): number {
|
|
|
259
210
|
return offset;
|
|
260
211
|
}
|
|
261
212
|
|
|
262
|
-
function
|
|
263
|
-
if (!isApplicationArkTsSource(id) || code.includes(TRANSFORM_MARKER)
|
|
213
|
+
export function transformConsoleLogSource(ts: TypeScriptApi, id: string, code: string, readSource?: SourceReader): string {
|
|
214
|
+
if (!isApplicationArkTsSource(id) || code.includes(TRANSFORM_MARKER) ||
|
|
215
|
+
!hasRuntimeDependency(ts, id, '@guancecloud/ft_sdk', readSource)) {
|
|
264
216
|
return code;
|
|
265
217
|
}
|
|
266
218
|
const sourceFile = ts.createSourceFile(id, code, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
@@ -269,7 +221,9 @@ function rewriteSource(ts: TypeScriptApi, id: string, code: string): string {
|
|
|
269
221
|
return code;
|
|
270
222
|
}
|
|
271
223
|
|
|
272
|
-
const
|
|
224
|
+
const identifiers = collectIdentifierNames(ts, sourceFile);
|
|
225
|
+
const bridgeName = allocateName(identifiers, '__ftConsoleLogBridge');
|
|
226
|
+
candidates.forEach(candidate => { candidate.generatedNames = allocateCandidateNames(candidate, identifiers); });
|
|
273
227
|
const transformedCode = renderCandidates(code, candidates, bridgeName, 0, code.length);
|
|
274
228
|
const insertionOffset = findImportInsertionOffset(ts, sourceFile);
|
|
275
229
|
const transformedInsertionOffset = renderCandidates(code, candidates, bridgeName, 0, insertionOffset).length;
|
|
@@ -279,61 +233,3 @@ function rewriteSource(ts: TypeScriptApi, id: string, code: string): string {
|
|
|
279
233
|
return transformedCode.slice(0, transformedInsertionOffset) + bridgeImport +
|
|
280
234
|
transformedCode.slice(transformedInsertionOffset);
|
|
281
235
|
}
|
|
282
|
-
|
|
283
|
-
function installSourceReader(context: CompilePluginContext): void {
|
|
284
|
-
if (registeredFileSystem !== null) {
|
|
285
|
-
return;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
const typeScriptPath = `${context.share.projectConfig.etsLoaderPath}/node_modules/typescript`;
|
|
289
|
-
const ts = require.main.require(typeScriptPath);
|
|
290
|
-
const fileSystem = require.main.require('fs');
|
|
291
|
-
let registry = fileSystem[SOURCE_TRANSFORM_REGISTRY_KEY] as SharedSourceTransformRegistry | undefined;
|
|
292
|
-
if (!registry) {
|
|
293
|
-
registry = {
|
|
294
|
-
originalReadFile: fileSystem.readFileSync as (fileName: string, options?: any) => any,
|
|
295
|
-
buffer: require.main.require('buffer'),
|
|
296
|
-
transforms: new Map<string, (fileName: string, code: string) => string>()
|
|
297
|
-
};
|
|
298
|
-
fileSystem[SOURCE_TRANSFORM_REGISTRY_KEY] = registry;
|
|
299
|
-
fileSystem.readFileSync = (fileName: string, options?: any): any => {
|
|
300
|
-
const content = registry!.originalReadFile(fileName, options);
|
|
301
|
-
if (typeof fileName !== 'string' || !isApplicationArkTsSource(fileName)) {
|
|
302
|
-
return content;
|
|
303
|
-
}
|
|
304
|
-
let transformedCode = typeof content === 'string' ? content : content.toString();
|
|
305
|
-
registry!.transforms.forEach((transform: (id: string, code: string) => string) => {
|
|
306
|
-
transformedCode = transform(fileName, transformedCode);
|
|
307
|
-
});
|
|
308
|
-
return typeof content === 'string' ? transformedCode : registry!.buffer.Buffer.from(transformedCode);
|
|
309
|
-
};
|
|
310
|
-
}
|
|
311
|
-
registry.transforms.set(TRANSFORM_ID, (fileName: string, code: string) => rewriteSource(ts, fileName, code));
|
|
312
|
-
registeredFileSystem = fileSystem;
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
function restoreSourceReader(): void {
|
|
316
|
-
if (registeredFileSystem === null) {
|
|
317
|
-
return;
|
|
318
|
-
}
|
|
319
|
-
const registry = registeredFileSystem[SOURCE_TRANSFORM_REGISTRY_KEY] as SharedSourceTransformRegistry | undefined;
|
|
320
|
-
if (registry) {
|
|
321
|
-
registry.transforms.delete(TRANSFORM_ID);
|
|
322
|
-
if (registry.transforms.size === 0) {
|
|
323
|
-
registeredFileSystem.readFileSync = registry.originalReadFile;
|
|
324
|
-
delete registeredFileSystem[SOURCE_TRANSFORM_REGISTRY_KEY];
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
registeredFileSystem = null;
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
export default {
|
|
331
|
-
name: 'guance-console-log-transform',
|
|
332
|
-
order: 'first',
|
|
333
|
-
buildStart(this: CompilePluginContext): void {
|
|
334
|
-
installSourceReader(this);
|
|
335
|
-
},
|
|
336
|
-
buildEnd(): void {
|
|
337
|
-
restoreSourceReader();
|
|
338
|
-
}
|
|
339
|
-
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** Reserve generated identifiers across all scopes and every nested rewrite. */
|
|
2
|
+
export function allocateName(identifiers: Set<string>, base: string): string {
|
|
3
|
+
let candidate = base;
|
|
4
|
+
let suffix = 1;
|
|
5
|
+
while (identifiers.has(candidate)) candidate = `${base}${suffix++}`;
|
|
6
|
+
identifiers.add(candidate);
|
|
7
|
+
return candidate;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function collectIdentifierNames(ts: any, source: any): Set<string> {
|
|
11
|
+
const identifiers = new Set<string>();
|
|
12
|
+
const visit = (node: any): void => {
|
|
13
|
+
if (ts.isIdentifier(node)) identifiers.add(node.text);
|
|
14
|
+
if (node.name && ts.isStringLiteral(node.name)) identifiers.add(node.name.text);
|
|
15
|
+
ts.forEachChild(node, visit);
|
|
16
|
+
};
|
|
17
|
+
visit(source);
|
|
18
|
+
return identifiers;
|
|
19
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/** Value bindings visible at a call site, including declarations later in the same scope. */
|
|
2
|
+
interface Scope {
|
|
3
|
+
parent?: Scope;
|
|
4
|
+
functionScope: boolean;
|
|
5
|
+
bindings: Map<string, any>;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function createLexicalResolver(ts: any, source: any): (location: any, name: string) => any | undefined {
|
|
9
|
+
const scopes = new WeakMap<object, Scope>();
|
|
10
|
+
const root: Scope = { functionScope: true, bindings: new Map() };
|
|
11
|
+
const bind = (scope: Scope, name: any, declaration: any): void => {
|
|
12
|
+
if (!name) return;
|
|
13
|
+
if (ts.isIdentifier(name)) scope.bindings.set(name.text, declaration);
|
|
14
|
+
else if (name.elements) name.elements.forEach((element: any) => {
|
|
15
|
+
if (ts.isBindingElement(element)) bind(scope, element.name, declaration);
|
|
16
|
+
});
|
|
17
|
+
};
|
|
18
|
+
const walk = (node: any, outer: Scope): void => {
|
|
19
|
+
const struct = typeof ts.isStructDeclaration === 'function' && ts.isStructDeclaration(node);
|
|
20
|
+
const classLike = ts.isClassDeclaration(node) || ts.isClassExpression(node) || struct;
|
|
21
|
+
if ((ts.isFunctionDeclaration(node) || ts.isClassDeclaration(node) || struct || ts.isEnumDeclaration(node) ||
|
|
22
|
+
ts.isModuleDeclaration(node)) && node.name) {
|
|
23
|
+
bind(outer, node.name, node);
|
|
24
|
+
}
|
|
25
|
+
let scope = outer;
|
|
26
|
+
const functionLike = ts.isFunctionLike(node);
|
|
27
|
+
if (functionLike || ts.isBlock(node) || ts.isCaseBlock(node) || ts.isCatchClause(node) ||
|
|
28
|
+
ts.isForStatement(node) || ts.isForInStatement(node) || ts.isForOfStatement(node) ||
|
|
29
|
+
ts.isModuleBlock(node) || classLike) {
|
|
30
|
+
scope = { parent: outer, functionScope: functionLike || ts.isModuleBlock(node), bindings: new Map() };
|
|
31
|
+
}
|
|
32
|
+
scopes.set(node, scope);
|
|
33
|
+
if (functionLike) {
|
|
34
|
+
if (ts.isFunctionExpression(node) && node.name) bind(scope, node.name, node);
|
|
35
|
+
node.parameters.forEach((parameter: any) => bind(scope, parameter.name, parameter));
|
|
36
|
+
}
|
|
37
|
+
if (classLike && node.name) bind(scope, node.name, node);
|
|
38
|
+
if (ts.isVariableDeclaration(node)) {
|
|
39
|
+
const list = node.parent;
|
|
40
|
+
let target = scope;
|
|
41
|
+
if (ts.isVariableDeclarationList(list) && !(list.flags & ts.NodeFlags.BlockScoped)) {
|
|
42
|
+
while (!target.functionScope && target.parent) target = target.parent;
|
|
43
|
+
}
|
|
44
|
+
bind(target, node.name, node);
|
|
45
|
+
}
|
|
46
|
+
if (ts.isImportDeclaration(node) && node.importClause && !node.importClause.isTypeOnly) {
|
|
47
|
+
const clause = node.importClause;
|
|
48
|
+
if (clause.name) bind(scope, clause.name, clause);
|
|
49
|
+
const bindings = clause.namedBindings;
|
|
50
|
+
if (bindings && ts.isNamespaceImport(bindings)) bind(scope, bindings.name, bindings);
|
|
51
|
+
else if (bindings) bindings.elements.forEach((element: any) => {
|
|
52
|
+
if (!element.isTypeOnly) bind(scope, element.name, element);
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
if (ts.isImportEqualsDeclaration(node) && !node.isTypeOnly) bind(scope, node.name, node);
|
|
56
|
+
ts.forEachChild(node, (child: any) => walk(child, scope));
|
|
57
|
+
};
|
|
58
|
+
walk(source, root);
|
|
59
|
+
return (location: any, name: string): any | undefined => {
|
|
60
|
+
for (let scope = scopes.get(location); scope; scope = scope.parent) {
|
|
61
|
+
if (scope.bindings.has(name)) return scope.bindings.get(name);
|
|
62
|
+
}
|
|
63
|
+
return undefined;
|
|
64
|
+
};
|
|
65
|
+
}
|