Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/build/lib/treeshaking.ts
4770 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import fs from 'fs';
7
import path from 'path';
8
import * as ts from 'typescript';
9
import { type IFileMap, TypeScriptLanguageServiceHost } from './typeScriptLanguageServiceHost.ts';
10
11
const ShakeLevel = Object.freeze({
12
Files: 0,
13
InnerFile: 1,
14
ClassMembers: 2
15
});
16
17
type ShakeLevel = typeof ShakeLevel[keyof typeof ShakeLevel];
18
19
export function toStringShakeLevel(shakeLevel: ShakeLevel): string {
20
switch (shakeLevel) {
21
case ShakeLevel.Files:
22
return 'Files (0)';
23
case ShakeLevel.InnerFile:
24
return 'InnerFile (1)';
25
case ShakeLevel.ClassMembers:
26
return 'ClassMembers (2)';
27
}
28
}
29
30
export interface ITreeShakingOptions {
31
/**
32
* The full path to the root where sources are.
33
*/
34
sourcesRoot: string;
35
/**
36
* Module ids.
37
* e.g. `vs/editor/editor.main` or `index`
38
*/
39
entryPoints: string[];
40
/**
41
* Inline usages.
42
*/
43
inlineEntryPoints: string[];
44
/**
45
* Other .d.ts files
46
*/
47
typings: string[];
48
/**
49
* TypeScript compiler options.
50
*/
51
compilerOptions?: any;
52
/**
53
* The shake level to perform.
54
*/
55
shakeLevel: ShakeLevel;
56
/**
57
* regex pattern to ignore certain imports e.g. `.css` imports
58
*/
59
importIgnorePattern: RegExp;
60
}
61
62
export interface ITreeShakingResult {
63
[file: string]: string;
64
}
65
66
function printDiagnostics(options: ITreeShakingOptions, diagnostics: ReadonlyArray<ts.Diagnostic>): void {
67
for (const diag of diagnostics) {
68
let result = '';
69
if (diag.file) {
70
result += `${path.join(options.sourcesRoot, diag.file.fileName)}`;
71
}
72
if (diag.file && diag.start) {
73
const location = diag.file.getLineAndCharacterOfPosition(diag.start);
74
result += `:${location.line + 1}:${location.character}`;
75
}
76
result += ` - ` + JSON.stringify(diag.messageText);
77
console.log(result);
78
}
79
}
80
81
export function shake(options: ITreeShakingOptions): ITreeShakingResult {
82
const languageService = createTypeScriptLanguageService(ts, options);
83
const program = languageService.getProgram()!;
84
85
const globalDiagnostics = program.getGlobalDiagnostics();
86
if (globalDiagnostics.length > 0) {
87
printDiagnostics(options, globalDiagnostics);
88
throw new Error(`Compilation Errors encountered.`);
89
}
90
91
const syntacticDiagnostics = program.getSyntacticDiagnostics();
92
if (syntacticDiagnostics.length > 0) {
93
printDiagnostics(options, syntacticDiagnostics);
94
throw new Error(`Compilation Errors encountered.`);
95
}
96
97
const semanticDiagnostics = program.getSemanticDiagnostics();
98
if (semanticDiagnostics.length > 0) {
99
printDiagnostics(options, semanticDiagnostics);
100
throw new Error(`Compilation Errors encountered.`);
101
}
102
103
markNodes(ts, languageService, options);
104
105
return generateResult(ts, languageService, options.shakeLevel);
106
}
107
108
//#region Discovery, LanguageService & Setup
109
function createTypeScriptLanguageService(ts: typeof import('typescript'), options: ITreeShakingOptions): ts.LanguageService {
110
// Discover referenced files
111
const FILES: IFileMap = new Map();
112
113
// Add entrypoints
114
options.entryPoints.forEach(entryPoint => {
115
const filePath = path.join(options.sourcesRoot, entryPoint);
116
FILES.set(path.normalize(filePath), fs.readFileSync(filePath).toString());
117
});
118
119
// Add fake usage files
120
options.inlineEntryPoints.forEach((inlineEntryPoint, index) => {
121
FILES.set(path.normalize(path.join(options.sourcesRoot, `inlineEntryPoint.${index}.ts`)), inlineEntryPoint);
122
});
123
124
// Add additional typings
125
options.typings.forEach((typing) => {
126
const filePath = path.join(options.sourcesRoot, typing);
127
FILES.set(path.normalize(filePath), fs.readFileSync(filePath).toString());
128
});
129
130
const basePath = path.join(options.sourcesRoot, '..');
131
const compilerOptions = ts.convertCompilerOptionsFromJson(options.compilerOptions, basePath).options;
132
const host = new TypeScriptLanguageServiceHost(ts, FILES, compilerOptions);
133
return ts.createLanguageService(host);
134
}
135
136
//#endregion
137
138
//#region Tree Shaking
139
140
const NodeColor = Object.freeze({
141
White: 0,
142
Gray: 1,
143
Black: 2
144
});
145
type NodeColor = typeof NodeColor[keyof typeof NodeColor];
146
147
type ObjectLiteralElementWithName = ts.ObjectLiteralElement & { name: ts.PropertyName; parent: ts.ObjectLiteralExpression | ts.JsxAttributes };
148
149
declare module 'typescript' {
150
interface Node {
151
$$$color?: NodeColor;
152
$$$neededSourceFile?: boolean;
153
symbol?: ts.Symbol;
154
}
155
156
function getContainingObjectLiteralElement(node: ts.Node): ObjectLiteralElementWithName | undefined;
157
function getNameFromPropertyName(name: ts.PropertyName): string | undefined;
158
function getPropertySymbolsFromContextualType(node: ObjectLiteralElementWithName, checker: ts.TypeChecker, contextualType: ts.Type, unionSymbolOk: boolean): ReadonlyArray<ts.Symbol>;
159
}
160
161
function getColor(node: ts.Node): NodeColor {
162
return node.$$$color || NodeColor.White;
163
}
164
function setColor(node: ts.Node, color: NodeColor): void {
165
node.$$$color = color;
166
}
167
function markNeededSourceFile(node: ts.SourceFile): void {
168
node.$$$neededSourceFile = true;
169
}
170
function isNeededSourceFile(node: ts.SourceFile): boolean {
171
return Boolean(node.$$$neededSourceFile);
172
}
173
function nodeOrParentIsBlack(node: ts.Node): boolean {
174
while (node) {
175
const color = getColor(node);
176
if (color === NodeColor.Black) {
177
return true;
178
}
179
node = node.parent;
180
}
181
return false;
182
}
183
function nodeOrChildIsBlack(node: ts.Node): boolean {
184
if (getColor(node) === NodeColor.Black) {
185
return true;
186
}
187
for (const child of node.getChildren()) {
188
if (nodeOrChildIsBlack(child)) {
189
return true;
190
}
191
}
192
return false;
193
}
194
195
function isSymbolWithDeclarations(symbol: ts.Symbol | undefined | null): symbol is ts.Symbol & { declarations: ts.Declaration[] } {
196
return !!(symbol && symbol.declarations);
197
}
198
199
function isVariableStatementWithSideEffects(ts: typeof import('typescript'), node: ts.Node): boolean {
200
if (!ts.isVariableStatement(node)) {
201
return false;
202
}
203
let hasSideEffects = false;
204
const visitNode = (node: ts.Node) => {
205
if (hasSideEffects) {
206
// no need to go on
207
return;
208
}
209
if (ts.isCallExpression(node) || ts.isNewExpression(node)) {
210
// TODO: assuming `createDecorator` and `refineServiceDecorator` calls are side-effect free
211
const isSideEffectFree = /(createDecorator|refineServiceDecorator)/.test(node.expression.getText());
212
if (!isSideEffectFree) {
213
hasSideEffects = true;
214
}
215
}
216
node.forEachChild(visitNode);
217
};
218
node.forEachChild(visitNode);
219
return hasSideEffects;
220
}
221
222
function isStaticMemberWithSideEffects(ts: typeof import('typescript'), node: ts.ClassElement | ts.TypeElement): boolean {
223
if (!ts.isPropertyDeclaration(node)) {
224
return false;
225
}
226
if (!node.modifiers) {
227
return false;
228
}
229
if (!node.modifiers.some(mod => mod.kind === ts.SyntaxKind.StaticKeyword)) {
230
return false;
231
}
232
let hasSideEffects = false;
233
const visitNode = (node: ts.Node) => {
234
if (hasSideEffects) {
235
// no need to go on
236
return;
237
}
238
if (ts.isCallExpression(node) || ts.isNewExpression(node)) {
239
hasSideEffects = true;
240
}
241
node.forEachChild(visitNode);
242
};
243
node.forEachChild(visitNode);
244
return hasSideEffects;
245
}
246
247
function markNodes(ts: typeof import('typescript'), languageService: ts.LanguageService, options: ITreeShakingOptions) {
248
const program = languageService.getProgram();
249
if (!program) {
250
throw new Error('Could not get program from language service');
251
}
252
253
if (options.shakeLevel === ShakeLevel.Files) {
254
// Mark all source files Black
255
program.getSourceFiles().forEach((sourceFile) => {
256
setColor(sourceFile, NodeColor.Black);
257
});
258
return;
259
}
260
261
const black_queue: ts.Node[] = [];
262
const gray_queue: ts.Node[] = [];
263
const export_import_queue: ts.Node[] = [];
264
const sourceFilesLoaded: { [fileName: string]: boolean } = {};
265
266
function enqueueTopLevelModuleStatements(sourceFile: ts.SourceFile): void {
267
268
sourceFile.forEachChild((node: ts.Node) => {
269
270
if (ts.isImportDeclaration(node)) {
271
if (!node.importClause && ts.isStringLiteral(node.moduleSpecifier)) {
272
setColor(node, NodeColor.Black);
273
enqueueImport(node, node.moduleSpecifier.text);
274
}
275
return;
276
}
277
278
if (ts.isExportDeclaration(node)) {
279
if (!node.exportClause && node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier)) {
280
// export * from "foo";
281
setColor(node, NodeColor.Black);
282
enqueueImport(node, node.moduleSpecifier.text);
283
}
284
if (node.exportClause && ts.isNamedExports(node.exportClause)) {
285
for (const exportSpecifier of node.exportClause.elements) {
286
export_import_queue.push(exportSpecifier);
287
}
288
}
289
return;
290
}
291
292
if (isVariableStatementWithSideEffects(ts, node)) {
293
enqueue_black(node);
294
}
295
296
if (
297
ts.isExpressionStatement(node)
298
|| ts.isIfStatement(node)
299
|| ts.isIterationStatement(node, true)
300
|| ts.isExportAssignment(node)
301
) {
302
enqueue_black(node);
303
}
304
305
if (ts.isImportEqualsDeclaration(node)) {
306
if (/export/.test(node.getFullText(sourceFile))) {
307
// e.g. "export import Severity = BaseSeverity;"
308
enqueue_black(node);
309
}
310
}
311
312
});
313
}
314
315
/**
316
* Return the parent of `node` which is an ImportDeclaration
317
*/
318
function findParentImportDeclaration(node: ts.Declaration): ts.ImportDeclaration | null {
319
let _node: ts.Node = node;
320
do {
321
if (ts.isImportDeclaration(_node)) {
322
return _node;
323
}
324
_node = _node.parent;
325
} while (_node);
326
return null;
327
}
328
329
function enqueue_gray(node: ts.Node): void {
330
if (nodeOrParentIsBlack(node) || getColor(node) === NodeColor.Gray) {
331
return;
332
}
333
setColor(node, NodeColor.Gray);
334
gray_queue.push(node);
335
}
336
337
function enqueue_black(node: ts.Node): void {
338
const previousColor = getColor(node);
339
340
if (previousColor === NodeColor.Black) {
341
return;
342
}
343
344
if (previousColor === NodeColor.Gray) {
345
// remove from gray queue
346
gray_queue.splice(gray_queue.indexOf(node), 1);
347
setColor(node, NodeColor.White);
348
349
// add to black queue
350
enqueue_black(node);
351
352
// move from one queue to the other
353
// black_queue.push(node);
354
// setColor(node, NodeColor.Black);
355
return;
356
}
357
358
if (nodeOrParentIsBlack(node)) {
359
return;
360
}
361
362
const fileName = node.getSourceFile().fileName;
363
if (/^defaultLib:/.test(fileName) || /\.d\.ts$/.test(fileName)) {
364
setColor(node, NodeColor.Black);
365
return;
366
}
367
368
const sourceFile = node.getSourceFile();
369
if (!sourceFilesLoaded[sourceFile.fileName]) {
370
sourceFilesLoaded[sourceFile.fileName] = true;
371
enqueueTopLevelModuleStatements(sourceFile);
372
}
373
374
if (ts.isSourceFile(node)) {
375
return;
376
}
377
378
setColor(node, NodeColor.Black);
379
black_queue.push(node);
380
381
if (options.shakeLevel === ShakeLevel.ClassMembers && (ts.isMethodDeclaration(node) || ts.isMethodSignature(node) || ts.isPropertySignature(node) || ts.isPropertyDeclaration(node) || ts.isGetAccessor(node) || ts.isSetAccessor(node))) {
382
const references = languageService.getReferencesAtPosition(node.getSourceFile().fileName, node.name.pos + node.name.getLeadingTriviaWidth());
383
if (references) {
384
for (let i = 0, len = references.length; i < len; i++) {
385
const reference = references[i];
386
const referenceSourceFile = program!.getSourceFile(reference.fileName);
387
if (!referenceSourceFile) {
388
continue;
389
}
390
391
const referenceNode = getTokenAtPosition(ts, referenceSourceFile, reference.textSpan.start, false, false);
392
if (
393
ts.isMethodDeclaration(referenceNode.parent)
394
|| ts.isPropertyDeclaration(referenceNode.parent)
395
|| ts.isGetAccessor(referenceNode.parent)
396
|| ts.isSetAccessor(referenceNode.parent)
397
) {
398
enqueue_gray(referenceNode.parent);
399
}
400
}
401
}
402
}
403
}
404
405
function enqueueFile(filename: string): void {
406
const sourceFile = program!.getSourceFile(filename);
407
if (!sourceFile) {
408
console.warn(`Cannot find source file ${filename}`);
409
return;
410
}
411
// This source file should survive even if it is empty
412
markNeededSourceFile(sourceFile);
413
enqueue_black(sourceFile);
414
}
415
416
function enqueueImport(node: ts.Node, importText: string): void {
417
if (options.importIgnorePattern.test(importText)) {
418
// this import should be ignored
419
return;
420
}
421
422
const nodeSourceFile = node.getSourceFile();
423
let fullPath: string;
424
if (/(^\.\/)|(^\.\.\/)/.test(importText)) {
425
if (importText.endsWith('.js')) { // ESM: code imports require to be relative and to have a '.js' file extension
426
importText = importText.substr(0, importText.length - 3);
427
}
428
fullPath = path.join(path.dirname(nodeSourceFile.fileName), importText);
429
} else {
430
fullPath = importText;
431
}
432
433
if (fs.existsSync(fullPath + '.ts')) {
434
fullPath = fullPath + '.ts';
435
} else {
436
fullPath = fullPath + '.js';
437
}
438
439
enqueueFile(fullPath);
440
}
441
442
options.entryPoints.forEach(moduleId => enqueueFile(path.join(options.sourcesRoot, moduleId)));
443
// Add fake usage files
444
options.inlineEntryPoints.forEach((_, index) => enqueueFile(path.join(options.sourcesRoot, `inlineEntryPoint.${index}.ts`)));
445
446
let step = 0;
447
448
const checker = program.getTypeChecker();
449
while (black_queue.length > 0 || gray_queue.length > 0) {
450
++step;
451
let node: ts.Node;
452
453
if (step % 100 === 0) {
454
console.log(`Treeshaking - ${Math.floor(100 * step / (step + black_queue.length + gray_queue.length))}% - ${step}/${step + black_queue.length + gray_queue.length} (${black_queue.length}, ${gray_queue.length})`);
455
}
456
457
if (black_queue.length === 0) {
458
for (let i = 0; i < gray_queue.length; i++) {
459
const node = gray_queue[i];
460
const nodeParent = node.parent;
461
if ((ts.isClassDeclaration(nodeParent) || ts.isInterfaceDeclaration(nodeParent)) && nodeOrChildIsBlack(nodeParent)) {
462
gray_queue.splice(i, 1);
463
black_queue.push(node);
464
setColor(node, NodeColor.Black);
465
i--;
466
}
467
}
468
}
469
470
if (black_queue.length > 0) {
471
node = black_queue.shift()!;
472
} else {
473
// only gray nodes remaining...
474
break;
475
}
476
const nodeSourceFile = node.getSourceFile();
477
478
const loop = (node: ts.Node) => {
479
const symbols = getRealNodeSymbol(ts, checker, node);
480
for (const { symbol, symbolImportNode } of symbols) {
481
if (symbolImportNode) {
482
setColor(symbolImportNode, NodeColor.Black);
483
const importDeclarationNode = findParentImportDeclaration(symbolImportNode);
484
if (importDeclarationNode && ts.isStringLiteral(importDeclarationNode.moduleSpecifier)) {
485
enqueueImport(importDeclarationNode, importDeclarationNode.moduleSpecifier.text);
486
}
487
}
488
489
if (isSymbolWithDeclarations(symbol) && !nodeIsInItsOwnDeclaration(nodeSourceFile, node, symbol)) {
490
for (let i = 0, len = symbol.declarations.length; i < len; i++) {
491
const declaration = symbol.declarations[i];
492
if (ts.isSourceFile(declaration)) {
493
// Do not enqueue full source files
494
// (they can be the declaration of a module import)
495
continue;
496
}
497
498
if (options.shakeLevel === ShakeLevel.ClassMembers && (ts.isClassDeclaration(declaration) || ts.isInterfaceDeclaration(declaration)) && !isLocalCodeExtendingOrInheritingFromDefaultLibSymbol(ts, program, checker, declaration)) {
499
enqueue_black(declaration.name!);
500
501
for (let j = 0; j < declaration.members.length; j++) {
502
const member = declaration.members[j];
503
const memberName = member.name ? member.name.getText() : null;
504
if (
505
ts.isConstructorDeclaration(member)
506
|| ts.isConstructSignatureDeclaration(member)
507
|| ts.isIndexSignatureDeclaration(member)
508
|| ts.isCallSignatureDeclaration(member)
509
|| memberName === '[Symbol.iterator]'
510
|| memberName === '[Symbol.toStringTag]'
511
|| memberName === 'toJSON'
512
|| memberName === 'toString'
513
|| memberName === 'dispose'// TODO: keeping all `dispose` methods
514
|| /^_(.*)Brand$/.test(memberName || '') // TODO: keeping all members ending with `Brand`...
515
) {
516
enqueue_black(member);
517
}
518
519
if (isStaticMemberWithSideEffects(ts, member)) {
520
enqueue_black(member);
521
}
522
}
523
524
// queue the heritage clauses
525
if (declaration.heritageClauses) {
526
for (const heritageClause of declaration.heritageClauses) {
527
enqueue_black(heritageClause);
528
}
529
}
530
} else {
531
enqueue_black(declaration);
532
}
533
}
534
}
535
}
536
node.forEachChild(loop);
537
};
538
node.forEachChild(loop);
539
}
540
541
while (export_import_queue.length > 0) {
542
const node = export_import_queue.shift()!;
543
if (nodeOrParentIsBlack(node)) {
544
continue;
545
}
546
if (!node.symbol) {
547
continue;
548
}
549
const aliased = checker.getAliasedSymbol(node.symbol);
550
if (aliased.declarations && aliased.declarations.length > 0) {
551
if (nodeOrParentIsBlack(aliased.declarations[0]) || nodeOrChildIsBlack(aliased.declarations[0])) {
552
setColor(node, NodeColor.Black);
553
}
554
}
555
}
556
}
557
558
function nodeIsInItsOwnDeclaration(nodeSourceFile: ts.SourceFile, node: ts.Node, symbol: ts.Symbol & { declarations: ts.Declaration[] }): boolean {
559
for (let i = 0, len = symbol.declarations.length; i < len; i++) {
560
const declaration = symbol.declarations[i];
561
const declarationSourceFile = declaration.getSourceFile();
562
563
if (nodeSourceFile === declarationSourceFile) {
564
if (declaration.pos <= node.pos && node.end <= declaration.end) {
565
return true;
566
}
567
}
568
}
569
570
return false;
571
}
572
573
function generateResult(ts: typeof import('typescript'), languageService: ts.LanguageService, shakeLevel: ShakeLevel): ITreeShakingResult {
574
const program = languageService.getProgram();
575
if (!program) {
576
throw new Error('Could not get program from language service');
577
}
578
579
const result: ITreeShakingResult = {};
580
const writeFile = (filePath: string, contents: string): void => {
581
result[filePath] = contents;
582
};
583
584
program.getSourceFiles().forEach((sourceFile) => {
585
const fileName = sourceFile.fileName;
586
if (/^defaultLib:/.test(fileName)) {
587
return;
588
}
589
const destination = fileName;
590
if (/\.d\.ts$/.test(fileName)) {
591
if (nodeOrChildIsBlack(sourceFile)) {
592
writeFile(destination, sourceFile.text);
593
}
594
return;
595
}
596
597
const text = sourceFile.text;
598
let result = '';
599
600
function keep(node: ts.Node): void {
601
result += text.substring(node.pos, node.end);
602
}
603
function write(data: string): void {
604
result += data;
605
}
606
607
function writeMarkedNodes(node: ts.Node): void {
608
if (getColor(node) === NodeColor.Black) {
609
return keep(node);
610
}
611
612
// Always keep certain top-level statements
613
if (ts.isSourceFile(node.parent)) {
614
if (ts.isExpressionStatement(node) && ts.isStringLiteral(node.expression) && node.expression.text === 'use strict') {
615
return keep(node);
616
}
617
618
if (ts.isVariableStatement(node) && nodeOrChildIsBlack(node)) {
619
return keep(node);
620
}
621
}
622
623
// Keep the entire import in import * as X cases
624
if (ts.isImportDeclaration(node)) {
625
if (node.importClause && node.importClause.namedBindings) {
626
if (ts.isNamespaceImport(node.importClause.namedBindings)) {
627
if (getColor(node.importClause.namedBindings) === NodeColor.Black) {
628
return keep(node);
629
}
630
} else {
631
const survivingImports: string[] = [];
632
for (const importNode of node.importClause.namedBindings.elements) {
633
if (getColor(importNode) === NodeColor.Black) {
634
survivingImports.push(importNode.getFullText(sourceFile));
635
}
636
}
637
const leadingTriviaWidth = node.getLeadingTriviaWidth();
638
const leadingTrivia = sourceFile.text.substr(node.pos, leadingTriviaWidth);
639
if (survivingImports.length > 0) {
640
if (node.importClause && node.importClause.name && getColor(node.importClause) === NodeColor.Black) {
641
return write(`${leadingTrivia}import ${node.importClause.name.text}, {${survivingImports.join(',')} } from${node.moduleSpecifier.getFullText(sourceFile)};`);
642
}
643
return write(`${leadingTrivia}import {${survivingImports.join(',')} } from${node.moduleSpecifier.getFullText(sourceFile)};`);
644
} else {
645
if (node.importClause && node.importClause.name && getColor(node.importClause) === NodeColor.Black) {
646
return write(`${leadingTrivia}import ${node.importClause.name.text} from${node.moduleSpecifier.getFullText(sourceFile)};`);
647
}
648
}
649
}
650
} else {
651
if (node.importClause && getColor(node.importClause) === NodeColor.Black) {
652
return keep(node);
653
}
654
}
655
}
656
657
if (ts.isExportDeclaration(node)) {
658
if (node.exportClause && node.moduleSpecifier && ts.isNamedExports(node.exportClause)) {
659
const survivingExports: string[] = [];
660
for (const exportSpecifier of node.exportClause.elements) {
661
if (getColor(exportSpecifier) === NodeColor.Black) {
662
survivingExports.push(exportSpecifier.getFullText(sourceFile));
663
}
664
}
665
const leadingTriviaWidth = node.getLeadingTriviaWidth();
666
const leadingTrivia = sourceFile.text.substr(node.pos, leadingTriviaWidth);
667
if (survivingExports.length > 0) {
668
return write(`${leadingTrivia}export {${survivingExports.join(',')} } from${node.moduleSpecifier.getFullText(sourceFile)};`);
669
}
670
}
671
}
672
673
if (shakeLevel === ShakeLevel.ClassMembers && (ts.isClassDeclaration(node) || ts.isInterfaceDeclaration(node)) && nodeOrChildIsBlack(node)) {
674
let toWrite = node.getFullText();
675
for (let i = node.members.length - 1; i >= 0; i--) {
676
const member = node.members[i];
677
if (getColor(member) === NodeColor.Black || !member.name) {
678
// keep method
679
continue;
680
}
681
682
const pos = member.pos - node.pos;
683
const end = member.end - node.pos;
684
toWrite = toWrite.substring(0, pos) + toWrite.substring(end);
685
}
686
return write(toWrite);
687
}
688
689
if (ts.isFunctionDeclaration(node)) {
690
// Do not go inside functions if they haven't been marked
691
return;
692
}
693
694
node.forEachChild(writeMarkedNodes);
695
}
696
697
if (getColor(sourceFile) !== NodeColor.Black) {
698
if (!nodeOrChildIsBlack(sourceFile)) {
699
// none of the elements are reachable
700
if (isNeededSourceFile(sourceFile)) {
701
// this source file must be written, even if nothing is used from it
702
// because there is an import somewhere for it.
703
// However, TS complains with empty files with the error "x" is not a module,
704
// so we will export a dummy variable
705
result = 'export const __dummy = 0;';
706
} else {
707
// don't write this file at all!
708
return;
709
}
710
} else {
711
sourceFile.forEachChild(writeMarkedNodes);
712
result += sourceFile.endOfFileToken.getFullText(sourceFile);
713
}
714
} else {
715
result = text;
716
}
717
718
writeFile(destination, result);
719
});
720
721
return result;
722
}
723
724
//#endregion
725
726
//#region Utils
727
728
function isLocalCodeExtendingOrInheritingFromDefaultLibSymbol(ts: typeof import('typescript'), program: ts.Program, checker: ts.TypeChecker, declaration: ts.ClassDeclaration | ts.InterfaceDeclaration): boolean {
729
if (!program.isSourceFileDefaultLibrary(declaration.getSourceFile()) && declaration.heritageClauses) {
730
for (const heritageClause of declaration.heritageClauses) {
731
for (const type of heritageClause.types) {
732
const symbol = findSymbolFromHeritageType(ts, checker, type);
733
if (symbol) {
734
const decl = symbol.valueDeclaration || (symbol.declarations && symbol.declarations[0]);
735
if (decl && program.isSourceFileDefaultLibrary(decl.getSourceFile())) {
736
return true;
737
}
738
}
739
}
740
}
741
}
742
return false;
743
}
744
745
function findSymbolFromHeritageType(ts: typeof import('typescript'), checker: ts.TypeChecker, type: ts.ExpressionWithTypeArguments | ts.Expression | ts.PrivateIdentifier): ts.Symbol | null {
746
if (ts.isExpressionWithTypeArguments(type)) {
747
return findSymbolFromHeritageType(ts, checker, type.expression);
748
}
749
if (ts.isIdentifier(type)) {
750
const tmp = getRealNodeSymbol(ts, checker, type);
751
return (tmp.length > 0 ? tmp[0].symbol : null);
752
}
753
if (ts.isPropertyAccessExpression(type)) {
754
return findSymbolFromHeritageType(ts, checker, type.name);
755
}
756
return null;
757
}
758
759
class SymbolImportTuple {
760
public readonly symbol: ts.Symbol | null;
761
public readonly symbolImportNode: ts.Declaration | null;
762
763
constructor(
764
symbol: ts.Symbol | null,
765
symbolImportNode: ts.Declaration | null
766
) {
767
this.symbol = symbol;
768
this.symbolImportNode = symbolImportNode;
769
}
770
}
771
772
/**
773
* Returns the node's symbol and the `import` node (if the symbol resolved from a different module)
774
*/
775
function getRealNodeSymbol(ts: typeof import('typescript'), checker: ts.TypeChecker, node: ts.Node): SymbolImportTuple[] {
776
777
// Go to the original declaration for cases:
778
//
779
// (1) when the aliased symbol was declared in the location(parent).
780
// (2) when the aliased symbol is originating from an import.
781
//
782
function shouldSkipAlias(node: ts.Node, declaration: ts.Node): boolean {
783
if (!ts.isShorthandPropertyAssignment(node) && node.kind !== ts.SyntaxKind.Identifier) {
784
return false;
785
}
786
if (node.parent === declaration) {
787
return true;
788
}
789
switch (declaration.kind) {
790
case ts.SyntaxKind.ImportClause:
791
case ts.SyntaxKind.ImportEqualsDeclaration:
792
return true;
793
case ts.SyntaxKind.ImportSpecifier:
794
return declaration.parent.kind === ts.SyntaxKind.NamedImports;
795
default:
796
return false;
797
}
798
}
799
800
if (!ts.isShorthandPropertyAssignment(node)) {
801
if (node.getChildCount() !== 0) {
802
return [];
803
}
804
}
805
806
const { parent } = node;
807
808
let symbol = (
809
ts.isShorthandPropertyAssignment(node)
810
? checker.getShorthandAssignmentValueSymbol(node)
811
: checker.getSymbolAtLocation(node)
812
);
813
814
let importNode: ts.Declaration | null = null;
815
// If this is an alias, and the request came at the declaration location
816
// get the aliased symbol instead. This allows for goto def on an import e.g.
817
// import {A, B} from "mod";
818
// to jump to the implementation directly.
819
if (symbol && symbol.flags & ts.SymbolFlags.Alias && symbol.declarations && shouldSkipAlias(node, symbol.declarations[0])) {
820
const aliased = checker.getAliasedSymbol(symbol);
821
if (aliased.declarations) {
822
// We should mark the import as visited
823
importNode = symbol.declarations[0];
824
symbol = aliased;
825
}
826
}
827
828
if (symbol) {
829
// Because name in short-hand property assignment has two different meanings: property name and property value,
830
// using go-to-definition at such position should go to the variable declaration of the property value rather than
831
// go to the declaration of the property name (in this case stay at the same position). However, if go-to-definition
832
// is performed at the location of property access, we would like to go to definition of the property in the short-hand
833
// assignment. This case and others are handled by the following code.
834
if (node.parent.kind === ts.SyntaxKind.ShorthandPropertyAssignment) {
835
symbol = checker.getShorthandAssignmentValueSymbol(symbol.valueDeclaration);
836
}
837
838
// If the node is the name of a BindingElement within an ObjectBindingPattern instead of just returning the
839
// declaration the symbol (which is itself), we should try to get to the original type of the ObjectBindingPattern
840
// and return the property declaration for the referenced property.
841
// For example:
842
// import('./foo').then(({ b/*goto*/ar }) => undefined); => should get use to the declaration in file "./foo"
843
//
844
// function bar<T>(onfulfilled: (value: T) => void) { //....}
845
// interface Test {
846
// pr/*destination*/op1: number
847
// }
848
// bar<Test>(({pr/*goto*/op1})=>{});
849
if (ts.isPropertyName(node) && ts.isBindingElement(parent) && ts.isObjectBindingPattern(parent.parent) &&
850
(node === (parent.propertyName || parent.name))) {
851
const name = ts.getNameFromPropertyName(node);
852
const type = checker.getTypeAtLocation(parent.parent);
853
if (name && type) {
854
if (type.isUnion()) {
855
return generateMultipleSymbols(type, name, importNode);
856
} else {
857
const prop = type.getProperty(name);
858
if (prop) {
859
symbol = prop;
860
}
861
}
862
}
863
}
864
865
// If the current location we want to find its definition is in an object literal, try to get the contextual type for the
866
// object literal, lookup the property symbol in the contextual type, and use this for goto-definition.
867
// For example
868
// interface Props{
869
// /*first*/prop1: number
870
// prop2: boolean
871
// }
872
// function Foo(arg: Props) {}
873
// Foo( { pr/*1*/op1: 10, prop2: false })
874
const element = ts.getContainingObjectLiteralElement(node);
875
if (element) {
876
const contextualType = element && checker.getContextualType(element.parent);
877
if (contextualType) {
878
const propertySymbols = ts.getPropertySymbolsFromContextualType(element, checker, contextualType, /*unionSymbolOk*/ false);
879
if (propertySymbols) {
880
symbol = propertySymbols[0];
881
}
882
}
883
}
884
}
885
886
if (symbol && symbol.declarations) {
887
return [new SymbolImportTuple(symbol, importNode)];
888
}
889
890
return [];
891
892
function generateMultipleSymbols(type: ts.UnionType, name: string, importNode: ts.Declaration | null): SymbolImportTuple[] {
893
const result: SymbolImportTuple[] = [];
894
for (const t of type.types) {
895
const prop = t.getProperty(name);
896
if (prop && prop.declarations) {
897
result.push(new SymbolImportTuple(prop, importNode));
898
}
899
}
900
return result;
901
}
902
}
903
904
/** Get the token whose text contains the position */
905
function getTokenAtPosition(ts: typeof import('typescript'), sourceFile: ts.SourceFile, position: number, allowPositionInLeadingTrivia: boolean, includeEndPosition: boolean): ts.Node {
906
let current: ts.Node = sourceFile;
907
outer: while (true) {
908
// find the child that contains 'position'
909
for (const child of current.getChildren()) {
910
const start = allowPositionInLeadingTrivia ? child.getFullStart() : child.getStart(sourceFile, /*includeJsDoc*/ true);
911
if (start > position) {
912
// If this child begins after position, then all subsequent children will as well.
913
break;
914
}
915
916
const end = child.getEnd();
917
if (position < end || (position === end && (child.kind === ts.SyntaxKind.EndOfFileToken || includeEndPosition))) {
918
current = child;
919
continue outer;
920
}
921
}
922
923
return current;
924
}
925
}
926
927
//#endregion
928
929