-
Notifications
You must be signed in to change notification settings - Fork 11
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: transform tslib __rest helper using goog.reflect.objectProperty
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
Showing
15 changed files
with
922 additions
and
351 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
Oops, something went wrong.