package scanner import ( "fmt" "iter" "maps" "strconv" "strings" "unicode" "unicode/utf16" "unicode/utf8" "github.com/microsoft/typescript-go/internal/ast" "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/debug" "github.com/microsoft/typescript-go/internal/diagnostics" "github.com/microsoft/typescript-go/internal/jsnum" "github.com/microsoft/typescript-go/internal/stringutil" ) type EscapeSequenceScanningFlags int32 const ( EscapeSequenceScanningFlagsString EscapeSequenceScanningFlags = 1 << 0 EscapeSequenceScanningFlagsReportErrors EscapeSequenceScanningFlags = 1 << 1 EscapeSequenceScanningFlagsRegularExpression EscapeSequenceScanningFlags = 1 << 2 EscapeSequenceScanningFlagsAnnexB EscapeSequenceScanningFlags = 1 << 3 EscapeSequenceScanningFlagsAnyUnicodeMode EscapeSequenceScanningFlags = 1 << 4 EscapeSequenceScanningFlagsAtomEscape EscapeSequenceScanningFlags = 1 << 5 EscapeSequenceScanningFlagsReportInvalidEscapeErrors EscapeSequenceScanningFlags = EscapeSequenceScanningFlagsRegularExpression | EscapeSequenceScanningFlagsReportErrors EscapeSequenceScanningFlagsAllowExtendedUnicodeEscape EscapeSequenceScanningFlags = EscapeSequenceScanningFlagsString | EscapeSequenceScanningFlagsAnyUnicodeMode ) type ErrorCallback func(diagnostic *diagnostics.Message, start, length int, args ...any) var textToKeyword = map[string]ast.Kind{ "abstract": ast.KindAbstractKeyword, "accessor": ast.KindAccessorKeyword, "any": ast.KindAnyKeyword, "as": ast.KindAsKeyword, "asserts": ast.KindAssertsKeyword, "assert": ast.KindAssertKeyword, "bigint": ast.KindBigIntKeyword, "boolean": ast.KindBooleanKeyword, "break": ast.KindBreakKeyword, "case": ast.KindCaseKeyword, "catch": ast.KindCatchKeyword, "class": ast.KindClassKeyword, "continue": ast.KindContinueKeyword, "const": ast.KindConstKeyword, "constructor": ast.KindConstructorKeyword, "debugger": ast.KindDebuggerKeyword, "declare": ast.KindDeclareKeyword, "default": ast.KindDefaultKeyword, "defer": ast.KindDeferKeyword, "delete": ast.KindDeleteKeyword, "do": ast.KindDoKeyword, "else": ast.KindElseKeyword, "enum": ast.KindEnumKeyword, "export": ast.KindExportKeyword, "extends": ast.KindExtendsKeyword, "false": ast.KindFalseKeyword, "finally": ast.KindFinallyKeyword, "for": ast.KindForKeyword, "from": ast.KindFromKeyword, "function": ast.KindFunctionKeyword, "get": ast.KindGetKeyword, "if": ast.KindIfKeyword, "immediate": ast.KindImmediateKeyword, "implements": ast.KindImplementsKeyword, "import": ast.KindImportKeyword, "in": ast.KindInKeyword, "infer": ast.KindInferKeyword, "instanceof": ast.KindInstanceOfKeyword, "interface": ast.KindInterfaceKeyword, "intrinsic": ast.KindIntrinsicKeyword, "is": ast.KindIsKeyword, "keyof": ast.KindKeyOfKeyword, "let": ast.KindLetKeyword, "module": ast.KindModuleKeyword, "namespace": ast.KindNamespaceKeyword, "never": ast.KindNeverKeyword, "new": ast.KindNewKeyword, "null": ast.KindNullKeyword, "number": ast.KindNumberKeyword, "object": ast.KindObjectKeyword, "package": ast.KindPackageKeyword, "private": ast.KindPrivateKeyword, "protected": ast.KindProtectedKeyword, "public": ast.KindPublicKeyword, "override": ast.KindOverrideKeyword, "out": ast.KindOutKeyword, "readonly": ast.KindReadonlyKeyword, "require": ast.KindRequireKeyword, "global": ast.KindGlobalKeyword, "return": ast.KindReturnKeyword, "satisfies": ast.KindSatisfiesKeyword, "set": ast.KindSetKeyword, "static": ast.KindStaticKeyword, "string": ast.KindStringKeyword, "super": ast.KindSuperKeyword, "switch": ast.KindSwitchKeyword, "symbol": ast.KindSymbolKeyword, "this": ast.KindThisKeyword, "throw": ast.KindThrowKeyword, "true": ast.KindTrueKeyword, "try": ast.KindTryKeyword, "type": ast.KindTypeKeyword, "typeof": ast.KindTypeOfKeyword, "undefined": ast.KindUndefinedKeyword, "unique": ast.KindUniqueKeyword, "unknown": ast.KindUnknownKeyword, "using": ast.KindUsingKeyword, "var": ast.KindVarKeyword, "void": ast.KindVoidKeyword, "while": ast.KindWhileKeyword, "with": ast.KindWithKeyword, "yield": ast.KindYieldKeyword, "async": ast.KindAsyncKeyword, "await": ast.KindAwaitKeyword, "of": ast.KindOfKeyword, } var textToToken = func() map[string]ast.Kind { m := map[string]ast.Kind{ "{": ast.KindOpenBraceToken, "}": ast.KindCloseBraceToken, "(": ast.KindOpenParenToken, ")": ast.KindCloseParenToken, "[": ast.KindOpenBracketToken, "]": ast.KindCloseBracketToken, ".": ast.KindDotToken, "...": ast.KindDotDotDotToken, ";": ast.KindSemicolonToken, ",": ast.KindCommaToken, "<": ast.KindLessThanToken, ">": ast.KindGreaterThanToken, "<=": ast.KindLessThanEqualsToken, ">=": ast.KindGreaterThanEqualsToken, "==": ast.KindEqualsEqualsToken, "!=": ast.KindExclamationEqualsToken, "===": ast.KindEqualsEqualsEqualsToken, "!==": ast.KindExclamationEqualsEqualsToken, "=>": ast.KindEqualsGreaterThanToken, "+": ast.KindPlusToken, "-": ast.KindMinusToken, "**": ast.KindAsteriskAsteriskToken, "*": ast.KindAsteriskToken, "/": ast.KindSlashToken, "%": ast.KindPercentToken, "++": ast.KindPlusPlusToken, "--": ast.KindMinusMinusToken, "<<": ast.KindLessThanLessThanToken, ">": ast.KindGreaterThanGreaterThanToken, ">>>": ast.KindGreaterThanGreaterThanGreaterThanToken, "&": ast.KindAmpersandToken, "|": ast.KindBarToken, "^": ast.KindCaretToken, "!": ast.KindExclamationToken, "~": ast.KindTildeToken, "&&": ast.KindAmpersandAmpersandToken, "||": ast.KindBarBarToken, "?": ast.KindQuestionToken, "??": ast.KindQuestionQuestionToken, "?.": ast.KindQuestionDotToken, ":": ast.KindColonToken, "=": ast.KindEqualsToken, "+=": ast.KindPlusEqualsToken, "-=": ast.KindMinusEqualsToken, "*=": ast.KindAsteriskEqualsToken, "**=": ast.KindAsteriskAsteriskEqualsToken, "/=": ast.KindSlashEqualsToken, "%=": ast.KindPercentEqualsToken, "<<=": ast.KindLessThanLessThanEqualsToken, ">>=": ast.KindGreaterThanGreaterThanEqualsToken, ">>>=": ast.KindGreaterThanGreaterThanGreaterThanEqualsToken, "&=": ast.KindAmpersandEqualsToken, "|=": ast.KindBarEqualsToken, "^=": ast.KindCaretEqualsToken, "||=": ast.KindBarBarEqualsToken, "&&=": ast.KindAmpersandAmpersandEqualsToken, "??=": ast.KindQuestionQuestionEqualsToken, "@": ast.KindAtToken, "#": ast.KindHashToken, "`": ast.KindBacktickToken, } maps.Copy(m, textToKeyword) return m }() type ScannerState struct { pos int // Current position in text (and ending position of current token) fullStartPos int // Starting position of current token including preceding whitespace tokenStart int // Starting position of non-whitespace part of current token token ast.Kind // Kind of current token tokenValue string // Parsed value of current token tokenFlags ast.TokenFlags // Flags for current token commentDirectives []ast.CommentDirective skipJSDocLeadingAsterisks int // Leading asterisks to skip when scanning types inside JSDoc. Should be 0 outside JSDoc } type Scanner struct { text string end int languageVariant core.LanguageVariant scriptTarget core.ScriptTarget onError ErrorCallback skipTrivia bool ScannerState numberCache map[string]string hexNumberCache map[string]string hexDigitCache map[string]string } func defaultScanner() Scanner { // Using a function rather than a global is intentional; this function is // inlined as pure code (zeroing + moves), whereas a global requires write // barriers since the memory is mutable. return Scanner{skipTrivia: true} } func NewScanner() *Scanner { s := defaultScanner() return &s } func (s *Scanner) Reset() { numberCache := cleared(s.numberCache) hexNumberCache := cleared(s.hexNumberCache) hexDigitCache := cleared(s.hexDigitCache) *s = defaultScanner() s.numberCache = numberCache s.hexNumberCache = hexNumberCache s.hexDigitCache = hexDigitCache } func cleared[M ~map[K]V, K comparable, V any](m M) M { clear(m) return m } func (s *Scanner) Text() string { return s.text } func (s *Scanner) Token() ast.Kind { return s.token } func (s *Scanner) TokenFlags() ast.TokenFlags { return s.tokenFlags } func (s *Scanner) TokenFullStart() int { return s.fullStartPos } func (s *Scanner) TokenStart() int { return s.tokenStart } func (s *Scanner) TokenEnd() int { return s.pos } func (s *Scanner) TokenText() string { return s.text[s.tokenStart:s.pos] } func (s *Scanner) TokenValue() string { return s.tokenValue } func (s *Scanner) TokenRange() core.TextRange { return core.NewTextRange(s.tokenStart, s.pos) } func (s *Scanner) CommentDirectives() []ast.CommentDirective { return s.commentDirectives } func (s *Scanner) Mark() ScannerState { return s.ScannerState } func (s *Scanner) Rewind(state ScannerState) { s.ScannerState = state } func (s *Scanner) ResetPos(pos int) { if pos < 0 { panic("Cannot reset token state to negative position") } s.pos = pos s.fullStartPos = pos s.tokenStart = pos } func (s *Scanner) ResetTokenState(pos int) { s.ResetPos(pos) s.token = ast.KindUnknown s.tokenValue = "" s.tokenFlags = ast.TokenFlagsNone } func (scanner *Scanner) SetSkipJSDocLeadingAsterisks(skip bool) { if skip { scanner.skipJSDocLeadingAsterisks += 1 } else { scanner.skipJSDocLeadingAsterisks += -1 } } func (scanner *Scanner) SetSkipTrivia(skip bool) { scanner.skipTrivia = skip } func (s *Scanner) HasUnicodeEscape() bool { return s.tokenFlags&ast.TokenFlagsUnicodeEscape != 0 } func (s *Scanner) HasExtendedUnicodeEscape() bool { return s.tokenFlags&ast.TokenFlagsExtendedUnicodeEscape != 0 } func (s *Scanner) HasPrecedingLineBreak() bool { return s.tokenFlags&ast.TokenFlagsPrecedingLineBreak != 0 } func (s *Scanner) HasPrecedingJSDocComment() bool { return s.tokenFlags&ast.TokenFlagsPrecedingJSDocComment != 0 } func (s *Scanner) HasPrecedingJSDocLeadingAsterisks() bool { return s.tokenFlags&ast.TokenFlagsPrecedingJSDocLeadingAsterisks != 0 } func (s *Scanner) HasPrecedingJSDocWithDeprecatedTag() bool { return s.tokenFlags&ast.TokenFlagsPrecedingJSDocWithDeprecated != 0 } func (s *Scanner) HasPrecedingJSDocWithSeeOrLink() bool { return s.tokenFlags&ast.TokenFlagsPrecedingJSDocWithSeeOrLink != 0 } // scanJSDocCommentForTags scans a JSDoc comment for @deprecated, @see, and @link tags, // setting the appropriate token flags. Called during scanning when a JSDoc comment is detected. func (s *Scanner) scanJSDocCommentForTags(commentText string) { for { i := strings.IndexByte(commentText, '@') if i < 0 { return } commentText = commentText[i+1:] if s.tokenFlags&ast.TokenFlagsPrecedingJSDocWithDeprecated == 0 && hasJSDocTag(commentText, "deprecated") { s.tokenFlags |= ast.TokenFlagsPrecedingJSDocWithDeprecated } if s.tokenFlags&ast.TokenFlagsPrecedingJSDocWithSeeOrLink == 0 && hasJSDocTag(commentText, "see", "link", "linkcode", "linkplain") { s.tokenFlags |= ast.TokenFlagsPrecedingJSDocWithSeeOrLink } if s.tokenFlags&(ast.TokenFlagsPrecedingJSDocWithDeprecated|ast.TokenFlagsPrecedingJSDocWithSeeOrLink) == (ast.TokenFlagsPrecedingJSDocWithDeprecated | ast.TokenFlagsPrecedingJSDocWithSeeOrLink) { return } } } // hasJSDocTag reports whether text starts with one of the given tag names followed // by a valid JSDoc tag terminator (whitespace, '}', '*', or end-of-string). func hasJSDocTag(text string, tags ...string) bool { for _, tag := range tags { if !strings.HasPrefix(text, tag) { continue } if len(text) == len(tag) { return true } ch := text[len(tag)] if ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r' || ch == '}' || ch == '*' { return true } } return false } func (s *Scanner) SetText(text string) { s.text = text s.end = len(text) s.ScannerState = ScannerState{} } func (s *Scanner) SetOnError(errorCallback ErrorCallback) { s.onError = errorCallback } func (s *Scanner) SetLanguageVariant(languageVariant core.LanguageVariant) { s.languageVariant = languageVariant } func (s *Scanner) SetScriptTarget(scriptTarget core.ScriptTarget) { s.scriptTarget = scriptTarget } func (s *Scanner) languageVersion() core.ScriptTarget { if s.scriptTarget == core.ScriptTargetNone { return core.ScriptTargetLatest } return s.scriptTarget } func (s *Scanner) error(diagnostic *diagnostics.Message) { s.errorAt(diagnostic, s.pos, 0) } func (s *Scanner) errorAt(diagnostic *diagnostics.Message, pos int, length int, args ...any) { if s.onError != nil { s.onError(diagnostic, pos, length, args...) } } // NOTE: even though this returns a rune, it only decodes the current byte. // It must be checked against utf8.RuneSelf to verify that a call to charAndSize // is not needed. func (s *Scanner) char() rune { if s.pos < s.end { return rune(s.text[s.pos]) } return -1 } // NOTE: this returns a rune, but only decodes the byte at the offset. func (s *Scanner) charAt(offset int) rune { if s.pos+offset < s.end { return rune(s.text[s.pos+offset]) } return -1 } func (s *Scanner) charAndSize() (rune, int) { // Fast path: a single ASCII byte. The vast majority of source bytes are // ASCII; handling them here avoids constructing a string slice header and // calling the non-inlined utf8.DecodeRuneInString on every byte. if s.pos < s.end { if b := s.text[s.pos]; b < utf8.RuneSelf { return rune(b), 1 } } return utf8.DecodeRuneInString(s.text[s.pos:]) } // scanASCIIWhile advances s.pos over the longest run of ASCII bytes for which // pred returns true. It stops at end-of-text, the first non-ASCII byte, or the // first byte where pred is false. func (s *Scanner) scanASCIIWhile(pred func(byte) bool) { text := s.text[s.pos:s.end] i := 0 for i < len(text) { b := text[i] if b >= utf8.RuneSelf || !pred(b) { break } i++ } s.pos += i } func (s *Scanner) Scan() ast.Kind { s.fullStartPos = s.pos s.tokenFlags = ast.TokenFlagsNone for { ch := s.char() s.tokenStart = s.pos switch ch { case '\t', '\v', '\f', ' ': s.pos++ if s.skipTrivia { continue } for { ch, size := s.charAndSize() if !stringutil.IsWhiteSpaceSingleLine(ch) { break } s.pos += size } s.token = ast.KindWhitespaceTrivia case '\n', '\r': s.tokenFlags |= ast.TokenFlagsPrecedingLineBreak if s.skipTrivia { s.pos++ s.scanASCIIWhile(func(b byte) bool { return b == ' ' || (b >= '\t' && b <= '\r') }) continue } if ch == '\r' && s.charAt(1) == '\n' { s.pos += 2 } else { s.pos++ } s.token = ast.KindNewLineTrivia case '!': if s.charAt(1) == '=' { if s.charAt(2) == '=' { s.pos += 3 s.token = ast.KindExclamationEqualsEqualsToken } else { s.pos += 2 s.token = ast.KindExclamationEqualsToken } } else { s.pos++ s.token = ast.KindExclamationToken } case '"', '\'': s.tokenValue = s.scanString(false /*jsxAttributeString*/) s.token = ast.KindStringLiteral case '`': s.token = s.scanTemplateAndSetTokenValue(false /*shouldEmitInvalidEscapeError*/) case '%': if s.charAt(1) == '=' { s.pos += 2 s.token = ast.KindPercentEqualsToken } else { s.pos++ s.token = ast.KindPercentToken } case '&': next := s.charAt(1) if next == '&' { if s.charAt(2) == '=' { s.pos += 3 s.token = ast.KindAmpersandAmpersandEqualsToken } else { s.pos += 2 s.token = ast.KindAmpersandAmpersandToken } } else if next == '=' { s.pos += 2 s.token = ast.KindAmpersandEqualsToken } else { s.pos++ s.token = ast.KindAmpersandToken } case '(': s.pos++ s.token = ast.KindOpenParenToken case ')': s.pos++ s.token = ast.KindCloseParenToken case '*': next := s.charAt(1) if next == '=' { s.pos += 2 s.token = ast.KindAsteriskEqualsToken } else if next == '*' { if s.charAt(2) == '=' { s.pos += 3 s.token = ast.KindAsteriskAsteriskEqualsToken } else { s.pos += 2 s.token = ast.KindAsteriskAsteriskToken } } else { s.pos++ if s.skipJSDocLeadingAsterisks != 0 && (s.tokenFlags&ast.TokenFlagsPrecedingJSDocLeadingAsterisks) == 0 && (s.tokenFlags&ast.TokenFlagsPrecedingLineBreak) != 0 { s.tokenFlags |= ast.TokenFlagsPrecedingJSDocLeadingAsterisks continue } s.token = ast.KindAsteriskToken } case '+': next := s.charAt(1) if next == '=' { s.pos += 2 s.token = ast.KindPlusEqualsToken } else if next == '+' { s.pos += 2 s.token = ast.KindPlusPlusToken } else { s.pos++ s.token = ast.KindPlusToken } case ',': s.pos++ s.token = ast.KindCommaToken case '-': next := s.charAt(1) if next == '=' { s.pos += 2 s.token = ast.KindMinusEqualsToken } else if next == '-' { s.pos += 2 s.token = ast.KindMinusMinusToken } else { s.pos++ s.token = ast.KindMinusToken } case '.': next := s.charAt(1) if stringutil.IsDigit(next) { s.token = s.scanNumber() } else if next == '.' && s.charAt(2) == '.' { s.pos += 3 s.token = ast.KindDotDotDotToken } else { s.pos++ s.token = ast.KindDotToken } case '/': // Single-line comment if s.charAt(1) == '/' { s.pos += 2 for { s.scanASCIIWhile(func(b byte) bool { return b != '\n' && b != '\r' }) ch1, size := s.charAndSize() if size == 0 || stringutil.IsLineBreak(ch1) { break } s.pos += size } s.processCommentDirective(s.tokenStart, s.pos, false) if s.skipTrivia { continue } s.token = ast.KindSingleLineCommentTrivia return s.token } // Multi-line comment if s.charAt(1) == '*' { s.pos += 2 isJSDoc := s.char() == '*' && s.charAt(1) != '/' commentClosed := false lastLineStart := s.tokenStart for { s.scanASCIIWhile(func(b byte) bool { return b != '*' && b != '\n' && b != '\r' }) ch1, size := s.charAndSize() if size == 0 { break } if ch1 == '*' && s.charAt(1) == '/' { s.pos += 2 commentClosed = true break } s.pos += size if stringutil.IsLineBreak(ch1) { lastLineStart = s.pos s.tokenFlags |= ast.TokenFlagsPrecedingLineBreak } } if isJSDoc { s.tokenFlags |= ast.TokenFlagsPrecedingJSDocComment s.scanJSDocCommentForTags(s.text[s.tokenStart:s.pos]) } s.processCommentDirective(lastLineStart, s.pos, true) if !commentClosed { s.error(diagnostics.Asterisk_Slash_expected) } if s.skipTrivia { continue } if !commentClosed { s.tokenFlags |= ast.TokenFlagsUnterminated } s.token = ast.KindMultiLineCommentTrivia return s.token } if s.charAt(1) == '=' { s.pos += 2 s.token = ast.KindSlashEqualsToken } else { s.pos++ s.token = ast.KindSlashToken } case '0': if s.charAt(1) == 'X' || s.charAt(1) == 'x' { start := s.pos s.pos += 2 digits := s.scanHexDigits(1, true, true) if digits == "" { s.error(diagnostics.Hexadecimal_digit_expected) digits = "0" } if s.hexNumberCache == nil { s.hexNumberCache = make(map[string]string) } if cachedValue, ok := s.hexNumberCache[digits]; ok { s.tokenValue = cachedValue } else { rawText := s.text[start:s.pos] if strings.HasPrefix(rawText, "0x") && rawText[2:] == digits { s.tokenValue = rawText } else { s.tokenValue = "0x" + digits } s.hexNumberCache[digits] = s.tokenValue } s.tokenFlags |= ast.TokenFlagsHexSpecifier s.token = s.scanBigIntSuffix() break } if s.charAt(1) == 'B' || s.charAt(1) == 'b' { s.pos += 2 digits := s.scanBinaryOrOctalDigits(2) if digits == "" { s.error(diagnostics.Binary_digit_expected) digits = "0" } s.tokenValue = "0b" + digits s.tokenFlags |= ast.TokenFlagsBinarySpecifier s.token = s.scanBigIntSuffix() break } if s.charAt(1) == 'O' || s.charAt(1) == 'o' { s.pos += 2 digits := s.scanBinaryOrOctalDigits(8) if digits == "" { s.error(diagnostics.Octal_digit_expected) digits = "0" } s.tokenValue = "0o" + digits s.tokenFlags |= ast.TokenFlagsOctalSpecifier s.token = s.scanBigIntSuffix() break } fallthrough case '1', '2', '3', '4', '5', '6', '7', '8', '9': s.token = s.scanNumber() case ':': s.pos++ s.token = ast.KindColonToken case ';': s.pos++ s.token = ast.KindSemicolonToken case '<': if s.charAt(1) == '<' && isConflictMarkerTrivia(s.text, s.pos) { s.pos = scanConflictMarkerTrivia(s.text, s.pos, s.errorAt) if s.skipTrivia { continue } else { s.token = ast.KindConflictMarkerTrivia return s.token } } if s.charAt(1) == '<' { if s.charAt(2) == '=' { s.pos += 3 s.token = ast.KindLessThanLessThanEqualsToken } else { s.pos += 2 s.token = ast.KindLessThanLessThanToken } } else if s.charAt(1) == '=' { s.pos += 2 s.token = ast.KindLessThanEqualsToken } else if s.languageVariant == core.LanguageVariantJSX && s.charAt(1) == '/' && s.charAt(2) != '*' { s.pos += 2 s.token = ast.KindLessThanSlashToken } else { s.pos++ s.token = ast.KindLessThanToken } case '=': if s.charAt(1) == '=' && isConflictMarkerTrivia(s.text, s.pos) { s.pos = scanConflictMarkerTrivia(s.text, s.pos, s.errorAt) if s.skipTrivia { continue } else { s.token = ast.KindConflictMarkerTrivia return s.token } } if s.charAt(1) == '=' { if s.charAt(2) == '=' { s.pos += 3 s.token = ast.KindEqualsEqualsEqualsToken } else { s.pos += 2 s.token = ast.KindEqualsEqualsToken } } else if s.charAt(1) == '>' { s.pos += 2 s.token = ast.KindEqualsGreaterThanToken } else { s.pos++ s.token = ast.KindEqualsToken } case '>': if s.charAt(1) == '>' && isConflictMarkerTrivia(s.text, s.pos) { s.pos = scanConflictMarkerTrivia(s.text, s.pos, s.errorAt) if s.skipTrivia { continue } else { s.token = ast.KindConflictMarkerTrivia return s.token } } s.pos++ s.token = ast.KindGreaterThanToken case '?': if s.charAt(1) == '.' && !stringutil.IsDigit(s.charAt(2)) { s.pos += 2 s.token = ast.KindQuestionDotToken } else if s.charAt(1) == '?' { if s.charAt(2) == '=' { s.pos += 3 s.token = ast.KindQuestionQuestionEqualsToken } else { s.pos += 2 s.token = ast.KindQuestionQuestionToken } } else { s.pos++ s.token = ast.KindQuestionToken } case '[': s.pos++ s.token = ast.KindOpenBracketToken case ']': s.pos++ s.token = ast.KindCloseBracketToken case '^': if s.charAt(1) == '=' { s.pos += 2 s.token = ast.KindCaretEqualsToken } else { s.pos++ s.token = ast.KindCaretToken } case '{': s.pos++ s.token = ast.KindOpenBraceToken case '|': if s.charAt(1) == '|' && isConflictMarkerTrivia(s.text, s.pos) { s.pos = scanConflictMarkerTrivia(s.text, s.pos, s.errorAt) if s.skipTrivia { continue } else { s.token = ast.KindConflictMarkerTrivia return s.token } } if s.charAt(1) == '|' { if s.charAt(2) == '=' { s.pos += 3 s.token = ast.KindBarBarEqualsToken } else { s.pos += 2 s.token = ast.KindBarBarToken } } else if s.charAt(1) == '=' { s.pos += 2 s.token = ast.KindBarEqualsToken } else { s.pos++ s.token = ast.KindBarToken } case '}': s.pos++ s.token = ast.KindCloseBraceToken case '~': s.pos++ s.token = ast.KindTildeToken case '@': s.pos++ s.token = ast.KindAtToken case '\\': cp := s.peekUnicodeEscape() if cp >= 0 && IsIdentifierStart(cp) { s.tokenValue = string(s.scanUnicodeEscape(true)) + s.scanIdentifierParts() s.token = GetIdentifierToken(s.tokenValue) } else { s.scanInvalidCharacter() } case '#': if s.charAt(1) == '!' { if s.pos == 0 { s.pos += 2 for ch, size := s.charAndSize(); size > 0 && !stringutil.IsLineBreak(ch); ch, size = s.charAndSize() { s.pos += size } continue } s.errorAt(diagnostics.X_can_only_be_used_at_the_start_of_a_file, s.pos, 2) s.pos++ s.token = ast.KindUnknown break } if s.charAt(1) == '\\' { s.pos++ cp := s.peekUnicodeEscape() if cp >= 0 && IsIdentifierStart(cp) { s.tokenValue = "#" + string(s.scanUnicodeEscape(true)) + s.scanIdentifierParts() s.token = ast.KindPrivateIdentifier break } s.pos-- } if !s.scanIdentifier(1) { s.errorAt(diagnostics.Invalid_character, s.pos-1, 1) s.tokenValue = "#" } s.token = ast.KindPrivateIdentifier default: if ch < 0 { s.token = ast.KindEndOfFile break } if s.scanIdentifier(0) { s.token = GetIdentifierToken(s.tokenValue) break } ch, size := s.charAndSize() if ch == utf8.RuneError { s.errorAt(diagnostics.File_appears_to_be_binary, 0, 0) s.pos = len(s.text) s.token = ast.KindNonTextFileMarkerTrivia break } if stringutil.IsWhiteSpaceSingleLine(ch) { s.pos += size // If we get here and it's not 0x0085 (nextLine), then we're handling non-ASCII whitespace. // Handle skipTrivia like we do in the space case above. if ch == 0x0085 || s.skipTrivia { continue } for { ch, size = s.charAndSize() if !stringutil.IsWhiteSpaceSingleLine(ch) { break } s.pos += size } s.token = ast.KindWhitespaceTrivia return s.token } if stringutil.IsLineBreak(ch) { s.tokenFlags |= ast.TokenFlagsPrecedingLineBreak s.pos += size continue } s.scanInvalidCharacter() } return s.token } } func (s *Scanner) processCommentDirective(start int, end int, multiline bool) { // Skip starting slashes and whitespace pos := start if multiline { // Skip whitespace for pos < end && (s.text[pos] == ' ' || s.text[pos] == '\t') { pos++ } // Skip combinations of / and * for pos < end && (s.text[pos] == '/' || s.text[pos] == '*') { pos++ } } else { // Skip opening // pos += 2 // Skip another / if present for pos < end && s.text[pos] == '/' { pos++ } } // Skip whitespace for pos < end && (s.text[pos] == ' ' || s.text[pos] == '\t') { pos++ } // Directive must start with '@' if !(pos < end && s.text[pos] == '@') { return } pos++ var kind ast.CommentDirectiveKind switch { case strings.HasPrefix(s.text[pos:], "ts-expect-error"): kind = ast.CommentDirectiveKindExpectError case strings.HasPrefix(s.text[pos:], "ts-ignore"): kind = ast.CommentDirectiveKindIgnore default: return } s.commentDirectives = append(s.commentDirectives, ast.CommentDirective{Loc: core.NewTextRange(start, end), Kind: kind}) } func (s *Scanner) ReScanLessThanToken() ast.Kind { if s.token == ast.KindLessThanLessThanToken { s.pos = s.tokenStart + 1 s.token = ast.KindLessThanToken } return s.token } func (s *Scanner) ReScanGreaterThanToken() ast.Kind { if s.token == ast.KindGreaterThanToken { s.reScanGreaterThanTokenInner() } return s.token } func (s *Scanner) reScanGreaterThanTokenInner() { s.pos = s.tokenStart + 1 if s.char() == '>' { if s.charAt(1) == '>' { if s.charAt(2) == '=' { s.pos += 3 s.token = ast.KindGreaterThanGreaterThanGreaterThanEqualsToken } else { s.pos += 2 s.token = ast.KindGreaterThanGreaterThanGreaterThanToken } } else if s.charAt(1) == '=' { s.pos += 2 s.token = ast.KindGreaterThanGreaterThanEqualsToken } else { s.pos++ s.token = ast.KindGreaterThanGreaterThanToken } } else if s.char() == '=' { s.pos++ s.token = ast.KindGreaterThanEqualsToken } } func (s *Scanner) ReScanTemplateToken(isTaggedTemplate bool) ast.Kind { s.pos = s.tokenStart s.token = s.scanTemplateAndSetTokenValue(!isTaggedTemplate) return s.token } func (s *Scanner) ReScanAsteriskEqualsToken() ast.Kind { if s.token != ast.KindAsteriskEqualsToken { panic("'ReScanAsteriskEqualsToken' should only be called on a '*='") } s.pos = s.tokenStart + 1 s.token = ast.KindEqualsToken return s.token } func (s *Scanner) ReScanSlashToken(reportErrors ...bool) ast.Kind { shouldReportErrors := len(reportErrors) > 0 && reportErrors[0] if s.token == ast.KindSlashToken || s.token == ast.KindSlashEqualsToken { // Quickly get to the end of regex such that we know the flags startOfRegExpBody := s.tokenStart + 1 p := startOfRegExpBody inEscape := false namedCaptureGroups := false // Although nested character classes are allowed in Unicode Sets mode, // an unescaped slash is nevertheless invalid even in a character class in any Unicode mode. // This is indicated by Section 12.9.5 Regular Expression Literals of the specification, // where nested character classes are not considered at all. (A `[` RegularExpressionClassChar // does nothing in a RegularExpressionClass, and a `]` always closes the class.) // Additionally, parsing nested character classes will misinterpret regexes like `/[[]/` // as unterminated, consuming characters beyond the slash. (This even applies to `/[[]/v`, // which should be parsed as a well-terminated regex with an incomplete character class.) // Thus we must not handle nested character classes in the first pass. inCharacterClass := false loop: for { // If we reach the end of a file, or hit a newline, then this is an unterminated // regex. Report error and return what we have so far. if p >= s.end { s.tokenFlags |= ast.TokenFlagsUnterminated break loop } ch := rune(s.text[p]) switch { case stringutil.IsLineBreak(ch): s.tokenFlags |= ast.TokenFlagsUnterminated break loop case inEscape: // Parsing an escape character; // reset the flag and just advance to the next char. inEscape = false case ch == '/' && !inCharacterClass: // A slash within a character class is permissible, // but in general it signals the end of the regexp literal. break loop case ch == '[': inCharacterClass = true case ch == '\\': inEscape = true case ch == ']': inCharacterClass = false case !inCharacterClass && ch == '(' && p+1 < s.end && s.text[p+1] == '?' && p+2 < s.end && s.text[p+2] == '<' && (p+3 >= s.end || (s.text[p+3] != '=' && s.text[p+3] != '!')): namedCaptureGroups = true } p++ } endOfRegExpBody := p if s.tokenFlags&ast.TokenFlagsUnterminated != 0 { // Search for the nearest unbalanced bracket for better recovery. Since the expression is // invalid anyways, we take nested square brackets into consideration for the best guess. p = startOfRegExpBody inEscape = false characterClassDepth := 0 inDecimalQuantifier := false groupDepth := 0 for p < endOfRegExpBody { ch := rune(s.text[p]) if inEscape { inEscape = false } else if ch == '\\' { inEscape = true } else if ch == '[' { characterClassDepth++ } else if ch == ']' && characterClassDepth != 0 { characterClassDepth-- } else if characterClassDepth == 0 { if ch == '{' { inDecimalQuantifier = true } else if ch == '}' && inDecimalQuantifier { inDecimalQuantifier = false } else if !inDecimalQuantifier { if ch == '(' { groupDepth++ } else if ch == ')' && groupDepth != 0 { groupDepth-- } else if ch == ')' || ch == ']' || ch == '}' { // We encountered an unbalanced bracket outside a character class. Treat this position as the end of regex. break } } } p++ } // Whitespaces and semicolons at the end are not likely to be part of the regex for p > startOfRegExpBody { ch, size := utf8.DecodeLastRuneInString(s.text[:p]) if stringutil.IsWhiteSpaceLike(ch) || ch == ';' { p -= size } else { break } } s.errorAt(diagnostics.Unterminated_regular_expression_literal, s.tokenStart, p-s.tokenStart) } else { // Consume the slash character p++ var regExpFlags regularExpressionFlags for p < s.end { ch, size := utf8.DecodeRuneInString(s.text[p:]) if ch == utf8.RuneError || !IsIdentifierPart(ch) { break } if shouldReportErrors { flag, ok := charCodeToRegExpFlag[ch] if !ok { s.errorAt(diagnostics.Unknown_regular_expression_flag, p, size) } else if regExpFlags&flag != 0 { s.errorAt(diagnostics.Duplicate_regular_expression_flag, p, size) } else if (regExpFlags|flag)®ularExpressionFlagsAnyUnicodeMode == regularExpressionFlagsAnyUnicodeMode { s.errorAt(diagnostics.The_Unicode_u_flag_and_the_Unicode_Sets_v_flag_cannot_be_set_simultaneously, p, size) } else { regExpFlags |= flag s.checkRegularExpressionFlagAvailability(flag, p, size) } } p += size } if shouldReportErrors { s.pos = startOfRegExpBody saveEnd := s.end saveTokenPos := s.tokenStart saveTokenFlags := s.tokenFlags s.end = endOfRegExpBody parser := ®ExpParser{ scanner: s, end: endOfRegExpBody, regExpFlags: regExpFlags, anyUnicodeMode: regExpFlags®ularExpressionFlagsAnyUnicodeMode != 0, unicodeSetsMode: regExpFlags®ularExpressionFlagsUnicodeSets != 0, annexB: true, namedCaptureGroups: namedCaptureGroups, groupSpecifiers: make(map[string]bool), } parser.run() s.end = saveEnd s.pos = p s.tokenStart = saveTokenPos s.tokenFlags = saveTokenFlags } else { s.pos = p } } s.pos = p s.tokenValue = s.text[s.tokenStart:s.pos] s.token = ast.KindRegularExpressionLiteral } return s.token } func (s *Scanner) ReScanJsxToken(allowMultilineJsxText bool) ast.Kind { s.pos = s.fullStartPos s.tokenStart = s.fullStartPos s.token = s.ScanJsxTokenEx(allowMultilineJsxText) return s.token } func (s *Scanner) ReScanHashToken() ast.Kind { if s.token == ast.KindPrivateIdentifier { s.pos = s.tokenStart + 1 s.token = ast.KindHashToken } return s.token } func (s *Scanner) ReScanQuestionToken() ast.Kind { if s.token != ast.KindQuestionQuestionToken { panic("'reScanQuestionToken' should only be called on a '??'") } s.pos = s.tokenStart + 1 s.token = ast.KindQuestionToken return s.token } func (s *Scanner) ScanJsxToken() ast.Kind { return s.ScanJsxTokenEx(true /*allowMultilineJsxText*/) } func (s *Scanner) ScanJsxTokenEx(allowMultilineJsxText bool) ast.Kind { s.fullStartPos = s.pos s.tokenStart = s.pos ch := s.char() switch { case ch < 0: s.token = ast.KindEndOfFile case ch == '<': if s.charAt(1) == '/' { s.pos += 2 s.token = ast.KindLessThanSlashToken } else { s.pos++ s.token = ast.KindLessThanToken } case ch == '{': s.pos++ s.token = ast.KindOpenBraceToken default: // First non-whitespace character on this line. firstNonWhitespace := 0 // These initial values are special because the first line is: // firstNonWhitespace = 0 to indicate that we want leading whitespace for { ch, size := s.charAndSize() if size == 0 || ch == '{' { break } if ch == '<' { if isConflictMarkerTrivia(s.text, s.pos) { s.pos = scanConflictMarkerTrivia(s.text, s.pos, s.errorAt) s.token = ast.KindConflictMarkerTrivia return s.token } break } if ch == '>' { s.errorAt(diagnostics.Unexpected_token_Did_you_mean_or_gt, s.pos, 1) } else if ch == '}' { s.errorAt(diagnostics.Unexpected_token_Did_you_mean_or_rbrace, s.pos, 1) } // FirstNonWhitespace is 0, then we only see whitespaces so far. If we see a linebreak, we want to ignore that whitespaces. // i.e (- : whitespace) //
---- //
becomes
// //
----
becomes
----
if stringutil.IsLineBreak(ch) && firstNonWhitespace == 0 { firstNonWhitespace = -1 } else if !allowMultilineJsxText && stringutil.IsLineBreak(ch) && firstNonWhitespace > 0 { // Stop JsxText on each line during formatting. This allows the formatter to // indent each line correctly. break } else if !stringutil.IsWhiteSpaceLike(ch) { firstNonWhitespace = s.pos } s.pos += size } s.tokenValue = s.text[s.fullStartPos:s.pos] s.token = ast.KindJsxText if firstNonWhitespace == -1 { s.token = ast.KindJsxTextAllWhiteSpaces } } return s.token } // Scans a JSX identifier; these differ from normal identifiers in that they allow dashes func (s *Scanner) ScanJsxIdentifier() ast.Kind { if tokenIsIdentifierOrKeyword(s.token) { // An identifier or keyword has already been parsed - check for a `-` or a single instance of `:` and then append it and // everything after it to the token // Do note that this means that `scanJsxIdentifier` effectively _mutates_ the visible token without advancing to a new token // Any caller should be expecting this behavior and should only read the pos or token value after calling it. for { ch := s.char() if ch < 0 { break } if ch == '-' { s.tokenValue += "-" s.pos++ continue } oldPos := s.pos s.tokenValue += s.scanIdentifierParts() // reuse `scanIdentifierParts` so unicode escapes are handled if s.pos == oldPos { break } } s.token = GetIdentifierToken(s.tokenValue) } return s.token } func (s *Scanner) ScanJsxAttributeValue() ast.Kind { s.fullStartPos = s.pos // Skip whitespace between '=' and the value so tokenStart lands on the // opening quote, not on trivia. for ch, size := s.charAndSize(); size > 0 && stringutil.IsWhiteSpaceLike(ch); ch, size = s.charAndSize() { s.pos += size } s.tokenStart = s.pos switch s.char() { case '"', '\'': s.tokenValue = s.scanString(true /*jsxAttributeString*/) s.token = ast.KindStringLiteral return s.token default: // If this scans anything other than `{`, it's a parse error. return s.Scan() } } func (s *Scanner) ReScanJsxAttributeValue() ast.Kind { s.pos = s.fullStartPos s.tokenStart = s.fullStartPos return s.ScanJsxAttributeValue() } /** In addition to the usual JSDoc ast.Kinds, can also return ast.KindJSDocCommentTextToken */ func (s *Scanner) ScanJSDocCommentTextToken(inBackticks bool) ast.Kind { s.fullStartPos = s.pos s.tokenFlags = ast.TokenFlagsNone if s.pos >= len(s.text) { s.token = ast.KindEndOfFile return s.token } s.tokenStart = s.pos for ch, size := s.charAndSize(); s.pos < len(s.text) && !stringutil.IsLineBreak(ch) && ch != '`'; ch, size = s.charAndSize() { if !inBackticks { if ch == '{' { break } else if ch == '@' && s.pos >= 0 { // @ doesn't start a new tag inside ``, and elsewhere, only after whitespace and before identifier previous, _ := utf8.DecodeLastRuneInString(s.text[:s.pos]) if stringutil.IsWhiteSpaceSingleLine(previous) { next, _ := utf8.DecodeRuneInString(s.text[s.pos+size:]) if IsIdentifierStart(next) { break } } } } s.pos += size } if s.pos == s.tokenStart { return s.ScanJSDocToken() } s.tokenValue = s.text[s.tokenStart:s.pos] s.token = ast.KindJSDocCommentTextToken return s.token } // Peek at the character at the current scanner position (expected to be right after '@') // and return true if a JSDoc tag can follow. Identifier starts indicate a tag name. // Whitespace, newlines, and EOF are also accepted to support incomplete tags for code completion. func (s *Scanner) CanFollowJSDocAt() bool { if s.pos >= len(s.text) { return true } ch, _ := utf8.DecodeRuneInString(s.text[s.pos:]) return IsIdentifierStart(ch) || stringutil.IsWhiteSpaceSingleLine(ch) || stringutil.IsLineBreak(ch) } func (s *Scanner) ScanJSDocToken() ast.Kind { s.fullStartPos = s.pos s.tokenFlags = ast.TokenFlagsNone if s.pos >= len(s.text) { s.token = ast.KindEndOfFile return s.token } s.tokenStart = s.pos ch, size := s.charAndSize() s.pos += size switch ch { case '\t', '\v', '\f', ' ': for ch2, size2 := s.charAndSize(); size2 > 0 && stringutil.IsWhiteSpaceSingleLine(ch2); ch2, size2 = s.charAndSize() { s.pos += size2 } s.token = ast.KindWhitespaceTrivia return s.token case '@': s.token = ast.KindAtToken return s.token case '\r': if s.char() == '\n' { s.pos++ } fallthrough case '\n': s.tokenFlags |= ast.TokenFlagsPrecedingLineBreak s.token = ast.KindNewLineTrivia return s.token case '*': s.token = ast.KindAsteriskToken return s.token case '{': s.token = ast.KindOpenBraceToken return s.token case '}': s.token = ast.KindCloseBraceToken return s.token case '[': s.token = ast.KindOpenBracketToken return s.token case ']': s.token = ast.KindCloseBracketToken return s.token case '(': s.token = ast.KindOpenParenToken return s.token case ')': s.token = ast.KindCloseParenToken return s.token case '<': s.token = ast.KindLessThanToken return s.token case '>': s.token = ast.KindGreaterThanToken return s.token case '=': s.token = ast.KindEqualsToken return s.token case ',': s.token = ast.KindCommaToken return s.token case '.': s.token = ast.KindDotToken return s.token case '`': s.token = ast.KindBacktickToken return s.token case '#': s.token = ast.KindHashToken return s.token case '\\': s.pos-- cp := s.peekUnicodeEscape() if cp >= 0 && IsIdentifierStart(cp) { s.tokenValue = string(s.scanUnicodeEscape(true)) + s.scanIdentifierParts() s.token = GetIdentifierToken(s.tokenValue) } else { s.pos++ s.token = ast.KindUnknown } return s.token } if IsIdentifierStart(ch) { char := ch for { if s.pos >= len(s.text) { break } char, size = s.charAndSize() if !IsIdentifierPart(char) && char != '-' { break } s.pos += size } s.tokenValue = s.text[s.tokenStart:s.pos] if char == '\\' { s.tokenValue += s.scanIdentifierParts() } s.token = GetIdentifierToken(s.tokenValue) return s.token } else { s.token = ast.KindUnknown return s.token } } func (s *Scanner) scanIdentifier(prefixLength int) bool { start := s.pos s.pos += prefixLength ch := s.char() // Fast path for simple ASCII identifiers if stringutil.IsASCIILetter(ch) || ch == '_' || ch == '$' { s.pos++ s.scanASCIIWhile(func(b byte) bool { return (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') || (b >= '0' && b <= '9') || b == '_' || b == '$' }) ch = s.char() if ch < utf8.RuneSelf && ch != '\\' { s.tokenValue = s.text[start:s.pos] return true } s.pos = start + prefixLength } ch, size := s.charAndSize() if IsIdentifierStart(ch) { for { s.pos += size ch, size = s.charAndSize() if !IsIdentifierPart(ch) { break } } s.tokenValue = s.text[start:s.pos] if ch == '\\' { s.tokenValue += s.scanIdentifierParts() } return true } return false } func (s *Scanner) scanIdentifierParts() string { var sb strings.Builder start := s.pos for { ch, size := s.charAndSize() if IsIdentifierPart(ch) { s.pos += size continue } if ch == '\\' { escaped := s.peekUnicodeEscape() if escaped >= 0 && IsIdentifierPart(escaped) { sb.WriteString(s.text[start:s.pos]) sb.WriteRune(s.scanUnicodeEscape(true)) start = s.pos continue } } break } sb.WriteString(s.text[start:s.pos]) return sb.String() } func (s *Scanner) scanString(jsxAttributeString bool) string { quote := s.char() if quote == '\'' { s.tokenFlags |= ast.TokenFlagsSingleQuote } s.pos++ // Fast path for simple strings without escape sequences. strLen := strings.IndexByte(s.text[s.pos:], byte(quote)) if strLen == 0 { s.pos++ return "" } if strLen > 0 { str := s.text[s.pos : s.pos+strLen] if jsxAttributeString || strings.IndexByte(str, '\\') < 0 && strings.IndexByte(str, '\r') < 0 && strings.IndexByte(str, '\n') < 0 { s.pos += strLen + 1 return str } } var sb strings.Builder start := s.pos for { ch := s.char() if ch < 0 { sb.WriteString(s.text[start:s.pos]) s.tokenFlags |= ast.TokenFlagsUnterminated s.error(diagnostics.Unterminated_string_literal) break } if ch == quote { sb.WriteString(s.text[start:s.pos]) s.pos++ break } if ch == '\\' && !jsxAttributeString { sb.WriteString(s.text[start:s.pos]) sb.WriteString(s.scanEscapeSequence(EscapeSequenceScanningFlagsString | EscapeSequenceScanningFlagsReportErrors)) start = s.pos continue } if (ch == '\n' || ch == '\r') && !jsxAttributeString { sb.WriteString(s.text[start:s.pos]) s.tokenFlags |= ast.TokenFlagsUnterminated s.error(diagnostics.Unterminated_string_literal) break } s.pos++ } return sb.String() } func (s *Scanner) scanTemplateAndSetTokenValue(shouldEmitInvalidEscapeError bool) ast.Kind { startedWithBacktick := s.char() == '`' s.pos++ start := s.pos parts := make([]string, 0, 4) var token ast.Kind for { s.scanASCIIWhile(func(b byte) bool { return b != '`' && b != '$' && b != '\\' && b != '\r' }) ch := s.char() if ch < 0 || ch == '`' { parts = append(parts, s.text[start:s.pos]) if ch == '`' { s.pos++ } else { s.tokenFlags |= ast.TokenFlagsUnterminated s.error(diagnostics.Unterminated_template_literal) } token = core.IfElse(startedWithBacktick, ast.KindNoSubstitutionTemplateLiteral, ast.KindTemplateTail) break } if ch == '$' && s.charAt(1) == '{' { parts = append(parts, s.text[start:s.pos]) s.pos += 2 token = core.IfElse(startedWithBacktick, ast.KindTemplateHead, ast.KindTemplateMiddle) break } if ch == '\\' { parts = append(parts, s.text[start:s.pos]) parts = append(parts, s.scanEscapeSequence(EscapeSequenceScanningFlagsString|core.IfElse(shouldEmitInvalidEscapeError, EscapeSequenceScanningFlagsReportErrors, 0))) start = s.pos continue } // Speculated ECMAScript 6 Spec 11.8.6.1: // and LineTerminatorSequences are normalized to for Template Values if ch == '\r' { parts = append(parts, s.text[start:s.pos]) s.pos++ if s.char() == '\n' { s.pos++ } parts = append(parts, "\n") start = s.pos continue } s.pos++ } s.tokenValue = strings.Join(parts, "") return token } func (s *Scanner) scanEscapeSequence(flags EscapeSequenceScanningFlags) string { start := s.pos s.pos++ ch := s.char() if ch < 0 { s.error(diagnostics.Unexpected_end_of_text) return "" } s.pos++ switch ch { case '0': // Although '0' preceding any digit is treated as LegacyOctalEscapeSequence, // '\08' should separately be interpreted as '\0' + '8'. if !stringutil.IsDigit(s.char()) { return "\x00" } // '\01', '\011' fallthrough case '1', '2', '3': // '\1', '\17', '\177' if stringutil.IsOctalDigit(s.char()) { s.pos++ } // '\17', '\177' fallthrough case '4', '5', '6', '7': // '\4', '\47' but not '\477' if stringutil.IsOctalDigit(s.char()) { s.pos++ } // '\47' s.tokenFlags |= ast.TokenFlagsContainsInvalidEscape if flags&EscapeSequenceScanningFlagsReportInvalidEscapeErrors != 0 { code, _ := strconv.ParseInt(s.text[start+1:s.pos], 8, 32) if flags&EscapeSequenceScanningFlagsRegularExpression != 0 && flags&EscapeSequenceScanningFlagsAtomEscape == 0 && ch != '0' { s.errorAt(diagnostics.Octal_escape_sequences_and_backreferences_are_not_allowed_in_a_character_class_If_this_was_intended_as_an_escape_sequence_use_the_syntax_0_instead, start, s.pos-start, fmt.Sprintf("\\x%02x", code)) } else { s.errorAt(diagnostics.Octal_escape_sequences_are_not_allowed_Use_the_syntax_0, start, s.pos-start, fmt.Sprintf("\\x%02x", code)) } return string(rune(code)) } return s.text[start:s.pos] case '8', '9': // the invalid '\8' and '\9' s.tokenFlags |= ast.TokenFlagsContainsInvalidEscape if flags&EscapeSequenceScanningFlagsReportInvalidEscapeErrors != 0 { if flags&EscapeSequenceScanningFlagsRegularExpression != 0 && flags&EscapeSequenceScanningFlagsAtomEscape == 0 { s.errorAt(diagnostics.Decimal_escape_sequences_and_backreferences_are_not_allowed_in_a_character_class, start, s.pos-start) } else { s.errorAt(diagnostics.Escape_sequence_0_is_not_allowed, start, s.pos-start, s.text[start:s.pos]) } return string(ch) } return s.text[start:s.pos] case 'b': return "\b" case 't': return "\t" case 'n': return "\n" case 'v': return "\v" case 'f': return "\f" case 'r': return "\r" case '\'': return "'" case '"': return "\"" case 'u': // '\uDDDD' and '\u{DDDDDD}' extended := s.char() == '{' s.pos -= 2 codePoint := s.scanUnicodeEscape(flags&EscapeSequenceScanningFlagsReportInvalidEscapeErrors != 0) if extended { if flags&EscapeSequenceScanningFlagsAllowExtendedUnicodeEscape == 0 { s.tokenFlags |= ast.TokenFlagsContainsInvalidEscape if flags&EscapeSequenceScanningFlagsReportInvalidEscapeErrors != 0 { s.errorAt(diagnostics.Unicode_escape_sequences_are_only_available_when_the_Unicode_u_flag_or_the_Unicode_Sets_v_flag_is_set, start, s.pos-start) } } if codePoint < 0 { return s.text[start:s.pos] } // In string literals, a high surrogate \u{...} followed by a low // surrogate escape forms a single code point, exactly as adjacent // UTF-16 code units would in a JavaScript string. if flags&EscapeSequenceScanningFlagsRegularExpression == 0 && stringutil.IsHighSurrogate(codePoint) { if combined, ok := s.scanLowSurrogateEscape(codePoint); ok { return string(combined) } } return stringutil.EncodeJSStringRune(codePoint) } if codePoint < 0 { return s.text[start:s.pos] } else if stringutil.IsHighSurrogate(codePoint) { if flags&EscapeSequenceScanningFlagsRegularExpression == 0 { // Combine \uHigh followed by any low surrogate escape (\uLow or // \u{Low}) into a single code point in string literals, matching // how adjacent UTF-16 code units pair in a JavaScript string. if combined, ok := s.scanLowSurrogateEscape(codePoint); ok { return string(combined) } } else if flags&EscapeSequenceScanningFlagsAnyUnicodeMode != 0 && s.char() == '\\' && s.charAt(1) == 'u' && s.charAt(2) != '{' { // In regex AnyUnicodeMode, combine \uHigh\uLow so scanClassRanges // can compare the pair numerically. In non-unicode regex mode they // are separate atoms, and extended \u{...} escapes never combine. savedPos := s.pos nextCodePoint := s.scanUnicodeEscape(flags&EscapeSequenceScanningFlagsReportInvalidEscapeErrors != 0) if stringutil.IsLowSurrogate(nextCodePoint) { return string(stringutil.SurrogatePairToCodePoint(codePoint, nextCodePoint)) } s.pos = savedPos } } // Lone surrogate: encode as CESU-8 so it survives losslessly. In a // non-unicode regex this also lets scanClassRanges compare it numerically. return stringutil.EncodeJSStringRune(codePoint) case 'x': // '\xDD' for ; s.pos < start+4; s.pos++ { if !stringutil.IsHexDigit(s.char()) { s.tokenFlags |= ast.TokenFlagsContainsInvalidEscape if flags&EscapeSequenceScanningFlagsReportInvalidEscapeErrors != 0 { s.error(diagnostics.Hexadecimal_digit_expected) } return s.text[start:s.pos] } } s.tokenFlags |= ast.TokenFlagsHexEscape escapedValue, _ := strconv.ParseInt(s.text[start+2:s.pos], 16, 32) return string(rune(escapedValue)) case '\r': // when encountering a LineContinuation (i.e. a backslash and a line terminator sequence), // the line terminator is interpreted to be "the empty code unit sequence". if s.char() == '\n' { s.pos++ } fallthrough case '\n': return "" default: // ch was read as a single byte; for multi-byte UTF-8 characters, // we need to decode the full rune and advance past all its bytes. if ch >= utf8.RuneSelf { s.pos-- // back up past the single-byte advance var size int ch, size = utf8.DecodeRuneInString(s.text[s.pos:]) s.pos += size } // LineContinuation: a backslash followed by a line terminator is "the empty code unit sequence". if ch == '\u2028' || ch == '\u2029' { return "" } if flags&EscapeSequenceScanningFlagsAnyUnicodeMode != 0 || flags&EscapeSequenceScanningFlagsRegularExpression != 0 && flags&EscapeSequenceScanningFlagsAnnexB == 0 && IsIdentifierPart(ch) { s.errorAt(diagnostics.This_character_cannot_be_escaped_in_a_regular_expression, start, s.pos-start) } return string(ch) } } // Known to be at \u func (s *Scanner) scanUnicodeEscape(shouldEmitInvalidEscapeError bool) rune { s.pos += 2 start := s.pos extended := s.char() == '{' var hexDigits string if extended { s.pos++ hexDigits = s.scanHexDigits(1, true, false) } else { s.tokenFlags |= ast.TokenFlagsUnicodeEscape hexDigits = s.scanHexDigits(4, false, false) } if hexDigits == "" { s.tokenFlags |= ast.TokenFlagsContainsInvalidEscape if shouldEmitInvalidEscapeError { s.error(diagnostics.Hexadecimal_digit_expected) } return -1 } hexValue, _ := strconv.ParseInt(hexDigits, 16, 32) if extended { isInvalidExtendedEscape := false if hexValue > 0x10FFFF { if shouldEmitInvalidEscapeError { s.errorAt(diagnostics.An_extended_Unicode_escape_value_must_be_between_0x0_and_0x10FFFF_inclusive, start+1, s.pos-start-1) } isInvalidExtendedEscape = true } if s.pos >= s.end { if shouldEmitInvalidEscapeError { s.error(diagnostics.Unexpected_end_of_text) } isInvalidExtendedEscape = true } else if s.char() == '}' { s.pos++ } else { if shouldEmitInvalidEscapeError { s.error(diagnostics.Unterminated_Unicode_escape_sequence) } isInvalidExtendedEscape = true } if isInvalidExtendedEscape { s.tokenFlags |= ast.TokenFlagsContainsInvalidEscape return -1 } s.tokenFlags |= ast.TokenFlagsExtendedUnicodeEscape } return rune(hexValue) } // scanLowSurrogateEscape attempts to consume a low-surrogate Unicode escape // (either '\uLow' or '\u{Low}') immediately following an already-scanned high // surrogate and combine them into a single supplementary code point. This // mirrors how adjacent UTF-16 code units form a surrogate pair in a JavaScript // string, regardless of which escape syntax produced each half. On success it // returns the combined code point and true; otherwise it restores the scanner // position and returns false. func (s *Scanner) scanLowSurrogateEscape(high rune) (rune, bool) { if s.char() != '\\' || s.charAt(1) != 'u' { return 0, false } savedPos := s.pos savedTokenFlags := s.tokenFlags // Speculatively scan the escape with diagnostics suppressed: if it isn't a // low surrogate we rewind below, and the caller re-scans the same escape and // reports any error then, so reporting here would duplicate diagnostics. low := s.scanUnicodeEscape(false) if stringutil.IsLowSurrogate(low) { return stringutil.SurrogatePairToCodePoint(high, low), true } s.pos = savedPos s.tokenFlags = savedTokenFlags return 0, false } // Current character is known to be a backslash. Check for Unicode escape of the form '\uXXXX' // or '\u{XXXXXX}' and return code point value if valid Unicode escape is found. Otherwise return -1. func (s *Scanner) peekUnicodeEscape() rune { if s.charAt(1) == 'u' { savePos := s.pos saveTokenFlags := s.tokenFlags codePoint := s.scanUnicodeEscape(false) s.pos = savePos s.tokenFlags = saveTokenFlags return codePoint } return -1 } func (s *Scanner) scanNumber() ast.Kind { start := s.pos var fixedPart string if s.char() == '0' { s.pos++ if s.char() == '_' { s.tokenFlags |= ast.TokenFlagsContainsSeparator | ast.TokenFlagsContainsInvalidSeparator s.errorAt(diagnostics.Numeric_separators_are_not_allowed_here, s.pos, 1) s.pos = start fixedPart = s.scanNumberFragment() } else { digits, isOctal := s.scanDigits() if digits == "" { fixedPart = "0" } else if !isOctal { s.tokenFlags |= ast.TokenFlagsContainsLeadingZero fixedPart = digits } else { val, _ := strconv.ParseInt(digits, 8, 64) s.tokenValue = strconv.FormatInt(val, 10) s.tokenFlags |= ast.TokenFlagsOctal withMinus := s.token == ast.KindMinusToken literal := core.IfElse(withMinus, "-", "") + "0o" + strconv.FormatInt(val, 8) if withMinus { start-- } s.errorAt(diagnostics.Octal_literals_are_not_allowed_Use_the_syntax_0, start, s.pos-start, literal) return ast.KindNumericLiteral } } } else { fixedPart = s.scanNumberFragment() } fixedPartEnd := s.pos fractionalPart := "" exponentPreamble := "" exponentPart := "" if s.char() == '.' { s.pos++ fractionalPart = s.scanNumberFragment() } end := s.pos if s.char() == 'E' || s.char() == 'e' { s.pos++ s.tokenFlags |= ast.TokenFlagsScientific if s.char() == '+' || s.char() == '-' { s.pos++ } startNumericPart := s.pos exponentPart = s.scanNumberFragment() if exponentPart == "" { s.error(diagnostics.Digit_expected) } else { exponentPreamble = s.text[end:startNumericPart] end = s.pos } } if s.tokenFlags&ast.TokenFlagsContainsSeparator != 0 { s.tokenValue = fixedPart if fractionalPart != "" { s.tokenValue += "." + fractionalPart } if exponentPart != "" { s.tokenValue += exponentPreamble + exponentPart } } else { s.tokenValue = s.text[start:end] } if s.tokenFlags&ast.TokenFlagsContainsLeadingZero != 0 { s.errorAt(diagnostics.Decimals_with_leading_zeros_are_not_allowed, start, s.pos-start) s.tokenValue = jsnum.FromString(s.tokenValue).String() return ast.KindNumericLiteral } var result ast.Kind if fixedPartEnd == s.pos { result = s.scanBigIntSuffix() } else { s.tokenValue = jsnum.FromString(s.tokenValue).String() result = ast.KindNumericLiteral } ch, _ := s.charAndSize() if IsIdentifierStart(ch) { idStart := s.pos id := s.scanIdentifierParts() if result != ast.KindBigIntLiteral && len(id) == 1 && s.text[idStart] == 'n' { if s.tokenFlags&ast.TokenFlagsScientific != 0 { s.errorAt(diagnostics.A_bigint_literal_cannot_use_exponential_notation, start, s.pos-start) return result } if fixedPartEnd < idStart { s.errorAt(diagnostics.A_bigint_literal_must_be_an_integer, start, s.pos-start) return result } } s.errorAt(diagnostics.An_identifier_or_keyword_cannot_immediately_follow_a_numeric_literal, idStart, s.pos-idStart) s.pos = idStart } return result } func (s *Scanner) scanNumberFragment() string { start := s.pos allowSeparator := false isPreviousTokenSeparator := false var result strings.Builder for { before := s.pos s.scanASCIIWhile(func(b byte) bool { return b >= '0' && b <= '9' }) if s.pos > before { allowSeparator = true isPreviousTokenSeparator = false } ch := s.char() if ch == '_' { s.tokenFlags |= ast.TokenFlagsContainsSeparator if allowSeparator { allowSeparator = false isPreviousTokenSeparator = true result.WriteString(s.text[start:s.pos]) } else { s.tokenFlags |= ast.TokenFlagsContainsInvalidSeparator if isPreviousTokenSeparator { s.errorAt(diagnostics.Multiple_consecutive_numeric_separators_are_not_permitted, s.pos, 1) } else { s.errorAt(diagnostics.Numeric_separators_are_not_allowed_here, s.pos, 1) } } s.pos++ start = s.pos continue } break } if isPreviousTokenSeparator { s.tokenFlags |= ast.TokenFlagsContainsInvalidSeparator s.errorAt(diagnostics.Numeric_separators_are_not_allowed_here, s.pos-1, 1) } if result.Len() == 0 { return s.text[start:s.pos] } result.WriteString(s.text[start:s.pos]) return result.String() } func (s *Scanner) scanDigits() (string, bool) { start := s.pos isOctal := true for stringutil.IsDigit(s.char()) { if !stringutil.IsOctalDigit(s.char()) { isOctal = false } s.pos++ } return s.text[start:s.pos], isOctal } func (s *Scanner) scanHexDigits(minCount int, scanAsManyAsPossible bool, canHaveSeparators bool) string { digitCount := 0 start := s.pos allowSeparator := false isPreviousTokenSeparator := false for digitCount < minCount || scanAsManyAsPossible { ch := s.char() if stringutil.IsHexDigit(ch) { allowSeparator = canHaveSeparators isPreviousTokenSeparator = false digitCount++ } else if canHaveSeparators && ch == '_' { s.tokenFlags |= ast.TokenFlagsContainsSeparator if allowSeparator { allowSeparator = false isPreviousTokenSeparator = true } else if isPreviousTokenSeparator { s.errorAt(diagnostics.Multiple_consecutive_numeric_separators_are_not_permitted, s.pos, 1) } else { s.errorAt(diagnostics.Numeric_separators_are_not_allowed_here, s.pos, 1) } } else { break } s.pos++ } if isPreviousTokenSeparator { s.errorAt(diagnostics.Numeric_separators_are_not_allowed_here, s.pos-1, 1) } if digitCount < minCount { return "" } digits := s.text[start:s.pos] if s.hexDigitCache == nil { s.hexDigitCache = make(map[string]string) } if cached, ok := s.hexDigitCache[digits]; ok { return cached } else { original := digits if s.tokenFlags&ast.TokenFlagsContainsSeparator != 0 { digits = strings.ReplaceAll(digits, "_", "") } digits = strings.ToLower(digits) // standardize hex literals to lowercase s.hexDigitCache[original] = digits return digits } } func (s *Scanner) scanBinaryOrOctalDigits(base int32) string { var sb strings.Builder allowSeparator := false isPreviousTokenSeparator := false for { ch := s.char() if stringutil.IsDigit(ch) && ch-'0' < base { sb.WriteByte(byte(ch)) allowSeparator = true isPreviousTokenSeparator = false } else if ch == '_' { s.tokenFlags |= ast.TokenFlagsContainsSeparator if allowSeparator { allowSeparator = false isPreviousTokenSeparator = true } else if isPreviousTokenSeparator { s.errorAt(diagnostics.Multiple_consecutive_numeric_separators_are_not_permitted, s.pos, 1) } else { s.errorAt(diagnostics.Numeric_separators_are_not_allowed_here, s.pos, 1) } } else { break } s.pos++ } if isPreviousTokenSeparator { s.errorAt(diagnostics.Numeric_separators_are_not_allowed_here, s.pos-1, 1) } return sb.String() } func (s *Scanner) scanBigIntSuffix() ast.Kind { if s.char() == 'n' { s.tokenValue += "n" if s.tokenFlags&ast.TokenFlagsBinaryOrOctalSpecifier != 0 { s.tokenValue = jsnum.ParsePseudoBigInt(s.tokenValue) + "n" } s.pos++ return ast.KindBigIntLiteral } if s.numberCache == nil { s.numberCache = make(map[string]string) } if cached, ok := s.numberCache[s.tokenValue]; ok { s.tokenValue = cached } else { tokenValue := jsnum.FromString(s.tokenValue).String() if tokenValue == s.tokenValue { tokenValue = s.tokenValue } s.numberCache[s.tokenValue] = tokenValue s.tokenValue = tokenValue } return ast.KindNumericLiteral } func (s *Scanner) scanInvalidCharacter() { _, size := s.charAndSize() s.errorAt(diagnostics.Invalid_character, s.pos, size) s.pos += size s.token = ast.KindUnknown } func GetIdentifierToken(str string) ast.Kind { if len(str) >= 2 && len(str) <= 12 && str[0] >= 'a' && str[0] <= 'z' { keyword := textToKeyword[str] if keyword != ast.KindUnknown { return keyword } } return ast.KindIdentifier } func IsValidIdentifier(s string) bool { if len(s) == 0 { return false } for i, ch := range s { if i == 0 && !IsIdentifierStart(ch) || i != 0 && !IsIdentifierPart(ch) { return false } } return true } // Section 6.1.4 func isWordCharacter(ch rune) bool { return stringutil.IsASCIILetter(ch) || stringutil.IsDigit(ch) || ch == '_' } func IsIdentifierStart(ch rune) bool { return stringutil.IsASCIILetter(ch) || ch == '_' || ch == '$' || ch >= utf8.RuneSelf && stringutil.IsUnicodeIdentifierStart(ch) } func IsIdentifierPart(ch rune) bool { return IsIdentifierPartEx(ch, core.LanguageVariantStandard) } func IsIdentifierPartEx(ch rune, languageVariant core.LanguageVariant) bool { return isWordCharacter(ch) || ch == '$' || ch >= utf8.RuneSelf && stringutil.IsUnicodeIdentifierPart(ch) || languageVariant == core.LanguageVariantJSX && (ch == '-' || ch == ':') // "-" and ":" are valid in JSX Identifiers } var tokenToText = func() [ast.KindCount]string { var result [ast.KindCount]string for text, kind := range textToToken { result[kind] = text } return result }() func TokenToString(token ast.Kind) string { return tokenToText[token] } func StringToToken(s string) ast.Kind { kind, ok := textToToken[s] if ok { return kind } return ast.KindUnknown } func GetViableKeywordSuggestions() []string { result := make([]string, 0, len(textToKeyword)) for text := range textToKeyword { if len(text) > 2 { result = append(result, text) } } return result } func couldStartTrivia(text string, pos int) bool { // Keep in sync with skipTrivia switch ch := text[pos]; ch { // Characters that could start normal trivia case '\r', '\n', '\t', '\v', '\f', ' ', '/', // Characters that could start conflict marker trivia '<', '|', '=', '>': return true case '#': // Only if its the beginning can we have #! trivia return pos == 0 default: return ch > maxAsciiCharacter } } type SkipTriviaOptions struct { StopAfterLineBreak bool StopAtComments bool InJSDoc bool } func SkipTrivia(text string, pos int) int { return SkipTriviaEx(text, pos, nil) } func SkipTriviaEx(text string, pos int, options *SkipTriviaOptions) int { if ast.PositionIsSynthesized(pos) { return pos } if options == nil { options = &SkipTriviaOptions{} } textLen := len(text) canConsumeStar := false // Keep in sync with couldStartTrivia for { if pos >= textLen { return pos } ch, size := utf8.DecodeRuneInString(text[pos:]) switch ch { case '\r': if pos+1 < textLen && text[pos+1] == '\n' { pos++ } fallthrough case '\n': pos++ if options.StopAfterLineBreak { return pos } canConsumeStar = options.InJSDoc continue case '\t', '\v', '\f', ' ': pos++ continue case '/': if options.StopAtComments { break } if pos+1 < textLen { if text[pos+1] == '/' { pos += 2 for pos < textLen { ch, size := utf8.DecodeRuneInString(text[pos:]) if stringutil.IsLineBreak(ch) { break } pos += size } canConsumeStar = false continue } if text[pos+1] == '*' { pos += 2 for pos < textLen { if text[pos] == '*' && (pos+1 < textLen) && text[pos+1] == '/' { pos += 2 break } _, size := utf8.DecodeRuneInString(text[pos:]) pos += size } canConsumeStar = false continue } } case '<', '|', '=', '>': if isConflictMarkerTrivia(text, pos) { pos = scanConflictMarkerTrivia(text, pos, nil) canConsumeStar = false continue } case '#': if pos == 0 && isShebangTrivia(text, pos) { pos = scanShebangTrivia(text, pos) canConsumeStar = false continue } case '*': if canConsumeStar { pos++ canConsumeStar = false continue } default: if ch > rune(maxAsciiCharacter) && stringutil.IsWhiteSpaceLike(ch) { pos += size continue } } return pos } } // All conflict markers consist of the same character repeated seven times. If it is // a <<<<<<< or >>>>>>> marker then it is also followed by a space. var ( mergeConflictMarkerLength = len("<<<<<<<") maxAsciiCharacter byte = 127 ) func isConflictMarkerTrivia(text string, pos int) bool { if pos < 0 { panic("pos < 0") } // Fast reject: a conflict marker is the same byte repeated seven times. If the // second byte differs (the overwhelmingly common case for `<`, `>`, `=`, `|` // tokens), it cannot be a marker, so skip the line-start check entirely. if pos+1 >= len(text) || text[pos+1] != text[pos] { return false } // Conflict markers must be at the start of a line. atLineStart := pos == 0 || stringutil.IsLineBreak(rune(text[pos-1])) if !atLineStart && pos >= 2 { prev, _ := utf8.DecodeLastRuneInString(text[:pos-2]) atLineStart = stringutil.IsLineBreak(prev) } if atLineStart { ch := text[pos] if (pos + mergeConflictMarkerLength) < len(text) { for i := range mergeConflictMarkerLength { if text[pos+i] != ch { return false } } return ch == '=' || text[pos+mergeConflictMarkerLength] == ' ' } } return false } func scanConflictMarkerTrivia(text string, pos int, reportError func(diag *diagnostics.Message, pos int, length int, args ...any)) int { if reportError != nil { reportError(diagnostics.Merge_conflict_marker_encountered, pos, mergeConflictMarkerLength) } ch, size := utf8.DecodeRuneInString(text[pos:]) length := len(text) if ch == '<' || ch == '>' { for pos < length && !stringutil.IsLineBreak(ch) { pos += size ch, size = utf8.DecodeRuneInString(text[pos:]) } } else { if ch != '|' && ch != '=' { panic("Assertion failed: ch must be either '|' or '='") } // Consume everything from the start of a ||||||| or ======= marker to the start // of the next ======= or >>>>>>> marker. for pos < length { currentChar := text[pos] if (currentChar == '=' || currentChar == '>') && rune(currentChar) != ch && isConflictMarkerTrivia(text, pos) { break } pos++ } } return pos } func isShebangTrivia(text string, pos int) bool { if len(text) < 2 { return false } if pos != 0 { panic("Shebangs check must only be done at the start of the file") } return text[0] == '#' && text[1] == '!' } func scanShebangTrivia(text string, pos int) int { pos += 2 for pos < len(text) { ch, size := utf8.DecodeRuneInString(text[pos:]) if stringutil.IsLineBreak(ch) { break } pos += size } return pos } func GetShebang(text string) string { if !isShebangTrivia(text, 0) { return "" } end := scanShebangTrivia(text, 0) return text[:end] } func GetScannerForSourceFile(sourceFile *ast.SourceFile, pos int) *Scanner { s := NewScanner() s.text = sourceFile.Text() s.pos = pos s.end = len(s.text) s.languageVariant = sourceFile.LanguageVariant s.Scan() return s } func ScanTokenAtPosition(sourceFile *ast.SourceFile, pos int) ast.Kind { s := GetScannerForSourceFile(sourceFile, pos) return s.token } func GetRangeOfTokenAtPosition(sourceFile *ast.SourceFile, pos int) core.TextRange { s := GetScannerForSourceFile(sourceFile, pos) return core.NewTextRange(s.tokenStart, s.pos) } func GetTokenPosOfNode(node *ast.Node, sourceFile *ast.SourceFile, includeJSDoc bool) int { // With nodes that have no width (i.e. 'Missing' nodes), we actually *don't* // want to skip trivia because this will launch us forward to the next token. if ast.NodeIsMissing(node) { return node.Pos() } if ast.IsJSDocNode(node) || node.Kind == ast.KindJsxText { // JsxText cannot actually contain comments, even though the scanner will think it sees comments return SkipTriviaEx(sourceFile.Text(), node.Pos(), &SkipTriviaOptions{StopAtComments: true}) } if includeJSDoc && len(node.JSDoc(sourceFile)) > 0 { return GetTokenPosOfNode(node.JSDoc(sourceFile)[0], sourceFile, false /*includeJSDoc*/) } return SkipTriviaEx(sourceFile.Text(), node.Pos(), &SkipTriviaOptions{InJSDoc: node.Flags&ast.NodeFlagsJSDoc != 0}) } func getErrorRangeForArrowFunction(sourceFile *ast.SourceFile, node *ast.Node) core.TextRange { pos := SkipTrivia(sourceFile.Text(), node.Pos()) body := node.Body() if body != nil && body.Kind == ast.KindBlock { startLine := GetECMALineOfPosition(sourceFile, body.Pos()) endLine := GetECMALineOfPosition(sourceFile, body.End()) if startLine < endLine { // The arrow function spans multiple lines, make the error span be the first line, inclusive. return core.NewTextRange(pos, GetECMAEndLinePosition(sourceFile, startLine)+1) } } return core.NewTextRange(pos, node.End()) } func findOriginatingJSDocSatisfiesTag(sourceFile *ast.SourceFile, node *ast.Node) *ast.Node { targetType := node.AsSatisfiesExpression().Type if targetType.Flags&ast.NodeFlagsReparsed == 0 { return nil } for current := node.Parent; current != nil; current = current.Parent { if current.Flags&ast.NodeFlagsHasJSDoc == 0 { continue } var firstSatisfiesTag *ast.Node for _, jsDoc := range current.EagerJSDoc(sourceFile) { if tags := jsDoc.AsJSDoc().Tags; tags != nil { for _, tag := range tags.Nodes { if !ast.IsJSDocSatisfiesTag(tag) { continue } if firstSatisfiesTag == nil { firstSatisfiesTag = tag } if typeExpr := tag.AsJSDocSatisfiesTag().TypeExpression; typeExpr != nil { if t := typeExpr.Type(); t != nil && t.Loc == targetType.Loc { return tag } } } } } return firstSatisfiesTag } return nil } func GetErrorRangeForNode(sourceFile *ast.SourceFile, node *ast.Node) core.TextRange { errorNode := node switch node.Kind { case ast.KindSourceFile: pos := SkipTrivia(sourceFile.Text(), 0) if pos == len(sourceFile.Text()) { return core.NewTextRange(0, 0) } return GetRangeOfTokenAtPosition(sourceFile, pos) // This list is a work in progress. Add missing node kinds to improve their error spans case ast.KindFunctionDeclaration, ast.KindMethodDeclaration: if node.Flags&ast.NodeFlagsReparsed != 0 { errorNode = node break } fallthrough case ast.KindVariableDeclaration, ast.KindBindingElement, ast.KindClassDeclaration, ast.KindInterfaceDeclaration, ast.KindModuleDeclaration, ast.KindEnumDeclaration, ast.KindEnumMember, ast.KindFunctionExpression, ast.KindGetAccessor, ast.KindSetAccessor, ast.KindTypeAliasDeclaration, ast.KindJSTypeAliasDeclaration, ast.KindPropertyDeclaration, ast.KindPropertySignature, ast.KindNamespaceImport: errorNode = ast.GetNameOfDeclaration(node) case ast.KindClassExpression: errorNode = node.Name() case ast.KindArrowFunction: return getErrorRangeForArrowFunction(sourceFile, node) case ast.KindCaseClause, ast.KindDefaultClause: start := SkipTrivia(sourceFile.Text(), node.Pos()) end := node.End() statements := node.Statements() if len(statements) != 0 { end = statements[0].Pos() } return core.NewTextRange(start, end) case ast.KindReturnStatement, ast.KindYieldExpression: pos := SkipTrivia(sourceFile.Text(), node.Pos()) return GetRangeOfTokenAtPosition(sourceFile, pos) case ast.KindSatisfiesExpression: if jsDocSatisfiesTag := findOriginatingJSDocSatisfiesTag(sourceFile, node); jsDocSatisfiesTag != nil { pos := SkipTrivia(sourceFile.Text(), jsDocSatisfiesTag.TagName().Pos()) return GetRangeOfTokenAtPosition(sourceFile, pos) } pos := SkipTrivia(sourceFile.Text(), node.AsSatisfiesExpression().Expression.End()) return GetRangeOfTokenAtPosition(sourceFile, pos) case ast.KindConstructor: if node.Flags&ast.NodeFlagsReparsed != 0 { errorNode = node break } scanner := GetScannerForSourceFile(sourceFile, node.Pos()) start := scanner.TokenStart() for scanner.Token() != ast.KindConstructorKeyword && scanner.Token() != ast.KindStringLiteral && scanner.Token() != ast.KindEndOfFile { scanner.Scan() } return core.NewTextRange(start, scanner.TokenEnd()) } if errorNode == nil { // If we don't have a better node, then just set the error on the first token of // construct. return GetRangeOfTokenAtPosition(sourceFile, node.Pos()) } pos := errorNode.Pos() if !ast.NodeIsMissing(errorNode) && !ast.IsJsxText(errorNode) { pos = SkipTrivia(sourceFile.Text(), pos) } return core.NewTextRange(pos, errorNode.End()) } func ComputeLineOfPosition(lineStarts []core.TextPos, pos int) int { low := 0 high := len(lineStarts) - 1 for low <= high { middle := low + ((high - low) >> 1) value := int(lineStarts[middle]) if value < pos { low = middle + 1 } else if value > pos { high = middle - 1 } else { return middle } } return low - 1 } func GetECMALineStarts(sourceFile ast.SourceFileLike) []core.TextPos { return sourceFile.ECMALineMap() } func GetECMALineOfPosition(sourceFile ast.SourceFileLike, pos int) int { lineMap := GetECMALineStarts(sourceFile) return ComputeLineOfPosition(lineMap, pos) } // GetECMALineAndUTF16CharacterOfPosition returns the 0-based line number and the // UTF-16 code unit offset from the start of that line for the given byte position. // Uses ECMAScript line separators (LF, CR, CRLF, LS, PS). func GetECMALineAndUTF16CharacterOfPosition(sourceFile ast.SourceFileLike, pos int) (line int, character core.UTF16Offset) { lineMap := GetECMALineStarts(sourceFile) line = ComputeLineOfPosition(lineMap, pos) character = core.UTF16Len(sourceFile.Text()[lineMap[line]:pos]) return line, character } // GetECMALineAndByteOffsetOfPosition returns the 0-based line number and the // raw UTF-8 byte offset from the start of that line for the given byte position. // Uses ECMAScript line separators (LF, CR, CRLF, LS, PS). // Unlike GetECMALineAndUTF16CharacterOfPosition, the offset is in bytes, not UTF-16 code units. func GetECMALineAndByteOffsetOfPosition(sourceFile ast.SourceFileLike, pos int) (line int, byteOffset int) { lineMap := GetECMALineStarts(sourceFile) line = ComputeLineOfPosition(lineMap, pos) byteOffset = pos - int(lineMap[line]) return line, byteOffset } func GetECMAEndLinePosition(sourceFile *ast.SourceFile, line int) int { pos := int(GetECMALineStarts(sourceFile)[line]) for { ch, size := utf8.DecodeRuneInString(sourceFile.Text()[pos:]) if size == 0 || stringutil.IsLineBreak(ch) { return pos - 1 } pos += size } } // GetECMAPositionOfLineAndUTF16Character converts a 0-based line number and UTF-16 // code unit character offset back to an absolute byte position in the source text. // Uses ECMAScript line separators. func GetECMAPositionOfLineAndUTF16Character(sourceFile ast.SourceFileLike, line int, character core.UTF16Offset) int { lineStarts := GetECMALineStarts(sourceFile) return ComputePositionOfLineAndUTF16Character(lineStarts, line, character, sourceFile.Text(), false) } // GetECMAPositionOfLineAndByteOffset converts a 0-based line number and byte offset // from line start back to an absolute byte position in the source text. // Uses ECMAScript line separators. func GetECMAPositionOfLineAndByteOffset(sourceFile ast.SourceFileLike, line int, byteOffset int) int { return ComputePositionOfLineAndByteOffset(GetECMALineStarts(sourceFile), line, byteOffset) } // ComputePositionOfLineAndByteOffset computes a byte position from a line and // raw byte offset from the line start. This is a simple addition with validation. func ComputePositionOfLineAndByteOffset(lineStarts []core.TextPos, line int, byteOffset int) int { if line < 0 || line >= len(lineStarts) { panic(fmt.Sprintf("Bad line number. Line: %d, lineStarts.length: %d.", line, len(lineStarts))) } return int(lineStarts[line]) + byteOffset } // ComputePositionOfLineAndUTF16Character converts a line and UTF-16 character offset // back to a byte position. The character parameter is measured in UTF-16 code units. // It scans from the line start to correctly handle multi-byte characters. // When allowEdits is true, out-of-range values are clamped instead of panicking. func ComputePositionOfLineAndUTF16Character(lineStarts []core.TextPos, line int, character core.UTF16Offset, text string, allowEdits bool) int { if line < 0 || line >= len(lineStarts) { if allowEdits { // Clamp line to nearest allowable value if line < 0 { line = 0 } else if line >= len(lineStarts) { line = len(lineStarts) - 1 } } else { panic(fmt.Sprintf("Bad line number. Line: %d, lineStarts.length: %d.", line, len(lineStarts))) } } lineStart := int(lineStarts[line]) if character > 0 { // UTF-16 character offset: scan from line start counting UTF-16 code units. lineEnd := len(text) if line+1 < len(lineStarts) { lineEnd = int(lineStarts[line+1]) } utf16Count := core.UTF16Offset(0) pos := lineStart for pos < lineEnd { if utf16Count >= character { break } r, size := utf8.DecodeRuneInString(text[pos:]) utf16Count += core.UTF16Offset(utf16.RuneLen(r)) pos += size } if !allowEdits { if pos == lineEnd && utf16Count < character { panic(fmt.Sprintf("Bad UTF-16 character offset. Line: %d, character: %d.", line, character)) } debug.Assert(pos <= len(text)) return pos } if pos > len(text) { return len(text) } return pos } // Character is 0: line start position. res := lineStart if allowEdits { if res > len(text) { return len(text) } return res } debug.Assert(res <= len(text)) // Allow single character overflow for trailing newline return res } func GetLeadingCommentRanges(f *ast.NodeFactory, text string, pos int) iter.Seq[ast.CommentRange] { return iterateCommentRanges(f, text, pos, false) } func GetTrailingCommentRanges(f *ast.NodeFactory, text string, pos int) iter.Seq[ast.CommentRange] { return iterateCommentRanges(f, text, pos, true) } /* Returns an iterator over each comment range following the provided position. Single-line comment ranges include the leading double-slash characters but not the ending line break. Multi-line comment ranges include the leading slash-asterisk and trailing asterisk-slash characters. */ func iterateCommentRanges(f *ast.NodeFactory, text string, pos int, trailing bool) iter.Seq[ast.CommentRange] { return func(yield func(ast.CommentRange) bool) { var pendingPos int var pendingEnd int var pendingKind ast.Kind var pendingHasTrailingNewLine bool hasPendingCommentRange := false collecting := trailing if pos == 0 { collecting = true if isShebangTrivia(text, pos) { pos = scanShebangTrivia(text, pos) } } scan: for pos >= 0 && pos < len(text) { ch, size := utf8.DecodeRuneInString(text[pos:]) switch ch { case '\r': if pos+1 < len(text) && text[pos+1] == '\n' { pos++ } fallthrough case '\n': pos++ if trailing { break scan } collecting = true if hasPendingCommentRange { pendingHasTrailingNewLine = true } continue case '\t', '\v', '\f', ' ': pos++ continue case '/': var nextChar byte if pos+1 < len(text) { nextChar = text[pos+1] } hasTrailingNewLine := false if nextChar == '/' || nextChar == '*' { var kind ast.Kind if nextChar == '/' { kind = ast.KindSingleLineCommentTrivia } else { kind = ast.KindMultiLineCommentTrivia } startPos := pos pos += 2 if nextChar == '/' { for pos < len(text) { c, s := utf8.DecodeRuneInString(text[pos:]) if stringutil.IsLineBreak(c) { hasTrailingNewLine = true break } pos += s } } else { if i := strings.Index(text[pos:], "*/"); i >= 0 { pos += i + 2 } else { pos = len(text) } } if collecting { if hasPendingCommentRange { if !yield(f.NewCommentRange(pendingKind, pendingPos, pendingEnd, pendingHasTrailingNewLine)) { return } } pendingPos = startPos pendingEnd = pos pendingKind = kind pendingHasTrailingNewLine = hasTrailingNewLine hasPendingCommentRange = true } continue } break scan default: if ch > unicode.MaxASCII && stringutil.IsWhiteSpaceLike(ch) { if hasPendingCommentRange && stringutil.IsLineBreak(ch) { pendingHasTrailingNewLine = true } pos += size continue } break scan } } if hasPendingCommentRange { yield(f.NewCommentRange(pendingKind, pendingPos, pendingEnd, pendingHasTrailingNewLine)) } } }