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

Added transform for logical assignment operators #557

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
15 changes: 15 additions & 0 deletions src/HelperManager.ts
Expand Up @@ -121,6 +121,21 @@ const HELPERS = {
return result == null ? true : result;
}
`,
logicalAssign: `
function logicalAssign(obj, prop, op, rhsFn) {
if (op === '||=') {
return obj[prop] || (obj[prop] = rhsFn())
} else if (op === '&&=') {
return obj[prop] && (obj[prop] = rhsFn())
} else if (op === '??=') {
const val = obj[prop];
if (val == null) {
return obj[prop] = rhsFn()
}
return val
}
}
`,
};

export class HelperManager {
Expand Down
4 changes: 4 additions & 0 deletions src/TokenProcessor.ts
Expand Up @@ -238,6 +238,10 @@ export default class TokenProcessor {
}
this.resultCode += "([";
}
if (token.isLogicalAssignStart) {
this.resultCode += this.helperManager.getHelperName("logicalAssign");
this.resultCode += "(";
}
}

private appendTokenSuffix(): void {
Expand Down
3 changes: 3 additions & 0 deletions src/parser/tokenizer/index.ts
Expand Up @@ -105,6 +105,7 @@ export class Token {
this.numNullishCoalesceEnds = 0;
this.isOptionalChainStart = false;
this.isOptionalChainEnd = false;
this.isLogicalAssignStart = false;
this.subscriptStartIndex = null;
this.nullishStartIndex = null;
}
Expand Down Expand Up @@ -135,6 +136,8 @@ export class Token {
isOptionalChainStart: boolean;
// If true, insert a `])` snippet after this token.
isOptionalChainEnd: boolean;
// If true, insert a `logicalAssign(` snippet before this token.
isLogicalAssignStart: boolean;
// Tag for `.`, `?.`, `[`, `?.[`, `(`, and `?.(` to denote the "root" token for this
// subscript chain. This can be used to determine if this chain is an optional chain.
subscriptStartIndex: number | null;
Expand Down
31 changes: 30 additions & 1 deletion src/parser/traverser/expression.ts
Expand Up @@ -59,7 +59,14 @@ import {Scope} from "../tokenizer/state";
import {TokenType, TokenType as tt} from "../tokenizer/types";
import {charCodes} from "../util/charcodes";
import {IS_IDENTIFIER_START} from "../util/identifier";
import {getNextContextId, isFlowEnabled, isJSXEnabled, isTypeScriptEnabled, state} from "./base";
import {
getNextContextId,
input,
isFlowEnabled,
isJSXEnabled,
isTypeScriptEnabled,
state,
} from "./base";
import {
markPriorBindingIdentifier,
parseBindingIdentifier,
Expand Down Expand Up @@ -132,6 +139,9 @@ export function baseParseMaybeAssign(noIn: boolean, isWithinParens: boolean): bo
return false;
}

// In case it's a logical assignment we want to keep track of where it starts.
const startIndex = state.tokens.length;

if (match(tt.parenL) || match(tt.name) || match(tt._yield)) {
state.potentialArrowAt = state.start;
}
Expand All @@ -142,7 +152,26 @@ export function baseParseMaybeAssign(noIn: boolean, isWithinParens: boolean): bo
}
if (state.type & TokenType.IS_ASSIGN) {
next();
const assignTokenIndex = state.tokens.length - 1;
const opToken = state.tokens[assignTokenIndex];

parseMaybeAssign(noIn);

if (opToken.type === tt.assign) {
const opCode = input.slice(opToken.start, opToken.end);

// Check whether the assignment is a logical assignment, and in that case assign
// the needed token properties for the transform
if (["&&=", "||=", "??="].includes(opCode)) {
opToken.rhsEndIndex = state.tokens.length;

// If the LHS is a single token we don't need to use the helper, it can use the simpler
// transform method, e.g. `x &&= y` -> `x && (x = y)`
if (assignTokenIndex - startIndex > 1) {
state.tokens[startIndex].isLogicalAssignStart = true;
}
}
}
return false;
}
return wasArrow;
Expand Down
164 changes: 164 additions & 0 deletions src/transformers/LogicalAssignmentTransformer.ts
@@ -0,0 +1,164 @@
import type {HelperManager} from "../HelperManager";
import type {Token} from "../parser/tokenizer";
import {TokenType as tt} from "../parser/tokenizer/types";
import type TokenProcessor from "../TokenProcessor";
import type RootTransformer from "./RootTransformer";
import Transformer from "./Transformer";

const LOGICAL_OPERATORS = ["&&=", "||=", "??="];

interface ComputedAccess {
// A snapshot of the result just before the `[` of the computed access
snapshot: {
resultCode: string;
tokenIndex: number;
};
// The result code position at the start of the computed property access
start: number;
}

export default class LogicalAssignmentTransformer extends Transformer {
// This stack stores the state needed to transform computed property access
private readonly computedAccessStack: Array<ComputedAccess> = [];
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Switched to a manual stack instead of relying on the JS execution stack. This seemed to avoid some of the competition going on with other processors, but could switch back if you think the old approach is better.


constructor(
readonly rootTransformer: RootTransformer,
readonly tokens: TokenProcessor,
readonly helperManager: HelperManager,
) {
super();
}

process(): boolean {
if (this.tokens.currentToken().contextId !== null) {
return false;
}

// This searches for the start of computed property access e.g. `x[y]`, or `_.x[y = z + f()]`
if (this.tokens.matches1(tt.bracketL)) {
// This access may end put using a logical assignment operator, but
// we don't know yet. We save a snapshot on our stack and then wait
// until we reach the `]` that ends the computed access.
const snapshot = this.tokens.snapshot(); // A snapshot that we use to extract the code within `[]`

this.tokens.copyExpectedToken(tt.bracketL);

const start = this.tokens.getResultCodeIndex();
this.tokens.restoreToSnapshot(snapshot);

this.computedAccessStack.push({snapshot, start});

return false;
}

// This finds the end of the computed property access
if (this.tokens.matches1(tt.bracketR)) {
const stackItem = this.computedAccessStack.pop();
if (!stackItem) {
throw new Error(`Unexpected ']' at ${this.tokens.getResultCodeIndex()}`);
}

// Check if the token after `]` as a logical assignment, if not, we exit
if (!this.tokens.matches1AtIndex(this.tokens.currentIndex() + 1, tt.assign)) {
return false;
}
const op = this.findOpToken(1);
if (!op) {
return false;
}

// Save the result code position just before `]`, so that we can extract the
// contents of the `[]` pair
const end = this.tokens.getResultCodeIndex();

this.tokens.copyExpectedToken(tt.bracketR);

const {snapshot, start} = stackItem;

// This is the fully transformed contents of `[]`. For `obj[x + 1]` this would be `x + 1`
const propAccess = this.tokens.snapshot().resultCode.slice(start, end);

// Skip forward to after the assignment operator and complete the `_logicalAssign()` helper call
snapshot.tokenIndex = this.tokens.currentIndex() + 1;
this.tokens.restoreToSnapshot(snapshot);
this.tokens.appendCode(`, ${propAccess}, '${op.code}', () => `);
this.processRhs(op.token);
this.tokens.appendCode(")");

return true;
}

// This searches for dot property access e.g. `_.key &&=`
if (this.tokens.matches3(tt.dot, tt.name, tt.assign)) {
const op = this.findOpToken(2);
if (!op) {
return false;
}

// As opposed to the computed prop case, this is a lot simpler, because
// we know upfront what tokens can be part of the access on the lhs.

// Skip over the tokens and complete the `_logicalAssign()` helper call
this.tokens.nextToken(); // Skip the tt.dot
const propName = this.tokens.identifierName();
this.tokens.nextToken(); // Skip the tt.name
this.tokens.nextToken(); // Skip the tt.assign
this.tokens.appendCode(`, '${propName}', '${op.code}', () => `);
this.processRhs(op.token);
this.tokens.appendCode(")");

return true;
}

// This searches for plain variable assignment, e.g. `a &&= b`
if (this.tokens.matches2(tt.name, tt.assign)) {
const op = this.findOpToken(1);
if (!op) {
return false;
}

// At this point we know that this is a simple `a &&= b` to assignment, and we can
// use a simple transform to e.g. `a && (a = b)` without using the helper function.

const plainName = this.tokens.identifierName();

this.tokens.copyToken(); // Copy the identifier
this.tokens.nextToken(); // Skip the original assignment operator

if (op.code === "??=") {
// We transform null coalesce ourselves here, e.g. `a != null ? a : (a = b)`
this.tokens.appendCode(` != null ? ${plainName} : (${plainName} =`);
} else {
this.tokens.appendCode(` ${op.code.slice(0, 2)} (${plainName} =`);
}
this.processRhs(op.token);
this.tokens.appendCode(")");

return true;
}

return false;
}

// Checks whether there's a matching logical assignment operator token at provided relative token index
private findOpToken(relativeIndex: number = 0): {token: Token; code: string} | undefined {
const token = this.tokens.tokenAtRelativeIndex(relativeIndex);
const code = this.tokens.rawCodeForToken(token);
if (!LOGICAL_OPERATORS.includes(code)) {
return undefined;
}
return {token, code};
}

// This processes the right hand side of a logical assignment expression. We process
// until the hit the rhsEndIndex as specified by the logical assignment operator token.
private processRhs(token: Token): void {
if (token.rhsEndIndex === null) {
throw new Error("Unknown end of logical assignment, this is a bug in Sucrase");
}

while (this.tokens.currentIndex() < token.rhsEndIndex) {
this.rootTransformer.processToken();
}
}
}
4 changes: 4 additions & 0 deletions src/transformers/RootTransformer.ts
Expand Up @@ -9,6 +9,7 @@ import CJSImportTransformer from "./CJSImportTransformer";
import ESMImportTransformer from "./ESMImportTransformer";
import FlowTransformer from "./FlowTransformer";
import JSXTransformer from "./JSXTransformer";
import LogicalAssignmentTransformer from "./LogicalAssignmentTransformer";
import NumericSeparatorTransformer from "./NumericSeparatorTransformer";
import OptionalCatchBindingTransformer from "./OptionalCatchBindingTransformer";
import OptionalChainingNullishTransformer from "./OptionalChainingNullishTransformer";
Expand Down Expand Up @@ -39,6 +40,9 @@ export default class RootTransformer {
this.isImportsTransformEnabled = transforms.includes("imports");
this.isReactHotLoaderTransformEnabled = transforms.includes("react-hot-loader");

this.transformers.push(
new LogicalAssignmentTransformer(this, tokenProcessor, this.helperManager),
);
this.transformers.push(
new OptionalChainingNullishTransformer(tokenProcessor, this.nameManager),
);
Expand Down
30 changes: 15 additions & 15 deletions test/index-test.ts
Expand Up @@ -13,21 +13,21 @@ if (foo) {
{transforms: ["jsx", "imports"]},
),
`\
Location Label Raw contextualKeyword scopeDepth isType identifierRole shadowsGlobal contextId rhsEndIndex isExpression numNullishCoalesceStarts numNullishCoalesceEnds isOptionalChainStart isOptionalChainEnd subscriptStartIndex nullishStartIndex
1:1-1:3 if if 0 0 0 0
1:4-1:5 ( ( 0 0 0 0
1:5-1:8 name foo 0 0 0 0 0
1:8-1:9 ) ) 0 0 0 0
1:10-1:11 { { 0 1 0 0
2:3-2:10 name console 0 1 0 0 0
2:10-2:11 . . 0 1 0 0 5
2:11-2:14 name log 0 1 0 0
2:14-2:15 ( ( 0 1 1 0 0 5
2:15-2:29 string 'Hello world!' 0 1 0 0
2:29-2:30 ) ) 0 1 1 0 0
2:30-2:31 ; ; 0 1 0 0
3:1-3:2 } } 0 1 0 0
3:2-3:2 eof 0 0 0 0 `,
Location Label Raw contextualKeyword scopeDepth isType identifierRole shadowsGlobal contextId rhsEndIndex isExpression numNullishCoalesceStarts numNullishCoalesceEnds isOptionalChainStart isOptionalChainEnd isLogicalAssignStart subscriptStartIndex nullishStartIndex
1:1-1:3 if if 0 0 0 0
1:4-1:5 ( ( 0 0 0 0
1:5-1:8 name foo 0 0 0 0 0
1:8-1:9 ) ) 0 0 0 0
1:10-1:11 { { 0 1 0 0
2:3-2:10 name console 0 1 0 0 0
2:10-2:11 . . 0 1 0 0 5
2:11-2:14 name log 0 1 0 0
2:14-2:15 ( ( 0 1 1 0 0 5
2:15-2:29 string 'Hello world!' 0 1 0 0
2:29-2:30 ) ) 0 1 1 0 0
2:30-2:31 ; ; 0 1 0 0
3:1-3:2 } } 0 1 0 0
3:2-3:2 eof 0 0 0 0 `,
);
});
});
5 changes: 5 additions & 0 deletions test/prefixes.ts
Expand Up @@ -43,3 +43,8 @@ export const OPTIONAL_CHAIN_DELETE_PREFIX = ` function _optionalChainDelete(ops)
const result = _optionalChain(ops); return result == null ? true : result; }`;
export const ASYNC_OPTIONAL_CHAIN_DELETE_PREFIX = ` async function _asyncOptionalChainDelete(ops) { \
const result = await _asyncOptionalChain(ops); return result == null ? true : result; }`;

export const LOGICAL_ASSIGN_PREFIX = ` function _logicalAssign(obj, prop, op, rhsFn) { \
if (op === '||=') { return obj[prop] || (obj[prop] = rhsFn()) } else if (op === '&&=') { \
return obj[prop] && (obj[prop] = rhsFn()) } else if (op === '??=') { \
const val = obj[prop]; if (val == null) { return obj[prop] = rhsFn() } return val } }`;