feat(ivy): static evaluation of TypeScript's __spread helper (#30492)

The usage of array spread syntax in source code may be downleveled to a
call to TypeScript's `__spread` helper function from `tslib`, depending
on the options `downlevelIteration` and `emitHelpers`. This proves
problematic for ngcc when it is processing ES5 formats, as the static
evaluator won't be able to interpret those calls.

A custom foreign function resolver is not sufficient in this case, as
`tslib` may be emitted into the library code itself. In that case, a
helper function can be resolved to an actual function with body, such
that it won't be considered as foreign function. Instead, a reflection
host can now indicate that the definition of a function corresponds with
a certain TypeScript helper, such that it becomes statically evaluable
in ngtsc.

Resolves #30299

PR Close #30492
This commit is contained in:
JoostK
2019-05-15 21:10:47 +02:00
committed by Igor Minar
parent c0386757b1
commit 9d9c9e43e5
10 changed files with 231 additions and 31 deletions

View File

@ -17,6 +17,7 @@ import {ArrayConcatBuiltinFn, ArraySliceBuiltinFn} from './builtin';
import {DynamicValue} from './dynamic';
import {DependencyTracker, ForeignFunctionResolver} from './interface';
import {BuiltinFn, EnumValue, ResolvedValue, ResolvedValueArray, ResolvedValueMap} from './result';
import {evaluateTsHelperInline} from './ts_helpers';
/**
@ -386,11 +387,22 @@ export class StaticInterpreter {
if (!(lhs instanceof Reference)) {
return DynamicValue.fromInvalidExpressionType(node.expression, lhs);
} else if (!isFunctionOrMethodReference(lhs)) {
return DynamicValue.fromInvalidExpressionType(node.expression, lhs);
}
const fn = this.host.getDefinitionOfFunction(lhs.node);
if (fn === null) {
return DynamicValue.fromInvalidExpressionType(node.expression, lhs);
}
// If the function corresponds with a tslib helper function, evaluate it with custom logic.
if (fn.helper !== null) {
const args = this.evaluateFunctionArguments(node, context);
return evaluateTsHelperInline(fn.helper, node, args);
}
if (!isFunctionOrMethodReference(lhs)) {
return DynamicValue.fromInvalidExpressionType(node.expression, lhs);
}
// If the function is foreign (declared through a d.ts file), attempt to resolve it with the
// foreignFunctionResolver, if one is specified.

View File

@ -0,0 +1,37 @@
/**
* @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 {TsHelperFn} from '../../reflection';
import {DynamicValue} from './dynamic';
import {ResolvedValue, ResolvedValueArray} from './result';
export function evaluateTsHelperInline(
helper: TsHelperFn, node: ts.Node, args: ResolvedValueArray): ResolvedValue {
if (helper === TsHelperFn.Spread) {
return evaluateTsSpreadHelper(node, args);
} else {
throw new Error(`Cannot evaluate unknown helper ${helper} inline`);
}
}
function evaluateTsSpreadHelper(node: ts.Node, args: ResolvedValueArray): ResolvedValueArray {
const result: ResolvedValueArray = [];
for (const arg of args) {
if (arg instanceof DynamicValue) {
result.push(DynamicValue.fromDynamicInput(node, arg));
} else if (Array.isArray(arg)) {
result.push(...arg);
} else {
result.push(arg);
}
}
return result;
}

View File

@ -9,8 +9,10 @@
import * as ts from 'typescript';
import {Reference} from '../../imports';
import {FunctionDefinition, TsHelperFn, TypeScriptReflectionHost} from '../../reflection';
import {getDeclaration, makeProgram} from '../../testing/in_memory_typescript';
import {DynamicValue} from '../src/dynamic';
import {PartialEvaluator} from '../src/interface';
import {EnumValue} from '../src/result';
import {evaluate, firstArgFfr, makeEvaluator, makeExpression, owningModuleOf} from './utils';
@ -343,6 +345,27 @@ describe('ngtsc metadata', () => {
expect((value.node as ts.CallExpression).expression.getText()).toBe('foo');
});
it('should evaluate TypeScript __spread helper', () => {
const {checker, expression} = makeExpression(
`
import * as tslib from 'tslib';
const a = [1];
const b = [2, 3];
`,
'tslib.__spread(a, b)', [
{
name: 'node_modules/tslib/index.d.ts',
contents: `
export declare function __spread(...args: any[]): any[];
`
},
]);
const reflectionHost = new TsLibAwareReflectionHost(checker);
const evaluator = new PartialEvaluator(reflectionHost, checker);
const value = evaluator.evaluate(expression);
expect(value).toEqual([1, 2, 3]);
});
describe('(visited file tracking)', () => {
it('should track each time a source file is visited', () => {
const trackFileDependency = jasmine.createSpy('DependencyTracker');
@ -393,3 +416,34 @@ describe('ngtsc metadata', () => {
});
});
});
/**
* Customizes the resolution of functions to recognize functions from tslib. Such functions are not
* handled specially in the default TypeScript host, as only ngcc's ES5 host will have special
* powers to recognize functions from tslib.
*/
class TsLibAwareReflectionHost extends TypeScriptReflectionHost {
getDefinitionOfFunction(node: ts.Node): FunctionDefinition|null {
if (ts.isFunctionDeclaration(node)) {
const helper = getTsHelperFn(node);
if (helper !== null) {
return {
node,
body: null, helper,
parameters: [],
};
}
}
return super.getDefinitionOfFunction(node);
}
}
function getTsHelperFn(node: ts.FunctionDeclaration): TsHelperFn|null {
const name = node.name !== undefined && ts.isIdentifier(node.name) && node.name.text;
if (name === '__spread') {
return TsHelperFn.Spread;
} else {
return null;
}
}

View File

@ -254,27 +254,44 @@ export interface CtorParameter {
* 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> {
export interface FunctionDefinition {
/**
* A reference to the node which declares the function.
*/
node: T;
node: ts.MethodDeclaration|ts.FunctionDeclaration|ts.FunctionExpression|ts.VariableDeclaration;
/**
* Statements of the function body, if a body is present, or null if no body is present.
* Statements of the function body, if a body is present, or null if no body is present or the
* function is identified to represent a tslib helper function, in which case `helper` will
* indicate which helper this function represents.
*
* This list may have been filtered to exclude statements which perform parameter default value
* initialization.
*/
body: ts.Statement[]|null;
/**
* The type of tslib helper function, if the function is determined to represent a tslib helper
* function. Otherwise, this will be null.
*/
helper: TsHelperFn|null;
/**
* Metadata regarding the function's parameters, including possible default value expressions.
*/
parameters: Parameter[];
}
/**
* Possible functions from TypeScript's helper library.
*/
export enum TsHelperFn {
/**
* Indicates the `__spread` function.
*/
Spread,
}
/**
* A parameter to a function or method.
*/
@ -404,8 +421,7 @@ export interface ReflectionHost {
*
* @returns a `FunctionDefinition` giving metadata about the function definition.
*/
getDefinitionOfFunction<T extends ts.MethodDeclaration|ts.FunctionDeclaration|
ts.FunctionExpression>(fn: T): FunctionDefinition<T>;
getDefinitionOfFunction(fn: ts.Node): FunctionDefinition|null;
/**
* Determine if an identifier was imported from another module and return `Import` metadata

View File

@ -8,7 +8,7 @@
import * as ts from 'typescript';
import {ClassDeclaration, ClassMember, ClassMemberKind, CtorParameter, Declaration, Decorator, FunctionDefinition, Import, ReflectionHost} from './host';
import {ClassDeclaration, ClassMember, ClassMemberKind, CtorParameter, Declaration, Decorator, FunctionDefinition, Import, ReflectionHost, TsHelperFn} from './host';
import {typeToValue} from './type_to_value';
/**
@ -125,11 +125,15 @@ export class TypeScriptReflectionHost implements ReflectionHost {
return this.getDeclarationOfSymbol(symbol);
}
getDefinitionOfFunction<T extends ts.FunctionDeclaration|ts.MethodDeclaration|
ts.FunctionExpression>(node: T): FunctionDefinition<T> {
getDefinitionOfFunction(node: ts.Node): FunctionDefinition|null {
if (!ts.isFunctionDeclaration(node) && !ts.isMethodDeclaration(node) &&
!ts.isFunctionExpression(node)) {
return null;
}
return {
node,
body: node.body !== undefined ? Array.from(node.body.statements) : null,
helper: null,
parameters: node.parameters.map(param => {
const name = parameterName(param.name);
const initializer = param.initializer || null;