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:
@ -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.
|
||||
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user