mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
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:
parent
4d8cc709e2
commit
256e6826bc
6 changed files with 179 additions and 5 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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`).
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue