Skip to content

Commit a0dfa5f

Browse files
crisbetokirjs
authored andcommitted
feat(core): support rest arguments in function calls
Updates the template syntax to support rest arguments in function calls. This can be handy for functions with a variable number of arguments.
1 parent 6e18fa8 commit a0dfa5f

File tree

9 files changed

+205
-23
lines changed

9 files changed

+205
-23
lines changed

packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/components_and_directives/value_composition/GOLDEN_PARTIAL.js

Lines changed: 55 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -519,14 +519,11 @@ export declare class MyModule {
519519
import { Component } from '@angular/core';
520520
import * as i0 from "@angular/core";
521521
export class ArrayComp {
522-
constructor() {
523-
this.foo = [];
524-
this.bar = [];
525-
this.baz = [];
526-
}
527-
}
528-
ArrayComp.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: ArrayComp, deps: [], target: i0.ɵɵFactoryTarget.Component });
529-
ArrayComp.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: ArrayComp, isStandalone: true, selector: "ng-component", ngImport: i0, template: `
522+
foo = [];
523+
bar = [];
524+
baz = [];
525+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: ArrayComp, deps: [], target: i0.ɵɵFactoryTarget.Component });
526+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: ArrayComp, isStandalone: true, selector: "ng-component", ngImport: i0, template: `
530527
@let simple = [...foo];
531528
@let otherEntries = [1, ...foo, 2];
532529
@let multipleSpreads = [...foo, 1, ...bar, ...baz, 2];
@@ -535,6 +532,7 @@ ArrayComp.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "0.
535532
<!-- Use the arrays so they don't get flagged as unused. -->
536533
{{simple}} {{otherEntries}} {{multipleSpreads}} {{inlineArraySpread}}
537534
`, isInline: true });
535+
}
538536
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: ArrayComp, decorators: [{
539537
type: Component,
540538
args: [{
@@ -982,3 +980,52 @@ export declare class TestComp {
982980
static ɵcmp: i0.ɵɵComponentDeclaration<TestComp, "ng-component", never, {}, {}, never, never, true, never>;
983981
}
984982

983+
/****************************************************************************************************
984+
* PARTIAL FILE: call_rest.js
985+
****************************************************************************************************/
986+
import { Component } from '@angular/core';
987+
import * as i0 from "@angular/core";
988+
export class TestComp {
989+
foo = [];
990+
bar = [];
991+
baz = [];
992+
fn(..._) { }
993+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: TestComp, deps: [], target: i0.ɵɵFactoryTarget.Component });
994+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: TestComp, isStandalone: true, selector: "ng-component", ngImport: i0, template: `
995+
{{fn(...foo)}}
996+
<hr>
997+
{{fn(1, ...foo, 2)}}
998+
<hr>
999+
{{fn(...foo, 1, ...bar, ...baz, 2)}}
1000+
<hr>
1001+
{{fn(1, ...[2, ...[3]])}}
1002+
`, isInline: true });
1003+
}
1004+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: TestComp, decorators: [{
1005+
type: Component,
1006+
args: [{
1007+
template: `
1008+
{{fn(...foo)}}
1009+
<hr>
1010+
{{fn(1, ...foo, 2)}}
1011+
<hr>
1012+
{{fn(...foo, 1, ...bar, ...baz, 2)}}
1013+
<hr>
1014+
{{fn(1, ...[2, ...[3]])}}
1015+
`,
1016+
}]
1017+
}] });
1018+
1019+
/****************************************************************************************************
1020+
* PARTIAL FILE: call_rest.d.ts
1021+
****************************************************************************************************/
1022+
import * as i0 from "@angular/core";
1023+
export declare class TestComp {
1024+
foo: never[];
1025+
bar: never[];
1026+
baz: never[];
1027+
fn(..._: any[]): void;
1028+
static ɵfac: i0.ɵɵFactoryDeclaration<TestComp, never>;
1029+
static ɵcmp: i0.ɵɵComponentDeclaration<TestComp, "ng-component", never, {}, {}, never, never, true, never>;
1030+
}
1031+

packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/components_and_directives/value_composition/TEST_CASES.json

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -60,11 +60,7 @@
6060
]
6161
}
6262
],
63-
"compilationModeFilter": [
64-
"full compile",
65-
"local compile",
66-
"declaration-only emit"
67-
]
63+
"compilationModeFilter": ["full compile", "local compile", "declaration-only emit"]
6864
},
6965
{
7066
"description": "should support complex selectors",
@@ -320,6 +316,16 @@
320316
"files": ["regular_expression_with_global_flag.js"]
321317
}
322318
]
319+
},
320+
{
321+
"description": "should support rest arguments in a function call",
322+
"inputFiles": ["call_rest.ts"],
323+
"expectations": [
324+
{
325+
"failureMessage": "Invalid object literal binding",
326+
"files": ["call_rest.js"]
327+
}
328+
]
323329
}
324330
]
325331
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
const $c0$ = () => [3];
2+
const $c1$ = a0 => [2, ...a0];
3+
4+
5+
6+
$r3$.ɵɵdefineComponent({
7+
8+
decls: 7,
9+
vars: 7,
10+
template: function TestComp_Template(rf, ctx) {
11+
12+
if (rf & 2) {
13+
$r3$.ɵɵtextInterpolate1(" ", ctx.fn(...ctx.foo), " ");
14+
$r3$.ɵɵadvance(2);
15+
$r3$.ɵɵtextInterpolate1(" ", ctx.fn(1, ...ctx.foo, 2), " ");
16+
$r3$.ɵɵadvance(2);
17+
$r3$.ɵɵtextInterpolate1(" ", ctx.fn(...ctx.foo, 1, ...ctx.bar, ...ctx.baz, 2), " ");
18+
$r3$.ɵɵadvance(2);
19+
$r3$.ɵɵtextInterpolate1(" ", ctx.fn(1, ...$r3$.ɵɵpureFunction1(5, $c1$, $r3$.ɵɵpureFunction0(4, $c0$))), " ");
20+
}
21+
},
22+
23+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import {Component} from '@angular/core';
2+
3+
@Component({
4+
template: `
5+
{{fn(...foo)}}
6+
<hr>
7+
{{fn(1, ...foo, 2)}}
8+
<hr>
9+
{{fn(...foo, 1, ...bar, ...baz, 2)}}
10+
<hr>
11+
{{fn(1, ...[2, ...[3]])}}
12+
`,
13+
})
14+
export class TestComp {
15+
foo = [];
16+
bar = [];
17+
baz = [];
18+
fn(..._: any[]) {}
19+
}

packages/compiler-cli/test/ngtsc/template_typecheck_spec.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3390,6 +3390,30 @@ runInEachFileSystem(() => {
33903390
);
33913391
});
33923392

3393+
it('should type check rest arguments in a function call', () => {
3394+
env.write(
3395+
'test.ts',
3396+
`
3397+
import {Component} from '@angular/core';
3398+
3399+
@Component({
3400+
selector: 'test',
3401+
template: \`{{fn('one', ...rest)}}\`,
3402+
})
3403+
export class TestCmp {
3404+
rest = [2];
3405+
fn(first: string, ...rest: string[]) {}
3406+
}
3407+
`,
3408+
);
3409+
3410+
const diags = env.driveDiagnostics();
3411+
expect(diags.length).toEqual(1);
3412+
expect(diags[0].messageText).toBe(
3413+
`Argument of type 'number' is not assignable to parameter of type 'string'.`,
3414+
);
3415+
});
3416+
33933417
describe('template literals', () => {
33943418
it('should treat template literals as strings', () => {
33953419
env.write(

packages/compiler/src/expression_parser/parser.ts

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1178,12 +1178,7 @@ class _ParseAST {
11781178

11791179
do {
11801180
if (this.next.isOperator('...')) {
1181-
const spreadStart = this.inputIndex;
1182-
this.advance();
1183-
const expression = this.parsePipe();
1184-
const span = this.span(spreadStart);
1185-
const sourceSpan = this.sourceSpan(spreadStart);
1186-
elements.push(new SpreadElement(span, sourceSpan, expression));
1181+
elements.push(this.parseSpreadElement());
11871182
} else if (!this.next.isCharacter(chars.$RBRACKET)) {
11881183
elements.push(this.parsePipe());
11891184
} else {
@@ -1330,14 +1325,32 @@ class _ParseAST {
13301325
}
13311326

13321327
private parseCallArguments(): BindingPipe[] {
1333-
if (this.next.isCharacter(chars.$RPAREN)) return [];
1328+
if (this.next.isCharacter(chars.$RPAREN)) {
1329+
return [];
1330+
}
1331+
13341332
const positionals: AST[] = [];
1333+
13351334
do {
1336-
positionals.push(this.parsePipe());
1335+
positionals.push(this.next.isOperator('...') ? this.parseSpreadElement() : this.parsePipe());
13371336
} while (this.consumeOptionalCharacter(chars.$COMMA));
1337+
13381338
return positionals as BindingPipe[];
13391339
}
13401340

1341+
private parseSpreadElement(): SpreadElement {
1342+
if (!this.next.isOperator('...')) {
1343+
this.error("Spread element must start with '...' operator");
1344+
}
1345+
1346+
const spreadStart = this.inputIndex;
1347+
this.advance();
1348+
const expression = this.parsePipe();
1349+
const span = this.span(spreadStart);
1350+
const sourceSpan = this.sourceSpan(spreadStart);
1351+
return new SpreadElement(span, sourceSpan, expression);
1352+
}
1353+
13411354
/**
13421355
* Parses an identifier, a keyword, a string with an optional `-` in between,
13431356
* and returns the string along with its absolute source span.

packages/compiler/test/expression_parser/parser_spec.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,20 @@ describe('parser', () => {
317317
checkAction('fn?.().add?.(1, 2)');
318318
checkAction('fn?.()?.(1, 2)');
319319
});
320+
321+
it('should parse rest arguments in calls', () => {
322+
checkAction('fn(...foo)');
323+
checkAction('fn(1, ...foo, 2)');
324+
checkAction('fn(...foo, middle, ...bar)');
325+
checkAction('fn(a, ...b, ...[1, 2, 3])');
326+
});
327+
328+
it('should parse rest arguments in safe calls', () => {
329+
checkAction('fn?.(...foo)');
330+
checkAction('fn?.(1, ...foo, 2)');
331+
checkAction('fn?.(...foo, middle, ...bar)');
332+
checkAction('fn?.(a, ...b, ...[1, 2, 3])');
333+
});
320334
});
321335

322336
describe('keyed read', () => {
@@ -772,6 +786,21 @@ describe('parser', () => {
772786
['', ''],
773787
]);
774788
});
789+
790+
it('should record span for rest arguments in functions', () => {
791+
expect(unparseWithSpan(parseBinding('fn(1, ...foo)'))).toEqual([
792+
['fn(1, ...foo)', 'fn(1, ...foo)'],
793+
['fn(1, ...foo)', '[argumentSpan] 1, ...foo'],
794+
['fn', 'fn'],
795+
['fn', '[nameSpan] fn'],
796+
['', ''],
797+
['1', '1'],
798+
['...foo', '...foo'],
799+
['foo', 'foo'],
800+
['foo', '[nameSpan] foo'],
801+
['', ''],
802+
]);
803+
});
775804
});
776805

777806
describe('general error handling', () => {

packages/compiler/test/render3/r3_template_transform_spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2255,8 +2255,8 @@ describe('R3 template transform', () => {
22552255
});
22562256

22572257
it('should report syntax error in for loop expression', () => {
2258-
expect(() => parse(`@for (item of items..foo) {hello}`)).toThrowError(
2259-
/Unexpected token \./,
2258+
expect(() => parse(`@for (item of items#foo) {hello}`)).toThrowError(
2259+
/Unexpected token '#foo'/,
22602260
);
22612261
});
22622262

packages/core/test/acceptance/integration_spec.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2775,6 +2775,27 @@ describe('acceptance integration tests', () => {
27752775
expect(fixture.nativeElement.textContent).toContain('Hello, Bilbo Baggins');
27762776
});
27772777

2778+
it('should support calls with rest arguments in templates', () => {
2779+
@Component({
2780+
template: "{{fn('Hello', ...foo)}}",
2781+
})
2782+
class TestComponent {
2783+
foo = ['Frodo', 'Baggins'];
2784+
2785+
fn(prefix: string, ...args: string[]) {
2786+
return `${prefix}, ${args.join(' ')}`;
2787+
}
2788+
}
2789+
2790+
const fixture = TestBed.createComponent(TestComponent);
2791+
fixture.detectChanges();
2792+
expect(fixture.nativeElement.textContent).toContain('Hello, Frodo Baggins');
2793+
2794+
fixture.componentInstance.foo = ['J.', 'R.', 'R.', 'Tolkien'];
2795+
fixture.detectChanges();
2796+
expect(fixture.nativeElement.textContent).toContain('Hello, J. R. R. Tolkien');
2797+
});
2798+
27782799
it('should have correct operator precedence', () => {
27792800
@Component({
27802801
template: '{{1 + 10 ** -2 * 3}}',

0 commit comments

Comments
 (0)