Skip to content

Commit

Permalink
feat: transform tslib __rest helper using goog.reflect.objectProperty
Browse files Browse the repository at this point in the history
Typescript when target is less than ES2018 emits `__rest` helper for object spread syntax,
which references properties' name via string literal hence breaks Closure Compilation.
We transforms such names to appropriate `goog.reflect.objectProperty` calls to make it compatible with
Closure Compiler, as described in  angular/tsickle#1047.
  • Loading branch information
theseanl committed Sep 25, 2019
1 parent b1ab5ff commit 41f2439
Show file tree
Hide file tree
Showing 16 changed files with 915 additions and 351 deletions.
93 changes: 93 additions & 0 deletions packages/tscc/src/transformer/TsHelperTransformer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import * as ts from 'typescript';
import {TsickleHost} from 'tsickle';
import {createGoogCall, findImportedVariable, findGoogRequiredVariable, identifierIsEmitHelper} from './transformer_utils'

export default abstract class TsHelperTransformer {
constructor(
private tsickleHost: TsickleHost,
private context: ts.TransformationContext,
private sf: ts.SourceFile
) {}

protected abstract readonly HELPER_NAME: string;

/**
* Determines whether the given node is a tslib helper call. Such a call expression looks similar
* to usual `__decorate(...)` function calls, except that the identifier node __decorate has
* a hidden emitNode property. This is encoded in `identifierIsEmitHelper` call and this method
* uses it.
*/
protected isTsGeneratedHelperCall(node: ts.Node): node is ts.CallExpression {
if (!ts.isCallExpression(node)) return false;
const caller = node.expression;
if (!ts.isIdentifier(caller)) return false;
if (caller.escapedText !== ts.escapeLeadingUnderscores(this.HELPER_NAME)) return false;
if (!identifierIsEmitHelper(caller)) return false;
return true;
}

/**
* Queries whether a visiting node is a call to tslib helper functions, such as
* `tslib_1.__decorate(...)` that is generated by the TS compiler, and if so, returns a new node.
* Otherwise, return undefined.
*/
protected abstract onHelperCall(node: ts.CallExpression, googReflectImport: ts.Identifier): ts.CallExpression

private maybeTsGeneratedHelperCall(node: ts.Node, googReflectImport: ts.Identifier): ts.Node {
if (!this.isTsGeneratedHelperCall(node)) return;
return this.onHelperCall(node, googReflectImport);
}

transformSourceFile(): ts.SourceFile {
const sf = this.sf;
// There's nothing to do when tslib was not imported to the module.
if (!findImportedVariable(sf, 'tslib')) return sf;
const existingGoogReflectImport =
findImportedVariable(sf, 'goog:goog.reflect') ||
findGoogRequiredVariable(sf, 'goog.reflect');
const googReflectImport =
existingGoogReflectImport ||
ts.createIdentifier(`tscc_goog_reflect_injected`);

let foundTransformedDecorateCall = false;
const visitor = (node: ts.Node): ts.Node => {
let transformed = this.maybeTsGeneratedHelperCall(node, googReflectImport);
if (transformed) {
foundTransformedDecorateCall = true;
return transformed;
}
return ts.visitEachChild(node, visitor, this.context);
};

const newSf = visitor(sf) as ts.SourceFile;
if (!foundTransformedDecorateCall) return newSf;
const stmts = this.combineStatements(
newSf.statements.slice(),
existingGoogReflectImport ? undefined : googReflectImport
);

return ts.updateSourceFileNode(newSf, ts.setTextRange(ts.createNodeArray(stmts), newSf.statements));
}

protected combineStatements(stmts: ts.Statement[], googReflectImport?: ts.Identifier) {
if (!googReflectImport) return stmts;
const requireReflect = this.createGoogReflectRequire(googReflectImport);
stmts.unshift(requireReflect);
return stmts;
}

private createGoogReflectRequire(ident: ts.Identifier) {
return ts.createVariableStatement(
undefined,
ts.createVariableDeclarationList(
[
ts.createVariableDeclaration(
ident,
undefined,
createGoogCall("require", ts.createStringLiteral('goog.reflect'))
)
],
this.tsickleHost.es5Mode ? undefined : ts.NodeFlags.Const)
);
}
}
201 changes: 72 additions & 129 deletions packages/tscc/src/transformer/decoratorPropertyTransformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,144 +21,87 @@
* not guaranteed to work but was the only way to make this work. Also has to be careful about
* accessor decorators.
*/

import * as ts from 'typescript';
import {TsickleHost} from 'tsickle';
import {isVariableRequireStatement, createGoogCall} from './transformer_utils'


function findImportedVariable(sf: ts.SourceFile, moduleName: string): ts.Identifier {
for (let stmt of sf.statements) {
let _ = isVariableRequireStatement(stmt);
if (!_) continue;
if (_.importedUrl !== moduleName) continue;
return _.newIdent
}
}
/**
* The transformer needs to discern "tslib" function calls (called EmitHelpers in TS),
* but they are simply identifiers of name `__decorate` and such, all the difference
* lies in their `emitNode` internal property. Any functionality related to this is
* under internal and is not available in public API.
* This function currently access Node.emitNode.flags to achieve this
*/
function identifierIsEmitHelper(ident: ts.Identifier): boolean {
let emitNode = ident["emitNode"];
if (emitNode === undefined) return false;
let flags = emitNode["flags"];
if (typeof flags !== "number") return false;
return (flags & ts.EmitFlags.HelperName) !== 0;
}
import TsHelperTransformer from './TsHelperTransformer';

export default function decoratorPropertyTransformer(tsickleHost: TsickleHost):
(context: ts.TransformationContext) => ts.Transformer<ts.SourceFile> {
return (context: ts.TransformationContext) => {
return (sf: ts.SourceFile) => {
// There's nothing to do when tslib was not imported to the module.
if (!findImportedVariable(sf, 'tslib')) return sf;
const existingGoogReflectImport = findImportedVariable(sf, 'goog:goog.reflect');
const googReflectImport =
existingGoogReflectImport ||
ts.createIdentifier(`tscc_goog_reflect_injected`);
const globalAssignments: ts.Statement[] = [];
const getId = (() => {
let counter = 1;
return () => `tscc_global_access_name_${counter++}`
})();
/**
* Queries whether this expression is
* `tslib_1.__decorate([decorators], target, prop, descriptor)`
* that is generated by the TS compiler, and if so, returns a new node
* for __decoratr(..., goog.reflect.objectProperty(prop, target), descriptor).
*/
const maybeTsGeneratedDecorateCall = (node: ts.Node): ts.Node => {
if (!ts.isCallExpression(node)) return;
const caller = node.expression;
return new DecoratorTransformer(tsickleHost, context, sf).transformSourceFile();
};
};
}

if (!ts.isIdentifier(caller)) return;
if (caller.escapedText !== ts.escapeLeadingUnderscores('__decorate')) return;
if (!identifierIsEmitHelper(caller)) return;
// Found a candidate. Decorator helper call signature:
// __decorate([decoratorsArray], <target>, <propertyName>, <desc>)
// Note that class decorator only has 2 arguments.
let propNameLiteral = node.arguments[2];
if (!propNameLiteral || !ts.isStringLiteral(propNameLiteral)) return;
let propName = propNameLiteral.text;
class DecoratorTransformer extends TsHelperTransformer {
protected HELPER_NAME = "__decorate";

// Create goog.reflect.objectProperty
const target = node.arguments[1];
const googReflectObjectProperty = ts.setTextRange(
ts.createCall(
ts.createPropertyAccess(
googReflectImport,
ts.createIdentifier('objectProperty')
),
undefined,
[
ts.createStringLiteral(propName),
ts.getMutableClone(target)
]
),
propNameLiteral
);
// Replace third argument of __decorate call to goog.reflect.objectProperty.
// If TS output is in ES3 mode, there will be 3 arguments in __decorate call.
// if its higher than or equal to ES5 mode, there will be 4 arguments.
// The number of arguments must be preserved.
const decorateArgs = node.arguments.slice();
decorateArgs.splice(2, 1, googReflectObjectProperty);
const newCallExpression = ts.createCall(caller, undefined, decorateArgs);
const globalAssignment = ts.createBinary(
ts.createElementAccess(
ts.createIdentifier("self"),
ts.createStringLiteral(getId())
),
ts.createToken(ts.SyntaxKind.FirstAssignment),
ts.createPropertyAccess(
ts.createParen(ts.getMutableClone(target)),
ts.createIdentifier(propName)
)
);
globalAssignments.push(
ts.setEmitFlags(
ts.createExpressionStatement(globalAssignment),
ts.EmitFlags.NoSourceMap | ts.EmitFlags.NoTokenSourceMaps | ts.EmitFlags.NoNestedSourceMaps
)
);
return newCallExpression;
};
let foundTransformedDecorateCall = false;
const visitor = (node: ts.Node): ts.Node => {
let transformed = maybeTsGeneratedDecorateCall(node);
if (transformed) {
foundTransformedDecorateCall = true;
return transformed;
}
return ts.visitEachChild(node, visitor, context);
}
const newSf = visitor(sf) as ts.SourceFile;
if (!foundTransformedDecorateCall) return newSf;
const requireReflect = ts.createVariableStatement(
private tempGlobalAssignments: ts.Statement[] = [];
private getId() {
return `tscc_global_access_name_${this.counter++}`;
}
private counter = 1;

protected onHelperCall(node: ts.CallExpression, googReflectImport:ts.Identifier) {
// Found a candidate. Decorator helper call signature:
// __decorate([decoratorsArray], <target>, <propertyName>, <desc>)
// Note that class decorator only has 2 arguments.
let propNameLiteral = node.arguments[2];
if (!propNameLiteral || !ts.isStringLiteral(propNameLiteral)) return;
let propName = propNameLiteral.text;

// Create goog.reflect.objectProperty
const target = node.arguments[1];
const googReflectObjectProperty = ts.setTextRange(
ts.createCall(
ts.createPropertyAccess(
googReflectImport,
ts.createIdentifier('objectProperty')
),
undefined,
ts.createVariableDeclarationList(
[
ts.createVariableDeclaration(
googReflectImport,
undefined,
createGoogCall("require", ts.createStringLiteral('goog.reflect'))
)
],
tsickleHost.es5Mode ? undefined : ts.NodeFlags.Const)
[
ts.createStringLiteral(propName),
ts.getMutableClone(target)
]
),
propNameLiteral
);
// Replace third argument of __decorate call to goog.reflect.objectProperty.
// If TS output is in ES3 mode, there will be 3 arguments in __decorate call.
// if its higher than or equal to ES5 mode, there will be 4 arguments.
// The number of arguments must be preserved.
const caller = node.expression;
const decorateArgs = node.arguments.slice();
decorateArgs.splice(2, 1, googReflectObjectProperty);
const newCallExpression = ts.createCall(caller, undefined, decorateArgs);
const globalAssignment = ts.createBinary(
ts.createElementAccess(
ts.createIdentifier("self"),
ts.createStringLiteral(this.getId())
),
ts.createToken(ts.SyntaxKind.FirstAssignment),
ts.createPropertyAccess(
ts.createParen(ts.getMutableClone(target)),
ts.createIdentifier(propName)
)
const stmts = newSf.statements.slice();
stmts.unshift(requireReflect);
stmts.push(
ts.createExpressionStatement(ts.createStringLiteral("__tscc_export_start__")),
ts.createBlock(globalAssignments),
ts.createExpressionStatement(ts.createStringLiteral('__tscc_export_end__'))
);
return ts.updateSourceFileNode(newSf, ts.setTextRange(ts.createNodeArray(stmts), newSf.statements));
};
};
}
);
this.tempGlobalAssignments.push(
ts.setEmitFlags(
ts.createExpressionStatement(globalAssignment),
ts.EmitFlags.NoSourceMap | ts.EmitFlags.NoTokenSourceMaps | ts.EmitFlags.NoNestedSourceMaps
)
);
return newCallExpression;
}

protected combineStatements(stmts:ts.Statement[], googReflectImport:ts.Identifier) {
super.combineStatements(stmts, googReflectImport);
stmts.push(
ts.createExpressionStatement(ts.createStringLiteral("__tscc_export_start__")),
ts.createBlock(this.tempGlobalAssignments),
ts.createExpressionStatement(ts.createStringLiteral('__tscc_export_end__'))
);
return stmts;
}
}
59 changes: 59 additions & 0 deletions packages/tscc/src/transformer/restPropertyTransformer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import * as ts from 'typescript';
import {TsickleHost} from 'tsickle';
import TsHelperTransformer from './TsHelperTransformer';

export default function decoratorPropertyTransformer(tsickleHost: TsickleHost):
(context: ts.TransformationContext) => ts.Transformer<ts.SourceFile> {
return (context: ts.TransformationContext) => {
return (sf: ts.SourceFile) => {
return new RestHelperTransformer(tsickleHost, context, sf).transformSourceFile();
};
};
}

class RestHelperTransformer extends TsHelperTransformer {
protected HELPER_NAME = "__rest";
/**
* Rest helper call signature:
* __rest(<target>, [propertiesArray])
*/
protected onHelperCall(node: ts.CallExpression, googReflectImport: ts.Identifier) {
let caller = node.expression;
let target = node.arguments[0];
let propertiesArray = <ts.ArrayLiteralExpression>node.arguments[1];

// Create new array with goog.reflect.objectProperty
// Note that for computed properties, Typescript creates a temp variable
// that stores the computed value (_p), and put
// ```
// typeof _p === 'symbol' ? _c : _c + ""
// ```
const convertedArray = ts.setTextRange(
ts.createArrayLiteral(
propertiesArray.elements.map((propNameLiteral: ts.Expression) => {
if (!ts.isStringLiteral(propNameLiteral)) return propNameLiteral;
const googReflectObjectProperty = ts.setTextRange(
ts.createCall(
ts.createPropertyAccess(
googReflectImport,
ts.createIdentifier('objectProperty')
),
undefined,
[
ts.createStringLiteral(propNameLiteral.text),
ts.getMutableClone(target)
]
),
propNameLiteral
);
return googReflectObjectProperty;
})
),
propertiesArray
);
const restArgs = node.arguments.slice();
restArgs.splice(1, 1, convertedArray);
const newCallExpression = ts.createCall(caller, undefined, restArgs);
return newCallExpression;
}
}
Loading

0 comments on commit 41f2439

Please sign in to comment.