Skip to content
43 changes: 43 additions & 0 deletions packages/compiler-cli/src/ngtsc/typecheck/src/expression.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
*/

import {
ArrowFunction,
AST,
AstVisitor,
ASTWithSource,
Expand Down Expand Up @@ -43,6 +44,7 @@ import ts from 'typescript';
import {TypeCheckingConfig} from '../api';
import {addParseSpanInfo, wrapForDiagnostics, wrapForTypeChecker} from './diagnostics';
import {tsCastToAny, tsNumericExpression} from './ts_util';
import {markIgnoreDiagnostics} from './comments';

/**
* Gets an expression that is cast to any. Currently represented as `0 as any`.
Expand Down Expand Up @@ -493,6 +495,44 @@ class AstTranslator implements AstVisitor {
return node;
}

visitArrowFunction(ast: ArrowFunction): ts.ArrowFunction {
const params = ast.parameters.map((param) => {
const paramNode = ts.factory.createParameterDeclaration(undefined, undefined, param.name);
// Ignore diagnostics on the node to skip diagnostics from `noImplicitAny` since
// users aren't able to set types on the parameters. Note that this is preferable
// to setting their types to `any`, because it allows us to infer the types when
// the arrow function is passed as a callback.
markIgnoreDiagnostics(paramNode);
return paramNode;
});

const body = astToTypescript(
ast.body,
(innerAst) => {
if (
!(innerAst instanceof PropertyRead) ||
innerAst.receiver instanceof ThisReceiver ||
!(innerAst.receiver instanceof ImplicitReceiver)
) {
return this.maybeResolve(innerAst);
}

const correspondingParam = ast.parameters.find((arg) => arg.name === innerAst.name);

if (correspondingParam) {
const node = ts.factory.createIdentifier(innerAst.name);
addParseSpanInfo(node, innerAst.sourceSpan);
return node;
}

return this.maybeResolve(innerAst);
},
this.config,
);

return ts.factory.createArrowFunction(undefined, undefined, params, undefined, undefined, body);
}

private convertToSafeCall(
ast: Call | SafeCall,
expr: ts.Expression,
Expand Down Expand Up @@ -630,4 +670,7 @@ class VeSafeLhsInferenceBugDetector implements AstVisitor {
visitSpreadElement(ast: SpreadElement) {
return ast.expression.visit(this);
}
visitArrowFunction(ast: ArrowFunction, context: any) {
return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2267,6 +2267,58 @@ describe('type check blocks', () => {
});
});

describe('arrow functions', () => {
it('should generate an arrow function that uses only local parameters', () => {
const TEMPLATE = `<button (click)="withCallback((a, b) => a + b)"></button>`;
expect(tcb(TEMPLATE)).toContain('(this).withCallback((a, b) => (a) + (b));');
});

it('should generate an arrow function that references class members', () => {
const TEMPLATE = `<button (click)="withCallback((a) => componentFn(a + componentProp))"></button>`;
expect(tcb(TEMPLATE)).toContain(
'(this).withCallback(a => (this).componentFn((a) + (((this).componentProp))));',
);
});

it('should generate an arrow function that accesses higher-level parameters', () => {
expect(tcb(`{{() => () => (a) => () => () => a + 1}}`)).toContain(
'(() => () => a => () => () => (a) + (1)',
);
});

it('should generate an arrow function that accesses a class member with the same name as a parameter through `this`', () => {
const TEMPLATE = `<button (click)="withCallback((a) => a + this.a)"></button>`;
expect(tcb(TEMPLATE)).toContain('(this).withCallback(a => (a) + (((this).a)));');
});

it('should generate an arrow function that accesses a class member with the same name as a higher-level parameter through `this`', () => {
const TEMPLATE = `<button (click)="withCallback(a => b => () => a + b + this.a + this.b + someOtherProp)"></button>`;
expect(tcb(TEMPLATE)).toContain(
'(this).withCallback(a => b => () => ((((a) + (b)) + (((this).a))) + (((this).b))) + (((this).someOtherProp)));',
);
});

it('should generate an arrow function that references local symbols', () => {
const TEMPLATE = `
@let one = 1;
<input #someInput/>
<button (click)="withCallback(() => one + someInput.value)"></button>
`;

const result = tcb(TEMPLATE);
expect(result).toContain('const _t1 = (1);');
// _t2 is the button which we don't care about in this test.
expect(result).toContain('var _t3 = _t4;');
expect(result).toContain('var _t4 = document.createElement("input");');
expect(tcb(TEMPLATE)).toContain('(this).withCallback(() => (_t1) + (((_t3).value)));');
});

it('should ignore diagnostics on arrow function parameters', () => {
const result = tcb(`{{(a, b) => a + b}}`, undefined, undefined, {emitSpans: true});
expect(result).toContain('((a /*D:ignore*/, b /*D:ignore*/) =>');
});
});

describe('import generation', () => {
const TEMPLATE = `<div dir [test]="null"></div>`;
const DIRECTIVE: TestDeclaration = {
Expand Down
Loading