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 7, 2020
1 parent b3ce7fd commit 2ce4b07
Show file tree
Hide file tree
Showing 9 changed files with 236 additions and 25 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
133 changes: 133 additions & 0 deletions src/transformers/LogicalAssignmentTransformer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
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 = ["&&=", "||=", "??="];

export default class LogicalAssignmentTransformer extends Transformer {
constructor(
readonly rootTransformer: RootTransformer,
readonly tokens: TokenProcessor,
readonly helperManager: HelperManager,
) {
super();
}

process(): boolean {
if (this.tokens.matches2(tt.name, tt.bracketL)) {
// This searches for computed property access e.g. `_.x[y] &&=`, or `_.x[y = z + f()] &&=`

// At this point we may end up at a logical assignment operator but
// we don't know yet. We continue processing the code as usual, but save
// some information that we will need later in case it is.
// this.rootTransformer.processToken();
this.tokens.copyExpectedToken(tt.name);
const snapshot = this.tokens.snapshot(); // A snapshot that we use to extract the code within `[]`
this.rootTransformer.processToken();
// this.tokens.copyExpectedToken(tt.bracketL);
const start = this.tokens.getResultCodeIndex(); // code position just after `[`
this.rootTransformer.processBalancedCode();
const end = this.tokens.getResultCodeIndex(); // code position just before `]`
this.rootTransformer.processToken();
// this.tokens.copyExpectedToken(tt.bracketR);

if (!this.tokens.matches1(tt.assign)) {
return false;
}

const op = this.findOpToken();
if (!op) {
return false;
}

const propAccess = this.tokens.snapshot().resultCode.slice(start, end);

// Skip forward to after the assignment operator
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;
} else if (this.tokens.matches3(tt.dot, tt.name, tt.assign)) {
// This searches for dot property access e.g. `_.key &&=`

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.

// We skip over the tokens and extract the name to ensure
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(")");
} else if (this.tokens.matches2(tt.name, tt.assign)) {
// This searches for plain variable assignment, e.g. `a &&= b`

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();
}
}
}
13 changes: 13 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 Expand Up @@ -133,6 +137,7 @@ export default class RootTransformer {
processBalancedCode(): void {
let braceDepth = 0;
let parenDepth = 0;
let bracketDepth = 0;
while (!this.tokens.isAtEnd()) {
if (this.tokens.matches1(tt.braceL) || this.tokens.matches1(tt.dollarBraceL)) {
braceDepth++;
Expand All @@ -150,6 +155,14 @@ export default class RootTransformer {
}
parenDepth--;
}
if (this.tokens.matches1(tt.bracketL)) {
bracketDepth++;
} else if (this.tokens.matches1(tt.bracketR)) {
if (bracketDepth === 0) {
return;
}
bracketDepth--;
}
this.processToken();
}
}
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 } }`;
27 changes: 18 additions & 9 deletions test/sucrase-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
ASYNC_OPTIONAL_CHAIN_PREFIX,
ESMODULE_PREFIX,
IMPORT_DEFAULT_PREFIX,
LOGICAL_ASSIGN_PREFIX,
NULLISH_COALESCE_PREFIX,
OPTIONAL_CHAIN_DELETE_PREFIX,
OPTIONAL_CHAIN_PREFIX,
Expand Down Expand Up @@ -515,17 +516,25 @@ describe("sucrase", () => {
);
});

it("handles logical assignment operators", () => {
it("transforms logical assignment operations", () => {
assertResult(
`
a &&= b;
c ||= d;
e ??= f;
`,
`"use strict";
a &&= b;
c ||= d;
e ??= f;
x1 &&= y1
x2 ??= z?.y2()
y/* */ . /* ok */x3 ||= (x3a &&= y3b)
y.x['x4'] ??= y4
x5 ??= await y5
y6a[y6b[x6] &&= z6a] ||= z6b
x ??= y ??= z ?? w
`,
`"use strict";${NULLISH_COALESCE_PREFIX}${OPTIONAL_CHAIN_PREFIX}${LOGICAL_ASSIGN_PREFIX}
x1 && (x1 = y1)
x2 != null ? x2 : (x2 = _optionalChain([z, 'optionalAccess', _ => _.y2, 'call', _2 => _2()]))
_logicalAssign(y, 'x3', '||=', () => (x3a && (x3a = y3b)))
_logicalAssign(y.x, 'x4', '??=', () => y4)
x5 != null ? x5 : (x5 = await y5)
_logicalAssign(y6a, _logicalAssign(y6b, x6, '&&=', () => z6a), '||=', () => z6b)
x != null ? x : (x = y != null ? y : (y = _nullishCoalesce(z, () => ( w))))
`,
{transforms: ["jsx", "imports", "typescript"]},
);
Expand Down

0 comments on commit 2ce4b07

Please sign in to comment.