Skip to content

Commit fe12d0d

Browse files
petebacondarwinatscott
authored andcommitted
fix(ngcc): render adjacent statements after static properties (angular#33630)
See angular#33337 (comment) Fixes FW-1664 PR Close angular#33630
1 parent 7b87392 commit fe12d0d

File tree

11 files changed

+397
-61
lines changed

11 files changed

+397
-61
lines changed

integration/ngcc/test.sh

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,10 @@ assertSucceeded "Expected 'ngcc' to log 'Compiling'."
7878
grep "ApplicationModule.ɵmod = ɵngcc0.ɵɵdefineNgModule" node_modules/@angular/core/esm5/src/application_module.js
7979
assertSucceeded "Expected 'ngcc' to correctly compile 'ApplicationModule' in '@angular/core' (esm5)."
8080

81+
# Did it place the `setClassMetadata` call correctly?
82+
cat node_modules/@angular/core/fesm2015/core.js | awk 'ORS=" "' | grep "ApplicationRef.ctorParameters.*setClassMetadata(ApplicationRef"
83+
assertSucceeded "Expected 'ngcc' to place 'setClassMetadata' after static properties like 'ctorParameters' in '@angular/core' (fesm2015)."
84+
8185

8286
# Did it transform @angular/core typing files correctly?
8387
grep "import [*] as ɵngcc0 from './src/r3_symbols';" node_modules/@angular/core/core.d.ts

packages/compiler-cli/ngcc/src/rendering/esm5_rendering_formatter.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,25 @@ export class Esm5RenderingFormatter extends EsmRenderingFormatter {
3535
const insertionPoint = returnStatement.getFullStart();
3636
output.appendLeft(insertionPoint, '\n' + definitions);
3737
}
38+
39+
/**
40+
* Add the adjacent statements inside the IIFE of each decorated class
41+
*/
42+
addAdjacentStatements(output: MagicString, compiledClass: CompiledClass, statements: string):
43+
void {
44+
const iifeBody = getIifeBody(compiledClass.declaration);
45+
if (!iifeBody) {
46+
throw new Error(
47+
`Compiled class declaration is not inside an IIFE: ${compiledClass.name} in ${compiledClass.declaration.getSourceFile().fileName}`);
48+
}
49+
50+
const returnStatement = iifeBody.statements.find(ts.isReturnStatement);
51+
if (!returnStatement) {
52+
throw new Error(
53+
`Compiled class wrapper IIFE does not have a return statement: ${compiledClass.name} in ${compiledClass.declaration.getSourceFile().fileName}`);
54+
}
55+
56+
const insertionPoint = returnStatement.getFullStart();
57+
output.appendLeft(insertionPoint, '\n' + statements);
58+
}
3859
}

packages/compiler-cli/ngcc/src/rendering/esm_rendering_formatter.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,30 @@ export class EsmRenderingFormatter implements RenderingFormatter {
100100
output.appendLeft(insertionPoint, '\n' + definitions);
101101
}
102102

103+
/**
104+
* Add the adjacent statements after all static properties of the class.
105+
*/
106+
addAdjacentStatements(output: MagicString, compiledClass: CompiledClass, statements: string):
107+
void {
108+
const classSymbol = this.host.getClassSymbol(compiledClass.declaration);
109+
if (!classSymbol) {
110+
throw new Error(`Compiled class does not have a valid symbol: ${compiledClass.name}`);
111+
}
112+
113+
let insertionPoint = classSymbol.declaration.valueDeclaration.getEnd();
114+
115+
// If there are static members on this class then insert after the last one
116+
if (classSymbol.declaration.exports !== undefined) {
117+
classSymbol.declaration.exports.forEach(exportSymbol => {
118+
const exportStatement = getContainingStatement(exportSymbol);
119+
if (exportStatement !== null) {
120+
insertionPoint = Math.max(insertionPoint, exportStatement.getEnd());
121+
}
122+
});
123+
}
124+
output.appendLeft(insertionPoint, '\n' + statements);
125+
}
126+
103127
/**
104128
* Remove static decorator properties from classes.
105129
*/
@@ -244,3 +268,21 @@ function getNextSiblingInArray<T extends ts.Node>(node: T, array: ts.NodeArray<T
244268
const index = array.indexOf(node);
245269
return index !== -1 && array.length > index + 1 ? array[index + 1] : null;
246270
}
271+
272+
/**
273+
* Find the statement that contains the given class member
274+
* @param symbol the symbol of a static member of a class
275+
*/
276+
function getContainingStatement(symbol: ts.Symbol): ts.ExpressionStatement|null {
277+
if (symbol.valueDeclaration === undefined) {
278+
return null;
279+
}
280+
let node: ts.Node|null = symbol.valueDeclaration;
281+
while (node) {
282+
if (ts.isExpressionStatement(node)) {
283+
break;
284+
}
285+
node = node.parent;
286+
}
287+
return node || null;
288+
}

packages/compiler-cli/ngcc/src/rendering/renderer.ts

Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,10 @@ export class Renderer {
8686
this.renderDefinitions(compiledFile.sourceFile, clazz, importManager);
8787
this.srcFormatter.addDefinitions(outputText, clazz, renderedDefinition);
8888

89+
const renderedStatements =
90+
this.renderAdjacentStatements(compiledFile.sourceFile, clazz, importManager);
91+
this.srcFormatter.addAdjacentStatements(outputText, clazz, renderedStatements);
92+
8993
if (!isEntryPoint && clazz.reexports.length > 0) {
9094
this.srcFormatter.addDirectExports(
9195
outputText, clazz.reexports, importManager, compiledFile.sourceFile);
@@ -147,26 +151,45 @@ export class Renderer {
147151
}
148152

149153
/**
150-
* Render the definitions as source code for the given class.
151-
* @param sourceFile The file containing the class to process.
152-
* @param clazz The class whose definitions are to be rendered.
153-
* @param compilation The results of analyzing the class - this is used to generate the rendered
154-
* definitions.
155-
* @param imports An object that tracks the imports that are needed by the rendered definitions.
156-
*/
154+
* Render the definitions as source code for the given class.
155+
* @param sourceFile The file containing the class to process.
156+
* @param clazz The class whose definitions are to be rendered.
157+
* @param compilation The results of analyzing the class - this is used to generate the rendered
158+
* definitions.
159+
* @param imports An object that tracks the imports that are needed by the rendered definitions.
160+
*/
157161
private renderDefinitions(
158162
sourceFile: ts.SourceFile, compiledClass: CompiledClass, imports: ImportManager): string {
159-
const printer = createPrinter();
160163
const name = this.host.getInternalNameOfClass(compiledClass.declaration);
161-
const translate = (stmt: Statement) =>
162-
translateStatement(stmt, imports, NOOP_DEFAULT_IMPORT_RECORDER);
163-
const print = (stmt: Statement) =>
164-
printer.printNode(ts.EmitHint.Unspecified, translate(stmt), sourceFile);
165164
const statements: Statement[] = compiledClass.compilation.map(
166165
c => { return createAssignmentStatement(name, c.name, c.initializer); });
166+
return this.renderStatements(sourceFile, statements, imports);
167+
}
168+
169+
/**
170+
* Render the adjacent statements as source code for the given class.
171+
* @param sourceFile The file containing the class to process.
172+
* @param clazz The class whose statements are to be rendered.
173+
* @param compilation The results of analyzing the class - this is used to generate the rendered
174+
* definitions.
175+
* @param imports An object that tracks the imports that are needed by the rendered definitions.
176+
*/
177+
private renderAdjacentStatements(
178+
sourceFile: ts.SourceFile, compiledClass: CompiledClass, imports: ImportManager): string {
179+
const statements: Statement[] = [];
167180
for (const c of compiledClass.compilation) {
168181
statements.push(...c.statements);
169182
}
183+
return this.renderStatements(sourceFile, statements, imports);
184+
}
185+
186+
private renderStatements(
187+
sourceFile: ts.SourceFile, statements: Statement[], imports: ImportManager): string {
188+
const printer = createPrinter();
189+
const translate = (stmt: Statement) =>
190+
translateStatement(stmt, imports, NOOP_DEFAULT_IMPORT_RECORDER);
191+
const print = (stmt: Statement) =>
192+
printer.printNode(ts.EmitHint.Unspecified, translate(stmt), sourceFile);
170193
return statements.map(print).join('\n');
171194
}
172195
}

packages/compiler-cli/ngcc/src/rendering/rendering_formatter.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ export interface RenderingFormatter {
3636
output: MagicString, exports: Reexport[], importManager: ImportManager,
3737
file: ts.SourceFile): void;
3838
addDefinitions(output: MagicString, compiledClass: CompiledClass, definitions: string): void;
39+
addAdjacentStatements(output: MagicString, compiledClass: CompiledClass, statements: string):
40+
void;
3941
removeDecorators(output: MagicString, decoratorsToRemove: RedundantDecoratorMap): void;
4042
rewriteSwitchableDeclarations(
4143
outputText: MagicString, sourceFile: ts.SourceFile,

packages/compiler-cli/ngcc/test/rendering/commonjs_rendering_formatter_spec.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,55 @@ SOME DEFINITION TEXT
345345
});
346346
});
347347

348+
describe('addAdjacentStatements', () => {
349+
const contents = `var core = require('@angular/core');\n` +
350+
`var SomeDirective = /** @class **/ (function () {\n` +
351+
` function SomeDirective(zone, cons) {}\n` +
352+
` SomeDirective.prototype.method = function() {}\n` +
353+
` SomeDirective.decorators = [\n` +
354+
` { type: core.Directive, args: [{ selector: '[a]' }] },\n` +
355+
` { type: OtherA }\n` +
356+
` ];\n` +
357+
` SomeDirective.ctorParameters = () => [\n` +
358+
` { type: core.NgZone },\n` +
359+
` { type: core.Console }\n` +
360+
` ];\n` +
361+
` return SomeDirective;\n` +
362+
`}());\n` +
363+
`export {SomeDirective};`;
364+
365+
it('should insert the statements after all the static methods of the class', () => {
366+
const program = {name: _('/node_modules/test-package/some/file.js'), contents};
367+
const {renderer, decorationAnalyses, sourceFile} = setup(program);
368+
const output = new MagicString(contents);
369+
const compiledClass = decorationAnalyses.get(sourceFile) !.compiledClasses.find(
370+
c => c.name === 'SomeDirective') !;
371+
renderer.addAdjacentStatements(output, compiledClass, 'SOME STATEMENTS');
372+
expect(output.toString())
373+
.toContain(
374+
` SomeDirective.ctorParameters = () => [\n` +
375+
` { type: core.NgZone },\n` +
376+
` { type: core.Console }\n` +
377+
` ];\n` +
378+
`SOME STATEMENTS\n` +
379+
` return SomeDirective;\n`);
380+
});
381+
382+
it('should insert the statements after any definitions', () => {
383+
const program = {name: _('/node_modules/test-package/some/file.js'), contents};
384+
const {renderer, decorationAnalyses, sourceFile} = setup(program);
385+
const output = new MagicString(contents);
386+
const compiledClass = decorationAnalyses.get(sourceFile) !.compiledClasses.find(
387+
c => c.name === 'SomeDirective') !;
388+
renderer.addDefinitions(output, compiledClass, 'SOME DEFINITIONS');
389+
renderer.addAdjacentStatements(output, compiledClass, 'SOME STATEMENTS');
390+
const definitionsPosition = output.toString().indexOf('SOME DEFINITIONS');
391+
const statementsPosition = output.toString().indexOf('SOME STATEMENTS');
392+
expect(definitionsPosition).not.toEqual(-1, 'definitions should exist');
393+
expect(statementsPosition).not.toEqual(-1, 'statements should exist');
394+
expect(statementsPosition).toBeGreaterThan(definitionsPosition);
395+
});
396+
});
348397

349398
describe('removeDecorators', () => {
350399

packages/compiler-cli/ngcc/test/rendering/dts_renderer_spec.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ class TestRenderingFormatter implements RenderingFormatter {
3939
addDefinitions(output: MagicString, compiledClass: CompiledClass, definitions: string) {
4040
output.prepend('\n// ADD DEFINITIONS\n');
4141
}
42+
addAdjacentStatements(output: MagicString, compiledClass: CompiledClass, statements: string) {
43+
output.prepend('\n// ADD ADJACENT STATEMENTS\n');
44+
}
4245
removeDecorators(output: MagicString, decoratorsToRemove: RedundantDecoratorMap) {
4346
output.prepend('\n// REMOVE DECORATORS\n');
4447
}
@@ -79,6 +82,7 @@ function createTestRenderer(
7982
spyOn(testFormatter, 'addExports').and.callThrough();
8083
spyOn(testFormatter, 'addImports').and.callThrough();
8184
spyOn(testFormatter, 'addDefinitions').and.callThrough();
85+
spyOn(testFormatter, 'addAdjacentStatements').and.callThrough();
8286
spyOn(testFormatter, 'addConstants').and.callThrough();
8387
spyOn(testFormatter, 'removeDecorators').and.callThrough();
8488
spyOn(testFormatter, 'rewriteSwitchableDeclarations').and.callThrough();

packages/compiler-cli/ngcc/test/rendering/esm5_rendering_formatter_spec.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,56 @@ SOME DEFINITION TEXT
334334
});
335335
});
336336

337+
describe('addAdjacentStatements', () => {
338+
const contents = `import {Directive, NgZone, Console} from '@angular/core';\n` +
339+
`var SomeDirective = /** @class **/ (function () {\n` +
340+
` function SomeDirective(zone, cons) {}\n` +
341+
` SomeDirective.prototype.method = function() {}\n` +
342+
` SomeDirective.decorators = [\n` +
343+
` { type: Directive, args: [{ selector: '[a]' }] },\n` +
344+
` { type: OtherA }\n` +
345+
` ];\n` +
346+
` SomeDirective.ctorParameters = () => [\n` +
347+
` { type: NgZone },\n` +
348+
` { type: Console }\n` +
349+
` ];\n` +
350+
` return SomeDirective;\n` +
351+
`}());\n` +
352+
`export {SomeDirective};`;
353+
354+
it('should insert the statements after all the static methods of the class', () => {
355+
const program = {name: _('/node_modules/test-package/some/file.js'), contents};
356+
const {renderer, decorationAnalyses, sourceFile} = setup(program);
357+
const output = new MagicString(contents);
358+
const compiledClass = decorationAnalyses.get(sourceFile) !.compiledClasses.find(
359+
c => c.name === 'SomeDirective') !;
360+
renderer.addAdjacentStatements(output, compiledClass, 'SOME STATEMENTS');
361+
expect(output.toString())
362+
.toContain(
363+
` SomeDirective.ctorParameters = () => [\n` +
364+
` { type: NgZone },\n` +
365+
` { type: Console }\n` +
366+
` ];\n` +
367+
`SOME STATEMENTS\n` +
368+
` return SomeDirective;\n`);
369+
});
370+
371+
it('should insert the statements after any definitions', () => {
372+
const program = {name: _('/node_modules/test-package/some/file.js'), contents};
373+
const {renderer, decorationAnalyses, sourceFile} = setup(program);
374+
const output = new MagicString(contents);
375+
const compiledClass = decorationAnalyses.get(sourceFile) !.compiledClasses.find(
376+
c => c.name === 'SomeDirective') !;
377+
renderer.addDefinitions(output, compiledClass, 'SOME DEFINITIONS');
378+
renderer.addAdjacentStatements(output, compiledClass, 'SOME STATEMENTS');
379+
const definitionsPosition = output.toString().indexOf('SOME DEFINITIONS');
380+
const statementsPosition = output.toString().indexOf('SOME STATEMENTS');
381+
expect(definitionsPosition).not.toEqual(-1, 'definitions should exist');
382+
expect(statementsPosition).not.toEqual(-1, 'statements should exist');
383+
expect(statementsPosition).toBeGreaterThan(definitionsPosition);
384+
});
385+
});
386+
337387

338388
describe('removeDecorators', () => {
339389

packages/compiler-cli/ngcc/test/rendering/esm_rendering_formatter_spec.ts

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -241,9 +241,54 @@ SOME DEFINITION TEXT
241241
A.decorators = [
242242
`);
243243
});
244-
245244
});
246245

246+
describe('addAdjacentStatements', () => {
247+
const contents = `import {Directive, NgZone, Console} from '@angular/core';\n` +
248+
`export class SomeDirective {\n` +
249+
` constructor(zone, cons) {}\n` +
250+
` method() {}\n` +
251+
`}\n` +
252+
`SomeDirective.decorators = [\n` +
253+
` { type: Directive, args: [{ selector: '[a]' }] },\n` +
254+
` { type: OtherA }\n` +
255+
`];\n` +
256+
`SomeDirective.ctorParameters = () => [\n` +
257+
` { type: NgZone },\n` +
258+
` { type: Console }\n` +
259+
`];`;
260+
261+
it('should insert the statements after all the static methods of the class', () => {
262+
const program = {name: _('/node_modules/test-package/some/file.js'), contents};
263+
const {renderer, decorationAnalyses, sourceFile} = setup([program]);
264+
const output = new MagicString(contents);
265+
const compiledClass = decorationAnalyses.get(sourceFile) !.compiledClasses.find(
266+
c => c.name === 'SomeDirective') !;
267+
renderer.addAdjacentStatements(output, compiledClass, 'SOME STATEMENTS');
268+
expect(output.toString())
269+
.toContain(
270+
`SomeDirective.ctorParameters = () => [\n` +
271+
` { type: NgZone },\n` +
272+
` { type: Console }\n` +
273+
`];\n` +
274+
`SOME STATEMENTS`);
275+
});
276+
277+
it('should insert the statements after any definitions', () => {
278+
const program = {name: _('/node_modules/test-package/some/file.js'), contents};
279+
const {renderer, decorationAnalyses, sourceFile} = setup([program]);
280+
const output = new MagicString(contents);
281+
const compiledClass = decorationAnalyses.get(sourceFile) !.compiledClasses.find(
282+
c => c.name === 'SomeDirective') !;
283+
renderer.addDefinitions(output, compiledClass, 'SOME DEFINITIONS');
284+
renderer.addAdjacentStatements(output, compiledClass, 'SOME STATEMENTS');
285+
const definitionsPosition = output.toString().indexOf('SOME DEFINITIONS');
286+
const statementsPosition = output.toString().indexOf('SOME STATEMENTS');
287+
expect(definitionsPosition).not.toEqual(-1, 'definitions should exist');
288+
expect(statementsPosition).not.toEqual(-1, 'statements should exist');
289+
expect(statementsPosition).toBeGreaterThan(definitionsPosition);
290+
});
291+
});
247292

248293
describe('removeDecorators', () => {
249294
describe('[static property declaration]', () => {

0 commit comments

Comments
 (0)