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:

committed by
Andrew Kushnir

parent
20a9dbef8e
commit
881807dc36
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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 {
|
||||
|
@ -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', () => {
|
||||
|
Reference in New Issue
Block a user