fix(ngcc): remove `__decorator` calls even when part of the IIFE return statement (#33777)

Previously we only removed `__decorate()` calls that looked like:

```
SomeClass = __decorate(...);
```

But in some minified scenarios this call gets wrapped up with the
return statement of the IIFE.

```
return SomeClass = __decorate(...);
```

This is now removed also, leaving just the return statement:

```
return SomeClass;
```

PR Close #33777
This commit is contained in:
Pete Bacon Darwin 2019-11-13 08:36:13 +00:00 committed by Kara Erickson
parent 9c6ee5fcd0
commit d21471e24e
2 changed files with 116 additions and 54 deletions

View File

@ -17,6 +17,7 @@ import {ExportInfo} from '../analysis/private_declarations_analyzer';
import {RenderingFormatter, RedundantDecoratorMap} from './rendering_formatter';
import {stripExtension} from './utils';
import {Reexport} from '../../../src/ngtsc/imports';
import {isAssignment} from '../host/esm2015_host';
/**
* A RenderingFormatter that works with ECMAScript Module import and export statements.
@ -124,7 +125,19 @@ export class EsmRenderingFormatter implements RenderingFormatter {
// Remove the entire statement
const statement = findStatement(containerNode);
if (statement) {
output.remove(statement.getFullStart(), statement.getEnd());
if (ts.isExpressionStatement(statement)) {
// The statement looks like: `SomeClass = __decorate(...);`
// Remove it completely
output.remove(statement.getFullStart(), statement.getEnd());
} else if (
ts.isReturnStatement(statement) && statement.expression &&
isAssignment(statement.expression)) {
// The statement looks like: `return SomeClass = __decorate(...);`
// We only want to end up with: `return SomeClass;`
const startOfRemoval = statement.expression.left.getEnd();
const endOfRemoval = getEndExceptSemicolon(statement);
output.remove(startOfRemoval, endOfRemoval);
}
}
} else {
nodesToRemove.forEach(node => {
@ -239,7 +252,7 @@ export class EsmRenderingFormatter implements RenderingFormatter {
function findStatement(node: ts.Node) {
while (node) {
if (ts.isExpressionStatement(node)) {
if (ts.isExpressionStatement(node) || ts.isReturnStatement(node)) {
return node;
}
node = node.parent;
@ -257,3 +270,9 @@ function getNextSiblingInArray<T extends ts.Node>(node: T, array: ts.NodeArray<T
const index = array.indexOf(node);
return index !== -1 && array.length > index + 1 ? array[index + 1] : null;
}
function getEndExceptSemicolon(statement: ts.Statement): number {
const lastToken = statement.getLastToken();
return (lastToken && lastToken.kind === ts.SyntaxKind.SemicolonToken) ? statement.getEnd() - 1 :
statement.getEnd();
}

View File

@ -160,6 +160,20 @@ var D = /** @class */ (function () {
return D;
}());
export { D };
var E = /** @class */ (function () {
function E() {}
return E = tslib_1.__decorate([
Directive({ selector: '[e]' })
], E);
}());
export { E };
var F = /** @class */ (function () {
function F() {}
return F = tslib_1.__decorate([
Directive({ selector: '[f]' })
], F)
}());
export { F };
// Some other content`
};
});
@ -445,62 +459,91 @@ SOME DEFINITION TEXT
expect(output.toString()).not.toContain(`C.decorators`);
});
});
describe('[__decorate declarations]', () => {
it('should delete the decorator (and following comma) that was matched in the analysis',
() => {
const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM_DECORATE_HELPER);
const output = new MagicString(PROGRAM_DECORATE_HELPER.contents);
const compiledClass =
decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'A') !;
const decorator = compiledClass.decorators !.find(d => d.name === 'Directive') !;
const decoratorsToRemove = new Map<ts.Node, ts.Node[]>();
decoratorsToRemove.set(decorator.node !.parent !, [decorator.node !]);
renderer.removeDecorators(output, decoratorsToRemove);
expect(output.toString()).not.toContain(`Directive({ selector: '[a]' }),`);
expect(output.toString()).toContain(`OtherA()`);
expect(output.toString()).toContain(`Directive({ selector: '[b]' })`);
expect(output.toString()).toContain(`OtherB()`);
expect(output.toString()).toContain(`Directive({ selector: '[c]' })`);
});
describe('[__decorate declarations]', () => {
it('should delete the decorator (and following comma) that was matched in the analysis',
() => {
const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM_DECORATE_HELPER);
const output = new MagicString(PROGRAM_DECORATE_HELPER.contents);
const compiledClass =
decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'A') !;
const decorator = compiledClass.decorators !.find(d => d.name === 'Directive') !;
const decoratorsToRemove = new Map<ts.Node, ts.Node[]>();
decoratorsToRemove.set(decorator.node !.parent !, [decorator.node !]);
renderer.removeDecorators(output, decoratorsToRemove);
expect(output.toString()).not.toContain(`Directive({ selector: '[a]' }),`);
expect(output.toString()).toContain(`OtherA()`);
expect(output.toString()).toContain(`Directive({ selector: '[b]' })`);
expect(output.toString()).toContain(`OtherB()`);
expect(output.toString()).toContain(`Directive({ selector: '[c]' })`);
});
it('should delete the decorator (but cope with no trailing comma) that was matched in the analysis',
() => {
const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM_DECORATE_HELPER);
const output = new MagicString(PROGRAM_DECORATE_HELPER.contents);
const compiledClass =
decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'B') !;
const decorator = compiledClass.decorators !.find(d => d.name === 'Directive') !;
const decoratorsToRemove = new Map<ts.Node, ts.Node[]>();
decoratorsToRemove.set(decorator.node !.parent !, [decorator.node !]);
renderer.removeDecorators(output, decoratorsToRemove);
expect(output.toString()).toContain(`Directive({ selector: '[a]' }),`);
expect(output.toString()).toContain(`OtherA()`);
expect(output.toString()).not.toContain(`Directive({ selector: '[b]' })`);
expect(output.toString()).toContain(`OtherB()`);
expect(output.toString()).toContain(`Directive({ selector: '[c]' })`);
});
it('should delete the decorator (but cope with no trailing comma) that was matched in the analysis',
() => {
const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM_DECORATE_HELPER);
const output = new MagicString(PROGRAM_DECORATE_HELPER.contents);
const compiledClass =
decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'B') !;
const decorator = compiledClass.decorators !.find(d => d.name === 'Directive') !;
const decoratorsToRemove = new Map<ts.Node, ts.Node[]>();
decoratorsToRemove.set(decorator.node !.parent !, [decorator.node !]);
renderer.removeDecorators(output, decoratorsToRemove);
expect(output.toString()).toContain(`Directive({ selector: '[a]' }),`);
expect(output.toString()).toContain(`OtherA()`);
expect(output.toString()).not.toContain(`Directive({ selector: '[b]' })`);
expect(output.toString()).toContain(`OtherB()`);
expect(output.toString()).toContain(`Directive({ selector: '[c]' })`);
});
it('should delete the decorator (and its container if there are not other decorators left) that was matched in the analysis',
() => {
const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM_DECORATE_HELPER);
const output = new MagicString(PROGRAM_DECORATE_HELPER.contents);
const compiledClass =
decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'C') !;
const decorator = compiledClass.decorators !.find(d => d.name === 'Directive') !;
const decoratorsToRemove = new Map<ts.Node, ts.Node[]>();
decoratorsToRemove.set(decorator.node !.parent !, [decorator.node !]);
renderer.removeDecorators(output, decoratorsToRemove);
expect(output.toString()).toContain(`Directive({ selector: '[a]' }),`);
expect(output.toString()).toContain(`OtherA()`);
expect(output.toString()).toContain(`Directive({ selector: '[b]' })`);
expect(output.toString()).toContain(`OtherB()`);
expect(output.toString()).not.toContain(`Directive({ selector: '[c]' })`);
expect(output.toString()).not.toContain(`C = tslib_1.__decorate([`);
expect(output.toString()).toContain(`function C() {\n }\n return C;`);
});
it('should delete the decorator (and its container if there are no other decorators left) that was matched in the analysis',
() => {
const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM_DECORATE_HELPER);
const output = new MagicString(PROGRAM_DECORATE_HELPER.contents);
const compiledClass =
decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'C') !;
const decorator = compiledClass.decorators !.find(d => d.name === 'Directive') !;
const decoratorsToRemove = new Map<ts.Node, ts.Node[]>();
decoratorsToRemove.set(decorator.node !.parent !, [decorator.node !]);
renderer.removeDecorators(output, decoratorsToRemove);
expect(output.toString()).toContain(`Directive({ selector: '[a]' }),`);
expect(output.toString()).toContain(`OtherA()`);
expect(output.toString()).toContain(`Directive({ selector: '[b]' })`);
expect(output.toString()).toContain(`OtherB()`);
expect(output.toString()).not.toContain(`Directive({ selector: '[c]' })`);
expect(output.toString()).not.toContain(`C = tslib_1.__decorate([`);
expect(output.toString()).toContain(`function C() {\n }\n return C;`);
});
it('should delete call to `__decorate` but keep a return statement and semi-colon if there are no other decorators left',
() => {
const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM_DECORATE_HELPER);
const output = new MagicString(PROGRAM_DECORATE_HELPER.contents);
const compiledClass =
decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'E') !;
const decorator = compiledClass.decorators !.find(d => d.name === 'Directive') !;
const decoratorsToRemove = new Map<ts.Node, ts.Node[]>();
decoratorsToRemove.set(decorator.node !.parent !, [decorator.node !]);
renderer.removeDecorators(output, decoratorsToRemove);
expect(output.toString()).not.toContain(`Directive({ selector: '[e]' })`);
expect(output.toString()).not.toContain(`E = tslib_1.__decorate([`);
expect(output.toString()).toContain(`function E() {}\n return E;`);
});
it('should delete call to `__decorate` but keep a return statement and no semi-colon if there are no other decorators left',
() => {
const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM_DECORATE_HELPER);
const output = new MagicString(PROGRAM_DECORATE_HELPER.contents);
const compiledClass =
decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'F') !;
const decorator = compiledClass.decorators !.find(d => d.name === 'Directive') !;
const decoratorsToRemove = new Map<ts.Node, ts.Node[]>();
decoratorsToRemove.set(decorator.node !.parent !, [decorator.node !]);
renderer.removeDecorators(output, decoratorsToRemove);
expect(output.toString()).not.toContain(`Directive({ selector: '[f]' })`);
expect(output.toString()).not.toContain(`F = tslib_1.__decorate([`);
expect(output.toString()).toContain(`function F() {}\n return F\n`);
});
});
});
});
});