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