Skip to content

Commit

Permalink
Disable the class fields transform when disableESTransforms is set (#625
Browse files Browse the repository at this point in the history
)

When disableESTransforms is specified, the class transform can almost entirely
be skipped, but we still need to do some work:
* TS constructor initializers (e.g. constructor params with `readonly` or
  `private`) need to be transformed to assignments within the constructor.
* Fields marked `declare` need to be removed. Uninitialized fields not marked
  declare are preserved, matching the ES spec and the `useDefineForClassFields`
  flag in TS.

For now, we still use the `getClassInfo` code path but give alternate results.
In the future we can hopefully simplify that code path, and possibly change
`declare` parsing so the whole line consists of type tokens that will naturally
be erased.
  • Loading branch information
alangpierce committed Jun 23, 2021
1 parent 6b6c0d7 commit 29970ce
Show file tree
Hide file tree
Showing 6 changed files with 195 additions and 22 deletions.
1 change: 1 addition & 0 deletions src/parser/plugins/flow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -698,6 +698,7 @@ function flowParseTypeAnnotatableIdentifier(): void {
export function flowParseVariance(): void {
if (match(tt.plus) || match(tt.minus)) {
next();
state.tokens[state.tokens.length - 1].isType = true;
}
}

Expand Down
4 changes: 3 additions & 1 deletion src/transformers/RootTransformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export default class RootTransformer {
private generatedVariables: Array<string> = [];
private isImportsTransformEnabled: boolean;
private isReactHotLoaderTransformEnabled: boolean;
private disableESTransforms: boolean;
private helperManager: HelperManager;

constructor(
Expand All @@ -39,6 +40,7 @@ export default class RootTransformer {
this.tokens = tokenProcessor;
this.isImportsTransformEnabled = transforms.includes("imports");
this.isReactHotLoaderTransformEnabled = transforms.includes("react-hot-loader");
this.disableESTransforms = Boolean(options.disableESTransforms);

if (!options.disableESTransforms) {
this.transformers.push(
Expand Down Expand Up @@ -193,7 +195,7 @@ export default class RootTransformer {
}

processClass(): void {
const classInfo = getClassInfo(this, this.tokens, this.nameManager);
const classInfo = getClassInfo(this, this.tokens, this.nameManager, this.disableESTransforms);

// Both static and instance initializers need a class name to use to invoke the initializer, so
// assign to one if necessary.
Expand Down
55 changes: 43 additions & 12 deletions src/util/getClassInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export default function getClassInfo(
rootTransformer: RootTransformer,
tokens: TokenProcessor,
nameManager: NameManager,
disableESTransforms: boolean,
): ClassInfo {
const snapshot = tokens.snapshot();

Expand All @@ -71,7 +72,9 @@ export default function getClassInfo(
if (tokens.matchesContextual(ContextualKeyword._constructor) && !tokens.currentToken().isType) {
({constructorInitializerStatements, constructorInsertPos} = processConstructor(tokens));
} else if (tokens.matches1(tt.semi)) {
rangesToRemove.push({start: tokens.currentIndex(), end: tokens.currentIndex() + 1});
if (!disableESTransforms) {
rangesToRemove.push({start: tokens.currentIndex(), end: tokens.currentIndex() + 1});
}
tokens.nextToken();
} else if (tokens.currentToken().isType) {
tokens.nextToken();
Expand All @@ -80,13 +83,17 @@ export default function getClassInfo(
const statementStartIndex = tokens.currentIndex();
let isStatic = false;
let isESPrivate = false;
let isDeclare = false;
while (isAccessModifier(tokens.currentToken())) {
if (tokens.matches1(tt._static)) {
isStatic = true;
}
if (tokens.matches1(tt.hash)) {
isESPrivate = true;
}
if (tokens.matches1(tt._declare)) {
isDeclare = true;
}
tokens.nextToken();
}
if (isStatic && tokens.matches1(tt.braceL)) {
Expand Down Expand Up @@ -144,23 +151,47 @@ export default function getClassInfo(
start: nameStartIndex,
end: tokens.currentIndex(),
});
} else {
// This is just a declaration, so doesn't need to produce any code in the output.
} else if (!disableESTransforms || isDeclare) {
// This is a regular field declaration, like `x;`. With the class transform enabled, we just
// remove the line so that no output is produced. With the class transform disabled, we
// usually want to preserve the declaration (but still strip types), but if the `declare`
// keyword is specified, we should remove the line to avoid initializing the value to
// undefined.
rangesToRemove.push({start: statementStartIndex, end: tokens.currentIndex()});
}
}
}

tokens.restoreToSnapshot(snapshot);
return {
headerInfo,
constructorInitializerStatements,
instanceInitializerNames,
staticInitializerNames,
constructorInsertPos,
fields,
rangesToRemove,
};
if (disableESTransforms) {
// With ES transforms disabled, we don't want to transform regular class
// field declarations, and we don't need to do any additional tricks to
// reference the constructor for static init, but we still need to transform
// TypeScript field initializers defined as constructor parameters and we
// still need to remove `declare` fields. For now, we run the same code
// path but omit any field information, as if the class had no field
// declarations. In the future, when we fully drop the class fields
// transform, we can simplify this code significantly.
return {
headerInfo,
constructorInitializerStatements,
instanceInitializerNames: [],
staticInitializerNames: [],
constructorInsertPos,
fields: [],
rangesToRemove,
};
} else {
return {
headerInfo,
constructorInitializerStatements,
instanceInitializerNames,
staticInitializerNames,
constructorInsertPos,
fields,
rangesToRemove,
};
}
}

/**
Expand Down
36 changes: 31 additions & 5 deletions test/flow-test.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
import {throws} from "assert";

import {transform} from "../src";
import {transform, Options} from "../src";
import {IMPORT_DEFAULT_PREFIX} from "./prefixes";
import {assertResult} from "./util";

function assertFlowResult(code: string, expectedResult: string): void {
assertResult(code, expectedResult, {transforms: ["jsx", "imports", "flow"]});
function assertFlowResult(
code: string,
expectedResult: string,
options: Partial<Options> = {},
): void {
assertResult(code, expectedResult, {transforms: ["jsx", "imports", "flow"], ...options});
}

function assertFlowESMResult(code: string, expectedResult: string): void {
assertResult(code, expectedResult, {transforms: ["jsx", "flow"]});
function assertFlowESMResult(
code: string,
expectedResult: string,
options: Partial<Options> = {},
): void {
assertResult(code, expectedResult, {transforms: ["jsx", "flow"], ...options});
}

describe("transform flow", () => {
Expand Down Expand Up @@ -427,4 +435,22 @@ describe("transform flow", () => {
`,
);
});

it("properly removes class property variance markers with ES transforms disabled", () => {
assertFlowResult(
`
class C {
+foo: number;
-bar: number;
}
`,
`"use strict";
class C {
foo;
bar;
}
`,
{disableESTransforms: true},
);
});
});
36 changes: 36 additions & 0 deletions test/sucrase-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1363,4 +1363,40 @@ describe("sucrase", () => {
{transforms: [], disableESTransforms: true},
);
});

it("skips class field transform when ES transforms are disabled", () => {
assertResult(
`
class A {
x = 1;
static y = 2;
}
`,
`
class A {
x = 1;
static y = 2;
}
`,
{transforms: [], disableESTransforms: true},
);
});

it("skips class field transform for expression classes when ES transforms are disabled", () => {
assertResult(
`
C = class {
x = 1;
static y = 2;
}
`,
`
C = class {
x = 1;
static y = 2;
}
`,
{transforms: [], disableESTransforms: true},
);
});
});
85 changes: 81 additions & 4 deletions test/typescript-test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type {Options} from "../src/Options";
import {
CREATE_STAR_EXPORT_PREFIX,
ESMODULE_PREFIX,
Expand All @@ -8,12 +9,20 @@ import {
} from "./prefixes";
import {assertResult, devProps} from "./util";

function assertTypeScriptResult(code: string, expectedResult: string): void {
assertResult(code, expectedResult, {transforms: ["jsx", "imports", "typescript"]});
function assertTypeScriptResult(
code: string,
expectedResult: string,
options: Partial<Options> = {},
): void {
assertResult(code, expectedResult, {transforms: ["jsx", "imports", "typescript"], ...options});
}

function assertTypeScriptESMResult(code: string, expectedResult: string): void {
assertResult(code, expectedResult, {transforms: ["jsx", "typescript"]});
function assertTypeScriptESMResult(
code: string,
expectedResult: string,
options: Partial<Options> = {},
): void {
assertResult(code, expectedResult, {transforms: ["jsx", "typescript"], ...options});
}

function assertTypeScriptImportResult(
Expand Down Expand Up @@ -2448,4 +2457,72 @@ describe("typescript transform", () => {
`,
);
});

it("transforms constructor initializers even with disableESTransforms", () => {
assertTypeScriptESMResult(
`
class A extends B {
constructor(readonly x, private y = 2, z: number = 3) {
console.log("Hello");
super();
console.log("World");
}
}
`,
`
class A extends B {
constructor( x, y = 2, z = 3) {
console.log("Hello");
super();this.x = x;this.y = y;;
console.log("World");
}
}
`,
{disableESTransforms: true},
);
});

it("removes types from class fields with disableESTransforms", () => {
assertTypeScriptESMResult(
`
class A {
x: number = 3;
static y: string = "Hello";
z: boolean;
static s: unknown;
}
`,
`
class A {
x = 3;
static y = "Hello";
z;
static s;
}
`,
{disableESTransforms: true},
);
});

it("removes declare fields with disableESTransforms", () => {
assertTypeScriptESMResult(
`
class A {
abstract readonly a: number = 3;
declare b: string;
declare static c: string;
static declare d: string;
}
`,
`
class A {
a = 3;
;
;
;
}
`,
{disableESTransforms: true},
);
});
});

0 comments on commit 29970ce

Please sign in to comment.