fix(ivy): add support for optional nullable injection tokens (#27552)
FW-778 #resolve PR Close #27552
This commit is contained in:
@ -82,7 +82,8 @@ export function generateSetClassMetadataCall(
|
||||
function ctorParameterToMetadata(param: CtorParameter, isCore: boolean): ts.Expression {
|
||||
// Parameters sometimes have a type that can be referenced. If so, then use it, otherwise
|
||||
// its type is undefined.
|
||||
const type = param.type !== null ? param.type : ts.createIdentifier('undefined');
|
||||
const type =
|
||||
param.typeExpression !== null ? param.typeExpression : ts.createIdentifier('undefined');
|
||||
const properties: ts.ObjectLiteralElementLike[] = [
|
||||
ts.createPropertyAssignment('type', type),
|
||||
];
|
||||
|
@ -26,7 +26,7 @@ export function getConstructorDependencies(
|
||||
}
|
||||
}
|
||||
ctorParams.forEach((param, idx) => {
|
||||
let tokenExpr = param.type;
|
||||
let tokenExpr = param.typeExpression;
|
||||
let optional = false, self = false, skipSelf = false, host = false;
|
||||
let resolved = R3ResolvedDependencyType.Token;
|
||||
(param.decorators || []).filter(dec => isCore || isAngularCore(dec)).forEach(dec => {
|
||||
@ -62,7 +62,7 @@ export function getConstructorDependencies(
|
||||
if (tokenExpr === null) {
|
||||
throw new FatalDiagnosticError(
|
||||
ErrorCode.PARAM_MISSING_TOKEN, param.nameNode,
|
||||
`No suitable token for parameter ${param.name || idx} of class ${clazz.name!.text}`);
|
||||
`No suitable injection token for parameter '${param.name || idx}' of class '${clazz.name!.text}'. Found: ${param.typeNode!.getText()}`);
|
||||
}
|
||||
const token = new WrappedNodeExpr(tokenExpr);
|
||||
useType.push({token, optional, self, skipSelf, host, resolved});
|
||||
|
@ -172,12 +172,22 @@ export interface CtorParameter {
|
||||
nameNode: ts.BindingName;
|
||||
|
||||
/**
|
||||
* TypeScript `ts.Expression` representing the type of the parameter, if the type is a simple
|
||||
* expression type.
|
||||
* TypeScript `ts.Expression` representing the type value of the parameter, if the type is a
|
||||
* simple
|
||||
* expression type that can be converted to a value.
|
||||
*
|
||||
* If the type is not present or cannot be represented as an expression, `type` is `null`.
|
||||
*/
|
||||
type: ts.Expression|null;
|
||||
typeExpression: ts.Expression|null;
|
||||
|
||||
/**
|
||||
* TypeScript `ts.TypeNode` representing the type node found in the type position.
|
||||
*
|
||||
* This field can be used for diagnostics reporting if `typeExpression` is `null`.
|
||||
*
|
||||
* Can be null, if the param has no type declared.
|
||||
*/
|
||||
typeNode: ts.TypeNode|null;
|
||||
|
||||
/**
|
||||
* Any `Decorator`s which are present on the parameter, or `null` if none are present.
|
||||
|
@ -49,25 +49,43 @@ export class TypeScriptReflectionHost implements ReflectionHost {
|
||||
// It may or may not be possible to write an expression that refers to the value side of the
|
||||
// type named for the parameter.
|
||||
let typeValueExpr: ts.Expression|null = null;
|
||||
let originalTypeNode = node.type || null;
|
||||
let typeNode = originalTypeNode;
|
||||
|
||||
// Check if we are dealing with a simple nullable union type e.g. `foo: Foo|null`
|
||||
// and extract the type. More complext union types e.g. `foo: Foo|Bar` are not supported.
|
||||
// We also don't need to support `foo: Foo|undefined` because Angular's DI injects `null` for
|
||||
// optional tokes that don't have providers.
|
||||
if (typeNode && ts.isUnionTypeNode(typeNode)) {
|
||||
let childTypeNodes = typeNode.types.filter(
|
||||
childTypeNode => childTypeNode.kind !== ts.SyntaxKind.NullKeyword);
|
||||
|
||||
if (childTypeNodes.length === 1) {
|
||||
typeNode = childTypeNodes[0];
|
||||
} else {
|
||||
typeNode = null;
|
||||
}
|
||||
}
|
||||
|
||||
// It's not possible to get a value expression if the parameter doesn't even have a type.
|
||||
if (node.type !== undefined) {
|
||||
if (typeNode) {
|
||||
// It's only valid to convert a type reference to a value reference if the type actually has
|
||||
// a
|
||||
// value declaration associated with it.
|
||||
const type = this.checker.getTypeFromTypeNode(node.type);
|
||||
if (type.symbol !== undefined && type.symbol.valueDeclaration !== undefined) {
|
||||
// a value declaration associated with it.
|
||||
let type: ts.Type|null = this.checker.getTypeFromTypeNode(typeNode);
|
||||
|
||||
if (type && type.symbol !== undefined && type.symbol.valueDeclaration !== undefined) {
|
||||
// The type points to a valid value declaration. Rewrite the TypeReference into an
|
||||
// Expression
|
||||
// which references the value pointed to by the TypeReference, if possible.
|
||||
typeValueExpr = typeNodeToValueExpr(node.type);
|
||||
typeValueExpr = typeNodeToValueExpr(typeNode);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
name,
|
||||
nameNode: node.name,
|
||||
type: typeValueExpr, decorators,
|
||||
typeExpression: typeValueExpr,
|
||||
typeNode: originalTypeNode, decorators,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
@ -47,7 +47,7 @@ describe('reflector', () => {
|
||||
contents: `
|
||||
import {dec} from './dec';
|
||||
class Bar {}
|
||||
|
||||
|
||||
class Foo {
|
||||
constructor(@dec bar: Bar) {}
|
||||
}
|
||||
@ -76,7 +76,7 @@ describe('reflector', () => {
|
||||
contents: `
|
||||
import {dec} from './dec';
|
||||
class Bar {}
|
||||
|
||||
|
||||
class Foo {
|
||||
constructor(@dec bar: Bar) {}
|
||||
}
|
||||
@ -104,7 +104,7 @@ describe('reflector', () => {
|
||||
contents: `
|
||||
import {Bar} from './bar';
|
||||
import * as star from './bar';
|
||||
|
||||
|
||||
class Foo {
|
||||
constructor(bar: Bar, otherBar: star.Bar) {}
|
||||
}
|
||||
@ -119,6 +119,34 @@ describe('reflector', () => {
|
||||
expectParameter(args[0], 'bar', 'Bar');
|
||||
expectParameter(args[1], 'otherBar', 'star.Bar');
|
||||
});
|
||||
|
||||
|
||||
it('should reflect an nullable argument', () => {
|
||||
const {program} = makeProgram([
|
||||
{
|
||||
name: 'bar.ts',
|
||||
contents: `
|
||||
export class Bar {}
|
||||
`
|
||||
},
|
||||
{
|
||||
name: 'entry.ts',
|
||||
contents: `
|
||||
import {Bar} from './bar';
|
||||
|
||||
class Foo {
|
||||
constructor(bar: Bar|null) {}
|
||||
}
|
||||
`
|
||||
}
|
||||
]);
|
||||
const clazz = getDeclaration(program, 'entry.ts', 'Foo', ts.isClassDeclaration);
|
||||
const checker = program.getTypeChecker();
|
||||
const host = new TypeScriptReflectionHost(checker);
|
||||
const args = host.getConstructorParameters(clazz) !;
|
||||
expect(args.length).toBe(1);
|
||||
expectParameter(args[0], 'bar', 'Bar');
|
||||
});
|
||||
});
|
||||
|
||||
it('should reflect a re-export', () => {
|
||||
@ -169,10 +197,10 @@ function expectParameter(
|
||||
decoratorFrom?: string): void {
|
||||
expect(param.name !).toEqual(name);
|
||||
if (type === undefined) {
|
||||
expect(param.type).toBeNull();
|
||||
expect(param.typeExpression).toBeNull();
|
||||
} else {
|
||||
expect(param.type).not.toBeNull();
|
||||
expect(argExpressionToString(param.type !)).toEqual(type);
|
||||
expect(param.typeExpression).not.toBeNull();
|
||||
expect(argExpressionToString(param.typeExpression !)).toEqual(type);
|
||||
}
|
||||
if (decorator !== undefined) {
|
||||
expect(param.decorators).not.toBeNull();
|
||||
@ -184,7 +212,11 @@ function expectParameter(
|
||||
}
|
||||
}
|
||||
|
||||
function argExpressionToString(name: ts.Node): string {
|
||||
function argExpressionToString(name: ts.Node | null): string {
|
||||
if (name == null) {
|
||||
throw new Error('\'name\' argument can\'t be null');
|
||||
}
|
||||
|
||||
if (ts.isIdentifier(name)) {
|
||||
return name.text;
|
||||
} else if (ts.isPropertyAccessExpression(name)) {
|
||||
|
Reference in New Issue
Block a user