fix(ivy): ngcc should process undecorated base classes (#30821)
Currently undecorated classes are intentionally not processed with ngcc. This is causing unexpected behavior because decorator handlers such as `base_def.ts` are specifically interested in class definitions without top-level decorators, so that the base definition can be generated if there are Angular-specific class members. In order to ensure that undecorated base-classes work as expected with Ivy, we need to run the decorator handlers for all top-level class declarations (not only for those with decorators). This is similar to when `ngtsc` runs decorator handlers when analyzing source-files. Resolves FW-1355. Fixes https://github.com/angular/components/issues/16178 PR Close #30821
This commit is contained in:

committed by
Igor Minar

parent
271d2b51a9
commit
2b4d5c7548
@ -14,10 +14,10 @@ import {AbsoluteModuleStrategy, LocalIdentifierStrategy, LogicalProjectStrategy,
|
||||
import {CompoundMetadataReader, CompoundMetadataRegistry, DtsMetadataReader, LocalMetadataRegistry} from '../../../src/ngtsc/metadata';
|
||||
import {PartialEvaluator} from '../../../src/ngtsc/partial_evaluator';
|
||||
import {AbsoluteFsPath, LogicalFileSystem} from '../../../src/ngtsc/path';
|
||||
import {ClassDeclaration, ClassSymbol, Decorator} from '../../../src/ngtsc/reflection';
|
||||
import {LocalModuleScopeRegistry, MetadataDtsModuleScopeResolver} from '../../../src/ngtsc/scope';
|
||||
import {CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence} from '../../../src/ngtsc/transform';
|
||||
import {FileSystem} from '../file_system/file_system';
|
||||
import {DecoratedClass} from '../host/decorated_class';
|
||||
import {NgccReflectionHost} from '../host/ngcc_host';
|
||||
import {isDefined} from '../utils';
|
||||
|
||||
@ -26,7 +26,10 @@ export interface AnalyzedFile {
|
||||
analyzedClasses: AnalyzedClass[];
|
||||
}
|
||||
|
||||
export interface AnalyzedClass extends DecoratedClass {
|
||||
export interface AnalyzedClass {
|
||||
name: string;
|
||||
decorators: Decorator[]|null;
|
||||
declaration: ClassDeclaration;
|
||||
diagnostics?: ts.Diagnostic[];
|
||||
matches: {handler: DecoratorHandler<any, any>; analysis: any;}[];
|
||||
}
|
||||
@ -133,19 +136,18 @@ export class DecorationAnalyzer {
|
||||
}
|
||||
|
||||
protected analyzeFile(sourceFile: ts.SourceFile): AnalyzedFile|undefined {
|
||||
const decoratedClasses = this.reflectionHost.findDecoratedClasses(sourceFile);
|
||||
return decoratedClasses.length ? {
|
||||
sourceFile,
|
||||
analyzedClasses: decoratedClasses.map(clazz => this.analyzeClass(clazz)).filter(isDefined)
|
||||
} :
|
||||
undefined;
|
||||
const analyzedClasses = this.reflectionHost.findClassSymbols(sourceFile)
|
||||
.map(symbol => this.analyzeClass(symbol))
|
||||
.filter(isDefined);
|
||||
return analyzedClasses.length ? {sourceFile, analyzedClasses} : undefined;
|
||||
}
|
||||
|
||||
protected analyzeClass(clazz: DecoratedClass): AnalyzedClass|null {
|
||||
protected analyzeClass(symbol: ClassSymbol): AnalyzedClass|null {
|
||||
const declaration = symbol.valueDeclaration;
|
||||
const decorators = this.reflectionHost.getDecoratorsOfSymbol(symbol);
|
||||
const matchingHandlers = this.handlers
|
||||
.map(handler => {
|
||||
const detected =
|
||||
handler.detect(clazz.declaration, clazz.decorators);
|
||||
const detected = handler.detect(declaration, decorators);
|
||||
return {handler, detected};
|
||||
})
|
||||
.filter(isMatchingHandler);
|
||||
@ -183,13 +185,19 @@ export class DecorationAnalyzer {
|
||||
const matches: {handler: DecoratorHandler<any, any>, analysis: any}[] = [];
|
||||
const allDiagnostics: ts.Diagnostic[] = [];
|
||||
for (const {handler, detected} of detections) {
|
||||
const {analysis, diagnostics} = handler.analyze(clazz.declaration, detected.metadata);
|
||||
const {analysis, diagnostics} = handler.analyze(declaration, detected.metadata);
|
||||
if (diagnostics !== undefined) {
|
||||
allDiagnostics.push(...diagnostics);
|
||||
}
|
||||
matches.push({handler, analysis});
|
||||
}
|
||||
return {...clazz, matches, diagnostics: allDiagnostics.length > 0 ? allDiagnostics : undefined};
|
||||
return {
|
||||
name: symbol.name,
|
||||
declaration,
|
||||
decorators,
|
||||
matches,
|
||||
diagnostics: allDiagnostics.length > 0 ? allDiagnostics : undefined
|
||||
};
|
||||
}
|
||||
|
||||
protected compileFile(analyzedFile: AnalyzedFile): CompiledFile {
|
||||
|
@ -1,28 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. 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 {ClassDeclaration, Decorator} from '../../../src/ngtsc/reflection';
|
||||
|
||||
/**
|
||||
* A simple container that holds the details of a decorated class that has been
|
||||
* found in a `DecoratedFile`.
|
||||
*/
|
||||
export class DecoratedClass {
|
||||
/**
|
||||
* Initialize a `DecoratedClass` that was found in a `DecoratedFile`.
|
||||
* @param name The name of the class that has been found. This is mostly used
|
||||
* for informational purposes.
|
||||
* @param declaration The TypeScript AST node where this class is declared. In ES5 code, where a
|
||||
* class can be represented by both a variable declaration and a function declaration (inside an
|
||||
* IIFE), `declaration` will always refer to the outer variable declaration, which represents the
|
||||
* class to the rest of the program.
|
||||
* @param decorators The collection of decorators that have been found on this class.
|
||||
*/
|
||||
constructor(
|
||||
public name: string, public declaration: ClassDeclaration, public decorators: Decorator[]) {}
|
||||
}
|
@ -13,7 +13,6 @@ import {Logger} from '../logging/logger';
|
||||
import {BundleProgram} from '../packages/bundle_program';
|
||||
import {findAll, getNameText, hasNameIdentifier, isDefined} from '../utils';
|
||||
|
||||
import {DecoratedClass} from './decorated_class';
|
||||
import {ModuleWithProvidersFunction, NgccReflectionHost, PRE_R3_MARKER, SwitchableVariableDeclaration, isSwitchableVariableDeclaration} from './ngcc_host';
|
||||
|
||||
export const DECORATORS = 'decorators' as ts.__String;
|
||||
@ -233,6 +232,16 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
|
||||
return superDeclaration;
|
||||
}
|
||||
|
||||
/** Gets all decorators of the given class symbol. */
|
||||
getDecoratorsOfSymbol(symbol: ClassSymbol): Decorator[]|null {
|
||||
const decoratorsProperty = this.getStaticProperty(symbol, DECORATORS);
|
||||
if (decoratorsProperty) {
|
||||
return this.getClassDecoratorsFromStaticProperty(decoratorsProperty);
|
||||
} else {
|
||||
return this.getClassDecoratorsFromHelperCall(symbol);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Search the given module for variable declarations in which the initializer
|
||||
* is an identifier marked with the `PRE_R3_MARKER`.
|
||||
@ -306,24 +315,24 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all the classes that contain decorations in a given file.
|
||||
* @param sourceFile The source file to search for decorated classes.
|
||||
* @returns An array of decorated classes.
|
||||
* Find all top-level class symbols in the given file.
|
||||
* @param sourceFile The source file to search for classes.
|
||||
* @returns An array of class symbols.
|
||||
*/
|
||||
findDecoratedClasses(sourceFile: ts.SourceFile): DecoratedClass[] {
|
||||
const classes: DecoratedClass[] = [];
|
||||
findClassSymbols(sourceFile: ts.SourceFile): ClassSymbol[] {
|
||||
const classes: ClassSymbol[] = [];
|
||||
this.getModuleStatements(sourceFile).forEach(statement => {
|
||||
if (ts.isVariableStatement(statement)) {
|
||||
statement.declarationList.declarations.forEach(declaration => {
|
||||
const decoratedClass = this.getDecoratedClassFromSymbol(this.getClassSymbol(declaration));
|
||||
if (decoratedClass) {
|
||||
classes.push(decoratedClass);
|
||||
const classSymbol = this.getClassSymbol(declaration);
|
||||
if (classSymbol) {
|
||||
classes.push(classSymbol);
|
||||
}
|
||||
});
|
||||
} else if (ts.isClassDeclaration(statement)) {
|
||||
const decoratedClass = this.getDecoratedClassFromSymbol(this.getClassSymbol(statement));
|
||||
if (decoratedClass) {
|
||||
classes.push(decoratedClass);
|
||||
const classSymbol = this.getClassSymbol(statement);
|
||||
if (classSymbol) {
|
||||
classes.push(classSymbol);
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -488,25 +497,6 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
|
||||
return Array.from(sourceFile.statements);
|
||||
}
|
||||
|
||||
protected getDecoratorsOfSymbol(symbol: ClassSymbol): Decorator[]|null {
|
||||
const decoratorsProperty = this.getStaticProperty(symbol, DECORATORS);
|
||||
if (decoratorsProperty) {
|
||||
return this.getClassDecoratorsFromStaticProperty(decoratorsProperty);
|
||||
} else {
|
||||
return this.getClassDecoratorsFromHelperCall(symbol);
|
||||
}
|
||||
}
|
||||
|
||||
protected getDecoratedClassFromSymbol(symbol: ClassSymbol|undefined): DecoratedClass|null {
|
||||
if (symbol) {
|
||||
const decorators = this.getDecoratorsOfSymbol(symbol);
|
||||
if (decorators && decorators.length) {
|
||||
return new DecoratedClass(symbol.name, symbol.valueDeclaration, decorators);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Walk the AST looking for an assignment to the specified symbol.
|
||||
* @param node The current node we are searching.
|
||||
|
@ -211,6 +211,16 @@ export class Esm5ReflectionHost extends Esm2015ReflectionHost {
|
||||
return this.getMembersOfSymbol(innerFunctionSymbol);
|
||||
}
|
||||
|
||||
/** Gets all decorators of the given class symbol. */
|
||||
getDecoratorsOfSymbol(symbol: ClassSymbol): Decorator[]|null {
|
||||
// The necessary info is on the inner function declaration (inside the ES5 class IIFE).
|
||||
const innerFunctionSymbol =
|
||||
this.getInnerFunctionSymbolFromClassDeclaration(symbol.valueDeclaration);
|
||||
if (!innerFunctionSymbol) return null;
|
||||
|
||||
return super.getDecoratorsOfSymbol(innerFunctionSymbol);
|
||||
}
|
||||
|
||||
|
||||
///////////// Protected Helpers /////////////
|
||||
|
||||
@ -321,15 +331,6 @@ export class Esm5ReflectionHost extends Esm2015ReflectionHost {
|
||||
return super.getConstructorParamInfo(innerFunctionSymbol, parameterNodes);
|
||||
}
|
||||
|
||||
protected getDecoratorsOfSymbol(symbol: ClassSymbol): Decorator[]|null {
|
||||
// The necessary info is on the inner function declaration (inside the ES5 class IIFE).
|
||||
const innerFunctionSymbol =
|
||||
this.getInnerFunctionSymbolFromClassDeclaration(symbol.valueDeclaration);
|
||||
if (!innerFunctionSymbol) return null;
|
||||
|
||||
return super.getDecoratorsOfSymbol(innerFunctionSymbol);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the parameter type and decorators for the constructor of a class,
|
||||
* where the information is stored on a static method of the class.
|
||||
|
@ -6,8 +6,7 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
import * as ts from 'typescript';
|
||||
import {ClassDeclaration, ClassSymbol, Declaration, ReflectionHost} from '../../../src/ngtsc/reflection';
|
||||
import {DecoratedClass} from './decorated_class';
|
||||
import {ClassDeclaration, ClassSymbol, Declaration, Decorator, ReflectionHost} from '../../../src/ngtsc/reflection';
|
||||
|
||||
export const PRE_R3_MARKER = '__PRE_R3__';
|
||||
export const POST_R3_MARKER = '__POST_R3__';
|
||||
@ -64,11 +63,18 @@ export interface NgccReflectionHost extends ReflectionHost {
|
||||
getSwitchableDeclarations(module: ts.Node): SwitchableVariableDeclaration[];
|
||||
|
||||
/**
|
||||
* Find all the classes that contain decorations in a given file.
|
||||
* @param sourceFile The source file to search for decorated classes.
|
||||
* @returns An array of decorated classes.
|
||||
* Retrieves all decorators of a given class symbol.
|
||||
* @param symbol Class symbol that can refer to a declaration which can hold decorators.
|
||||
* @returns An array of decorators or null if none are declared.
|
||||
*/
|
||||
findDecoratedClasses(sourceFile: ts.SourceFile): DecoratedClass[];
|
||||
getDecoratorsOfSymbol(symbol: ClassSymbol): Decorator[]|null;
|
||||
|
||||
/**
|
||||
* Retrieves all class symbols of a given source file.
|
||||
* @param sourceFile The source file to search for classes.
|
||||
* @returns An array of found class symbols.
|
||||
*/
|
||||
findClassSymbols(sourceFile: ts.SourceFile): ClassSymbol[];
|
||||
|
||||
/**
|
||||
* Search the given source file for exported functions and static class methods that return
|
||||
|
@ -122,6 +122,10 @@ export class Renderer {
|
||||
private computeDecoratorsToRemove(classes: CompiledClass[]): RedundantDecoratorMap {
|
||||
const decoratorsToRemove = new RedundantDecoratorMap();
|
||||
classes.forEach(clazz => {
|
||||
if (clazz.decorators === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
clazz.decorators.forEach(dec => {
|
||||
const decoratorArray = dec.node.parent !;
|
||||
if (!decoratorsToRemove.has(decoratorArray)) {
|
||||
|
Reference in New Issue
Block a user