fix(ivy): handle aliased Angular decorators (#29195)

Prior to this change the code didn't take into account the fact that decorators can be aliases while importing into a script. As a result, these decorators were not recognized by Angular and various failures happened because of that. Now we take aliases into account and resolve decorator name properly.

PR Close #29195
This commit is contained in:
Andrew Kushnir 2019-03-08 15:32:31 -08:00 committed by Kara Erickson
parent 99aa9674b2
commit 1d88c2bb81
10 changed files with 111 additions and 32 deletions

View File

@ -81,7 +81,7 @@ export class DecorationAnalyzer {
importGraph = new ImportGraph(this.moduleResolver);
cycleAnalyzer = new CycleAnalyzer(this.importGraph);
handlers: DecoratorHandler<any, any>[] = [
new BaseDefDecoratorHandler(this.reflectionHost, this.evaluator),
new BaseDefDecoratorHandler(this.reflectionHost, this.evaluator, this.isCore),
new ComponentDecoratorHandler(
this.reflectionHost, this.evaluator, this.scopeRegistry, this.isCore, this.resourceManager,
this.rootDirs, /* defaultPreserveWhitespaces */ false, /* i18nUseExternalIds */ true,

View File

@ -12,27 +12,30 @@ import * as ts from 'typescript';
import {PartialEvaluator} from '../../partial_evaluator';
import {ClassMember, Decorator, ReflectionHost} from '../../reflection';
import {AnalysisOutput, CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence} from '../../transform';
import {isAngularCore} from './util';
function containsNgTopLevelDecorator(decorators: Decorator[] | null): boolean {
import {isAngularDecorator} from './util';
function containsNgTopLevelDecorator(decorators: Decorator[] | null, isCore: boolean): boolean {
if (!decorators) {
return false;
}
return decorators.find(
decorator => (decorator.name === 'Component' || decorator.name === 'Directive' ||
decorator.name === 'NgModule') &&
isAngularCore(decorator)) !== undefined;
return decorators.some(
decorator => isAngularDecorator(decorator, 'Component', isCore) ||
isAngularDecorator(decorator, 'Directive', isCore) ||
isAngularDecorator(decorator, 'NgModule', isCore));
}
export class BaseDefDecoratorHandler implements
DecoratorHandler<R3BaseRefMetaData, R3BaseRefDecoratorDetection> {
constructor(private reflector: ReflectionHost, private evaluator: PartialEvaluator) {}
constructor(
private reflector: ReflectionHost, private evaluator: PartialEvaluator,
private isCore: boolean) {}
readonly precedence = HandlerPrecedence.WEAK;
detect(node: ts.ClassDeclaration, decorators: Decorator[]|null):
DetectResult<R3BaseRefDecoratorDetection>|undefined {
if (containsNgTopLevelDecorator(decorators)) {
if (containsNgTopLevelDecorator(decorators, this.isCore)) {
// If the class is already decorated by @Component or @Directive let that
// DecoratorHandler handle this. BaseDef is unnecessary.
return undefined;
@ -44,12 +47,11 @@ export class BaseDefDecoratorHandler implements
const {decorators} = property;
if (decorators) {
for (const decorator of decorators) {
const decoratorName = decorator.name;
if (decoratorName === 'Input' && isAngularCore(decorator)) {
if (isAngularDecorator(decorator, 'Input', this.isCore)) {
result = result || {};
const inputs = result.inputs = result.inputs || [];
inputs.push({decorator, property});
} else if (decoratorName === 'Output' && isAngularCore(decorator)) {
} else if (isAngularDecorator(decorator, 'Output', this.isCore)) {
result = result || {};
const outputs = result.outputs = result.outputs || [];
outputs.push({decorator, property});

View File

@ -23,7 +23,7 @@ import {tsSourceMapBug29300Fixed} from '../../util/src/ts_source_map_bug_29300';
import {ResourceLoader} from './api';
import {extractDirectiveMetadata, extractQueriesFromDecorator, parseFieldArrayValue, queriesFromFields} from './directive';
import {generateSetClassMetadataCall} from './metadata';
import {isAngularCore, isAngularCoreReference, unwrapExpression} from './util';
import {findAngularDecorator, isAngularCoreReference, unwrapExpression} from './util';
const EMPTY_MAP = new Map<string, Expression>();
const EMPTY_ARRAY: any[] = [];
@ -64,8 +64,7 @@ export class ComponentDecoratorHandler implements
if (!decorators) {
return undefined;
}
const decorator = decorators.find(
decorator => decorator.name === 'Component' && (this.isCore || isAngularCore(decorator)));
const decorator = findAngularDecorator(decorators, 'Component', this.isCore);
if (decorator !== undefined) {
return {
trigger: decorator.node,

View File

@ -7,7 +7,6 @@
*/
import {ConstantPool, Expression, ParseError, ParsedHostBindings, R3DirectiveMetadata, R3QueryMetadata, Statement, WrappedNodeExpr, compileDirectiveFromMetadata, makeBindingParser, parseHostBindings, verifyHostBindings} from '@angular/compiler';
import * as ts from 'typescript';
import {ErrorCode, FatalDiagnosticError} from '../../diagnostics';
@ -19,7 +18,7 @@ import {extractDirectiveGuards} from '../../scope/src/util';
import {AnalysisOutput, CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence} from '../../transform';
import {generateSetClassMetadataCall} from './metadata';
import {getValidConstructorDependencies, isAngularCore, unwrapExpression, unwrapForwardRef} from './util';
import {findAngularDecorator, getValidConstructorDependencies, unwrapExpression, unwrapForwardRef} from './util';
const EMPTY_OBJECT: {[key: string]: string} = {};
@ -39,8 +38,7 @@ export class DirectiveDecoratorHandler implements
if (!decorators) {
return undefined;
}
const decorator = decorators.find(
decorator => decorator.name === 'Directive' && (this.isCore || isAngularCore(decorator)));
const decorator = findAngularDecorator(decorators, 'Directive', this.isCore);
if (decorator !== undefined) {
return {
trigger: decorator.node,

View File

@ -14,7 +14,7 @@ import {Decorator, ReflectionHost, reflectObjectLiteral} from '../../reflection'
import {AnalysisOutput, CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence} from '../../transform';
import {generateSetClassMetadataCall} from './metadata';
import {getConstructorDependencies, getValidConstructorDependencies, isAngularCore, validateConstructorDependencies} from './util';
import {findAngularDecorator, getConstructorDependencies, getValidConstructorDependencies, validateConstructorDependencies} from './util';
export interface InjectableHandlerData {
meta: R3InjectableMetadata;
@ -36,8 +36,7 @@ export class InjectableDecoratorHandler implements
if (!decorators) {
return undefined;
}
const decorator = decorators.find(
decorator => decorator.name === 'Injectable' && (this.isCore || isAngularCore(decorator)));
const decorator = findAngularDecorator(decorators, 'Injectable', this.isCore);
if (decorator !== undefined) {
return {
trigger: decorator.node,

View File

@ -20,7 +20,7 @@ import {getSourceFile} from '../../util/src/typescript';
import {generateSetClassMetadataCall} from './metadata';
import {ReferencesRegistry} from './references_registry';
import {combineResolvers, forwardRefResolver, getValidConstructorDependencies, isAngularCore, toR3Reference, unwrapExpression} from './util';
import {combineResolvers, findAngularDecorator, forwardRefResolver, getValidConstructorDependencies, toR3Reference, unwrapExpression} from './util';
export interface NgModuleAnalysis {
ngModuleDef: R3NgModuleMetadata;
@ -47,8 +47,7 @@ export class NgModuleDecoratorHandler implements DecoratorHandler<NgModuleAnalys
if (!decorators) {
return undefined;
}
const decorator = decorators.find(
decorator => decorator.name === 'NgModule' && (this.isCore || isAngularCore(decorator)));
const decorator = findAngularDecorator(decorators, 'NgModule', this.isCore);
if (decorator !== undefined) {
return {
trigger: decorator.node,

View File

@ -17,7 +17,7 @@ import {LocalModuleScopeRegistry} from '../../scope/src/local';
import {AnalysisOutput, CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence} from '../../transform';
import {generateSetClassMetadataCall} from './metadata';
import {getValidConstructorDependencies, isAngularCore, unwrapExpression} from './util';
import {findAngularDecorator, getValidConstructorDependencies, unwrapExpression} from './util';
export interface PipeHandlerData {
meta: R3PipeMetadata;
@ -35,8 +35,7 @@ export class PipeDecoratorHandler implements DecoratorHandler<PipeHandlerData, D
if (!decorators) {
return undefined;
}
const decorator = decorators.find(
decorator => decorator.name === 'Pipe' && (this.isCore || isAngularCore(decorator)));
const decorator = findAngularDecorator(decorators, 'Pipe', this.isCore);
if (decorator !== undefined) {
return {
trigger: decorator.node,

View File

@ -12,7 +12,7 @@ import * as ts from 'typescript';
import {ErrorCode, FatalDiagnosticError} from '../../diagnostics';
import {ImportMode, Reference, ReferenceEmitter} from '../../imports';
import {ForeignFunctionResolver} from '../../partial_evaluator';
import {ClassMemberKind, CtorParameter, Decorator, ReflectionHost, TypeValueReference} from '../../reflection';
import {ClassMemberKind, CtorParameter, Decorator, Import, ReflectionHost, TypeValueReference} from '../../reflection';
export enum ConstructorDepErrorKind {
NO_SUITABLE_TOKEN,
@ -149,14 +149,28 @@ export function toR3Reference(
return {value, type};
}
export function isAngularCore(decorator: Decorator): boolean {
export function isAngularCore(decorator: Decorator): decorator is Decorator&{import: Import} {
return decorator.import !== null && decorator.import.from === '@angular/core';
}
export function isAngularCoreReference(reference: Reference, symbolName: string) {
export function isAngularCoreReference(reference: Reference, symbolName: string): boolean {
return reference.ownedByModuleGuess === '@angular/core' && reference.debugName === symbolName;
}
export function findAngularDecorator(
decorators: Decorator[], name: string, isCore: boolean): Decorator|undefined {
return decorators.find(decorator => isAngularDecorator(decorator, name, isCore));
}
export function isAngularDecorator(decorator: Decorator, name: string, isCore: boolean): boolean {
if (isCore) {
return decorator.name === name;
} else if (isAngularCore(decorator)) {
return decorator.import.name === name;
}
return false;
}
/**
* Unwrap a `ts.Expression`, removing outer type-casts or parentheses until the expression is in its
* lowest level form.

View File

@ -381,7 +381,7 @@ export class NgtscProgram implements api.Program {
// Set up the IvyCompilation, which manages state for the Ivy transformer.
const handlers = [
new BaseDefDecoratorHandler(this.reflector, evaluator),
new BaseDefDecoratorHandler(this.reflector, evaluator, this.isCore),
new ComponentDecoratorHandler(
this.reflector, evaluator, scopeRegistry, this.isCore, this.resourceManager,
this.rootDirs, this.options.preserveWhitespaces || false,

View File

@ -293,6 +293,75 @@ describe('ngtsc behavioral tests', () => {
expect(jsContents).toContain('/** @nocollapse */ TestCmp.ngComponentDef');
});
it('should recognize aliased decorators', () => {
env.tsconfig({});
env.write('test.ts', `
import {
Component as AngularComponent,
Directive as AngularDirective,
Pipe as AngularPipe,
Injectable as AngularInjectable,
NgModule as AngularNgModule,
Input as AngularInput,
Output as AngularOutput
} from '@angular/core';
export class TestBase {
@AngularInput() input: any;
@AngularOutput() output: any;
}
@AngularComponent({
selector: 'test-component',
template: '...'
})
export class TestComponent {
@AngularInput() input: any;
@AngularOutput() output: any;
}
@AngularDirective({
selector: 'test-directive'
})
export class TestDirective {}
@AngularPipe({
name: 'test-pipe'
})
export class TestPipe {}
@AngularInjectable({})
export class TestInjectable {}
@AngularNgModule({
declarations: [
TestComponent,
TestDirective,
TestPipe
],
exports: [
TestComponent,
TestDirective,
TestPipe
]
})
class MyModule {}
`);
env.driveMain();
const jsContents = env.getContents('test.js');
expect(jsContents).toContain('TestBase.ngBaseDef = i0.ɵdefineBase');
expect(jsContents).toContain('TestComponent.ngComponentDef = i0.ɵdefineComponent');
expect(jsContents).toContain('TestDirective.ngDirectiveDef = i0.ɵdefineDirective');
expect(jsContents).toContain('TestPipe.ngPipeDef = i0.ɵdefinePipe');
expect(jsContents).toContain('TestInjectable.ngInjectableDef = i0.defineInjectable');
expect(jsContents).toContain('MyModule.ngModuleDef = i0.ɵdefineNgModule');
expect(jsContents).toContain('MyModule.ngInjectorDef = i0.defineInjector');
expect(jsContents).toContain('inputs: { input: "input" }');
expect(jsContents).toContain('outputs: { output: "output" }');
});
it('should compile Components with a templateUrl in a different rootDir', () => {
env.tsconfig({}, ['./extraRootDir']);
env.write('extraRootDir/test.html', '<p>Hello World</p>');