Skip to content

Commit a789ff0

Browse files
crisbetokirjs
authored andcommitted
refactor(compiler-cli): account for spread assignments in linker
Updates the linker to account for spread assignments in object literals.
1 parent 05ce944 commit a789ff0

10 files changed

Lines changed: 106 additions & 43 deletions

File tree

packages/compiler-cli/linker/babel/src/ast/babel_ast_factory.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,10 @@ export class BabelAstFactory implements AstFactory<t.Statement, t.Expression> {
163163
createObjectLiteral(properties: ObjectLiteralProperty<t.Expression>[]): t.Expression {
164164
return t.objectExpression(
165165
properties.map((prop) => {
166+
if (prop.kind === 'spread') {
167+
return t.spreadElement(prop.expression);
168+
}
169+
166170
const key = prop.quoted
167171
? t.stringLiteral(prop.propertyName)
168172
: t.identifier(prop.propertyName);

packages/compiler-cli/linker/babel/test/ast/babel_ast_factory_spec.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -288,11 +288,15 @@ describe('BabelAstFactory', () => {
288288
it('should create an object literal node, with the given properties', () => {
289289
const prop1 = expression.ast`42`;
290290
const prop2 = expression.ast`"moo"`;
291+
const prop3 = expression.ast`foo`;
291292
const obj = factory.createObjectLiteral([
292-
{propertyName: 'prop1', value: prop1, quoted: false},
293-
{propertyName: 'prop2', value: prop2, quoted: true},
293+
{propertyName: 'prop1', value: prop1, kind: 'property', quoted: false},
294+
{propertyName: 'prop2', value: prop2, kind: 'property', quoted: true},
295+
{expression: prop3, kind: 'spread'},
294296
]);
295-
expect(generate(obj).code).toEqual(['{', ' prop1: 42,', ' "prop2": "moo"', '}'].join('\n'));
297+
expect(generate(obj).code).toEqual(
298+
['{', ' prop1: 42,', ' "prop2": "moo",', ' ...foo', '}'].join('\n'),
299+
);
296300
});
297301
});
298302

packages/compiler-cli/linker/test/ast/ast_value_spec.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,20 +25,20 @@ interface TestObject {
2525
const host: AstHost<ts.Expression> = new TypeScriptAstHost();
2626
const factory = new TypeScriptAstFactory(/* annotateForClosureCompiler */ false);
2727
const nestedObj = factory.createObjectLiteral([
28-
{propertyName: 'x', quoted: false, value: factory.createLiteral(42)},
29-
{propertyName: 'y', quoted: false, value: factory.createLiteral('X')},
28+
{propertyName: 'x', kind: 'property', quoted: false, value: factory.createLiteral(42)},
29+
{propertyName: 'y', kind: 'property', quoted: false, value: factory.createLiteral('X')},
3030
]);
3131
const nestedArray = factory.createArrayLiteral([
3232
factory.createLiteral(1),
3333
factory.createLiteral(2),
3434
]);
3535
const obj = AstObject.parse<TestObject, ts.Expression>(
3636
factory.createObjectLiteral([
37-
{propertyName: 'a', quoted: false, value: factory.createLiteral(42)},
38-
{propertyName: 'b', quoted: false, value: factory.createLiteral('X')},
39-
{propertyName: 'c', quoted: false, value: factory.createLiteral(true)},
40-
{propertyName: 'd', quoted: false, value: nestedObj},
41-
{propertyName: 'e', quoted: false, value: nestedArray},
37+
{propertyName: 'a', kind: 'property', quoted: false, value: factory.createLiteral(42)},
38+
{propertyName: 'b', kind: 'property', quoted: false, value: factory.createLiteral('X')},
39+
{propertyName: 'c', kind: 'property', quoted: false, value: factory.createLiteral(true)},
40+
{propertyName: 'd', kind: 'property', quoted: false, value: nestedObj},
41+
{propertyName: 'e', kind: 'property', quoted: false, value: nestedArray},
4242
]),
4343
host,
4444
);

packages/compiler-cli/linker/test/file_linker/file_linker_spec.ts

Lines changed: 33 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,9 @@ describe('FileLinker', () => {
4444
const version = factory.createLiteral('0.0.0-PLACEHOLDER');
4545
const ngImport = factory.createIdentifier('core');
4646
const declarationArg = factory.createObjectLiteral([
47-
{propertyName: 'minVersion', quoted: false, value: version},
48-
{propertyName: 'version', quoted: false, value: version},
49-
{propertyName: 'ngImport', quoted: false, value: ngImport},
47+
{propertyName: 'minVersion', quoted: false, kind: 'property', value: version},
48+
{propertyName: 'version', quoted: false, kind: 'property', value: version},
49+
{propertyName: 'ngImport', quoted: false, kind: 'property', value: ngImport},
5050
]);
5151
expect(() =>
5252
fileLinker.linkPartialDeclaration('foo', [declarationArg], new MockDeclarationScope()),
@@ -58,8 +58,8 @@ describe('FileLinker', () => {
5858
const version = factory.createLiteral('0.0.0-PLACEHOLDER');
5959
const ngImport = factory.createIdentifier('core');
6060
const declarationArg = factory.createObjectLiteral([
61-
{propertyName: 'version', quoted: false, value: version},
62-
{propertyName: 'ngImport', quoted: false, value: ngImport},
61+
{propertyName: 'version', quoted: false, kind: 'property', value: version},
62+
{propertyName: 'ngImport', quoted: false, kind: 'property', value: ngImport},
6363
]);
6464
expect(() =>
6565
fileLinker.linkPartialDeclaration(
@@ -75,8 +75,8 @@ describe('FileLinker', () => {
7575
const version = factory.createLiteral('0.0.0-PLACEHOLDER');
7676
const ngImport = factory.createIdentifier('core');
7777
const declarationArg = factory.createObjectLiteral([
78-
{propertyName: 'minVersion', quoted: false, value: version},
79-
{propertyName: 'ngImport', quoted: false, value: ngImport},
78+
{propertyName: 'minVersion', quoted: false, kind: 'property', value: version},
79+
{propertyName: 'ngImport', quoted: false, kind: 'property', value: ngImport},
8080
]);
8181
expect(() =>
8282
fileLinker.linkPartialDeclaration(
@@ -91,8 +91,8 @@ describe('FileLinker', () => {
9191
const {fileLinker} = createFileLinker();
9292
const version = factory.createLiteral('0.0.0-PLACEHOLDER');
9393
const declarationArg = factory.createObjectLiteral([
94-
{propertyName: 'minVersion', quoted: false, value: version},
95-
{propertyName: 'version', quoted: false, value: version},
94+
{propertyName: 'minVersion', quoted: false, kind: 'property', value: version},
95+
{propertyName: 'version', quoted: false, kind: 'property', value: version},
9696
]);
9797
expect(() =>
9898
fileLinker.linkPartialDeclaration(
@@ -116,9 +116,9 @@ describe('FileLinker', () => {
116116
const ngImport = factory.createIdentifier('core');
117117
const version = factory.createLiteral('0.0.0-PLACEHOLDER');
118118
const declarationArg = factory.createObjectLiteral([
119-
{propertyName: 'ngImport', quoted: false, value: ngImport},
120-
{propertyName: 'minVersion', quoted: false, value: version},
121-
{propertyName: 'version', quoted: false, value: version},
119+
{propertyName: 'ngImport', quoted: false, kind: 'property', value: ngImport},
120+
{propertyName: 'minVersion', quoted: false, kind: 'property', value: version},
121+
{propertyName: 'version', quoted: false, kind: 'property', value: version},
122122
]);
123123

124124
const compilationResult = fileLinker.linkPartialDeclaration(
@@ -187,13 +187,24 @@ describe('FileLinker', () => {
187187
// Here we use the `core` identifier for `ngImport` to trigger the use of a shared scope for
188188
// constant statements.
189189
const declarationArg = factory.createObjectLiteral([
190-
{propertyName: 'ngImport', quoted: false, value: factory.createIdentifier('core')},
190+
{
191+
propertyName: 'ngImport',
192+
quoted: false,
193+
kind: 'property',
194+
value: factory.createIdentifier('core'),
195+
},
191196
{
192197
propertyName: 'minVersion',
193198
quoted: false,
199+
kind: 'property',
200+
value: factory.createLiteral('0.0.0-PLACEHOLDER'),
201+
},
202+
{
203+
propertyName: 'version',
204+
quoted: false,
205+
kind: 'property',
194206
value: factory.createLiteral('0.0.0-PLACEHOLDER'),
195207
},
196-
{propertyName: 'version', quoted: false, value: factory.createLiteral('0.0.0-PLACEHOLDER')},
197208
]);
198209

199210
const replacement = fileLinker.linkPartialDeclaration(
@@ -217,15 +228,22 @@ describe('FileLinker', () => {
217228
// Here we use a string literal `"not-a-module"` for `ngImport` to cause constant
218229
// statements to be emitted in an IIFE rather than added to the shared constant scope.
219230
const declarationArg = factory.createObjectLiteral([
220-
{propertyName: 'ngImport', quoted: false, value: factory.createLiteral('not-a-module')},
231+
{
232+
propertyName: 'ngImport',
233+
quoted: false,
234+
kind: 'property',
235+
value: factory.createLiteral('not-a-module'),
236+
},
221237
{
222238
propertyName: 'minVersion',
223239
quoted: false,
240+
kind: 'property',
224241
value: factory.createLiteral('0.0.0-PLACEHOLDER'),
225242
},
226243
{
227244
propertyName: 'version',
228245
quoted: false,
246+
kind: 'property',
229247
value: factory.createLiteral('0.0.0-PLACEHOLDER'),
230248
},
231249
]);

packages/compiler-cli/src/ngtsc/translator/src/api/ast_factory.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -361,9 +361,11 @@ export interface SourceMapRange {
361361
}
362362

363363
/**
364-
* Information used by the `AstFactory` to create a property on an object literal expression.
364+
* Information used by the `AstFactory` to create a property assignment
365+
* on an object literal expression.
365366
*/
366-
export interface ObjectLiteralProperty<TExpression> {
367+
export interface ObjectLiteralAssignment<TExpression> {
368+
kind: 'property';
367369
propertyName: string;
368370
value: TExpression;
369371
/**
@@ -372,6 +374,19 @@ export interface ObjectLiteralProperty<TExpression> {
372374
quoted: boolean;
373375
}
374376

377+
/**
378+
* Information used by the `AstFactory` to create a spread on an object literal expression.
379+
*/
380+
export interface ObjectLiteralSpread<TExpression> {
381+
kind: 'spread';
382+
expression: TExpression;
383+
}
384+
385+
/** Possible properties in an object literal. */
386+
export type ObjectLiteralProperty<TExpression> =
387+
| ObjectLiteralAssignment<TExpression>
388+
| ObjectLiteralSpread<TExpression>;
389+
375390
/**
376391
* Information used by the `AstFactory` to create a template literal string (i.e. a back-ticked
377392
* string with interpolations).

packages/compiler-cli/src/ngtsc/translator/src/translator.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ import * as o from '@angular/compiler';
1010
import {
1111
AstFactory,
1212
BinaryOperator,
13+
ObjectLiteralAssignment,
1314
ObjectLiteralProperty,
15+
ObjectLiteralSpread,
1416
SourceMapRange,
1517
TemplateElement,
1618
TemplateLiteral,
@@ -390,11 +392,17 @@ export class ExpressionTranslatorVisitor<TFile, TStatement, TExpression>
390392

391393
visitLiteralMapExpr(ast: o.LiteralMapExpr, context: Context): TExpression {
392394
const properties: ObjectLiteralProperty<TExpression>[] = ast.entries.map((entry) => {
393-
return {
394-
propertyName: entry.key,
395-
quoted: entry.quoted,
396-
value: entry.value.visitExpression(this, context),
397-
};
395+
return entry instanceof o.LiteralMapPropertyAssignment
396+
? ({
397+
kind: 'property',
398+
propertyName: entry.key,
399+
quoted: entry.quoted,
400+
value: entry.value.visitExpression(this, context),
401+
} satisfies ObjectLiteralAssignment<TExpression>)
402+
: ({
403+
kind: 'spread',
404+
expression: entry.expression.visitExpression(this, context),
405+
} satisfies ObjectLiteralSpread<TExpression>);
398406
});
399407
return this.setSourceMapRange(this.factory.createObjectLiteral(properties), ast.sourceSpan);
400408
}

packages/compiler-cli/src/ngtsc/translator/src/type_translator.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,10 @@ class TypeTranslatorVisitor implements o.ExpressionVisitor, o.TypeVisitor {
228228

229229
visitLiteralMapExpr(ast: o.LiteralMapExpr, context: Context): ts.TypeLiteralNode {
230230
const entries = ast.entries.map((entry) => {
231+
if (entry instanceof o.LiteralMapSpreadAssignment) {
232+
throw new Error('Spread is not supported in this context');
233+
}
234+
231235
const {key, quoted} = entry;
232236
const type = this.translateExpression(entry.value, context);
233237
return ts.factory.createPropertySignature(

packages/compiler-cli/src/ngtsc/translator/src/typescript_ast_factory.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -243,14 +243,18 @@ export class TypeScriptAstFactory implements AstFactory<ts.Statement, ts.Express
243243

244244
createObjectLiteral(properties: ObjectLiteralProperty<ts.Expression>[]): ts.Expression {
245245
return ts.factory.createObjectLiteralExpression(
246-
properties.map((prop) =>
247-
ts.factory.createPropertyAssignment(
246+
properties.map((prop) => {
247+
if (prop.kind === 'spread') {
248+
return ts.factory.createSpreadAssignment(prop.expression);
249+
}
250+
251+
return ts.factory.createPropertyAssignment(
248252
prop.quoted
249253
? ts.factory.createStringLiteral(prop.propertyName)
250254
: ts.factory.createIdentifier(prop.propertyName),
251255
prop.value,
252-
),
253-
),
256+
);
257+
}),
254258
);
255259
}
256260

packages/compiler-cli/src/ngtsc/translator/test/typescript_ast_factory_spec.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -330,14 +330,15 @@ describe('TypeScriptAstFactory', () => {
330330
describe('createObjectLiteral()', () => {
331331
it('should create an object literal node, with the given properties', () => {
332332
const {
333-
items: [prop1, prop2],
333+
items: [prop1, prop2, prop3],
334334
generate,
335-
} = setupExpressions('42', '"moo"');
335+
} = setupExpressions('42', '"moo"', 'foo');
336336
const obj = factory.createObjectLiteral([
337-
{propertyName: 'prop1', value: prop1, quoted: false},
338-
{propertyName: 'prop2', value: prop2, quoted: true},
337+
{propertyName: 'prop1', value: prop1, kind: 'property', quoted: false},
338+
{propertyName: 'prop2', value: prop2, kind: 'property', quoted: true},
339+
{expression: prop3, kind: 'spread'},
339340
]);
340-
expect(generate(obj)).toEqual('{ prop1: 42, "prop2": "moo" }');
341+
expect(generate(obj)).toEqual('{ prop1: 42, "prop2": "moo", ...foo }');
341342
});
342343
});
343344

packages/compiler-cli/src/ngtsc/typecheck/src/expression.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -239,9 +239,14 @@ class AstTranslator implements AstVisitor {
239239
}
240240

241241
visitLiteralMap(ast: LiteralMap): ts.Expression {
242-
const properties = ast.keys.map(({key}, idx) => {
242+
const properties = ast.keys.map((key, idx) => {
243243
const value = this.translate(ast.values[idx]);
244-
return ts.factory.createPropertyAssignment(ts.factory.createStringLiteral(key), value);
244+
245+
if (key.kind === 'property') {
246+
return ts.factory.createPropertyAssignment(ts.factory.createStringLiteral(key.key), value);
247+
} else {
248+
return ts.factory.createSpreadAssignment(value);
249+
}
245250
});
246251
const literal = ts.factory.createObjectLiteralExpression(properties, true);
247252
// If strictLiteralTypes is disabled, object literals are cast to `any`.

0 commit comments

Comments
 (0)