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