156 lines
5.9 KiB
JavaScript
156 lines
5.9 KiB
JavaScript
|
|
||
|
import {TokenType as tt} from "../parser/tokenizer/types";
|
||
|
|
||
|
import Transformer from "./Transformer";
|
||
|
|
||
|
/**
|
||
|
* Transformer supporting the optional chaining and nullish coalescing operators.
|
||
|
*
|
||
|
* Tech plan here:
|
||
|
* https://github.com/alangpierce/sucrase/wiki/Sucrase-Optional-Chaining-and-Nullish-Coalescing-Technical-Plan
|
||
|
*
|
||
|
* The prefix and suffix code snippets are handled by TokenProcessor, and this transformer handles
|
||
|
* the operators themselves.
|
||
|
*/
|
||
|
export default class OptionalChainingNullishTransformer extends Transformer {
|
||
|
constructor( tokens, nameManager) {
|
||
|
super();this.tokens = tokens;this.nameManager = nameManager;;
|
||
|
}
|
||
|
|
||
|
process() {
|
||
|
if (this.tokens.matches1(tt.nullishCoalescing)) {
|
||
|
const token = this.tokens.currentToken();
|
||
|
if (this.tokens.tokens[token.nullishStartIndex].isAsyncOperation) {
|
||
|
this.tokens.replaceTokenTrimmingLeftWhitespace(", async () => (");
|
||
|
} else {
|
||
|
this.tokens.replaceTokenTrimmingLeftWhitespace(", () => (");
|
||
|
}
|
||
|
return true;
|
||
|
}
|
||
|
if (this.tokens.matches1(tt._delete)) {
|
||
|
const nextToken = this.tokens.tokenAtRelativeIndex(1);
|
||
|
if (nextToken.isOptionalChainStart) {
|
||
|
this.tokens.removeInitialToken();
|
||
|
return true;
|
||
|
}
|
||
|
}
|
||
|
const token = this.tokens.currentToken();
|
||
|
const chainStart = token.subscriptStartIndex;
|
||
|
if (
|
||
|
chainStart != null &&
|
||
|
this.tokens.tokens[chainStart].isOptionalChainStart &&
|
||
|
// Super subscripts can't be optional (since super is never null/undefined), and the syntax
|
||
|
// relies on the subscript being intact, so leave this token alone.
|
||
|
this.tokens.tokenAtRelativeIndex(-1).type !== tt._super
|
||
|
) {
|
||
|
const param = this.nameManager.claimFreeName("_");
|
||
|
let arrowStartSnippet;
|
||
|
if (
|
||
|
chainStart > 0 &&
|
||
|
this.tokens.matches1AtIndex(chainStart - 1, tt._delete) &&
|
||
|
this.isLastSubscriptInChain()
|
||
|
) {
|
||
|
// Delete operations are special: we already removed the delete keyword, and to still
|
||
|
// perform a delete, we need to insert a delete in the very last part of the chain, which
|
||
|
// in correct code will always be a property access.
|
||
|
arrowStartSnippet = `${param} => delete ${param}`;
|
||
|
} else {
|
||
|
arrowStartSnippet = `${param} => ${param}`;
|
||
|
}
|
||
|
if (this.tokens.tokens[chainStart].isAsyncOperation) {
|
||
|
arrowStartSnippet = `async ${arrowStartSnippet}`;
|
||
|
}
|
||
|
if (
|
||
|
this.tokens.matches2(tt.questionDot, tt.parenL) ||
|
||
|
this.tokens.matches2(tt.questionDot, tt.lessThan)
|
||
|
) {
|
||
|
if (this.justSkippedSuper()) {
|
||
|
this.tokens.appendCode(".bind(this)");
|
||
|
}
|
||
|
this.tokens.replaceTokenTrimmingLeftWhitespace(`, 'optionalCall', ${arrowStartSnippet}`);
|
||
|
} else if (this.tokens.matches2(tt.questionDot, tt.bracketL)) {
|
||
|
this.tokens.replaceTokenTrimmingLeftWhitespace(`, 'optionalAccess', ${arrowStartSnippet}`);
|
||
|
} else if (this.tokens.matches1(tt.questionDot)) {
|
||
|
this.tokens.replaceTokenTrimmingLeftWhitespace(`, 'optionalAccess', ${arrowStartSnippet}.`);
|
||
|
} else if (this.tokens.matches1(tt.dot)) {
|
||
|
this.tokens.replaceTokenTrimmingLeftWhitespace(`, 'access', ${arrowStartSnippet}.`);
|
||
|
} else if (this.tokens.matches1(tt.bracketL)) {
|
||
|
this.tokens.replaceTokenTrimmingLeftWhitespace(`, 'access', ${arrowStartSnippet}[`);
|
||
|
} else if (this.tokens.matches1(tt.parenL)) {
|
||
|
if (this.justSkippedSuper()) {
|
||
|
this.tokens.appendCode(".bind(this)");
|
||
|
}
|
||
|
this.tokens.replaceTokenTrimmingLeftWhitespace(`, 'call', ${arrowStartSnippet}(`);
|
||
|
} else {
|
||
|
throw new Error("Unexpected subscript operator in optional chain.");
|
||
|
}
|
||
|
return true;
|
||
|
}
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Determine if the current token is the last of its chain, so that we know whether it's eligible
|
||
|
* to have a delete op inserted.
|
||
|
*
|
||
|
* We can do this by walking forward until we determine one way or another. Each
|
||
|
* isOptionalChainStart token must be paired with exactly one isOptionalChainEnd token after it in
|
||
|
* a nesting way, so we can track depth and walk to the end of the chain (the point where the
|
||
|
* depth goes negative) and see if any other subscript token is after us in the chain.
|
||
|
*/
|
||
|
isLastSubscriptInChain() {
|
||
|
let depth = 0;
|
||
|
for (let i = this.tokens.currentIndex() + 1; ; i++) {
|
||
|
if (i >= this.tokens.tokens.length) {
|
||
|
throw new Error("Reached the end of the code while finding the end of the access chain.");
|
||
|
}
|
||
|
if (this.tokens.tokens[i].isOptionalChainStart) {
|
||
|
depth++;
|
||
|
} else if (this.tokens.tokens[i].isOptionalChainEnd) {
|
||
|
depth--;
|
||
|
}
|
||
|
if (depth < 0) {
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
// This subscript token is a later one in the same chain.
|
||
|
if (depth === 0 && this.tokens.tokens[i].subscriptStartIndex != null) {
|
||
|
return false;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Determine if we are the open-paren in an expression like super.a()?.b.
|
||
|
*
|
||
|
* We can do this by walking backward to find the previous subscript. If that subscript was
|
||
|
* preceded by a super, then we must be the subscript after it, so if this is a call expression,
|
||
|
* we'll need to attach the right context.
|
||
|
*/
|
||
|
justSkippedSuper() {
|
||
|
let depth = 0;
|
||
|
let index = this.tokens.currentIndex() - 1;
|
||
|
while (true) {
|
||
|
if (index < 0) {
|
||
|
throw new Error(
|
||
|
"Reached the start of the code while finding the start of the access chain.",
|
||
|
);
|
||
|
}
|
||
|
if (this.tokens.tokens[index].isOptionalChainStart) {
|
||
|
depth--;
|
||
|
} else if (this.tokens.tokens[index].isOptionalChainEnd) {
|
||
|
depth++;
|
||
|
}
|
||
|
if (depth < 0) {
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
// This subscript token is a later one in the same chain.
|
||
|
if (depth === 0 && this.tokens.tokens[index].subscriptStartIndex != null) {
|
||
|
return this.tokens.tokens[index - 1].type === tt._super;
|
||
|
}
|
||
|
index--;
|
||
|
}
|
||
|
}
|
||
|
}
|