-
Notifications
You must be signed in to change notification settings - Fork 138
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
Closed
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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.