Skip to content

Commit 6e18fa8

Browse files
crisbetokirjs
authored andcommitted
feat(core): support spread elements in array literals
Expands the template syntax to support spread elements inside arrays. This can be handy for some bindings.
1 parent 19ca3b6 commit 6e18fa8

12 files changed

Lines changed: 197 additions & 24 deletions

File tree

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

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -513,6 +513,55 @@ export declare class MyModule {
513513
static ɵinj: i0.ɵɵInjectorDeclaration<MyModule>;
514514
}
515515

516+
/****************************************************************************************************
517+
* PARTIAL FILE: array_literal_spread.js
518+
****************************************************************************************************/
519+
import { Component } from '@angular/core';
520+
import * as i0 from "@angular/core";
521+
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: `
530+
@let simple = [...foo];
531+
@let otherEntries = [1, ...foo, 2];
532+
@let multipleSpreads = [...foo, 1, ...bar, ...baz, 2];
533+
@let inlineArraySpread = [1, ...[2, ...[3]]];
534+
535+
<!-- Use the arrays so they don't get flagged as unused. -->
536+
{{simple}} {{otherEntries}} {{multipleSpreads}} {{inlineArraySpread}}
537+
`, isInline: true });
538+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: ArrayComp, decorators: [{
539+
type: Component,
540+
args: [{
541+
template: `
542+
@let simple = [...foo];
543+
@let otherEntries = [1, ...foo, 2];
544+
@let multipleSpreads = [...foo, 1, ...bar, ...baz, 2];
545+
@let inlineArraySpread = [1, ...[2, ...[3]]];
546+
547+
<!-- Use the arrays so they don't get flagged as unused. -->
548+
{{simple}} {{otherEntries}} {{multipleSpreads}} {{inlineArraySpread}}
549+
`,
550+
}]
551+
}] });
552+
553+
/****************************************************************************************************
554+
* PARTIAL FILE: array_literal_spread.d.ts
555+
****************************************************************************************************/
556+
import * as i0 from "@angular/core";
557+
export declare class ArrayComp {
558+
foo: never[];
559+
bar: never[];
560+
baz: never[];
561+
static ɵfac: i0.ɵɵFactoryDeclaration<ArrayComp, never>;
562+
static ɵcmp: i0.ɵɵComponentDeclaration<ArrayComp, "ng-component", never, {}, {}, never, never, true, never>;
563+
}
564+
516565
/****************************************************************************************************
517566
* PARTIAL FILE: object_literals.js
518567
****************************************************************************************************/

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,16 @@
228228
}
229229
]
230230
},
231+
{
232+
"description": "should support spread elements in array literals",
233+
"inputFiles": ["array_literal_spread.ts"],
234+
"expectations": [
235+
{
236+
"failureMessage": "Invalid array emit",
237+
"files": ["array_literal_spread.js"]
238+
}
239+
]
240+
},
231241
{
232242
"description": "should support object literals",
233243
"inputFiles": ["object_literals.ts"],
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
const $c0$ = a0 => [...a0];
2+
const $c1$ = a0 => [1, ...a0, 2];
3+
const $c2$ = (a0, a1, a2) => [...a0, 1, ...a1, ...a2, 2];
4+
const $c3$ = () => [3];
5+
const $c4$ = a0 => [2, ...a0];
6+
const $c5$ = a0 => [1, ...a0];
7+
8+
9+
10+
$r3$.ɵɵdefineComponent({
11+
12+
decls: 1,
13+
vars: 17,
14+
template: function ArrayComp_Template(rf, ctx) {
15+
16+
if (rf & 2) {
17+
const simple_r1 = $r3$.ɵɵpureFunction1(4, $c0$, ctx.foo);
18+
const otherEntries_r2 = $r3$.ɵɵpureFunction1(6, $c1$, ctx.foo);
19+
const multipleSpreads_r3 = $r3$.ɵɵpureFunction3(8, $c2$, ctx.foo, ctx.bar, ctx.baz);
20+
const inlineArraySpread_r4 = $r3$.ɵɵpureFunction1(15, $c5$, $r3$.ɵɵpureFunction1(13, $c4$, $r3$.ɵɵpureFunction0(12, $c3$)));
21+
22+
}
23+
},
24+
25+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import {Component} from '@angular/core';
2+
3+
@Component({
4+
template: `
5+
@let simple = [...foo];
6+
@let otherEntries = [1, ...foo, 2];
7+
@let multipleSpreads = [...foo, 1, ...bar, ...baz, 2];
8+
@let inlineArraySpread = [1, ...[2, ...[3]]];
9+
10+
<!-- Use the arrays so they don't get flagged as unused. -->
11+
{{simple}} {{otherEntries}} {{multipleSpreads}} {{inlineArraySpread}}
12+
`,
13+
})
14+
export class ArrayComp {
15+
foo = [];
16+
bar = [];
17+
baz = [];
18+
}

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

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3365,6 +3365,31 @@ runInEachFileSystem(() => {
33653365
);
33663366
});
33673367

3368+
it('should type check array spread elements in templates', () => {
3369+
env.write(
3370+
'test.ts',
3371+
`
3372+
import {Component} from '@angular/core';
3373+
3374+
@Component({
3375+
selector: 'test',
3376+
template: '@let array = [1, ...foo]; {{checkArray(array)}}',
3377+
})
3378+
export class TestCmp {
3379+
foo = ['two'];
3380+
3381+
checkArray(arr: number[]) {}
3382+
}
3383+
`,
3384+
);
3385+
3386+
const diags = env.driveDiagnostics();
3387+
expect(diags.length).toEqual(1);
3388+
expect((diags[0].messageText as ts.DiagnosticMessageChain).messageText).toBe(
3389+
`Argument of type '(string | number)[]' is not assignable to parameter of type 'number[]'.`,
3390+
);
3391+
});
3392+
33683393
describe('template literals', () => {
33693394
it('should treat template literals as strings', () => {
33703395
env.write(

packages/compiler/src/constant_pool.ts

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,6 @@ import * as o from './output/output_ast';
1010

1111
const CONSTANT_PREFIX = '_c';
1212

13-
/**
14-
* `ConstantPool` tries to reuse literal factories when two or more literals are identical.
15-
* We determine whether literals are identical by creating a key out of their AST using the
16-
* `KeyVisitor`. This constant is used to replace dynamic expressions which can't be safely
17-
* converted into a key. E.g. given an expression `{foo: bar()}`, since we don't know what
18-
* the result of `bar` will be, we create a key that looks like `{foo: <unknown>}`. Note
19-
* that we use a variable, rather than something like `null` in order to avoid collisions.
20-
*/
21-
const UNKNOWN_VALUE_KEY = o.variable('<unknown>');
22-
2313
/**
2414
* Context to use when producing a key.
2515
*
@@ -277,6 +267,8 @@ export class GenericKeyFn implements ExpressionKeyFn {
277267
return `read(${expr.name})`;
278268
} else if (expr instanceof o.TypeofExpr) {
279269
return `typeof(${this.keyOf(expr.expr)})`;
270+
} else if (expr instanceof o.SpreadElementExpr) {
271+
return `...${this.keyOf(expr.expression)}`;
280272
} else {
281273
throw new Error(
282274
`${this.constructor.name} does not handle expressions of type ${expr.constructor.name}`,
@@ -285,10 +277,6 @@ export class GenericKeyFn implements ExpressionKeyFn {
285277
}
286278
}
287279

288-
function isVariable(e: o.Expression): e is o.ReadVarExpr {
289-
return e instanceof o.ReadVarExpr;
290-
}
291-
292280
function isLongStringLiteral(expr: o.Expression): boolean {
293281
return (
294282
expr instanceof o.LiteralExpr &&

packages/compiler/src/expression_parser/parser.ts

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ import {
4545
SafeCall,
4646
SafeKeyedRead,
4747
SafePropertyRead,
48+
SpreadElement,
4849
TaggedTemplateLiteral,
4950
TemplateBinding,
5051
TemplateBindingIdentifier,
@@ -1136,11 +1137,7 @@ class _ParseAST {
11361137
this.advance();
11371138
return new ThisReceiver(this.span(start), this.sourceSpan(start));
11381139
} else if (this.consumeOptionalCharacter(chars.$LBRACKET)) {
1139-
this.rbracketsExpected++;
1140-
const elements = this.parseExpressionList(chars.$RBRACKET);
1141-
this.rbracketsExpected--;
1142-
this.expectCharacter(chars.$RBRACKET);
1143-
return new LiteralArray(this.span(start), this.sourceSpan(start), elements);
1140+
return this.parseLiteralArray(start);
11441141
} else if (this.next.isCharacter(chars.$LBRACE)) {
11451142
return this.parseLiteralMap();
11461143
} else if (this.next.isIdentifier()) {
@@ -1175,17 +1172,28 @@ class _ParseAST {
11751172
}
11761173
}
11771174

1178-
private parseExpressionList(terminator: number): AST[] {
1179-
const result: AST[] = [];
1175+
private parseLiteralArray(arrayStart: number): LiteralArray {
1176+
this.rbracketsExpected++;
1177+
const elements: AST[] = [];
11801178

11811179
do {
1182-
if (!this.next.isCharacter(terminator)) {
1183-
result.push(this.parsePipe());
1180+
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));
1187+
} else if (!this.next.isCharacter(chars.$RBRACKET)) {
1188+
elements.push(this.parsePipe());
11841189
} else {
11851190
break;
11861191
}
11871192
} while (this.consumeOptionalCharacter(chars.$COMMA));
1188-
return result;
1193+
1194+
this.rbracketsExpected--;
1195+
this.expectCharacter(chars.$RBRACKET);
1196+
return new LiteralArray(this.span(arrayStart), this.sourceSpan(arrayStart), elements);
11891197
}
11901198

11911199
private parseLiteralMap(): LiteralMap {

packages/compiler/src/template/pipeline/ir/src/expression.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1319,6 +1319,8 @@ export function transformExpressionsInExpression(
13191319
}
13201320
} else if (expr instanceof o.ParenthesizedExpr) {
13211321
expr.expr = transformExpressionsInExpression(expr.expr, transform, flags);
1322+
} else if (expr instanceof o.SpreadElementExpr) {
1323+
expr.expression = transformExpressionsInExpression(expr.expression, transform, flags);
13221324
} else if (
13231325
expr instanceof o.ReadVarExpr ||
13241326
expr instanceof o.ExternalExpr ||

packages/compiler/src/template/pipeline/src/ingest.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1218,6 +1218,8 @@ function convertAst(
12181218
);
12191219
} else if (ast instanceof e.RegularExpressionLiteral) {
12201220
return new o.RegularExpressionLiteralExpr(ast.body, ast.flags, baseSourceSpan);
1221+
} else if (ast instanceof e.SpreadElement) {
1222+
return new o.SpreadElementExpr(convertAst(ast.expression, job, baseSourceSpan));
12211223
} else {
12221224
throw new Error(
12231225
`Unhandled expression type "${ast.constructor.name}" in file "${baseSourceSpan?.start.file.url}"`,

packages/compiler/src/template/pipeline/src/phases/pure_literal_structures.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,17 @@ function transformLiteralArray(expr: o.LiteralArrayExpr): o.Expression {
3838
const derivedEntries: o.Expression[] = [];
3939
const nonConstantArgs: o.Expression[] = [];
4040
for (const entry of expr.entries) {
41+
if (entry instanceof o.SpreadElementExpr) {
42+
if (entry.expression.isConstant()) {
43+
derivedEntries.push(entry);
44+
} else {
45+
const idx = nonConstantArgs.length;
46+
nonConstantArgs.push(entry.expression);
47+
derivedEntries.push(new o.SpreadElementExpr(new ir.PureFunctionParameterExpr(idx)));
48+
}
49+
continue;
50+
}
51+
4152
if (entry.isConstant()) {
4253
derivedEntries.push(entry);
4354
} else {

0 commit comments

Comments
 (0)