refactor(ivy): correctly type class declarations in ngtsc/ngcc (#29209)

Previously, several `ngtsc` and `ngcc` APIs dealing with class
declaration nodes used inconsistent types. For example, some methods of
the `DecoratorHandler` interface expected a `ts.Declaration` argument,
but actual `DecoratorHandler` implementations specified a stricter
`ts.ClassDeclaration` type.

As a result, the stricter methods would operate under the incorrect
assumption that their arguments were of type `ts.ClassDeclaration`,
while the actual arguments might be of different types (e.g. `ngcc`
would call them with `ts.FunctionDeclaration` or
`ts.VariableDeclaration` arguments, when compiling ES5 code).

Additionally, since we need those class declarations to be referenced in
other parts of the program, `ngtsc`/`ngcc` had to either repeatedly
check for `ts.isIdentifier(node.name)` or assume there was a `name`
identifier and use `node.name!`. While this assumption happens to be
true in the current implementation, working around type-checking is
error-prone (e.g. the assumption might stop being true in the future).

This commit fixes this by introducing a new type to be used for such
class declarations (`ts.Declaration & {name: ts.Identifier}`) and using
it consistently throughput the code.

PR Close #29209
This commit is contained in:
George Kalpakas
2019-03-20 12:10:57 +02:00
committed by Miško Hevery
parent 2d859a8c3a
commit bb6a3632f6
36 changed files with 229 additions and 194 deletions

View File

@ -10,7 +10,7 @@ import {ConstantPool, Expression, Statement, Type} from '@angular/compiler';
import * as ts from 'typescript';
import {Reexport} from '../../imports';
import {Decorator} from '../../reflection';
import {ClassDeclaration, Decorator} from '../../reflection';
import {TypeCheckContext} from '../../typecheck';
export enum HandlerPrecedence {
@ -58,7 +58,7 @@ export interface DecoratorHandler<A, M> {
* Scan a set of reflected decorators and determine if this handler is responsible for compilation
* of one of them.
*/
detect(node: ts.Declaration, decorators: Decorator[]|null): DetectResult<M>|undefined;
detect(node: ClassDeclaration, decorators: Decorator[]|null): DetectResult<M>|undefined;
/**
@ -67,14 +67,14 @@ export interface DecoratorHandler<A, M> {
* `preAnalyze` is optional and is not guaranteed to be called through all compilation flows. It
* will only be called if asynchronicity is supported in the CompilerHost.
*/
preanalyze?(node: ts.Declaration, metadata: M): Promise<void>|undefined;
preanalyze?(node: ClassDeclaration, metadata: M): Promise<void>|undefined;
/**
* Perform analysis on the decorator/class combination, producing instructions for compilation
* if successful, or an array of diagnostic messages if the analysis fails or the decorator
* isn't valid.
*/
analyze(node: ts.Declaration, metadata: M): AnalysisOutput<A>;
analyze(node: ClassDeclaration, metadata: M): AnalysisOutput<A>;
/**
* Perform resolution on the given decorator along with the result of analysis.
@ -83,15 +83,15 @@ export interface DecoratorHandler<A, M> {
* `DecoratorHandler` a chance to leverage information from the whole compilation unit to enhance
* the `analysis` before the emit phase.
*/
resolve?(node: ts.Declaration, analysis: A): ResolveResult;
resolve?(node: ClassDeclaration, analysis: A): ResolveResult;
typeCheck?(ctx: TypeCheckContext, node: ts.Declaration, metadata: A): void;
typeCheck?(ctx: TypeCheckContext, node: ClassDeclaration, metadata: A): void;
/**
* Generate a description of the field which should be added to the class, including any
* initialization code to be generated.
*/
compile(node: ts.Declaration, analysis: A, constantPool: ConstantPool): CompileResult
compile(node: ClassDeclaration, analysis: A, constantPool: ConstantPool): CompileResult
|CompileResult[];
}

View File

@ -6,14 +6,14 @@
* found in the LICENSE file at https://angular.io/license
*/
import {ConstantPool, ExternalExpr} from '@angular/compiler';
import {ConstantPool} from '@angular/compiler';
import * as ts from 'typescript';
import {ErrorCode, FatalDiagnosticError} from '../../diagnostics';
import {ImportRewriter} from '../../imports';
import {ReflectionHost, reflectNameOfDeclaration} from '../../reflection';
import {ClassDeclaration, ReflectionHost, reflectNameOfDeclaration} from '../../reflection';
import {TypeCheckContext} from '../../typecheck';
import {getSourceFile} from '../../util/src/typescript';
import {getSourceFile, isNamedClassDeclaration} from '../../util/src/typescript';
import {AnalysisOutput, CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence} from './api';
import {DtsFileTransformer} from './declaration';
@ -48,7 +48,7 @@ export class IvyCompilation {
* Tracks classes which have been analyzed and found to have an Ivy decorator, and the
* information recorded about them for later compilation.
*/
private ivyClasses = new Map<ts.Declaration, IvyClass>();
private ivyClasses = new Map<ClassDeclaration, IvyClass>();
/**
* Tracks factory information which needs to be generated.
@ -84,7 +84,7 @@ export class IvyCompilation {
analyzeAsync(sf: ts.SourceFile): Promise<void>|undefined { return this.analyze(sf, true); }
private detectHandlersForClass(node: ts.Declaration): IvyClass|null {
private detectHandlersForClass(node: ClassDeclaration): IvyClass|null {
// The first step is to reflect the decorators.
const classDecorators = this.reflector.getDecoratorsOfDeclaration(node);
let ivyClass: IvyClass|null = null;
@ -169,7 +169,7 @@ export class IvyCompilation {
private analyze(sf: ts.SourceFile, preanalyze: boolean): Promise<void>|undefined {
const promises: Promise<void>[] = [];
const analyzeClass = (node: ts.Declaration): void => {
const analyzeClass = (node: ClassDeclaration): void => {
const ivyClass = this.detectHandlersForClass(node);
// If the class has no Ivy behavior (or had errors), skip it.
@ -227,7 +227,7 @@ export class IvyCompilation {
const visit = (node: ts.Node): void => {
// Process nodes recursively, and look for class declarations with decorators.
if (ts.isClassDeclaration(node)) {
if (isNamedClassDeclaration(node)) {
analyzeClass(node);
}
ts.forEachChild(node, visit);
@ -291,8 +291,8 @@ export class IvyCompilation {
*/
compileIvyFieldFor(node: ts.Declaration, constantPool: ConstantPool): CompileResult[]|undefined {
// Look to see whether the original node was analyzed. If not, there's nothing to do.
const original = ts.getOriginalNode(node) as ts.Declaration;
if (!this.ivyClasses.has(original)) {
const original = ts.getOriginalNode(node) as typeof node;
if (!isNamedClassDeclaration(original) || !this.ivyClasses.has(original)) {
return undefined;
}
@ -305,7 +305,8 @@ export class IvyCompilation {
continue;
}
const compileMatchRes = match.handler.compile(node, match.analyzed.analysis, constantPool);
const compileMatchRes =
match.handler.compile(node as ClassDeclaration, match.analyzed.analysis, constantPool);
if (!Array.isArray(compileMatchRes)) {
res.push(compileMatchRes);
} else {
@ -327,9 +328,9 @@ export class IvyCompilation {
* Lookup the `ts.Decorator` which triggered transformation of a particular class declaration.
*/
ivyDecoratorsFor(node: ts.Declaration): ts.Decorator[] {
const original = ts.getOriginalNode(node) as ts.Declaration;
const original = ts.getOriginalNode(node) as typeof node;
if (!this.ivyClasses.has(original)) {
if (!isNamedClassDeclaration(original) || !this.ivyClasses.has(original)) {
return EMPTY_ARRAY;
}
const ivyClass = this.ivyClasses.get(original) !;