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:
Paul Gschwendtner
2019-06-03 18:41:47 +02:00
committed by Igor Minar
parent 271d2b51a9
commit 2b4d5c7548
20 changed files with 520 additions and 367 deletions

View File

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

View File

@ -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[]) {}
}

View File

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

View File

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

View File

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

View File

@ -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)) {