Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Compile decorators properly when googmodule is false and transformTypesToClosure is true #2

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions demo/decorators.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
export function classDecorator(constructor: Function) {
Object.seal(constructor);
Object.seal(constructor.prototype);
}

export function methodDecorator(value: boolean) {
return function (target: any, propertyKey: string, descriptor?: PropertyDescriptor) {
if (descriptor) {
descriptor.enumerable = value;
}
};
}

export function accessorDecorator(value: boolean,) {
return function (target: any, propertyKey: string, descriptor?: PropertyDescriptor) {
if (descriptor) {
descriptor.configurable = value;
}
};
}

1 change: 1 addition & 0 deletions demo/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"strict": true,
"moduleResolution": "Bundler",
"esModuleInterop": true,
"experimentalDecorators": true,
},
"exclude": [
"test.ts"
Expand Down
28 changes: 28 additions & 0 deletions demo/using_decorators.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { accessorDecorator, classDecorator, methodDecorator } from "./decorators";

@classDecorator
export class Person {
private name_: string;
constructor(name: string) {
this.name_ = name;
}

@accessorDecorator(true)
get name() {
return this.name_;
}

@methodDecorator(true)
greet() {
console.log(`Hello, my name is ${this.name}.`);
}
}

const p = new Person("Ron");
p.greet();


export class Employee {
constructor(private age_: number) {}
get age() { return this.age_; }
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"@types/source-map-support": "^0.5.3",
"diff-match-patch": "^1.0.5",
"glob": "8.0.1",
"google-closure-compiler": "^20230411.0.0",
"google-closure-compiler": "^20230502.0.0",
"jasmine": "^4.1.0",
"jasmine-node": "^3.0.0",
"source-map": "^0.7.3",
Expand Down
71 changes: 43 additions & 28 deletions src/decorators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,12 +94,12 @@ function isExportingDecorator(decorator: ts.Decorator, typeChecker: ts.TypeCheck
* ], Foo.prototype,
* __googReflect.objectProperty("prop", Foo.prototype), void 0);
*/
export function transformDecoratorsOutputForClosurePropertyRenaming(diagnostics: ts.Diagnostic[]) {
export function transformDecoratorsOutputForClosurePropertyRenaming(diagnostics: ts.Diagnostic[], useGoogModule = true) {
return (context: ts.TransformationContext) => {
const result: ts.Transformer<ts.SourceFile> = (sourceFile: ts.SourceFile) => {
let nodeNeedingGoogReflect: undefined|ts.Node = undefined;
const visitor: ts.Visitor = (node) => {
const replacementNode = rewriteDecorator(node);
const replacementNode = rewriteDecorator(node, useGoogModule);
if (replacementNode) {
nodeNeedingGoogReflect = node;
return replacementNode;
Expand All @@ -113,30 +113,34 @@ export function transformDecoratorsOutputForClosurePropertyRenaming(diagnostics:
if (nodeNeedingGoogReflect !== undefined) {
const statements = [...updatedSourceFile.statements];
const googModuleIndex = statements.findIndex(isGoogModuleStatement);
if (googModuleIndex === -1) {
if (useGoogModule && googModuleIndex === -1) {
reportDiagnostic(
diagnostics, nodeNeedingGoogReflect,
'Internal tsickle error: could not find goog.module statement to import __tsickle_googReflect for decorator compilation.');
return sourceFile;
}
const googRequireReflectObjectProperty =
ts.factory.createVariableStatement(
undefined,
ts.factory.createVariableDeclarationList(
[ts.factory.createVariableDeclaration(
'__tsickle_googReflect',
/* exclamationToken */ undefined, /* type */ undefined,
ts.factory.createCallExpression(
ts.factory.createPropertyAccessExpression(
ts.factory.createIdentifier('goog'), 'require'),
undefined,
[ts.factory.createStringLiteral('goog.reflect')]))],
ts.NodeFlags.Const));
// The boilerplate we produce has a goog.module line, then two related
// lines dealing with the `module` variable. Insert our goog.require
// after that to avoid visually breaking up the module info, and to be
// with the rest of the goog.require statements.
statements.splice(googModuleIndex + 3, 0, googRequireReflectObjectProperty);
if (useGoogModule) {
const googRequireReflectObjectProperty =
ts.factory.createVariableStatement(
undefined,
ts.factory.createVariableDeclarationList(
[ts.factory.createVariableDeclaration(
'__tsickle_googReflect',
/* exclamationToken */ undefined, /* type */ undefined,
ts.factory.createCallExpression(
ts.factory.createPropertyAccessExpression(
ts.factory.createIdentifier('goog'), 'require'),
undefined,
[ts.factory.createStringLiteral('goog.reflect')]))],
ts.NodeFlags.Const))


// The boilerplate we produce has a goog.module line, then two related
// lines dealing with the `module` variable. Insert our goog.require
// after that to avoid visually breaking up the module info, and to be
// with the rest of the goog.require statements.
statements.splice(googModuleIndex + 3, 0, googRequireReflectObjectProperty);
}
updatedSourceFile = ts.factory.updateSourceFile(
updatedSourceFile,
ts.setTextRange(
Expand All @@ -161,7 +165,7 @@ export function transformDecoratorsOutputForClosurePropertyRenaming(diagnostics:
*
* Returns undefined if no modification is necessary.
*/
function rewriteDecorator(node: ts.Node): ts.Node|undefined {
function rewriteDecorator(node: ts.Node, useGoogModule = true): ts.Node|undefined {
if (!ts.isCallExpression(node)) {
return;
}
Expand All @@ -188,12 +192,23 @@ function rewriteDecorator(node: ts.Node): ts.Node|undefined {
return;
}
const fieldNameLiteral = untypedFieldNameLiteral;
args[2] = ts.factory.createCallExpression(
ts.factory.createPropertyAccessExpression(
ts.factory.createIdentifier('__tsickle_googReflect'),
'objectProperty'),
undefined,
[ts.factory.createStringLiteral(fieldNameLiteral.text), args[1]]);
if (useGoogModule) {
args[2] = ts.factory.createCallExpression(
ts.factory.createPropertyAccessExpression(
ts.factory.createIdentifier('__tsickle_googReflect'),
'objectProperty'),
undefined,
[ts.factory.createStringLiteral(fieldNameLiteral.text), args[1]]);
} else {
args[2] = ts.factory.createCallExpression(
ts.factory.createIdentifier('JSCompiler_renameProperty'),
undefined,
[
ts.factory.createStringLiteral(fieldNameLiteral.text),
args[1],
],
);
}
return ts.factory.updateCallExpression(
node, node.expression, node.typeArguments, args);
}
Expand Down
55 changes: 55 additions & 0 deletions src/fix_downleveled_decorators.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import * as ts from "typescript";

/**
* This fixes the downleveled decorators so Closure doesn't have
* trouble with them. The problem is that when `experimentalDecorators` is
* enabled, we TSC ends up converting a class decorator like this:
*
* @classDecorator
* export class Person { ... }
*
* to this:
*
* let Person = class Person { ... };
*
* as well as some calls to __decorate(Person, ...)
*
* The problem is that this causes Closure Compiler to fail with this error:
* ERROR - [JSC_CANNOT_CONVERT_YET] Transpilation of 'Classes with possible name shadowing' is not yet implemented.
* 21| let Person = class Person {
* ^^^^^^^^^^^^^^
*
* This transformer fixes the problem by converting the class expression
* to a class declaration.
*/
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you be more specific with the before/after ast?
Isn't it more like

let Person = __decorate(class Person{...});

or

let Person = class Person{...}
Person = __decorate(Person)

How does the assignment work if you converted the class expression to a decorator? I think removing the name from the class expression is more likely to work.

Maybe we need to write a test to make sure class decorators actually work, both compiled and uncompiled.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I'm planning on writing some tests to prove out this functionality as well as the import handling.

So this ends up transforming:

@classDecorator
export class Person {
    private name_: string;
    constructor(name: string) {
        this.name_ = name;
    }

    @accessorDecorator(true)
    get name() {
        return this.name_;
    }
  
    @methodDecorator(true)
    greet() {
        console.log(`Hello, my name is ${this.name}.`);
    }
  }

into:

class Person {
    name_;
    /**
     * @public
     * @param {string} name
     */
    constructor(name) {
        this.name_ = name;
    }
    /**
     * @public
     * @return {string}
     */
    get name() {
        return this.name_;
    }
    /**
     * @public
     * @return {void}
     */
    greet() {
        console.log(`Hello, my name is ${this.name}.`);
    }
};
__decorate([
    accessorDecorator(true)
], Person.prototype, JSCompiler_renameProperty("name", Person.prototype), null);
__decorate([
    methodDecorator(true)
], Person.prototype, JSCompiler_renameProperty("greet", Person.prototype), null);
/** @suppress {visibility} */
Person = __decorate([
    classDecorator
], Person);
export { Person };
/* istanbul ignore if */
if (false) {
    /**
     * @type {string}
     * @private
     */
    Person.prototype.name_;
}

Yeah I wonder if stripping the name from the class is really the right call and it'd end up just being something like

let Person = class { }

instead.

Closure itself doesn't have any problem compiling it as is though.... no guarantees at the moment on it actually working though post-Closure compiler.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My concern is I don't know what it means to re-assign name that was declared as a class.

class Person {...}
Person = ...

Does that work how you'd expect? Is it consistent between node, browser, closure?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure how it'd work out. I'm assuming that it's okay since that's what tsickle was doing before when downleveling decorators. That being said, I'll see about getting some tests in place to prove it out.

export function fixDownleveledDecorators() {
const printer = ts.createPrinter();
return (context: ts.TransformationContext): ts.Transformer<ts.SourceFile> => {
return (sourceFile: ts.SourceFile) => {
function visit(node: ts.Node): ts.Node {
// Check if the node is a VariableDeclarationList
if (ts.isVariableDeclarationList(node)) {
for (const declaration of node.declarations) {
if (
declaration.initializer &&
ts.isClassExpression(declaration.initializer)
) {
const className = declaration.name;
// convert the class expression to a class declaration
const classDeclaration = ts.factory.createClassDeclaration(
undefined,
className.getText(),
[],
[],
declaration.initializer.members
);
return classDeclaration;
}
}
}
return ts.visitEachChild(node, visit, context);
}
return ts.visitEachChild(sourceFile, visit, context);
};
};
}
7 changes: 7 additions & 0 deletions src/tsickle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {FileSummary, SummaryGenerationProcessorHost} from './summary';
import {isDtsFileName} from './transformer_util';
import * as tsmes from './ts_migration_exports_shim';
import {makeTsickleDeclarationMarkerTransformerFactory} from './tsickle_declaration_marker';
import {fixDownleveledDecorators} from './fix_downleveled_decorators';

// Exported for users as a default impl of pathToModuleName.
export {pathToModuleName} from './cli_support';
Expand Down Expand Up @@ -248,6 +249,12 @@ export function emit(
transformDecoratorsOutputForClosurePropertyRenaming(
tsickleDiagnostics));
tsTransformers.after!.push(transformDecoratorJsdoc());
} else if (host.transformTypesToClosure) {
tsTransformers.after!.push(
transformDecoratorsOutputForClosurePropertyRenaming(
tsickleDiagnostics, false));
tsTransformers.after!.push(transformDecoratorJsdoc());
tsTransformers.after!.push(fixDownleveledDecorators())
}
if (host.addDtsClutzAliases) {
tsTransformers.afterDeclarations!.push(
Expand Down
3 changes: 2 additions & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
"strict": true,
"outDir": "out",
"paths": {"tsickle": ["./src/tsickle.ts"]},
"esModuleInterop": true
"esModuleInterop": true,
"experimentalDecorators": true
},
"include": [
"src/*.ts",
Expand Down