fix(ivy): never use imported type references as values (#29111)

ngtsc occasionally converts a type reference (such as the type of a
parameter in a constructor) to a value reference (argument to a
directiveInject call). TypeScript has a bad habit of sometimes removing
the import statement associated with this type reference, because it's a
type only import when it initially looks at the file.

A solution to this is to always add an import to refer to a type position
value that's imported, and not rely on the existing import.

PR Close #29111
This commit is contained in:
Alex Rickabaugh
2019-03-04 11:43:55 -08:00
committed by Andrew Kushnir
parent 20a9dbef8e
commit 881807dc36
15 changed files with 308 additions and 87 deletions

View File

@ -6,11 +6,13 @@
* found in the LICENSE file at https://angular.io/license
*/
import {ExternalExpr, Identifiers, InvokeFunctionExpr, Statement, WrappedNodeExpr} from '@angular/compiler';
import {Expression, ExternalExpr, FunctionExpr, Identifiers, InvokeFunctionExpr, LiteralArrayExpr, LiteralExpr, ReturnStatement, Statement, WrappedNodeExpr, literalMap} from '@angular/compiler';
import * as ts from 'typescript';
import {CtorParameter, Decorator, ReflectionHost} from '../../reflection';
import {valueReferenceToExpression} from './util';
/**
* Given a class declaration, generate a call to `setClassMetadata` with the Angular metadata
* present on the class or its member fields.
@ -39,18 +41,13 @@ export function generateSetClassMetadataCall(
const metaDecorators = ts.createArrayLiteral(ngClassDecorators);
// Convert the constructor parameters to metadata, passing null if none are present.
let metaCtorParameters: ts.Expression = ts.createNull();
let metaCtorParameters: Expression = new LiteralExpr(null);
const classCtorParameters = reflection.getConstructorParameters(clazz);
if (classCtorParameters !== null) {
const ctorParameters = ts.createArrayLiteral(
classCtorParameters.map(param => ctorParameterToMetadata(param, isCore)));
metaCtorParameters = ts.createFunctionExpression(
/* modifiers */ undefined,
/* asteriskToken */ undefined,
/* name */ undefined,
/* typeParameters */ undefined,
/* parameters */ undefined,
/* type */ undefined, ts.createBlock([ts.createReturn(ctorParameters)]));
const ctorParameters = classCtorParameters.map(param => ctorParameterToMetadata(param, isCore));
metaCtorParameters = new FunctionExpr([], [
new ReturnStatement(new LiteralArrayExpr(ctorParameters)),
]);
}
// Do the same for property decorators.
@ -71,7 +68,7 @@ export function generateSetClassMetadataCall(
[
new WrappedNodeExpr(id),
new WrappedNodeExpr(metaDecorators),
new WrappedNodeExpr(metaCtorParameters),
metaCtorParameters,
new WrappedNodeExpr(metaPropDecorators),
],
/* type */ undefined,
@ -83,22 +80,25 @@ export function generateSetClassMetadataCall(
/**
* Convert a reflected constructor parameter to metadata.
*/
function ctorParameterToMetadata(param: CtorParameter, isCore: boolean): ts.Expression {
function ctorParameterToMetadata(param: CtorParameter, isCore: boolean): Expression {
// Parameters sometimes have a type that can be referenced. If so, then use it, otherwise
// its type is undefined.
const type =
param.typeExpression !== null ? param.typeExpression : ts.createIdentifier('undefined');
const properties: ts.ObjectLiteralElementLike[] = [
ts.createPropertyAssignment('type', type),
const type = param.typeValueReference !== null ?
valueReferenceToExpression(param.typeValueReference) :
new LiteralExpr(undefined);
const mapEntries: {key: string, value: Expression, quoted: false}[] = [
{key: 'type', value: type, quoted: false},
];
// If the parameter has decorators, include the ones from Angular.
if (param.decorators !== null) {
const ngDecorators =
param.decorators.filter(dec => isAngularDecorator(dec, isCore)).map(decoratorToMetadata);
properties.push(ts.createPropertyAssignment('decorators', ts.createArrayLiteral(ngDecorators)));
const value = new WrappedNodeExpr(ts.createArrayLiteral(ngDecorators));
mapEntries.push({key: 'decorators', value, quoted: false});
}
return ts.createObjectLiteral(properties, true);
return literalMap(mapEntries);
}
/**

View File

@ -6,13 +6,13 @@
* found in the LICENSE file at https://angular.io/license
*/
import {R3DependencyMetadata, R3Reference, R3ResolvedDependencyType, WrappedNodeExpr} from '@angular/compiler';
import {Expression, ExternalExpr, R3DependencyMetadata, R3Reference, R3ResolvedDependencyType, WrappedNodeExpr} from '@angular/compiler';
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} from '../../reflection';
import {ClassMemberKind, CtorParameter, Decorator, ReflectionHost, TypeValueReference} from '../../reflection';
export enum ConstructorDepErrorKind {
NO_SUITABLE_TOKEN,
@ -45,7 +45,7 @@ export function getConstructorDependencies(
}
}
ctorParams.forEach((param, idx) => {
let tokenExpr = param.typeExpression;
let token = valueReferenceToExpression(param.typeValueReference);
let optional = false, self = false, skipSelf = false, host = false;
let resolved = R3ResolvedDependencyType.Token;
(param.decorators || []).filter(dec => isCore || isAngularCore(dec)).forEach(dec => {
@ -56,7 +56,7 @@ export function getConstructorDependencies(
ErrorCode.DECORATOR_ARITY_WRONG, dec.node,
`Unexpected number of arguments to @Inject().`);
}
tokenExpr = dec.args[0];
token = new WrappedNodeExpr(dec.args[0]);
} else if (name === 'Optional') {
optional = true;
} else if (name === 'SkipSelf') {
@ -71,20 +71,19 @@ export function getConstructorDependencies(
ErrorCode.DECORATOR_ARITY_WRONG, dec.node,
`Unexpected number of arguments to @Attribute().`);
}
tokenExpr = dec.args[0];
token = new WrappedNodeExpr(dec.args[0]);
resolved = R3ResolvedDependencyType.Attribute;
} else {
throw new FatalDiagnosticError(
ErrorCode.DECORATOR_UNEXPECTED, dec.node, `Unexpected decorator ${name} on parameter.`);
}
});
if (tokenExpr === null) {
if (token === null) {
errors.push({
index: idx,
kind: ConstructorDepErrorKind.NO_SUITABLE_TOKEN, param,
});
} else {
const token = new WrappedNodeExpr(tokenExpr);
deps.push({token, optional, self, skipSelf, host, resolved});
}
});
@ -95,6 +94,27 @@ export function getConstructorDependencies(
}
}
/**
* Convert a `TypeValueReference` to an `Expression` which refers to the type as a value.
*
* Local references are converted to a `WrappedNodeExpr` of the TypeScript expression, and non-local
* references are converted to an `ExternalExpr`. Note that this is only valid in the context of the
* file in which the `TypeValueReference` originated.
*/
export function valueReferenceToExpression(valueRef: TypeValueReference): Expression;
export function valueReferenceToExpression(valueRef: null): null;
export function valueReferenceToExpression(valueRef: TypeValueReference | null): Expression|null;
export function valueReferenceToExpression(valueRef: TypeValueReference | null): Expression|null {
if (valueRef === null) {
return null;
} else if (valueRef.local) {
return new WrappedNodeExpr(valueRef.expression);
} else {
// TODO(alxhub): this cast is necessary because the g3 typescript version doesn't narrow here.
return new ExternalExpr(valueRef as{moduleName: string, name: string});
}
}
export function getValidConstructorDependencies(
clazz: ts.ClassDeclaration, reflector: ReflectionHost, isCore: boolean): R3DependencyMetadata[]|
null {

View File

@ -46,7 +46,7 @@ describe('ngtsc setClassMetadata converter', () => {
}
`);
expect(res).toContain(
`function () { return [{ type: undefined, decorators: [{ type: Inject, args: [FOO] }] }, { type: Injector }]; }, null);`);
`function () { return [{ type: undefined, decorators: [{ type: Inject, args: [FOO] }] }, { type: i0.Injector }]; }, null);`);
});
it('should convert decorated field metadata', () => {