-
Notifications
You must be signed in to change notification settings - Fork 139
/
OptionalChainingNullishTransformer.ts
99 lines (95 loc) · 4.08 KB
/
OptionalChainingNullishTransformer.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
import NameManager from "../NameManager";
import {TokenType as tt} from "../parser/tokenizer/types";
import TokenProcessor from "../TokenProcessor";
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(readonly tokens: TokenProcessor, readonly nameManager: NameManager) {
super();
}
process(): boolean {
if (this.tokens.matches1(tt.nullishCoalescing)) {
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) {
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.matches2(tt.questionDot, tt.parenL)) {
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)) {
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(): boolean {
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;
}
}
}
}