• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/* @internal */
2namespace ts.formatting {
3    const standardScanner = createScanner(ScriptTarget.Latest, /*skipTrivia*/ false, LanguageVariant.Standard);
4    const jsxScanner = createScanner(ScriptTarget.Latest, /*skipTrivia*/ false, LanguageVariant.JSX);
5
6    export interface FormattingScanner {
7        advance(): void;
8        getStartPos(): number;
9        isOnToken(): boolean;
10        isOnEOF(): boolean;
11        readTokenInfo(n: Node): TokenInfo;
12        readEOFTokenRange(): TextRangeWithKind;
13        getCurrentLeadingTrivia(): TextRangeWithKind[] | undefined;
14        lastTrailingTriviaWasNewLine(): boolean;
15        skipToEndOf(node: Node | NodeArray<Node>): void;
16        skipToStartOf(node: Node): void;
17    }
18
19    const enum ScanAction {
20        Scan,
21        RescanGreaterThanToken,
22        RescanSlashToken,
23        RescanTemplateToken,
24        RescanJsxIdentifier,
25        RescanJsxText,
26        RescanJsxAttributeValue,
27    }
28
29    export function getFormattingScanner<T>(text: string, languageVariant: LanguageVariant, startPos: number, endPos: number, cb: (scanner: FormattingScanner) => T): T {
30        const scanner = languageVariant === LanguageVariant.JSX ? jsxScanner : standardScanner;
31
32        scanner.setText(text);
33        scanner.setTextPos(startPos);
34
35        let wasNewLine = true;
36        let leadingTrivia: TextRangeWithTriviaKind[] | undefined;
37        let trailingTrivia: TextRangeWithTriviaKind[] | undefined;
38
39        let savedPos: number;
40        let lastScanAction: ScanAction | undefined;
41        let lastTokenInfo: TokenInfo | undefined;
42
43        const res = cb({
44            advance,
45            readTokenInfo,
46            readEOFTokenRange,
47            isOnToken,
48            isOnEOF,
49            getCurrentLeadingTrivia: () => leadingTrivia,
50            lastTrailingTriviaWasNewLine: () => wasNewLine,
51            skipToEndOf,
52            skipToStartOf,
53            getStartPos: () => lastTokenInfo?.token.pos ?? scanner.getTokenPos(),
54        });
55
56        lastTokenInfo = undefined;
57        scanner.setText(undefined);
58
59        return res;
60
61        function advance(): void {
62            lastTokenInfo = undefined;
63            const isStarted = scanner.getStartPos() !== startPos;
64
65            if (isStarted) {
66                wasNewLine = !!trailingTrivia && last(trailingTrivia).kind === SyntaxKind.NewLineTrivia;
67            }
68            else {
69                scanner.scan();
70            }
71
72            leadingTrivia = undefined;
73            trailingTrivia = undefined;
74
75            let pos = scanner.getStartPos();
76
77            // Read leading trivia and token
78            while (pos < endPos) {
79                const t = scanner.getToken();
80                if (!isTrivia(t)) {
81                    break;
82                }
83
84                // consume leading trivia
85                scanner.scan();
86                const item: TextRangeWithTriviaKind = {
87                    pos,
88                    end: scanner.getStartPos(),
89                    kind: t
90                };
91
92                pos = scanner.getStartPos();
93
94                leadingTrivia = append(leadingTrivia, item);
95            }
96
97            savedPos = scanner.getStartPos();
98        }
99
100        function shouldRescanGreaterThanToken(node: Node): boolean {
101            switch (node.kind) {
102                case SyntaxKind.GreaterThanEqualsToken:
103                case SyntaxKind.GreaterThanGreaterThanEqualsToken:
104                case SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken:
105                case SyntaxKind.GreaterThanGreaterThanGreaterThanToken:
106                case SyntaxKind.GreaterThanGreaterThanToken:
107                    return true;
108            }
109
110            return false;
111        }
112
113        function shouldRescanJsxIdentifier(node: Node): boolean {
114            if (node.parent) {
115                switch (node.parent.kind) {
116                    case SyntaxKind.JsxAttribute:
117                    case SyntaxKind.JsxOpeningElement:
118                    case SyntaxKind.JsxClosingElement:
119                    case SyntaxKind.JsxSelfClosingElement:
120                        // May parse an identifier like `module-layout`; that will be scanned as a keyword at first, but we should parse the whole thing to get an identifier.
121                        return isKeyword(node.kind) || node.kind === SyntaxKind.Identifier;
122                }
123            }
124
125            return false;
126        }
127
128        function shouldRescanJsxText(node: Node): boolean {
129            return isJsxText(node) || isJsxElement(node) && lastTokenInfo?.token.kind === SyntaxKind.JsxText;
130        }
131
132        function shouldRescanSlashToken(container: Node): boolean {
133            return container.kind === SyntaxKind.RegularExpressionLiteral;
134        }
135
136        function shouldRescanTemplateToken(container: Node): boolean {
137            return container.kind === SyntaxKind.TemplateMiddle ||
138                container.kind === SyntaxKind.TemplateTail;
139        }
140
141        function shouldRescanJsxAttributeValue(node: Node): boolean {
142            return node.parent && isJsxAttribute(node.parent) && node.parent.initializer === node;
143        }
144
145        function startsWithSlashToken(t: SyntaxKind): boolean {
146            return t === SyntaxKind.SlashToken || t === SyntaxKind.SlashEqualsToken;
147        }
148
149        function readTokenInfo(n: Node): TokenInfo {
150            Debug.assert(isOnToken());
151
152            // normally scanner returns the smallest available token
153            // check the kind of context node to determine if scanner should have more greedy behavior and consume more text.
154            const expectedScanAction = shouldRescanGreaterThanToken(n) ? ScanAction.RescanGreaterThanToken :
155                shouldRescanSlashToken(n) ? ScanAction.RescanSlashToken :
156                shouldRescanTemplateToken(n) ? ScanAction.RescanTemplateToken :
157                shouldRescanJsxIdentifier(n) ? ScanAction.RescanJsxIdentifier :
158                shouldRescanJsxText(n) ? ScanAction.RescanJsxText :
159                shouldRescanJsxAttributeValue(n) ? ScanAction.RescanJsxAttributeValue :
160                ScanAction.Scan;
161
162            if (lastTokenInfo && expectedScanAction === lastScanAction) {
163                // readTokenInfo was called before with the same expected scan action.
164                // No need to re-scan text, return existing 'lastTokenInfo'
165                // it is ok to call fixTokenKind here since it does not affect
166                // what portion of text is consumed. In contrast rescanning can change it,
167                // i.e. for '>=' when originally scanner eats just one character
168                // and rescanning forces it to consume more.
169                return fixTokenKind(lastTokenInfo, n);
170            }
171
172            if (scanner.getStartPos() !== savedPos) {
173                Debug.assert(lastTokenInfo !== undefined);
174                // readTokenInfo was called before but scan action differs - rescan text
175                scanner.setTextPos(savedPos);
176                scanner.scan();
177            }
178
179            let currentToken = getNextToken(n, expectedScanAction);
180
181            const token = createTextRangeWithKind(
182                scanner.getStartPos(),
183                scanner.getTextPos(),
184                currentToken,
185            );
186
187            // consume trailing trivia
188            if (trailingTrivia) {
189                trailingTrivia = undefined;
190            }
191            while (scanner.getStartPos() < endPos) {
192                currentToken = scanner.scan();
193                if (!isTrivia(currentToken)) {
194                    break;
195                }
196                const trivia = createTextRangeWithKind(
197                    scanner.getStartPos(),
198                    scanner.getTextPos(),
199                    currentToken,
200                );
201
202                if (!trailingTrivia) {
203                    trailingTrivia = [];
204                }
205
206                trailingTrivia.push(trivia);
207
208                if (currentToken === SyntaxKind.NewLineTrivia) {
209                    // move past new line
210                    scanner.scan();
211                    break;
212                }
213            }
214
215            lastTokenInfo = { leadingTrivia, trailingTrivia, token };
216
217            return fixTokenKind(lastTokenInfo, n);
218        }
219
220        function getNextToken(n: Node, expectedScanAction: ScanAction): SyntaxKind {
221            const token = scanner.getToken();
222            lastScanAction = ScanAction.Scan;
223            switch (expectedScanAction) {
224                case ScanAction.RescanGreaterThanToken:
225                    if (token === SyntaxKind.GreaterThanToken) {
226                        lastScanAction = ScanAction.RescanGreaterThanToken;
227                        const newToken = scanner.reScanGreaterToken();
228                        Debug.assert(n.kind === newToken);
229                        return newToken;
230                    }
231                    break;
232                case ScanAction.RescanSlashToken:
233                    if (startsWithSlashToken(token)) {
234                        lastScanAction = ScanAction.RescanSlashToken;
235                        const newToken = scanner.reScanSlashToken();
236                        Debug.assert(n.kind === newToken);
237                        return newToken;
238                    }
239                    break;
240                case ScanAction.RescanTemplateToken:
241                    if (token === SyntaxKind.CloseBraceToken) {
242                        lastScanAction = ScanAction.RescanTemplateToken;
243                        return scanner.reScanTemplateToken(/* isTaggedTemplate */ false);
244                    }
245                    break;
246                case ScanAction.RescanJsxIdentifier:
247                    lastScanAction = ScanAction.RescanJsxIdentifier;
248                    return scanner.scanJsxIdentifier();
249                case ScanAction.RescanJsxText:
250                    lastScanAction = ScanAction.RescanJsxText;
251                    return scanner.reScanJsxToken(/* allowMultilineJsxText */ false);
252                case ScanAction.RescanJsxAttributeValue:
253                    lastScanAction = ScanAction.RescanJsxAttributeValue;
254                    return scanner.reScanJsxAttributeValue();
255                case ScanAction.Scan:
256                    break;
257                default:
258                    Debug.assertNever(expectedScanAction);
259            }
260            return token;
261        }
262
263        function readEOFTokenRange(): TextRangeWithKind<SyntaxKind.EndOfFileToken> {
264            Debug.assert(isOnEOF());
265            return createTextRangeWithKind(scanner.getStartPos(), scanner.getTextPos(), SyntaxKind.EndOfFileToken);
266        }
267
268        function isOnToken(): boolean {
269            const current = lastTokenInfo ? lastTokenInfo.token.kind : scanner.getToken();
270            return current !== SyntaxKind.EndOfFileToken && !isTrivia(current);
271        }
272
273        function isOnEOF(): boolean {
274            const current = lastTokenInfo ? lastTokenInfo.token.kind : scanner.getToken();
275            return current === SyntaxKind.EndOfFileToken;
276        }
277
278        // when containing node in the tree is token
279        // but its kind differs from the kind that was returned by the scanner,
280        // then kind needs to be fixed. This might happen in cases
281        // when parser interprets token differently, i.e keyword treated as identifier
282        function fixTokenKind(tokenInfo: TokenInfo, container: Node): TokenInfo {
283            if (isToken(container) && tokenInfo.token.kind !== container.kind) {
284                tokenInfo.token.kind = container.kind;
285            }
286            return tokenInfo;
287        }
288
289        function skipToEndOf(node: Node | NodeArray<Node>): void {
290            scanner.setTextPos(node.end);
291            savedPos = scanner.getStartPos();
292            lastScanAction = undefined;
293            lastTokenInfo = undefined;
294            wasNewLine = false;
295            leadingTrivia = undefined;
296            trailingTrivia = undefined;
297        }
298
299        function skipToStartOf(node: Node): void {
300            scanner.setTextPos(node.pos);
301            savedPos = scanner.getStartPos();
302            lastScanAction = undefined;
303            lastTokenInfo = undefined;
304            wasNewLine = false;
305            leadingTrivia = undefined;
306            trailingTrivia = undefined;
307        }
308    }
309}
310