feat(ivy): use the ReflectionHost to resolve parameters and initializers (#25406)
ngtsc's static resolver can evaluate function calls where parameters have default values. In TypeScript code these default values live on the function definition, but in ES5 code the default values are represented by statements in the function body. A new ReflectionHost method getDefinitionOfFunction() abstracts over this difference, and allows the static reflector to more accurately evaluate ES5 code. PR Close #25406
This commit is contained in:
parent
94332affd3
commit
a45f2bfb8f
|
@ -147,9 +147,9 @@ export interface ClassMember {
|
|||
}
|
||||
|
||||
/**
|
||||
* A parameter to a function or constructor.
|
||||
* A parameter to a constructor.
|
||||
*/
|
||||
export interface Parameter {
|
||||
export interface CtorParameter {
|
||||
/**
|
||||
* Name of the parameter, if available.
|
||||
*
|
||||
|
@ -180,6 +180,54 @@ export interface Parameter {
|
|||
decorators: Decorator[]|null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Definition of a function or method, including its body if present and any parameters.
|
||||
*
|
||||
* In TypeScript code this metadata will be a simple reflection of the declarations in the node
|
||||
* itself. In ES5 code this can be more complicated, as the default values for parameters may
|
||||
* be extracted from certain body statements.
|
||||
*/
|
||||
export interface FunctionDefinition<T extends ts.MethodDeclaration|ts.FunctionDeclaration|
|
||||
ts.FunctionExpression> {
|
||||
/**
|
||||
* A reference to the node which declares the function.
|
||||
*/
|
||||
node: T;
|
||||
|
||||
/**
|
||||
* Statements of the function body, if a body is present, or null if no body is present.
|
||||
*
|
||||
* This list may have been filtered to exclude statements which perform parameter default value
|
||||
* initialization.
|
||||
*/
|
||||
body: ts.Statement[]|null;
|
||||
|
||||
/**
|
||||
* Metadata regarding the function's parameters, including possible default value expressions.
|
||||
*/
|
||||
parameters: Parameter[];
|
||||
}
|
||||
|
||||
/**
|
||||
* A parameter to a function or method.
|
||||
*/
|
||||
export interface Parameter {
|
||||
/**
|
||||
* Name of the parameter, if available.
|
||||
*/
|
||||
name: string|null;
|
||||
|
||||
/**
|
||||
* Declaration which created this parameter.
|
||||
*/
|
||||
node: ts.ParameterDeclaration;
|
||||
|
||||
/**
|
||||
* Expression which represents the default value of the parameter, if any.
|
||||
*/
|
||||
initializer: ts.Expression|null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The source of an imported symbol, including the original symbol name and the module from which it
|
||||
* was imported.
|
||||
|
@ -273,7 +321,30 @@ export interface ReflectionHost {
|
|||
* a constructor exists. If the constructor exists and has 0 parameters, this array will be empty.
|
||||
* If the class has no constructor, this method returns `null`.
|
||||
*/
|
||||
getConstructorParameters(declaration: ts.Declaration): Parameter[]|null;
|
||||
getConstructorParameters(declaration: ts.Declaration): CtorParameter[]|null;
|
||||
|
||||
/**
|
||||
* Reflect over a function and return metadata about its parameters and body.
|
||||
*
|
||||
* Functions in TypeScript and ES5 code have different AST representations, in particular around
|
||||
* default values for parameters. A TypeScript function has its default value as the initializer
|
||||
* on the parameter declaration, whereas an ES5 function has its default value set in a statement
|
||||
* of the form:
|
||||
*
|
||||
* if (param === void 0) { param = 3; }
|
||||
*
|
||||
* This method abstracts over these details, and interprets the function declaration and body to
|
||||
* extract parameter default values and the "real" body.
|
||||
*
|
||||
* A current limitation is that this metadata has no representation for shorthand assignment of
|
||||
* parameter objects in the function signature.
|
||||
*
|
||||
* @param fn a TypeScript `ts.Declaration` node representing the function over which to reflect.
|
||||
*
|
||||
* @returns a `FunctionDefinition` giving metadata about the function definition.
|
||||
*/
|
||||
getDefinitionOfFunction<T extends ts.MethodDeclaration|ts.FunctionDeclaration|
|
||||
ts.FunctionExpression>(fn: T): FunctionDefinition<T>;
|
||||
|
||||
/**
|
||||
* Determine if an identifier was imported from another module and return `Import` metadata
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {ClassMember, ClassMemberKind, Declaration, Decorator, Import, Parameter, ReflectionHost} from '../../host';
|
||||
import {ClassMember, ClassMemberKind, CtorParameter, Declaration, Decorator, FunctionDefinition, Import, ReflectionHost} from '../../host';
|
||||
|
||||
/**
|
||||
* reflector.ts implements static reflection of declarations using the TypeScript `ts.TypeChecker`.
|
||||
|
@ -31,7 +31,7 @@ export class TypeScriptReflectionHost implements ReflectionHost {
|
|||
.filter((member): member is ClassMember => member !== null);
|
||||
}
|
||||
|
||||
getConstructorParameters(declaration: ts.Declaration): Parameter[]|null {
|
||||
getConstructorParameters(declaration: ts.Declaration): CtorParameter[]|null {
|
||||
const clazz = castDeclarationToClassOrDie(declaration);
|
||||
|
||||
// First, find the constructor.
|
||||
|
@ -146,6 +146,19 @@ export class TypeScriptReflectionHost implements ReflectionHost {
|
|||
return this.getDeclarationOfSymbol(symbol);
|
||||
}
|
||||
|
||||
getDefinitionOfFunction<T extends ts.FunctionDeclaration|ts.MethodDeclaration|
|
||||
ts.FunctionExpression>(node: T): FunctionDefinition<T> {
|
||||
return {
|
||||
node,
|
||||
body: node.body !== undefined ? Array.from(node.body.statements) : null,
|
||||
parameters: node.parameters.map(param => {
|
||||
const name = parameterName(param.name);
|
||||
const initializer = param.initializer || null;
|
||||
return {name, node: param, initializer};
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a `ts.Symbol` to its declaration, keeping track of the `viaModule` along the way.
|
||||
*
|
||||
|
|
|
@ -547,11 +547,11 @@ class StaticInterpreter {
|
|||
`calling something that is not a function declaration? ${ts.SyntaxKind[lhs.node.kind]} (${node.getText()})`);
|
||||
}
|
||||
|
||||
const fn = lhs.node;
|
||||
const fn = this.host.getDefinitionOfFunction(lhs.node);
|
||||
|
||||
// If the function is foreign (declared through a d.ts file), attempt to resolve it with the
|
||||
// foreignFunctionResolver, if one is specified.
|
||||
if (fn.body === undefined) {
|
||||
if (fn.body === null) {
|
||||
let expr: ts.Expression|null = null;
|
||||
if (context.foreignFunctionResolver) {
|
||||
expr = context.foreignFunctionResolver(lhs, node.arguments);
|
||||
|
@ -572,10 +572,10 @@ class StaticInterpreter {
|
|||
}
|
||||
|
||||
const body = fn.body;
|
||||
if (body.statements.length !== 1 || !ts.isReturnStatement(body.statements[0])) {
|
||||
if (body.length !== 1 || !ts.isReturnStatement(body[0])) {
|
||||
throw new Error('Function body must have a single return statement only.');
|
||||
}
|
||||
const ret = body.statements[0] as ts.ReturnStatement;
|
||||
const ret = body[0] as ts.ReturnStatement;
|
||||
|
||||
const newScope: Scope = new Map<ts.ParameterDeclaration, ResolvedValue>();
|
||||
fn.parameters.forEach((param, index) => {
|
||||
|
@ -584,10 +584,10 @@ class StaticInterpreter {
|
|||
const arg = node.arguments[index];
|
||||
value = this.visitExpression(arg, context);
|
||||
}
|
||||
if (value === undefined && param.initializer !== undefined) {
|
||||
if (value === undefined && param.initializer !== null) {
|
||||
value = this.visitExpression(param.initializer, context);
|
||||
}
|
||||
newScope.set(param, value);
|
||||
newScope.set(param.node, value);
|
||||
});
|
||||
|
||||
return ret.expression !== undefined ?
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {Parameter} from '../../host';
|
||||
import {CtorParameter} from '../../host';
|
||||
import {getDeclaration, makeProgram} from '../../testing/in_memory_typescript';
|
||||
import {TypeScriptReflectionHost} from '../src/reflector';
|
||||
|
||||
|
@ -165,7 +165,7 @@ describe('reflector', () => {
|
|||
});
|
||||
|
||||
function expectParameter(
|
||||
param: Parameter, name: string, type?: string, decorator?: string,
|
||||
param: CtorParameter, name: string, type?: string, decorator?: string,
|
||||
decoratorFrom?: string): void {
|
||||
expect(param.name !).toEqual(name);
|
||||
if (type === undefined) {
|
||||
|
|
Loading…
Reference in New Issue