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
This commit is contained in:
Andrew Kushnir 2023-07-24 16:00:50 -07:00 committed by Alex Rickabaugh
parent 4d8cc709e2
commit 256e6826bc
6 changed files with 179 additions and 5 deletions

View file

@ -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,

View file

@ -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};
}

View file

@ -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`).

View file

@ -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';

View file

@ -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<ts.ImportDeclaration, Map<string, Set<ts.Identifier>|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<string, AssumeEager> {
const symbolMap = new Map<string, AssumeEager>();
// 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<ts.Identifier>;
// 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<ts.ImportDeclaration> {
const deferrableDecls = new Set<ts.ImportDeclaration>();
for (const [importDecl] of this.imports) {
if (this.canDefer(importDecl)) {
deferrableDecls.add(importDecl);
}
}
return deferrableDecls;
}
private lookupIdentifiersInSourceFile(name: string, importDecl: ts.ImportDeclaration):
Set<ts.Identifier> {
const results = new Set<ts.Identifier>();
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;
}
}

View file

@ -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;