fix(ivy): unwrap parenthesized or cast expressions for metadata (#24862)

Metadata in Ivy must be literal. For example,

@NgModule({...})

is legal, whereas

const meta = {...};
@NgModule(meta)

is not.

However, some code contains additional superfluous parentheses:

@NgModule(({...}))

It is desirable that ngtsc accept this form of literal object.

PR Close #24862
This commit is contained in:
Alex Rickabaugh 2018-07-09 10:16:47 -07:00 committed by Victor Berchet
parent 8a5cd2200a
commit 1008bb6287
6 changed files with 59 additions and 8 deletions

View File

@ -17,7 +17,7 @@ import {AnalysisOutput, CompileResult, DecoratorHandler} from '../../transform';
import {ResourceLoader} from './api';
import {extractDirectiveMetadata} from './directive';
import {SelectorScopeRegistry} from './selector_scope';
import {isAngularCore} from './util';
import {isAngularCore, unwrapExpression} from './util';
const EMPTY_MAP = new Map<string, Expression>();
@ -156,7 +156,8 @@ export class ComponentDecoratorHandler implements DecoratorHandler<R3ComponentMe
if (decorator.args === null || decorator.args.length !== 1) {
throw new Error(`Incorrect number of arguments to @Component decorator`);
}
const meta = decorator.args[0];
const meta = unwrapExpression(decorator.args[0]);
if (!ts.isObjectLiteralExpression(meta)) {
throw new Error(`Decorator argument must be literal.`);
}

View File

@ -15,7 +15,7 @@ import {filterToMembersWithDecorator} from '../../metadata/src/reflector';
import {AnalysisOutput, CompileResult, DecoratorHandler} from '../../transform';
import {SelectorScopeRegistry} from './selector_scope';
import {getConstructorDependencies, isAngularCore} from './util';
import {getConstructorDependencies, isAngularCore, unwrapExpression} from './util';
const EMPTY_OBJECT: {[key: string]: string} = {};
@ -63,7 +63,7 @@ export function extractDirectiveMetadata(
if (decorator.args === null || decorator.args.length !== 1) {
throw new Error(`Incorrect number of arguments to @${decorator.name} decorator`);
}
const meta = decorator.args[0];
const meta = unwrapExpression(decorator.args[0]);
if (!ts.isObjectLiteralExpression(meta)) {
throw new Error(`Decorator argument must be literal.`);
}

View File

@ -14,7 +14,7 @@ import {Reference, ResolvedValue, reflectObjectLiteral, staticallyResolve} from
import {AnalysisOutput, CompileResult, DecoratorHandler} from '../../transform';
import {SelectorScopeRegistry} from './selector_scope';
import {getConstructorDependencies, isAngularCore, referenceToExpression} from './util';
import {getConstructorDependencies, isAngularCore, referenceToExpression, unwrapExpression} from './util';
export interface NgModuleAnalysis {
ngModuleDef: R3NgModuleMetadata;
@ -43,7 +43,9 @@ export class NgModuleDecoratorHandler implements DecoratorHandler<NgModuleAnalys
// @NgModule can be invoked without arguments. In case it is, pretend as if a blank object
// literal was specified. This simplifies the code below.
const meta = decorator.args.length === 1 ? decorator.args[0] : ts.createObjectLiteral([]);
const meta = decorator.args.length === 1 ? unwrapExpression(decorator.args[0]) :
ts.createObjectLiteral([]);
if (!ts.isObjectLiteralExpression(meta)) {
throw new Error(`Decorator argument must be literal.`);
}

View File

@ -14,7 +14,7 @@ import {reflectObjectLiteral, staticallyResolve} from '../../metadata';
import {AnalysisOutput, CompileResult, DecoratorHandler} from '../../transform';
import {SelectorScopeRegistry} from './selector_scope';
import {getConstructorDependencies, isAngularCore} from './util';
import {getConstructorDependencies, isAngularCore, unwrapExpression} from './util';
export class PipeDecoratorHandler implements DecoratorHandler<R3PipeMetadata> {
constructor(
@ -35,7 +35,7 @@ export class PipeDecoratorHandler implements DecoratorHandler<R3PipeMetadata> {
if (decorator.args === null) {
throw new Error(`@Pipe must be called`);
}
const meta = decorator.args[0];
const meta = unwrapExpression(decorator.args[0]);
if (!ts.isObjectLiteralExpression(meta)) {
throw new Error(`Decorator argument must be literal.`);
}

View File

@ -87,3 +87,16 @@ export function referenceToExpression(ref: Reference, context: ts.SourceFile): E
export function isAngularCore(decorator: Decorator): boolean {
return decorator.import !== null && decorator.import.from === '@angular/core';
}
/**
* Unwrap a `ts.Expression`, removing outer type-casts or parentheses until the expression is in its
* lowest level form.
*
* For example, the expression "(foo as Type)" unwraps to "foo".
*/
export function unwrapExpression(node: ts.Expression): ts.Expression {
while (ts.isAsExpression(node) || ts.isParenthesizedExpression(node)) {
node = node.expression;
}
return node;
}

View File

@ -0,0 +1,35 @@
/**
* @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 * as ts from 'typescript';
import {unwrapExpression} from '../src/util';
describe('ngtsc annotation utilities', () => {
describe('unwrapExpression', () => {
const obj = ts.createObjectLiteral();
it('should pass through an ObjectLiteralExpression',
() => { expect(unwrapExpression(obj)).toBe(obj); });
it('should unwrap an ObjectLiteralExpression in parentheses', () => {
const wrapped = ts.createParen(obj);
expect(unwrapExpression(wrapped)).toBe(obj);
});
it('should unwrap an ObjectLiteralExpression with a type cast', () => {
const cast = ts.createAsExpression(obj, ts.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword));
expect(unwrapExpression(cast)).toBe(obj);
});
it('should unwrap an ObjectLiteralExpression with a type cast in parentheses', () => {
const cast = ts.createAsExpression(obj, ts.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword));
const wrapped = ts.createParen(cast);
expect(unwrapExpression(wrapped)).toBe(obj);
});
});
});