Skip to content

Commit

Permalink
Added transform for logical assignment operators
Browse files Browse the repository at this point in the history
  • Loading branch information
Rugvip committed Oct 10, 2020
1 parent b3ce7fd commit ae5f5cd
Show file tree
Hide file tree
Showing 9 changed files with 283 additions and 24 deletions.
15 changes: 15 additions & 0 deletions src/HelperManager.ts
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,14 @@ import {
import {ContextualKeyword} from "../tokenizer/keywords";
import {Scope} from "../tokenizer/state";
import {TokenType, TokenType as tt} from "../tokenizer/types";
import {getNextContextId, isFlowEnabled, isJSXEnabled, isTypeScriptEnabled, state} from "./base";
import {
getNextContextId,
input,
isFlowEnabled,
isJSXEnabled,
isTypeScriptEnabled,
state,
} from "./base";
import {
markPriorBindingIdentifier,
parseBindingIdentifier,
Expand Down Expand Up @@ -129,6 +136,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 @@ -139,7 +149,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
Original file line number Diff line number Diff line change
@@ -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> = [];

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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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 } }`;

0 comments on commit ae5f5cd

Please sign in to comment.