feat(compiler-cli): support producing Closure-specific PURE annotations (#41021)

For certain generated function calls, the compiler emits a 'PURE' annotation
which informs Terser (the optimizer) about the purity of a specific function
call. This commit expands that system to produce a new Closure-specific
'pureOrBreakMyCode' annotation when targeting the Closure optimizer instead
of Terser.

PR Close #41021
This commit is contained in:
Alex Rickabaugh 2021-02-26 11:55:19 -08:00 committed by Andrew Kushnir
parent 45216ccc0d
commit fbc9df181e
12 changed files with 81 additions and 25 deletions

View File

@ -23,7 +23,7 @@ interface TestObject {
}
const host: AstHost<ts.Expression> = new TypeScriptAstHost();
const factory = new TypeScriptAstFactory();
const factory = new TypeScriptAstFactory(/* annotateForClosureCompiler */ false);
const nestedObj = factory.createObjectLiteral([
{propertyName: 'x', quoted: false, value: factory.createLiteral(42)},
{propertyName: 'y', quoted: false, value: factory.createLiteral('X')},

View File

@ -16,7 +16,7 @@ import {generate} from '../helpers';
describe('EmitScope', () => {
describe('translateDefinition()', () => {
it('should translate the given output AST into a TExpression', () => {
const factory = new TypeScriptAstFactory();
const factory = new TypeScriptAstFactory(/* annotateForClosureCompiler */ false);
const translator = new Translator<ts.Statement, ts.Expression>(factory);
const ngImport = factory.createIdentifier('core');
const emitScope = new EmitScope<ts.Statement, ts.Expression>(ngImport, translator);
@ -26,7 +26,7 @@ describe('EmitScope', () => {
});
it('should use the `ngImport` idenfifier for imports when translating', () => {
const factory = new TypeScriptAstFactory();
const factory = new TypeScriptAstFactory(/* annotateForClosureCompiler */ false);
const translator = new Translator<ts.Statement, ts.Expression>(factory);
const ngImport = factory.createIdentifier('core');
const emitScope = new EmitScope<ts.Statement, ts.Expression>(ngImport, translator);
@ -37,7 +37,7 @@ describe('EmitScope', () => {
});
it('should not emit any shared constants in the replacement expression', () => {
const factory = new TypeScriptAstFactory();
const factory = new TypeScriptAstFactory(/* annotateForClosureCompiler */ false);
const translator = new Translator<ts.Statement, ts.Expression>(factory);
const ngImport = factory.createIdentifier('core');
const emitScope = new EmitScope<ts.Statement, ts.Expression>(ngImport, translator);
@ -54,7 +54,7 @@ describe('EmitScope', () => {
describe('getConstantStatements()', () => {
it('should return any constant statements that were added to the `constantPool`', () => {
const factory = new TypeScriptAstFactory();
const factory = new TypeScriptAstFactory(/* annotateForClosureCompiler */ false);
const translator = new Translator<ts.Statement, ts.Expression>(factory);
const ngImport = factory.createIdentifier('core');
const emitScope = new EmitScope<ts.Statement, ts.Expression>(ngImport, translator);

View File

@ -16,7 +16,7 @@ import {generate} from '../helpers';
describe('IifeEmitScope', () => {
describe('translateDefinition()', () => {
it('should translate the given output AST into a TExpression, wrapped in an IIFE', () => {
const factory = new TypeScriptAstFactory();
const factory = new TypeScriptAstFactory(/* annotateForClosureCompiler */ false);
const translator = new Translator<ts.Statement, ts.Expression>(factory);
const ngImport = factory.createIdentifier('core');
const emitScope =
@ -27,7 +27,7 @@ describe('IifeEmitScope', () => {
});
it('should use the `ngImport` idenfifier for imports when translating', () => {
const factory = new TypeScriptAstFactory();
const factory = new TypeScriptAstFactory(/* annotateForClosureCompiler */ false);
const translator = new Translator<ts.Statement, ts.Expression>(factory);
const ngImport = factory.createIdentifier('core');
const emitScope =
@ -39,7 +39,7 @@ describe('IifeEmitScope', () => {
});
it('should emit any shared constants in the replacement expression IIFE', () => {
const factory = new TypeScriptAstFactory();
const factory = new TypeScriptAstFactory(/* annotateForClosureCompiler */ false);
const translator = new Translator<ts.Statement, ts.Expression>(factory);
const ngImport = factory.createIdentifier('core');
const emitScope =
@ -58,7 +58,7 @@ describe('IifeEmitScope', () => {
describe('getConstantStatements()', () => {
it('should throw an error', () => {
const factory = new TypeScriptAstFactory();
const factory = new TypeScriptAstFactory(/* annotateForClosureCompiler */ false);
const translator = new Translator<ts.Statement, ts.Expression>(factory);
const ngImport = factory.createIdentifier('core');
const emitScope =

View File

@ -23,7 +23,7 @@ import {generate} from './helpers';
describe('FileLinker', () => {
let factory: TypeScriptAstFactory;
beforeEach(() => factory = new TypeScriptAstFactory());
beforeEach(() => factory = new TypeScriptAstFactory(/* annotateForClosureCompiler */ false));
describe('isPartialDeclaration()', () => {
it('should return true if the callee is recognized', () => {
@ -154,7 +154,8 @@ describe('FileLinker', () => {
const fs = new MockFileSystemNative();
const logger = new MockLogger();
const linkerEnvironment = LinkerEnvironment.create<ts.Statement, ts.Expression>(
fs, logger, new TypeScriptAstHost(), new TypeScriptAstFactory(), DEFAULT_LINKER_OPTIONS);
fs, logger, new TypeScriptAstHost(),
new TypeScriptAstFactory(/* annotateForClosureCompiler */ false), DEFAULT_LINKER_OPTIONS);
const fileLinker = new FileLinker<MockConstantScopeRef, ts.Statement, ts.Expression>(
linkerEnvironment, fs.resolve('/test.js'), '// test code');
return {host: linkerEnvironment.host, fileLinker};

View File

@ -33,7 +33,8 @@ describe('PartialLinkerSelector', () => {
fs = new MockFileSystemNative();
const logger = new MockLogger();
environment = LinkerEnvironment.create<ts.Statement, ts.Expression>(
fs, logger, new TypeScriptAstHost(), new TypeScriptAstFactory(), options);
fs, logger, new TypeScriptAstHost(),
new TypeScriptAstFactory(/* annotateForClosureCompiler */ false), options);
});
describe('supportsDeclaration()', () => {

View File

@ -14,7 +14,7 @@ import {generate} from './helpers';
describe('Translator', () => {
let factory: TypeScriptAstFactory;
beforeEach(() => factory = new TypeScriptAstFactory());
beforeEach(() => factory = new TypeScriptAstFactory(/* annotateForClosureCompiler */ false));
describe('translateExpression()', () => {
it('should generate expression specific output', () => {

View File

@ -11,7 +11,7 @@ import * as ts from 'typescript';
import {DefaultImportRecorder, ImportRewriter} from '../../imports';
import {Decorator, ReflectionHost} from '../../reflection';
import {ImportManager, RecordWrappedNodeExprFn, translateExpression, translateStatement} from '../../translator';
import {ImportManager, RecordWrappedNodeExprFn, translateExpression, translateStatement, TranslatorOptions} from '../../translator';
import {visit, VisitListEntryResult, Visitor} from '../../util/src/visitor';
import {CompileResult} from './api';
@ -91,15 +91,18 @@ class IvyTransformationVisitor extends Visitor {
return {node};
}
const translateOptions: TranslatorOptions<ts.Expression> = {
recordWrappedNodeExpr: this.recordWrappedNodeExpr,
annotateForClosureCompiler: this.isClosureCompilerEnabled,
};
// There is at least one field to add.
const statements: ts.Statement[] = [];
const members = [...node.members];
for (const field of this.classCompilationMap.get(node)!) {
// Translate the initializer for the field into TS nodes.
const exprNode = translateExpression(
field.initializer, this.importManager,
{recordWrappedNodeExpr: this.recordWrappedNodeExpr});
const exprNode = translateExpression(field.initializer, this.importManager, translateOptions);
// Create a static property declaration for the new field.
const property = ts.createProperty(
@ -116,10 +119,7 @@ class IvyTransformationVisitor extends Visitor {
/* hasTrailingNewLine */ false);
}
field.statements
.map(
stmt => translateStatement(
stmt, this.importManager, {recordWrappedNodeExpr: this.recordWrappedNodeExpr}))
field.statements.map(stmt => translateStatement(stmt, this.importManager, translateOptions))
.forEach(stmt => statements.push(stmt));
members.push(property);
@ -282,6 +282,7 @@ function transformIvySourceFile(
recordWrappedNodeExpr,
downlevelTaggedTemplates: downlevelTranslatedCode,
downlevelVariableDeclarations: downlevelTranslatedCode,
annotateForClosureCompiler: isClosureCompilerEnabled,
}));
// Preserve @fileoverview comments required by Closure, since the location might change as a

View File

@ -42,6 +42,7 @@ export interface TranslatorOptions<TExpression> {
downlevelTaggedTemplates?: boolean;
downlevelVariableDeclarations?: boolean;
recordWrappedNodeExpr?: RecordWrappedNodeExprFn<TExpression>;
annotateForClosureCompiler?: boolean;
}
export class ExpressionTranslatorVisitor<TStatement, TExpression> implements o.ExpressionVisitor,

View File

@ -9,6 +9,21 @@ import * as ts from 'typescript';
import {AstFactory, BinaryOperator, LeadingComment, ObjectLiteralProperty, SourceMapRange, TemplateLiteral, UnaryOperator, VariableDeclarationType} from './api/ast_factory';
/**
* Different optimizers use different annotations on a function or method call to indicate its pure
* status.
*/
enum PureAnnotation {
/**
* Closure's annotation for purity is `@pureOrBreakMyCode`, but this needs to be in a semantic
* (jsdoc) enabled comment. Thus, the actual comment text for Closure must include the `*` that
* turns a `/*` comment into a `/**` comment, as well as surrounding whitespace.
*/
CLOSURE = '* @pureOrBreakMyCode ',
TERSER = '@__PURE__',
}
const UNARY_OPERATORS: Record<UnaryOperator, ts.PrefixUnaryOperator> = {
'+': ts.SyntaxKind.PlusToken,
'-': ts.SyntaxKind.MinusToken,
@ -46,6 +61,8 @@ const VAR_TYPES: Record<VariableDeclarationType, ts.NodeFlags> = {
export class TypeScriptAstFactory implements AstFactory<ts.Statement, ts.Expression> {
private externalSourceFiles = new Map<string, ts.SourceMapSource>();
constructor(private annotateForClosureCompiler: boolean) {}
attachComments = attachComments;
createArrayLiteral = ts.createArrayLiteral;
@ -68,7 +85,9 @@ export class TypeScriptAstFactory implements AstFactory<ts.Statement, ts.Express
const call = ts.createCall(callee, undefined, args);
if (pure) {
ts.addSyntheticLeadingComment(
call, ts.SyntaxKind.MultiLineCommentTrivia, '@__PURE__', /* trailing newline */ false);
call, ts.SyntaxKind.MultiLineCommentTrivia,
this.annotateForClosureCompiler ? PureAnnotation.CLOSURE : PureAnnotation.TERSER,
/* trailing newline */ false);
}
return call;
}

View File

@ -19,7 +19,7 @@ export function translateExpression(
options: TranslatorOptions<ts.Expression> = {}): ts.Expression {
return expression.visitExpression(
new ExpressionTranslatorVisitor<ts.Statement, ts.Expression>(
new TypeScriptAstFactory(), imports, options),
new TypeScriptAstFactory(options.annotateForClosureCompiler === true), imports, options),
new Context(false));
}
@ -28,6 +28,6 @@ export function translateStatement(
options: TranslatorOptions<ts.Expression> = {}): ts.Statement {
return statement.visitStatement(
new ExpressionTranslatorVisitor<ts.Statement, ts.Expression>(
new TypeScriptAstFactory(), imports, options),
new TypeScriptAstFactory(options.annotateForClosureCompiler === true), imports, options),
new Context(true));
}

View File

@ -12,7 +12,7 @@ import {TypeScriptAstFactory} from '../src/typescript_ast_factory';
describe('TypeScriptAstFactory', () => {
let factory: TypeScriptAstFactory;
beforeEach(() => factory = new TypeScriptAstFactory());
beforeEach(() => factory = new TypeScriptAstFactory(/* annotateForClosureCompiler */ false));
describe('attachComments()', () => {
it('should add the comments to the given statement', () => {
@ -77,6 +77,14 @@ describe('TypeScriptAstFactory', () => {
const call = factory.createCallExpression(callee, [arg1, arg2], true);
expect(generate(call)).toEqual('/*@__PURE__*/ foo(42, "moo")');
});
it('should create a call marked with a closure-style pure comment if `pure` is true', () => {
factory = new TypeScriptAstFactory(/* annotateForClosureCompiler */ true);
const {items: [callee, arg1, arg2], generate} = setupExpressions(`foo`, `42`, `"moo"`);
const call = factory.createCallExpression(callee, [arg1, arg2], true);
expect(generate(call)).toEqual('/** @pureOrBreakMyCode */ foo(42, "moo")');
});
});
describe('createConditional()', () => {

View File

@ -393,6 +393,31 @@ runInEachFileSystem(os => {
// that start with `C:`.
if (os !== 'Windows' || platform() === 'win32') {
describe('when closure annotations are requested', () => {
it('should add @pureOrBreakMyCode to getInheritedFactory calls', () => {
env.tsconfig({
'annotateForClosureCompiler': true,
});
env.write(`test.ts`, `
import {Directive} from '@angular/core';
@Directive({
selector: '[base]',
})
class Base {}
@Directive({
selector: '[test]',
})
class Dir extends Base {
}
`);
env.driveMain();
const jsContents = env.getContents('test.js');
expect(jsContents).toContain('/** @pureOrBreakMyCode */ i0.ɵɵgetInheritedFactory(Dir)');
});
it('should add @nocollapse to static fields', () => {
env.tsconfig({
'annotateForClosureCompiler': true,