From 256e6826bca86d105f49d5ba6667adc03e2ef8bb Mon Sep 17 00:00:00 2001 From: Andrew Kushnir Date: Mon, 24 Jul 2023 16:00:50 -0700 Subject: [PATCH] refactor(compiler): add DeferredSymbolTracker class to keep track of symbol usages (#51162) This commit adds a new class called `DeferredSymbolTracker` to keep track of all usages of a particular symbol within a source file and allow to detect whether a symbol can be defer loaded (i.e. if there are any references to a symbol). PR Close #51162 --- .../annotations/component/src/handler.ts | 4 +- .../component/test/component_spec.ts | 3 +- .../src/ngtsc/core/src/compiler.ts | 6 +- .../compiler-cli/src/ngtsc/imports/index.ts | 1 + .../imports/src/deferred_symbol_tracker.ts | 168 ++++++++++++++++++ .../src/ngtsc/reflection/src/typescript.ts | 2 +- 6 files changed, 179 insertions(+), 5 deletions(-) create mode 100644 packages/compiler-cli/src/ngtsc/imports/src/deferred_symbol_tracker.ts diff --git a/packages/compiler-cli/src/ngtsc/annotations/component/src/handler.ts b/packages/compiler-cli/src/ngtsc/annotations/component/src/handler.ts index e1f68396e8f..927e3db528f 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/component/src/handler.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/component/src/handler.ts @@ -13,6 +13,7 @@ import {Cycle, CycleAnalyzer, CycleHandlingStrategy} from '../../../cycles'; import {ErrorCode, FatalDiagnosticError, makeDiagnostic, makeRelatedInformation} from '../../../diagnostics'; import {absoluteFrom, relative} from '../../../file_system'; import {assertSuccessfulReferenceEmit, ImportedFile, ModuleResolver, Reference, ReferenceEmitter} from '../../../imports'; +import {DeferredSymbolTracker} from '../../../imports/src/deferred_symbol_tracker'; import {DependencyTracker} from '../../../incremental/api'; import {extractSemanticTypeParameters, SemanticDepGraphUpdater} from '../../../incremental/semantic_graph'; import {IndexingContext} from '../../../indexer'; @@ -62,7 +63,8 @@ export class ComponentDecoratorHandler implements private semanticDepGraphUpdater: SemanticDepGraphUpdater|null, private annotateForClosureCompiler: boolean, private perf: PerfRecorder, private hostDirectivesResolver: HostDirectivesResolver, private includeClassMetadata: boolean, - private readonly compilationMode: CompilationMode) { + private readonly compilationMode: CompilationMode, + private readonly deferredSymbolsTracker: DeferredSymbolTracker) { this.extractTemplateOptions = { enableI18nLegacyMessageIdFormat: this.enableI18nLegacyMessageIdFormat, i18nNormalizeLineEndingsInICUs: this.i18nNormalizeLineEndingsInICUs, diff --git a/packages/compiler-cli/src/ngtsc/annotations/component/test/component_spec.ts b/packages/compiler-cli/src/ngtsc/annotations/component/test/component_spec.ts index 64591916072..ed4563a40d8 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/component/test/component_spec.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/component/test/component_spec.ts @@ -13,7 +13,7 @@ import {CycleAnalyzer, CycleHandlingStrategy, ImportGraph} from '../../../cycles import {ErrorCode, FatalDiagnosticError, ngErrorCode} from '../../../diagnostics'; import {absoluteFrom} from '../../../file_system'; import {runInEachFileSystem} from '../../../file_system/testing'; -import {ModuleResolver, Reference, ReferenceEmitter} from '../../../imports'; +import {DeferredSymbolTracker, ModuleResolver, Reference, ReferenceEmitter} from '../../../imports'; import {CompoundMetadataReader, DtsMetadataReader, HostDirectivesResolver, LocalMetadataRegistry, ResourceRegistry} from '../../../metadata'; import {PartialEvaluator} from '../../../partial_evaluator'; import {NOOP_PERF_RECORDER} from '../../../perf'; @@ -102,6 +102,7 @@ function setup( hostDirectivesResolver, true, compilationMode, + new DeferredSymbolTracker(checker), ); return {reflectionHost, handler, resourceLoader, metaRegistry}; } diff --git a/packages/compiler-cli/src/ngtsc/core/src/compiler.ts b/packages/compiler-cli/src/ngtsc/core/src/compiler.ts index 9c9b8d1a0d8..8f5a03561ec 100644 --- a/packages/compiler-cli/src/ngtsc/core/src/compiler.ts +++ b/packages/compiler-cli/src/ngtsc/core/src/compiler.ts @@ -14,7 +14,7 @@ import {CycleAnalyzer, CycleHandlingStrategy, ImportGraph} from '../../cycles'; import {COMPILER_ERRORS_WITH_GUIDES, ERROR_DETAILS_PAGE_BASE_URL, ErrorCode, FatalDiagnosticError, ngErrorCode} from '../../diagnostics'; import {checkForPrivateExports, ReferenceGraph} from '../../entry_point'; import {absoluteFromSourceFile, AbsoluteFsPath, LogicalFileSystem, resolve} from '../../file_system'; -import {AbsoluteModuleStrategy, AliasingHost, AliasStrategy, DefaultImportTracker, ImportRewriter, LocalIdentifierStrategy, LogicalProjectStrategy, ModuleResolver, NoopImportRewriter, PrivateExportAliasingHost, R3SymbolsImportRewriter, Reference, ReferenceEmitStrategy, ReferenceEmitter, RelativePathStrategy, UnifiedModulesAliasingHost, UnifiedModulesStrategy} from '../../imports'; +import {AbsoluteModuleStrategy, AliasingHost, AliasStrategy, DefaultImportTracker, DeferredSymbolTracker, ImportRewriter, LocalIdentifierStrategy, LogicalProjectStrategy, ModuleResolver, NoopImportRewriter, PrivateExportAliasingHost, R3SymbolsImportRewriter, Reference, ReferenceEmitStrategy, ReferenceEmitter, RelativePathStrategy, UnifiedModulesAliasingHost, UnifiedModulesStrategy} from '../../imports'; import {IncrementalBuildStrategy, IncrementalCompilation, IncrementalState} from '../../incremental'; import {SemanticSymbol} from '../../incremental/semantic_graph'; import {generateAnalysis, IndexedComponent, IndexingContext} from '../../indexer'; @@ -1017,6 +1017,8 @@ export class NgCompiler { const resourceRegistry = new ResourceRegistry(); + const deferredSymbolsTracker = new DeferredSymbolTracker(this.inputProgram.getTypeChecker()); + // Note: If this compilation builds `@angular/core`, we always build in full compilation // mode. Code inside the core package is always compatible with itself, so it does not // make sense to go through the indirection of partial compilation @@ -1070,7 +1072,7 @@ export class NgCompiler { this.moduleResolver, this.cycleAnalyzer, cycleHandlingStrategy, refEmitter, referencesRegistry, this.incrementalCompilation.depGraph, injectableRegistry, semanticDepGraphUpdater, this.closureCompilerEnabled, this.delegatingPerfRecorder, - hostDirectivesResolver, supportTestBed, compilationMode), + hostDirectivesResolver, supportTestBed, compilationMode, deferredSymbolsTracker), // TODO(alxhub): understand why the cast here is necessary (something to do with `null` // not being assignable to `unknown` when wrapped in `Readonly`). diff --git a/packages/compiler-cli/src/ngtsc/imports/index.ts b/packages/compiler-cli/src/ngtsc/imports/index.ts index 1f98c22fe6c..22abd2e8937 100644 --- a/packages/compiler-cli/src/ngtsc/imports/index.ts +++ b/packages/compiler-cli/src/ngtsc/imports/index.ts @@ -9,6 +9,7 @@ export {AliasingHost, AliasStrategy, PrivateExportAliasingHost, UnifiedModulesAliasingHost} from './src/alias'; export {ImportRewriter, NoopImportRewriter, R3SymbolsImportRewriter, validateAndRewriteCoreSymbol} from './src/core'; export {DefaultImportTracker} from './src/default'; +export {DeferredSymbolTracker} from './src/deferred_symbol_tracker'; export {AbsoluteModuleStrategy, assertSuccessfulReferenceEmit, EmittedReference, FailedEmitResult, ImportedFile, ImportFlags, LocalIdentifierStrategy, LogicalProjectStrategy, ReferenceEmitKind, ReferenceEmitResult, ReferenceEmitStrategy, ReferenceEmitter, RelativePathStrategy, UnifiedModulesStrategy} from './src/emitter'; export {isAliasImportDeclaration, loadIsReferencedAliasDeclarationPatch} from './src/patch_alias_reference_resolution'; export {Reexport} from './src/reexport'; diff --git a/packages/compiler-cli/src/ngtsc/imports/src/deferred_symbol_tracker.ts b/packages/compiler-cli/src/ngtsc/imports/src/deferred_symbol_tracker.ts new file mode 100644 index 00000000000..35fea607501 --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/imports/src/deferred_symbol_tracker.ts @@ -0,0 +1,168 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import ts from 'typescript'; + +import {getContainingImportDeclaration} from '../../reflection/src/typescript'; + +const AssumeEager = 'AssumeEager'; +type AssumeEager = typeof AssumeEager; + +/** + * Allows to register a symbol as deferrable and keep track of its usage. + * + * This information is later used to determine whether it's safe to drop + * a regular import of this symbol (actually the entire import declaration) + * in favor of using a dynamic import for cases when `{#defer}` blocks are used. + */ +export class DeferredSymbolTracker { + private readonly imports = + new Map|AssumeEager>>(); + + constructor(private readonly typeChecker: ts.TypeChecker) {} + + /** + * Given an import declaration node, extract the names of all imported symbols + * and return them as a map where each symbol is a key and `AssumeEager` is a value. + * + * The logic recognizes the following import shapes: + * + * Case 1: `import {a, b as B} from 'a'` + * Case 2: `import X from 'a'` + * Case 3: `import * as x from 'a'` + */ + private extractImportedSymbols(importDecl: ts.ImportDeclaration): Map { + const symbolMap = new Map(); + + // Unsupported case: `import 'a'` + if (importDecl.importClause === undefined) { + throw new Error(`Provided import declaration doesn't have any symbols.`); + } + + if (importDecl.importClause.namedBindings !== undefined) { + const bindings = importDecl.importClause.namedBindings; + if (ts.isNamedImports(bindings)) { + // Case 1: `import {a, b as B} from 'a'` + for (const element of bindings.elements) { + symbolMap.set(element.name.text, AssumeEager); + } + } else { + // Case 2: `import X from 'a'` + symbolMap.set(bindings.name.text, AssumeEager); + } + } else if (importDecl.importClause.name !== undefined) { + // Case 2: `import * as x from 'a'` + symbolMap.set(importDecl.importClause.name.text, AssumeEager); + } else { + throw new Error('Unrecognized import structure.'); + } + return symbolMap; + } + + /** + * Marks a given identifier and an associated import declaration as a candidate + * for defer loading. + */ + markAsDeferrableCandidate(identifier: ts.Identifier, importDecl: ts.ImportDeclaration): void { + // Do we come across this import for the first time? + if (!this.imports.has(importDecl)) { + const symbolMap = this.extractImportedSymbols(importDecl); + this.imports.set(importDecl, symbolMap); + } + + const symbolMap = this.imports.get(importDecl)!; + + if (!symbolMap.has(identifier.text)) { + throw new Error( + `The '${identifier.text}' identifier doesn't belong ` + + `to the provided import declaration.`); + } + + if (symbolMap.get(identifier.text) === AssumeEager) { + // We process this symbol for the first time, populate references. + symbolMap.set( + identifier.text, this.lookupIdentifiersInSourceFile(identifier.text, importDecl)); + } + + const identifiers = symbolMap.get(identifier.text) as Set; + + // Drop the current identifier, since we are trying to make it deferrable + // (it's used as a dependency in one of the `{#defer}` blocks). + identifiers.delete(identifier); + } + + /** + * Whether all symbols from a given import declaration have no references + * in a source file, thus it's safe to use dynamic imports. + */ + canDefer(importDecl: ts.ImportDeclaration): boolean { + if (!this.imports.has(importDecl)) { + return false; + } + + const symbolsMap = this.imports.get(importDecl)!; + for (const [symbol, refs] of symbolsMap) { + if (refs === AssumeEager || refs.size > 0) { + // There may be still eager references to this symbol. + return false; + } + } + + return true; + } + + /** + * Returns a set of import declarations that is safe to remove + * from the current source file and generate dynamic imports instead. + */ + getDeferrableImportDecls(): Set { + const deferrableDecls = new Set(); + for (const [importDecl] of this.imports) { + if (this.canDefer(importDecl)) { + deferrableDecls.add(importDecl); + } + } + return deferrableDecls; + } + + private lookupIdentifiersInSourceFile(name: string, importDecl: ts.ImportDeclaration): + Set { + const results = new Set(); + const visit = (node: ts.Node): void => { + if (node === importDecl) { + // Don't record references from the declaration itself. + return; + } + + if (ts.isIdentifier(node) && node.text === name) { + // Is `node` actually a reference to this symbol? + const sym = this.typeChecker.getSymbolAtLocation(node); + if (sym === undefined) { + return; + } + + if (sym.declarations === undefined || sym.declarations.length === 0) { + return; + } + const importClause = sym.declarations[0]; + // Is declaration from this import statement? + const decl = getContainingImportDeclaration(importClause); + if (decl !== importDecl) { + return; + } + + // `node` *is* a reference to the same import. + results.add(node); + } + ts.forEachChild(node, visit); + }; + + visit(importDecl.getSourceFile()); + return results; + } +} diff --git a/packages/compiler-cli/src/ngtsc/reflection/src/typescript.ts b/packages/compiler-cli/src/ngtsc/reflection/src/typescript.ts index 3546c6f4a28..1b1caeaadb8 100644 --- a/packages/compiler-cli/src/ngtsc/reflection/src/typescript.ts +++ b/packages/compiler-cli/src/ngtsc/reflection/src/typescript.ts @@ -659,7 +659,7 @@ function getFarLeftIdentifier(propertyAccess: ts.PropertyAccessExpression): ts.I * Return the ImportDeclaration for the given `node` if it is either an `ImportSpecifier` or a * `NamespaceImport`. If not return `null`. */ -function getContainingImportDeclaration(node: ts.Node): ts.ImportDeclaration|null { +export function getContainingImportDeclaration(node: ts.Node): ts.ImportDeclaration|null { return ts.isImportSpecifier(node) ? node.parent!.parent!.parent! : ts.isNamespaceImport(node) ? node.parent.parent : null;