mirror of
https://github.com/hanzoai/esbuild.git
synced 2026-09-24 16:58:50 +00:00
2666 lines
65 KiB
Go
2666 lines
65 KiB
Go
package js_lexer
|
|
|
|
// The lexer converts a source file to a stream of tokens. Unlike many
|
|
// compilers, esbuild does not run the lexer to completion before the parser is
|
|
// started. Instead, the lexer is called repeatedly by the parser as the parser
|
|
// parses the file. This is because many tokens are context-sensitive and need
|
|
// high-level information from the parser. Examples are regular expression
|
|
// literals and JSX elements.
|
|
//
|
|
// For efficiency, the text associated with textual tokens is stored in two
|
|
// separate ways depending on the token. Identifiers use UTF-8 encoding which
|
|
// allows them to be slices of the input file without allocating extra memory.
|
|
// Strings use UTF-16 encoding so they can represent unicode surrogates
|
|
// accurately.
|
|
|
|
import (
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
"unicode/utf8"
|
|
|
|
"github.com/evanw/esbuild/internal/ast"
|
|
"github.com/evanw/esbuild/internal/config"
|
|
"github.com/evanw/esbuild/internal/helpers"
|
|
"github.com/evanw/esbuild/internal/js_ast"
|
|
"github.com/evanw/esbuild/internal/logger"
|
|
)
|
|
|
|
type T uint8
|
|
|
|
// If you add a new token, remember to add it to "tokenToString" too
|
|
const (
|
|
TEndOfFile T = iota
|
|
TSyntaxError
|
|
|
|
// "#!/usr/bin/env node"
|
|
THashbang
|
|
|
|
// Literals
|
|
TNoSubstitutionTemplateLiteral // Contents are in lexer.StringLiteral ([]uint16)
|
|
TNumericLiteral // Contents are in lexer.Number (float64)
|
|
TStringLiteral // Contents are in lexer.StringLiteral ([]uint16)
|
|
TBigIntegerLiteral // Contents are in lexer.Identifier (string)
|
|
|
|
// Pseudo-literals
|
|
TTemplateHead // Contents are in lexer.StringLiteral ([]uint16)
|
|
TTemplateMiddle // Contents are in lexer.StringLiteral ([]uint16)
|
|
TTemplateTail // Contents are in lexer.StringLiteral ([]uint16)
|
|
|
|
// Punctuation
|
|
TAmpersand
|
|
TAmpersandAmpersand
|
|
TAsterisk
|
|
TAsteriskAsterisk
|
|
TAt
|
|
TBar
|
|
TBarBar
|
|
TCaret
|
|
TCloseBrace
|
|
TCloseBracket
|
|
TCloseParen
|
|
TColon
|
|
TComma
|
|
TDot
|
|
TDotDotDot
|
|
TEqualsEquals
|
|
TEqualsEqualsEquals
|
|
TEqualsGreaterThan
|
|
TExclamation
|
|
TExclamationEquals
|
|
TExclamationEqualsEquals
|
|
TGreaterThan
|
|
TGreaterThanEquals
|
|
TGreaterThanGreaterThan
|
|
TGreaterThanGreaterThanGreaterThan
|
|
TLessThan
|
|
TLessThanEquals
|
|
TLessThanLessThan
|
|
TMinus
|
|
TMinusMinus
|
|
TOpenBrace
|
|
TOpenBracket
|
|
TOpenParen
|
|
TPercent
|
|
TPlus
|
|
TPlusPlus
|
|
TQuestion
|
|
TQuestionDot
|
|
TQuestionQuestion
|
|
TSemicolon
|
|
TSlash
|
|
TTilde
|
|
|
|
// Assignments (keep in sync with IsAssign() below)
|
|
TAmpersandAmpersandEquals
|
|
TAmpersandEquals
|
|
TAsteriskAsteriskEquals
|
|
TAsteriskEquals
|
|
TBarBarEquals
|
|
TBarEquals
|
|
TCaretEquals
|
|
TEquals
|
|
TGreaterThanGreaterThanEquals
|
|
TGreaterThanGreaterThanGreaterThanEquals
|
|
TLessThanLessThanEquals
|
|
TMinusEquals
|
|
TPercentEquals
|
|
TPlusEquals
|
|
TQuestionQuestionEquals
|
|
TSlashEquals
|
|
|
|
// Class-private fields and methods
|
|
TPrivateIdentifier
|
|
|
|
// Identifiers
|
|
TIdentifier // Contents are in lexer.Identifier (string)
|
|
TEscapedKeyword // A keyword that has been escaped as an identifier
|
|
|
|
// Reserved words
|
|
TBreak
|
|
TCase
|
|
TCatch
|
|
TClass
|
|
TConst
|
|
TContinue
|
|
TDebugger
|
|
TDefault
|
|
TDelete
|
|
TDo
|
|
TElse
|
|
TEnum
|
|
TExport
|
|
TExtends
|
|
TFalse
|
|
TFinally
|
|
TFor
|
|
TFunction
|
|
TIf
|
|
TImport
|
|
TIn
|
|
TInstanceof
|
|
TNew
|
|
TNull
|
|
TReturn
|
|
TSuper
|
|
TSwitch
|
|
TThis
|
|
TThrow
|
|
TTrue
|
|
TTry
|
|
TTypeof
|
|
TVar
|
|
TVoid
|
|
TWhile
|
|
TWith
|
|
)
|
|
|
|
func (t T) IsAssign() bool {
|
|
return t >= TAmpersandAmpersandEquals && t <= TSlashEquals
|
|
}
|
|
|
|
var Keywords = map[string]T{
|
|
// Reserved words
|
|
"break": TBreak,
|
|
"case": TCase,
|
|
"catch": TCatch,
|
|
"class": TClass,
|
|
"const": TConst,
|
|
"continue": TContinue,
|
|
"debugger": TDebugger,
|
|
"default": TDefault,
|
|
"delete": TDelete,
|
|
"do": TDo,
|
|
"else": TElse,
|
|
"enum": TEnum,
|
|
"export": TExport,
|
|
"extends": TExtends,
|
|
"false": TFalse,
|
|
"finally": TFinally,
|
|
"for": TFor,
|
|
"function": TFunction,
|
|
"if": TIf,
|
|
"import": TImport,
|
|
"in": TIn,
|
|
"instanceof": TInstanceof,
|
|
"new": TNew,
|
|
"null": TNull,
|
|
"return": TReturn,
|
|
"super": TSuper,
|
|
"switch": TSwitch,
|
|
"this": TThis,
|
|
"throw": TThrow,
|
|
"true": TTrue,
|
|
"try": TTry,
|
|
"typeof": TTypeof,
|
|
"var": TVar,
|
|
"void": TVoid,
|
|
"while": TWhile,
|
|
"with": TWith,
|
|
}
|
|
|
|
var StrictModeReservedWords = map[string]bool{
|
|
"implements": true,
|
|
"interface": true,
|
|
"let": true,
|
|
"package": true,
|
|
"private": true,
|
|
"protected": true,
|
|
"public": true,
|
|
"static": true,
|
|
"yield": true,
|
|
}
|
|
|
|
// This represents a string that is maybe a substring of the current file's
|
|
// "source.Contents" string. The point of doing this is that if it is a
|
|
// substring (the common case), then we can represent it more efficiently.
|
|
//
|
|
// For compactness and performance, the JS AST represents identifiers as a
|
|
// symbol reference instead of as a string. However, we need to track the
|
|
// string between the first pass and the second pass because the string is only
|
|
// resolved to a symbol in the second pass. To avoid allocating extra memory
|
|
// to store the string, we instead use an index+length slice of the original JS
|
|
// source code. That index is what "Start" represents here. The length is just
|
|
// "len(String)".
|
|
//
|
|
// Set "Start" to invalid (the zero value) if "String" is not a substring of
|
|
// "source.Contents". This is the case for escaped identifiers. For example,
|
|
// the identifier "fo\u006f" would be "MaybeSubstring{String: "foo"}". It's
|
|
// critical that any code changing the "String" also set "Start" to the zero
|
|
// value, which is best done by just overwriting the whole "MaybeSubstring".
|
|
//
|
|
// The substring range used to be recovered automatically from the string but
|
|
// that relied on the Go "unsafe" package which can hypothetically break under
|
|
// certain Go compiler optimization passes, so it has been removed and replaced
|
|
// with this more error-prone approach that doesn't use "unsafe".
|
|
type MaybeSubstring struct {
|
|
String string
|
|
Start ast.Index32
|
|
}
|
|
|
|
type Lexer struct {
|
|
LegalCommentsBeforeToken []logger.Range
|
|
CommentsBeforeToken []logger.Range
|
|
AllComments []logger.Range
|
|
Identifier MaybeSubstring
|
|
log logger.Log
|
|
source logger.Source
|
|
JSXFactoryPragmaComment logger.Span
|
|
JSXFragmentPragmaComment logger.Span
|
|
JSXRuntimePragmaComment logger.Span
|
|
JSXImportSourcePragmaComment logger.Span
|
|
SourceMappingURL logger.Span
|
|
BadArrowInTSXSuggestion string
|
|
|
|
// Escape sequences in string literals are decoded lazily because they are
|
|
// not interpreted inside tagged templates, and tagged templates can contain
|
|
// invalid escape sequences. If the decoded array is nil, the encoded value
|
|
// should be passed to "tryToDecodeEscapeSequences" first.
|
|
decodedStringLiteralOrNil []uint16
|
|
encodedStringLiteralText string
|
|
|
|
errorSuffix string
|
|
tracker logger.LineColumnTracker
|
|
|
|
encodedStringLiteralStart int
|
|
|
|
Number float64
|
|
current int
|
|
start int
|
|
end int
|
|
ApproximateNewlineCount int
|
|
CouldBeBadArrowInTSX int
|
|
BadArrowInTSXRange logger.Range
|
|
LegacyOctalLoc logger.Loc
|
|
AwaitKeywordLoc logger.Loc
|
|
FnOrArrowStartLoc logger.Loc
|
|
PreviousBackslashQuoteInJSX logger.Range
|
|
LegacyHTMLCommentRange logger.Range
|
|
codePoint rune
|
|
prevErrorLoc logger.Loc
|
|
json JSONFlavor
|
|
Token T
|
|
ts config.TSOptions
|
|
HasNewlineBefore bool
|
|
HasCommentBefore CommentBefore
|
|
IsLegacyOctalLiteral bool
|
|
PrevTokenWasAwaitKeyword bool
|
|
rescanCloseBraceAsTemplateToken bool
|
|
forGlobalName bool
|
|
|
|
// The log is disabled during speculative scans that may backtrack
|
|
IsLogDisabled bool
|
|
}
|
|
|
|
type CommentBefore uint8
|
|
|
|
const (
|
|
PureCommentBefore CommentBefore = 1 << iota
|
|
KeyCommentBefore
|
|
NoSideEffectsCommentBefore
|
|
)
|
|
|
|
type LexerPanic struct{}
|
|
|
|
func NewLexer(log logger.Log, source logger.Source, ts config.TSOptions) Lexer {
|
|
lexer := Lexer{
|
|
log: log,
|
|
source: source,
|
|
tracker: logger.MakeLineColumnTracker(&source),
|
|
prevErrorLoc: logger.Loc{Start: -1},
|
|
FnOrArrowStartLoc: logger.Loc{Start: -1},
|
|
ts: ts,
|
|
json: NotJSON,
|
|
}
|
|
lexer.step()
|
|
lexer.Next()
|
|
return lexer
|
|
}
|
|
|
|
func NewLexerGlobalName(log logger.Log, source logger.Source) Lexer {
|
|
lexer := Lexer{
|
|
log: log,
|
|
source: source,
|
|
tracker: logger.MakeLineColumnTracker(&source),
|
|
prevErrorLoc: logger.Loc{Start: -1},
|
|
FnOrArrowStartLoc: logger.Loc{Start: -1},
|
|
forGlobalName: true,
|
|
json: NotJSON,
|
|
}
|
|
lexer.step()
|
|
lexer.Next()
|
|
return lexer
|
|
}
|
|
|
|
type JSONFlavor uint8
|
|
|
|
const (
|
|
// Specification: https://json.org/
|
|
JSON JSONFlavor = iota
|
|
|
|
// TypeScript's JSON superset is not documented but appears to allow:
|
|
// - Comments: https://github.com/microsoft/TypeScript/issues/4987
|
|
// - Trailing commas
|
|
// - Full JS number syntax
|
|
TSConfigJSON
|
|
|
|
// This is used by the JavaScript lexer
|
|
NotJSON
|
|
)
|
|
|
|
func NewLexerJSON(log logger.Log, source logger.Source, json JSONFlavor, errorSuffix string) Lexer {
|
|
lexer := Lexer{
|
|
log: log,
|
|
source: source,
|
|
tracker: logger.MakeLineColumnTracker(&source),
|
|
prevErrorLoc: logger.Loc{Start: -1},
|
|
FnOrArrowStartLoc: logger.Loc{Start: -1},
|
|
errorSuffix: errorSuffix,
|
|
json: json,
|
|
}
|
|
lexer.step()
|
|
lexer.Next()
|
|
return lexer
|
|
}
|
|
|
|
func (lexer *Lexer) Loc() logger.Loc {
|
|
return logger.Loc{Start: int32(lexer.start)}
|
|
}
|
|
|
|
func (lexer *Lexer) Range() logger.Range {
|
|
return logger.Range{Loc: logger.Loc{Start: int32(lexer.start)}, Len: int32(lexer.end - lexer.start)}
|
|
}
|
|
|
|
func (lexer *Lexer) Raw() string {
|
|
return lexer.source.Contents[lexer.start:lexer.end]
|
|
}
|
|
|
|
func (lexer *Lexer) rawIdentifier() MaybeSubstring {
|
|
return MaybeSubstring{lexer.Raw(), ast.MakeIndex32(uint32(lexer.start))}
|
|
}
|
|
|
|
func (lexer *Lexer) StringLiteral() []uint16 {
|
|
if lexer.decodedStringLiteralOrNil == nil {
|
|
// Lazily decode escape sequences if needed
|
|
if decoded, ok, end := lexer.tryToDecodeEscapeSequences(lexer.encodedStringLiteralStart, lexer.encodedStringLiteralText, true /* reportErrors */); !ok {
|
|
lexer.end = end
|
|
lexer.SyntaxError()
|
|
} else {
|
|
lexer.decodedStringLiteralOrNil = decoded
|
|
}
|
|
}
|
|
return lexer.decodedStringLiteralOrNil
|
|
}
|
|
|
|
func (lexer *Lexer) CookedAndRawTemplateContents() ([]uint16, string) {
|
|
var raw string
|
|
|
|
switch lexer.Token {
|
|
case TNoSubstitutionTemplateLiteral, TTemplateTail:
|
|
// "`x`" or "}x`"
|
|
raw = lexer.source.Contents[lexer.start+1 : lexer.end-1]
|
|
|
|
case TTemplateHead, TTemplateMiddle:
|
|
// "`x${" or "}x${"
|
|
raw = lexer.source.Contents[lexer.start+1 : lexer.end-2]
|
|
}
|
|
|
|
if strings.IndexByte(raw, '\r') != -1 {
|
|
// From the specification:
|
|
//
|
|
// 11.8.6.1 Static Semantics: TV and TRV
|
|
//
|
|
// TV excludes the code units of LineContinuation while TRV includes
|
|
// them. <CR><LF> and <CR> LineTerminatorSequences are normalized to
|
|
// <LF> for both TV and TRV. An explicit EscapeSequence is needed to
|
|
// include a <CR> or <CR><LF> sequence.
|
|
|
|
bytes := []byte(raw)
|
|
end := 0
|
|
i := 0
|
|
|
|
for i < len(bytes) {
|
|
c := bytes[i]
|
|
i++
|
|
|
|
if c == '\r' {
|
|
// Convert '\r\n' into '\n'
|
|
if i < len(bytes) && bytes[i] == '\n' {
|
|
i++
|
|
}
|
|
|
|
// Convert '\r' into '\n'
|
|
c = '\n'
|
|
}
|
|
|
|
bytes[end] = c
|
|
end++
|
|
}
|
|
|
|
raw = string(bytes[:end])
|
|
}
|
|
|
|
// This will return nil on failure, which will become "undefined" for the tag
|
|
cooked, _, _ := lexer.tryToDecodeEscapeSequences(lexer.start+1, raw, false /* reportErrors */)
|
|
return cooked, raw
|
|
}
|
|
|
|
func (lexer *Lexer) IsIdentifierOrKeyword() bool {
|
|
return lexer.Token >= TIdentifier
|
|
}
|
|
|
|
func (lexer *Lexer) IsContextualKeyword(text string) bool {
|
|
return lexer.Token == TIdentifier && lexer.Raw() == text
|
|
}
|
|
|
|
func (lexer *Lexer) ExpectContextualKeyword(text string) {
|
|
if !lexer.IsContextualKeyword(text) {
|
|
lexer.ExpectedString(fmt.Sprintf("%q", text))
|
|
}
|
|
lexer.Next()
|
|
}
|
|
|
|
func (lexer *Lexer) SyntaxError() {
|
|
loc := logger.Loc{Start: int32(lexer.end)}
|
|
message := "Unexpected end of file"
|
|
if lexer.end < len(lexer.source.Contents) {
|
|
c, _ := utf8.DecodeRuneInString(lexer.source.Contents[lexer.end:])
|
|
if c < 0x20 {
|
|
message = fmt.Sprintf("Syntax error \"\\x%02X\"", c)
|
|
} else if c >= 0x80 {
|
|
message = fmt.Sprintf("Syntax error \"\\u{%x}\"", c)
|
|
} else if c != '"' {
|
|
message = fmt.Sprintf("Syntax error \"%c\"", c)
|
|
} else {
|
|
message = "Syntax error '\"'"
|
|
}
|
|
}
|
|
lexer.addRangeError(logger.Range{Loc: loc}, message)
|
|
panic(LexerPanic{})
|
|
}
|
|
|
|
func (lexer *Lexer) ExpectedString(text string) {
|
|
// Provide a friendly error message about "await" without "async"
|
|
if lexer.PrevTokenWasAwaitKeyword {
|
|
var notes []logger.MsgData
|
|
if lexer.FnOrArrowStartLoc.Start != -1 {
|
|
note := lexer.tracker.MsgData(logger.Range{Loc: lexer.FnOrArrowStartLoc},
|
|
"Consider adding the \"async\" keyword here:")
|
|
note.Location.Suggestion = "async"
|
|
notes = []logger.MsgData{note}
|
|
}
|
|
lexer.AddRangeErrorWithNotes(RangeOfIdentifier(lexer.source, lexer.AwaitKeywordLoc),
|
|
"\"await\" can only be used inside an \"async\" function",
|
|
notes)
|
|
panic(LexerPanic{})
|
|
}
|
|
|
|
found := fmt.Sprintf("%q", lexer.Raw())
|
|
if lexer.start == len(lexer.source.Contents) {
|
|
found = "end of file"
|
|
}
|
|
|
|
suggestion := ""
|
|
if strings.HasPrefix(text, "\"") && strings.HasSuffix(text, "\"") {
|
|
suggestion = text[1 : len(text)-1]
|
|
}
|
|
|
|
lexer.addRangeErrorWithSuggestion(lexer.Range(), fmt.Sprintf("Expected %s%s but found %s", text, lexer.errorSuffix, found), suggestion)
|
|
panic(LexerPanic{})
|
|
}
|
|
|
|
func (lexer *Lexer) Expected(token T) {
|
|
if text, ok := tokenToString[token]; ok {
|
|
lexer.ExpectedString(text)
|
|
} else {
|
|
lexer.Unexpected()
|
|
}
|
|
}
|
|
|
|
func (lexer *Lexer) Unexpected() {
|
|
found := fmt.Sprintf("%q", lexer.Raw())
|
|
if lexer.start == len(lexer.source.Contents) {
|
|
found = "end of file"
|
|
}
|
|
lexer.addRangeError(lexer.Range(), fmt.Sprintf("Unexpected %s%s", found, lexer.errorSuffix))
|
|
panic(LexerPanic{})
|
|
}
|
|
|
|
func (lexer *Lexer) Expect(token T) {
|
|
if lexer.Token != token {
|
|
lexer.Expected(token)
|
|
}
|
|
lexer.Next()
|
|
}
|
|
|
|
func (lexer *Lexer) ExpectOrInsertSemicolon() {
|
|
if lexer.Token == TSemicolon || (!lexer.HasNewlineBefore &&
|
|
lexer.Token != TCloseBrace && lexer.Token != TEndOfFile) {
|
|
lexer.Expect(TSemicolon)
|
|
}
|
|
}
|
|
|
|
// This parses a single "<" token. If that is the first part of a longer token,
|
|
// this function splits off the first "<" and leaves the remainder of the
|
|
// current token as another, smaller token. For example, "<<=" becomes "<=".
|
|
func (lexer *Lexer) ExpectLessThan(isInsideJSXElement bool) {
|
|
switch lexer.Token {
|
|
case TLessThan:
|
|
if isInsideJSXElement {
|
|
lexer.NextInsideJSXElement()
|
|
} else {
|
|
lexer.Next()
|
|
}
|
|
|
|
case TLessThanEquals:
|
|
lexer.Token = TEquals
|
|
lexer.start++
|
|
lexer.maybeExpandEquals()
|
|
|
|
case TLessThanLessThan:
|
|
lexer.Token = TLessThan
|
|
lexer.start++
|
|
|
|
case TLessThanLessThanEquals:
|
|
lexer.Token = TLessThanEquals
|
|
lexer.start++
|
|
|
|
default:
|
|
lexer.Expected(TLessThan)
|
|
}
|
|
}
|
|
|
|
// This parses a single ">" token. If that is the first part of a longer token,
|
|
// this function splits off the first ">" and leaves the remainder of the
|
|
// current token as another, smaller token. For example, ">>=" becomes ">=".
|
|
func (lexer *Lexer) ExpectGreaterThan(isInsideJSXElement bool) {
|
|
switch lexer.Token {
|
|
case TGreaterThan:
|
|
if isInsideJSXElement {
|
|
lexer.NextInsideJSXElement()
|
|
} else {
|
|
lexer.Next()
|
|
}
|
|
|
|
case TGreaterThanEquals:
|
|
lexer.Token = TEquals
|
|
lexer.start++
|
|
lexer.maybeExpandEquals()
|
|
|
|
case TGreaterThanGreaterThan:
|
|
lexer.Token = TGreaterThan
|
|
lexer.start++
|
|
|
|
case TGreaterThanGreaterThanEquals:
|
|
lexer.Token = TGreaterThanEquals
|
|
lexer.start++
|
|
|
|
case TGreaterThanGreaterThanGreaterThan:
|
|
lexer.Token = TGreaterThanGreaterThan
|
|
lexer.start++
|
|
|
|
case TGreaterThanGreaterThanGreaterThanEquals:
|
|
lexer.Token = TGreaterThanGreaterThanEquals
|
|
lexer.start++
|
|
|
|
default:
|
|
lexer.Expected(TGreaterThan)
|
|
}
|
|
}
|
|
|
|
func (lexer *Lexer) maybeExpandEquals() {
|
|
switch lexer.codePoint {
|
|
case '>':
|
|
// "=" + ">" = "=>"
|
|
lexer.Token = TEqualsGreaterThan
|
|
lexer.step()
|
|
|
|
case '=':
|
|
// "=" + "=" = "=="
|
|
lexer.Token = TEqualsEquals
|
|
lexer.step()
|
|
|
|
if lexer.Token == '=' {
|
|
// "=" + "==" = "==="
|
|
lexer.Token = TEqualsEqualsEquals
|
|
lexer.step()
|
|
}
|
|
}
|
|
}
|
|
|
|
func RangeOfIdentifier(source logger.Source, loc logger.Loc) logger.Range {
|
|
text := source.Contents[loc.Start:]
|
|
if len(text) == 0 {
|
|
return logger.Range{Loc: loc, Len: 0}
|
|
}
|
|
|
|
i := 0
|
|
c, _ := utf8.DecodeRuneInString(text[i:])
|
|
|
|
// Handle private names
|
|
if c == '#' {
|
|
i++
|
|
c, _ = utf8.DecodeRuneInString(text[i:])
|
|
}
|
|
|
|
if js_ast.IsIdentifierStart(c) || c == '\\' {
|
|
// Search for the end of the identifier
|
|
for i < len(text) {
|
|
c2, width2 := utf8.DecodeRuneInString(text[i:])
|
|
if c2 == '\\' {
|
|
i += width2
|
|
|
|
// Skip over bracketed unicode escapes such as "\u{10000}"
|
|
if i+2 < len(text) && text[i] == 'u' && text[i+1] == '{' {
|
|
i += 2
|
|
for i < len(text) {
|
|
if text[i] == '}' {
|
|
i++
|
|
break
|
|
}
|
|
i++
|
|
}
|
|
}
|
|
} else if !js_ast.IsIdentifierContinue(c2) {
|
|
return logger.Range{Loc: loc, Len: int32(i)}
|
|
} else {
|
|
i += width2
|
|
}
|
|
}
|
|
}
|
|
|
|
// When minifying, this identifier may have originally been a string
|
|
return source.RangeOfString(loc)
|
|
}
|
|
|
|
type KeyOrValue uint8
|
|
|
|
const (
|
|
KeyRange KeyOrValue = iota
|
|
ValueRange
|
|
KeyAndValueRange
|
|
)
|
|
|
|
func RangeOfImportAssertOrWith(source logger.Source, assertOrWith ast.AssertOrWithEntry, which KeyOrValue) logger.Range {
|
|
if which == KeyRange {
|
|
return RangeOfIdentifier(source, assertOrWith.KeyLoc)
|
|
}
|
|
if which == ValueRange {
|
|
return source.RangeOfString(assertOrWith.ValueLoc)
|
|
}
|
|
loc := RangeOfIdentifier(source, assertOrWith.KeyLoc).Loc
|
|
return logger.Range{Loc: loc, Len: source.RangeOfString(assertOrWith.ValueLoc).End() - loc.Start}
|
|
}
|
|
|
|
func (lexer *Lexer) ExpectJSXElementChild(token T) {
|
|
if lexer.Token != token {
|
|
lexer.Expected(token)
|
|
}
|
|
lexer.NextJSXElementChild()
|
|
}
|
|
|
|
func (lexer *Lexer) NextJSXElementChild() {
|
|
lexer.HasNewlineBefore = false
|
|
originalStart := lexer.end
|
|
|
|
for {
|
|
lexer.start = lexer.end
|
|
lexer.Token = 0
|
|
|
|
switch lexer.codePoint {
|
|
case -1: // This indicates the end of the file
|
|
lexer.Token = TEndOfFile
|
|
|
|
case '{':
|
|
lexer.step()
|
|
lexer.Token = TOpenBrace
|
|
|
|
case '<':
|
|
lexer.step()
|
|
lexer.Token = TLessThan
|
|
|
|
default:
|
|
needsFixing := false
|
|
|
|
stringLiteral:
|
|
for {
|
|
switch lexer.codePoint {
|
|
case -1, '{', '<':
|
|
// Stop when the string ends
|
|
break stringLiteral
|
|
|
|
case '&', '\r', '\n', '\u2028', '\u2029':
|
|
// This needs fixing if it has an entity or if it's a multi-line string
|
|
needsFixing = true
|
|
lexer.step()
|
|
|
|
case '}', '>':
|
|
// These technically aren't valid JSX: https://facebook.github.io/jsx/
|
|
//
|
|
// JSXTextCharacter :
|
|
// * SourceCharacter but not one of {, <, > or }
|
|
//
|
|
var replacement string
|
|
if lexer.codePoint == '}' {
|
|
replacement = "{'}'}"
|
|
} else {
|
|
replacement = "{'>'}"
|
|
}
|
|
msg := logger.Msg{
|
|
Kind: logger.Error,
|
|
Data: lexer.tracker.MsgData(logger.Range{Loc: logger.Loc{Start: int32(lexer.end)}, Len: 1},
|
|
fmt.Sprintf("The character \"%c\" is not valid inside a JSX element", lexer.codePoint)),
|
|
}
|
|
|
|
// Attempt to provide a better error message if this looks like an arrow function
|
|
if lexer.CouldBeBadArrowInTSX > 0 && lexer.codePoint == '>' && lexer.source.Contents[lexer.end-1] == '=' {
|
|
msg.Notes = []logger.MsgData{lexer.tracker.MsgData(lexer.BadArrowInTSXRange,
|
|
"TypeScript's TSX syntax interprets arrow functions with a single generic type parameter as an opening JSX element. "+
|
|
"If you want it to be interpreted as an arrow function instead, you need to add a trailing comma after the type parameter to disambiguate:")}
|
|
msg.Notes[0].Location.Suggestion = lexer.BadArrowInTSXSuggestion
|
|
} else {
|
|
msg.Notes = []logger.MsgData{{Text: fmt.Sprintf("Did you mean to escape it as %q instead?", replacement)}}
|
|
msg.Data.Location.Suggestion = replacement
|
|
if !lexer.ts.Parse {
|
|
// TypeScript treats this as an error but Babel doesn't treat this
|
|
// as an error yet, so allow this in JS for now. Babel version 8
|
|
// was supposed to be released in 2021 but was never released. If
|
|
// it's released in the future, this can be changed to an error too.
|
|
//
|
|
// More context:
|
|
// * TypeScript change: https://github.com/microsoft/TypeScript/issues/36341
|
|
// * Babel 8 change: https://github.com/babel/babel/issues/11042
|
|
// * Babel 8 release: https://github.com/babel/babel/issues/10746
|
|
//
|
|
msg.Kind = logger.Warning
|
|
}
|
|
}
|
|
|
|
lexer.log.AddMsg(msg)
|
|
lexer.step()
|
|
|
|
default:
|
|
// Non-ASCII strings need the slow path
|
|
if lexer.codePoint >= 0x80 {
|
|
needsFixing = true
|
|
}
|
|
lexer.step()
|
|
}
|
|
}
|
|
|
|
lexer.Token = TStringLiteral
|
|
text := lexer.source.Contents[originalStart:lexer.end]
|
|
|
|
if needsFixing {
|
|
// Slow path
|
|
lexer.decodedStringLiteralOrNil = fixWhitespaceAndDecodeJSXEntities(text)
|
|
} else {
|
|
// Fast path
|
|
n := len(text)
|
|
copy := make([]uint16, n)
|
|
for i := 0; i < n; i++ {
|
|
copy[i] = uint16(text[i])
|
|
}
|
|
lexer.decodedStringLiteralOrNil = copy
|
|
}
|
|
}
|
|
|
|
break
|
|
}
|
|
}
|
|
|
|
func (lexer *Lexer) ExpectInsideJSXElement(token T) {
|
|
if lexer.Token != token {
|
|
lexer.Expected(token)
|
|
}
|
|
lexer.NextInsideJSXElement()
|
|
}
|
|
|
|
func (lexer *Lexer) NextInsideJSXElement() {
|
|
lexer.HasNewlineBefore = false
|
|
|
|
for {
|
|
lexer.start = lexer.end
|
|
lexer.Token = 0
|
|
|
|
switch lexer.codePoint {
|
|
case -1: // This indicates the end of the file
|
|
lexer.Token = TEndOfFile
|
|
|
|
case '\r', '\n', '\u2028', '\u2029':
|
|
lexer.step()
|
|
lexer.HasNewlineBefore = true
|
|
continue
|
|
|
|
case '\t', ' ':
|
|
lexer.step()
|
|
continue
|
|
|
|
case '.':
|
|
lexer.step()
|
|
lexer.Token = TDot
|
|
|
|
case ':':
|
|
lexer.step()
|
|
lexer.Token = TColon
|
|
|
|
case '=':
|
|
lexer.step()
|
|
lexer.Token = TEquals
|
|
|
|
case '{':
|
|
lexer.step()
|
|
lexer.Token = TOpenBrace
|
|
|
|
case '}':
|
|
lexer.step()
|
|
lexer.Token = TCloseBrace
|
|
|
|
case '<':
|
|
lexer.step()
|
|
lexer.Token = TLessThan
|
|
|
|
case '>':
|
|
lexer.step()
|
|
lexer.Token = TGreaterThan
|
|
|
|
case '/':
|
|
// '/' or '//' or '/* ... */'
|
|
lexer.step()
|
|
switch lexer.codePoint {
|
|
case '/':
|
|
singleLineComment:
|
|
for {
|
|
lexer.step()
|
|
switch lexer.codePoint {
|
|
case '\r', '\n', '\u2028', '\u2029':
|
|
break singleLineComment
|
|
|
|
case -1: // This indicates the end of the file
|
|
break singleLineComment
|
|
}
|
|
}
|
|
continue
|
|
|
|
case '*':
|
|
lexer.step()
|
|
startRange := lexer.Range()
|
|
multiLineComment:
|
|
for {
|
|
switch lexer.codePoint {
|
|
case '*':
|
|
lexer.step()
|
|
if lexer.codePoint == '/' {
|
|
lexer.step()
|
|
break multiLineComment
|
|
}
|
|
|
|
case '\r', '\n', '\u2028', '\u2029':
|
|
lexer.step()
|
|
lexer.HasNewlineBefore = true
|
|
|
|
case -1: // This indicates the end of the file
|
|
lexer.start = lexer.end
|
|
lexer.AddRangeErrorWithNotes(logger.Range{Loc: lexer.Loc()}, "Expected \"*/\" to terminate multi-line comment",
|
|
[]logger.MsgData{lexer.tracker.MsgData(startRange, "The multi-line comment starts here:")})
|
|
panic(LexerPanic{})
|
|
|
|
default:
|
|
lexer.step()
|
|
}
|
|
}
|
|
continue
|
|
|
|
default:
|
|
lexer.Token = TSlash
|
|
}
|
|
|
|
case '\'', '"':
|
|
var backslash logger.Range
|
|
quote := lexer.codePoint
|
|
needsDecode := false
|
|
lexer.step()
|
|
|
|
stringLiteral:
|
|
for {
|
|
switch lexer.codePoint {
|
|
case -1: // This indicates the end of the file
|
|
lexer.SyntaxError()
|
|
|
|
case '&':
|
|
needsDecode = true
|
|
lexer.step()
|
|
|
|
case '\\':
|
|
backslash = logger.Range{Loc: logger.Loc{Start: int32(lexer.end)}, Len: 1}
|
|
lexer.step()
|
|
continue
|
|
|
|
case quote:
|
|
if backslash.Len > 0 {
|
|
backslash.Len++
|
|
lexer.PreviousBackslashQuoteInJSX = backslash
|
|
}
|
|
lexer.step()
|
|
break stringLiteral
|
|
|
|
default:
|
|
// Non-ASCII strings need the slow path
|
|
if lexer.codePoint >= 0x80 {
|
|
needsDecode = true
|
|
}
|
|
lexer.step()
|
|
}
|
|
backslash = logger.Range{}
|
|
}
|
|
|
|
lexer.Token = TStringLiteral
|
|
text := lexer.source.Contents[lexer.start+1 : lexer.end-1]
|
|
|
|
if needsDecode {
|
|
// Slow path
|
|
lexer.decodedStringLiteralOrNil = decodeJSXEntities([]uint16{}, text)
|
|
} else {
|
|
// Fast path
|
|
n := len(text)
|
|
copy := make([]uint16, n)
|
|
for i := 0; i < n; i++ {
|
|
copy[i] = uint16(text[i])
|
|
}
|
|
lexer.decodedStringLiteralOrNil = copy
|
|
}
|
|
|
|
default:
|
|
// Check for unusual whitespace characters
|
|
if js_ast.IsWhitespace(lexer.codePoint) {
|
|
lexer.step()
|
|
continue
|
|
}
|
|
|
|
if js_ast.IsIdentifierStart(lexer.codePoint) {
|
|
lexer.step()
|
|
for js_ast.IsIdentifierContinue(lexer.codePoint) || lexer.codePoint == '-' {
|
|
lexer.step()
|
|
}
|
|
|
|
lexer.Identifier = lexer.rawIdentifier()
|
|
lexer.Token = TIdentifier
|
|
break
|
|
}
|
|
|
|
lexer.end = lexer.current
|
|
lexer.Token = TSyntaxError
|
|
}
|
|
|
|
return
|
|
}
|
|
}
|
|
|
|
func (lexer *Lexer) Next() {
|
|
lexer.HasNewlineBefore = lexer.end == 0
|
|
lexer.HasCommentBefore = 0
|
|
lexer.PrevTokenWasAwaitKeyword = false
|
|
lexer.LegalCommentsBeforeToken = lexer.LegalCommentsBeforeToken[:0]
|
|
lexer.CommentsBeforeToken = lexer.CommentsBeforeToken[:0]
|
|
|
|
for {
|
|
lexer.start = lexer.end
|
|
lexer.Token = 0
|
|
|
|
switch lexer.codePoint {
|
|
case -1: // This indicates the end of the file
|
|
lexer.Token = TEndOfFile
|
|
|
|
case '#':
|
|
if lexer.start == 0 && strings.HasPrefix(lexer.source.Contents, "#!") {
|
|
// "#!/usr/bin/env node"
|
|
lexer.Token = THashbang
|
|
hashbang:
|
|
for {
|
|
lexer.step()
|
|
switch lexer.codePoint {
|
|
case '\r', '\n', '\u2028', '\u2029':
|
|
break hashbang
|
|
|
|
case -1: // This indicates the end of the file
|
|
break hashbang
|
|
}
|
|
}
|
|
lexer.Identifier = lexer.rawIdentifier()
|
|
} else {
|
|
// "#foo"
|
|
lexer.step()
|
|
if lexer.codePoint == '\\' {
|
|
lexer.Identifier, _ = lexer.scanIdentifierWithEscapes(privateIdentifier)
|
|
} else {
|
|
if !js_ast.IsIdentifierStart(lexer.codePoint) {
|
|
lexer.SyntaxError()
|
|
}
|
|
lexer.step()
|
|
for js_ast.IsIdentifierContinue(lexer.codePoint) {
|
|
lexer.step()
|
|
}
|
|
if lexer.codePoint == '\\' {
|
|
lexer.Identifier, _ = lexer.scanIdentifierWithEscapes(privateIdentifier)
|
|
} else {
|
|
lexer.Identifier = lexer.rawIdentifier()
|
|
}
|
|
}
|
|
lexer.Token = TPrivateIdentifier
|
|
}
|
|
|
|
case '\r', '\n', '\u2028', '\u2029':
|
|
lexer.step()
|
|
lexer.HasNewlineBefore = true
|
|
continue
|
|
|
|
case '\t', ' ':
|
|
lexer.step()
|
|
continue
|
|
|
|
case '(':
|
|
lexer.step()
|
|
lexer.Token = TOpenParen
|
|
|
|
case ')':
|
|
lexer.step()
|
|
lexer.Token = TCloseParen
|
|
|
|
case '[':
|
|
lexer.step()
|
|
lexer.Token = TOpenBracket
|
|
|
|
case ']':
|
|
lexer.step()
|
|
lexer.Token = TCloseBracket
|
|
|
|
case '{':
|
|
lexer.step()
|
|
lexer.Token = TOpenBrace
|
|
|
|
case '}':
|
|
lexer.step()
|
|
lexer.Token = TCloseBrace
|
|
|
|
case ',':
|
|
lexer.step()
|
|
lexer.Token = TComma
|
|
|
|
case ':':
|
|
lexer.step()
|
|
lexer.Token = TColon
|
|
|
|
case ';':
|
|
lexer.step()
|
|
lexer.Token = TSemicolon
|
|
|
|
case '@':
|
|
lexer.step()
|
|
lexer.Token = TAt
|
|
|
|
case '~':
|
|
lexer.step()
|
|
lexer.Token = TTilde
|
|
|
|
case '?':
|
|
// '?' or '?.' or '??' or '??='
|
|
lexer.step()
|
|
switch lexer.codePoint {
|
|
case '?':
|
|
lexer.step()
|
|
switch lexer.codePoint {
|
|
case '=':
|
|
lexer.step()
|
|
lexer.Token = TQuestionQuestionEquals
|
|
default:
|
|
lexer.Token = TQuestionQuestion
|
|
}
|
|
case '.':
|
|
lexer.Token = TQuestion
|
|
current := lexer.current
|
|
contents := lexer.source.Contents
|
|
|
|
// Lookahead to disambiguate with 'a?.1:b'
|
|
if current < len(contents) {
|
|
c := contents[current]
|
|
if c < '0' || c > '9' {
|
|
lexer.step()
|
|
lexer.Token = TQuestionDot
|
|
}
|
|
}
|
|
default:
|
|
lexer.Token = TQuestion
|
|
}
|
|
|
|
case '%':
|
|
// '%' or '%='
|
|
lexer.step()
|
|
switch lexer.codePoint {
|
|
case '=':
|
|
lexer.step()
|
|
lexer.Token = TPercentEquals
|
|
default:
|
|
lexer.Token = TPercent
|
|
}
|
|
|
|
case '&':
|
|
// '&' or '&=' or '&&' or '&&='
|
|
lexer.step()
|
|
switch lexer.codePoint {
|
|
case '=':
|
|
lexer.step()
|
|
lexer.Token = TAmpersandEquals
|
|
case '&':
|
|
lexer.step()
|
|
switch lexer.codePoint {
|
|
case '=':
|
|
lexer.step()
|
|
lexer.Token = TAmpersandAmpersandEquals
|
|
default:
|
|
lexer.Token = TAmpersandAmpersand
|
|
}
|
|
default:
|
|
lexer.Token = TAmpersand
|
|
}
|
|
|
|
case '|':
|
|
// '|' or '|=' or '||' or '||='
|
|
lexer.step()
|
|
switch lexer.codePoint {
|
|
case '=':
|
|
lexer.step()
|
|
lexer.Token = TBarEquals
|
|
case '|':
|
|
lexer.step()
|
|
switch lexer.codePoint {
|
|
case '=':
|
|
lexer.step()
|
|
lexer.Token = TBarBarEquals
|
|
default:
|
|
lexer.Token = TBarBar
|
|
}
|
|
default:
|
|
lexer.Token = TBar
|
|
}
|
|
|
|
case '^':
|
|
// '^' or '^='
|
|
lexer.step()
|
|
switch lexer.codePoint {
|
|
case '=':
|
|
lexer.step()
|
|
lexer.Token = TCaretEquals
|
|
default:
|
|
lexer.Token = TCaret
|
|
}
|
|
|
|
case '+':
|
|
// '+' or '+=' or '++'
|
|
lexer.step()
|
|
switch lexer.codePoint {
|
|
case '=':
|
|
lexer.step()
|
|
lexer.Token = TPlusEquals
|
|
case '+':
|
|
lexer.step()
|
|
lexer.Token = TPlusPlus
|
|
default:
|
|
lexer.Token = TPlus
|
|
}
|
|
|
|
case '-':
|
|
// '-' or '-=' or '--' or '-->'
|
|
lexer.step()
|
|
switch lexer.codePoint {
|
|
case '=':
|
|
lexer.step()
|
|
lexer.Token = TMinusEquals
|
|
case '-':
|
|
lexer.step()
|
|
|
|
// Handle legacy HTML-style comments
|
|
if lexer.codePoint == '>' && lexer.HasNewlineBefore {
|
|
lexer.step()
|
|
lexer.LegacyHTMLCommentRange = lexer.Range()
|
|
lexer.log.AddID(logger.MsgID_JS_HTMLCommentInJS, logger.Warning, &lexer.tracker, lexer.Range(),
|
|
"Treating \"-->\" as the start of a legacy HTML single-line comment")
|
|
singleLineHTMLCloseComment:
|
|
for {
|
|
switch lexer.codePoint {
|
|
case '\r', '\n', '\u2028', '\u2029':
|
|
break singleLineHTMLCloseComment
|
|
|
|
case -1: // This indicates the end of the file
|
|
break singleLineHTMLCloseComment
|
|
}
|
|
lexer.step()
|
|
}
|
|
continue
|
|
}
|
|
|
|
lexer.Token = TMinusMinus
|
|
default:
|
|
lexer.Token = TMinus
|
|
if lexer.json == JSON && lexer.codePoint != '.' && (lexer.codePoint < '0' || lexer.codePoint > '9') {
|
|
lexer.Unexpected()
|
|
}
|
|
}
|
|
|
|
case '*':
|
|
// '*' or '*=' or '**' or '**='
|
|
lexer.step()
|
|
switch lexer.codePoint {
|
|
case '=':
|
|
lexer.step()
|
|
lexer.Token = TAsteriskEquals
|
|
|
|
case '*':
|
|
lexer.step()
|
|
switch lexer.codePoint {
|
|
case '=':
|
|
lexer.step()
|
|
lexer.Token = TAsteriskAsteriskEquals
|
|
|
|
default:
|
|
lexer.Token = TAsteriskAsterisk
|
|
}
|
|
|
|
default:
|
|
lexer.Token = TAsterisk
|
|
}
|
|
|
|
case '/':
|
|
// '/' or '/=' or '//' or '/* ... */'
|
|
lexer.step()
|
|
if lexer.forGlobalName {
|
|
lexer.Token = TSlash
|
|
break
|
|
}
|
|
switch lexer.codePoint {
|
|
case '=':
|
|
lexer.step()
|
|
lexer.Token = TSlashEquals
|
|
|
|
case '/':
|
|
singleLineComment:
|
|
for {
|
|
lexer.step()
|
|
switch lexer.codePoint {
|
|
case '\r', '\n', '\u2028', '\u2029':
|
|
break singleLineComment
|
|
|
|
case -1: // This indicates the end of the file
|
|
break singleLineComment
|
|
}
|
|
}
|
|
if lexer.json == JSON {
|
|
lexer.addRangeError(lexer.Range(), "JSON does not support comments")
|
|
}
|
|
lexer.scanCommentText()
|
|
continue
|
|
|
|
case '*':
|
|
lexer.step()
|
|
startRange := lexer.Range()
|
|
multiLineComment:
|
|
for {
|
|
switch lexer.codePoint {
|
|
case '*':
|
|
lexer.step()
|
|
if lexer.codePoint == '/' {
|
|
lexer.step()
|
|
break multiLineComment
|
|
}
|
|
|
|
case '\r', '\n', '\u2028', '\u2029':
|
|
lexer.step()
|
|
lexer.HasNewlineBefore = true
|
|
|
|
case -1: // This indicates the end of the file
|
|
lexer.start = lexer.end
|
|
lexer.AddRangeErrorWithNotes(logger.Range{Loc: lexer.Loc()}, "Expected \"*/\" to terminate multi-line comment",
|
|
[]logger.MsgData{lexer.tracker.MsgData(startRange, "The multi-line comment starts here:")})
|
|
panic(LexerPanic{})
|
|
|
|
default:
|
|
lexer.step()
|
|
}
|
|
}
|
|
if lexer.json == JSON {
|
|
lexer.addRangeError(lexer.Range(), "JSON does not support comments")
|
|
}
|
|
lexer.scanCommentText()
|
|
continue
|
|
|
|
default:
|
|
lexer.Token = TSlash
|
|
}
|
|
|
|
case '=':
|
|
// '=' or '=>' or '==' or '==='
|
|
lexer.step()
|
|
switch lexer.codePoint {
|
|
case '>':
|
|
lexer.step()
|
|
lexer.Token = TEqualsGreaterThan
|
|
case '=':
|
|
lexer.step()
|
|
switch lexer.codePoint {
|
|
case '=':
|
|
lexer.step()
|
|
lexer.Token = TEqualsEqualsEquals
|
|
default:
|
|
lexer.Token = TEqualsEquals
|
|
}
|
|
default:
|
|
lexer.Token = TEquals
|
|
}
|
|
|
|
case '<':
|
|
// '<' or '<<' or '<=' or '<<=' or '<!--'
|
|
lexer.step()
|
|
switch lexer.codePoint {
|
|
case '=':
|
|
lexer.step()
|
|
lexer.Token = TLessThanEquals
|
|
case '<':
|
|
lexer.step()
|
|
switch lexer.codePoint {
|
|
case '=':
|
|
lexer.step()
|
|
lexer.Token = TLessThanLessThanEquals
|
|
default:
|
|
lexer.Token = TLessThanLessThan
|
|
}
|
|
|
|
// Handle legacy HTML-style comments
|
|
case '!':
|
|
if strings.HasPrefix(lexer.source.Contents[lexer.start:], "<!--") {
|
|
lexer.step()
|
|
lexer.step()
|
|
lexer.step()
|
|
lexer.LegacyHTMLCommentRange = lexer.Range()
|
|
lexer.log.AddID(logger.MsgID_JS_HTMLCommentInJS, logger.Warning, &lexer.tracker, lexer.Range(),
|
|
"Treating \"<!--\" as the start of a legacy HTML single-line comment")
|
|
singleLineHTMLOpenComment:
|
|
for {
|
|
switch lexer.codePoint {
|
|
case '\r', '\n', '\u2028', '\u2029':
|
|
break singleLineHTMLOpenComment
|
|
|
|
case -1: // This indicates the end of the file
|
|
break singleLineHTMLOpenComment
|
|
}
|
|
lexer.step()
|
|
}
|
|
continue
|
|
}
|
|
|
|
lexer.Token = TLessThan
|
|
|
|
default:
|
|
lexer.Token = TLessThan
|
|
}
|
|
|
|
case '>':
|
|
// '>' or '>>' or '>>>' or '>=' or '>>=' or '>>>='
|
|
lexer.step()
|
|
switch lexer.codePoint {
|
|
case '=':
|
|
lexer.step()
|
|
lexer.Token = TGreaterThanEquals
|
|
case '>':
|
|
lexer.step()
|
|
switch lexer.codePoint {
|
|
case '=':
|
|
lexer.step()
|
|
lexer.Token = TGreaterThanGreaterThanEquals
|
|
case '>':
|
|
lexer.step()
|
|
switch lexer.codePoint {
|
|
case '=':
|
|
lexer.step()
|
|
lexer.Token = TGreaterThanGreaterThanGreaterThanEquals
|
|
default:
|
|
lexer.Token = TGreaterThanGreaterThanGreaterThan
|
|
}
|
|
default:
|
|
lexer.Token = TGreaterThanGreaterThan
|
|
}
|
|
default:
|
|
lexer.Token = TGreaterThan
|
|
}
|
|
|
|
case '!':
|
|
// '!' or '!=' or '!=='
|
|
lexer.step()
|
|
switch lexer.codePoint {
|
|
case '=':
|
|
lexer.step()
|
|
switch lexer.codePoint {
|
|
case '=':
|
|
lexer.step()
|
|
lexer.Token = TExclamationEqualsEquals
|
|
default:
|
|
lexer.Token = TExclamationEquals
|
|
}
|
|
default:
|
|
lexer.Token = TExclamation
|
|
}
|
|
|
|
case '\'', '"', '`':
|
|
quote := lexer.codePoint
|
|
needsSlowPath := false
|
|
suffixLen := 1
|
|
|
|
if quote != '`' {
|
|
lexer.Token = TStringLiteral
|
|
} else if lexer.rescanCloseBraceAsTemplateToken {
|
|
lexer.Token = TTemplateTail
|
|
} else {
|
|
lexer.Token = TNoSubstitutionTemplateLiteral
|
|
}
|
|
lexer.step()
|
|
|
|
stringLiteral:
|
|
for {
|
|
switch lexer.codePoint {
|
|
case '\\':
|
|
needsSlowPath = true
|
|
lexer.step()
|
|
|
|
// Handle Windows CRLF
|
|
if lexer.codePoint == '\r' && lexer.json != JSON {
|
|
lexer.step()
|
|
if lexer.codePoint == '\n' {
|
|
lexer.step()
|
|
}
|
|
continue
|
|
}
|
|
|
|
case -1: // This indicates the end of the file
|
|
lexer.addRangeError(logger.Range{Loc: logger.Loc{Start: int32(lexer.end)}}, "Unterminated string literal")
|
|
panic(LexerPanic{})
|
|
|
|
case '\r':
|
|
if quote != '`' {
|
|
lexer.addRangeError(logger.Range{Loc: logger.Loc{Start: int32(lexer.end)}}, "Unterminated string literal")
|
|
panic(LexerPanic{})
|
|
}
|
|
|
|
// Template literals require newline normalization
|
|
needsSlowPath = true
|
|
|
|
case '\n':
|
|
if quote != '`' {
|
|
lexer.addRangeError(logger.Range{Loc: logger.Loc{Start: int32(lexer.end)}}, "Unterminated string literal")
|
|
panic(LexerPanic{})
|
|
}
|
|
|
|
case '$':
|
|
if quote == '`' {
|
|
lexer.step()
|
|
if lexer.codePoint == '{' {
|
|
suffixLen = 2
|
|
lexer.step()
|
|
if lexer.rescanCloseBraceAsTemplateToken {
|
|
lexer.Token = TTemplateMiddle
|
|
} else {
|
|
lexer.Token = TTemplateHead
|
|
}
|
|
break stringLiteral
|
|
}
|
|
continue stringLiteral
|
|
}
|
|
|
|
case quote:
|
|
lexer.step()
|
|
break stringLiteral
|
|
|
|
default:
|
|
// Non-ASCII strings need the slow path
|
|
if lexer.codePoint >= 0x80 {
|
|
needsSlowPath = true
|
|
} else if lexer.json == JSON && lexer.codePoint < 0x20 {
|
|
lexer.SyntaxError()
|
|
}
|
|
}
|
|
lexer.step()
|
|
}
|
|
|
|
text := lexer.source.Contents[lexer.start+1 : lexer.end-suffixLen]
|
|
|
|
if needsSlowPath {
|
|
// Slow path
|
|
lexer.decodedStringLiteralOrNil = nil
|
|
lexer.encodedStringLiteralStart = lexer.start + 1
|
|
lexer.encodedStringLiteralText = text
|
|
} else {
|
|
// Fast path
|
|
n := len(text)
|
|
copy := make([]uint16, n)
|
|
for i := 0; i < n; i++ {
|
|
copy[i] = uint16(text[i])
|
|
}
|
|
lexer.decodedStringLiteralOrNil = copy
|
|
}
|
|
|
|
if quote == '\'' && (lexer.json == JSON || lexer.json == TSConfigJSON) {
|
|
lexer.addRangeError(lexer.Range(), "JSON strings must use double quotes")
|
|
}
|
|
|
|
// Note: This case is hot in profiles
|
|
case '_', '$',
|
|
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
|
|
'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
|
|
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
|
|
'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z':
|
|
// This is a fast path for long ASCII identifiers. Doing this in a loop
|
|
// first instead of doing "step()" and "js_ast.IsIdentifierContinue()" like we
|
|
// do after this is noticeably faster in the common case of ASCII-only
|
|
// text. For example, doing this sped up end-to-end consuming of a large
|
|
// TypeScript type declaration file from 97ms to 79ms (around 20% faster).
|
|
contents := lexer.source.Contents
|
|
n := len(contents)
|
|
i := lexer.current
|
|
for i < n {
|
|
c := contents[i]
|
|
if (c < 'a' || c > 'z') && (c < 'A' || c > 'Z') && (c < '0' || c > '9') && c != '_' && c != '$' {
|
|
break
|
|
}
|
|
i++
|
|
}
|
|
lexer.current = i
|
|
|
|
// Now do the slow path for any remaining non-ASCII identifier characters
|
|
lexer.step()
|
|
if lexer.codePoint >= 0x80 {
|
|
for js_ast.IsIdentifierContinue(lexer.codePoint) {
|
|
lexer.step()
|
|
}
|
|
}
|
|
|
|
// If there's a slash, then we're in the extra-slow (and extra-rare) case
|
|
// where the identifier has embedded escapes
|
|
if lexer.codePoint == '\\' {
|
|
lexer.Identifier, lexer.Token = lexer.scanIdentifierWithEscapes(normalIdentifier)
|
|
break
|
|
}
|
|
|
|
// Otherwise (if there was no escape) we can slice the code verbatim
|
|
lexer.Identifier = lexer.rawIdentifier()
|
|
lexer.Token = Keywords[lexer.Raw()]
|
|
if lexer.Token == 0 {
|
|
lexer.Token = TIdentifier
|
|
}
|
|
|
|
case '\\':
|
|
lexer.Identifier, lexer.Token = lexer.scanIdentifierWithEscapes(normalIdentifier)
|
|
|
|
case '.', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
|
|
lexer.parseNumericLiteralOrDot()
|
|
|
|
default:
|
|
// Check for unusual whitespace characters
|
|
if js_ast.IsWhitespace(lexer.codePoint) {
|
|
lexer.step()
|
|
continue
|
|
}
|
|
|
|
if js_ast.IsIdentifierStart(lexer.codePoint) {
|
|
lexer.step()
|
|
for js_ast.IsIdentifierContinue(lexer.codePoint) {
|
|
lexer.step()
|
|
}
|
|
if lexer.codePoint == '\\' {
|
|
lexer.Identifier, lexer.Token = lexer.scanIdentifierWithEscapes(normalIdentifier)
|
|
} else {
|
|
lexer.Token = TIdentifier
|
|
lexer.Identifier = lexer.rawIdentifier()
|
|
}
|
|
break
|
|
}
|
|
|
|
lexer.end = lexer.current
|
|
lexer.Token = TSyntaxError
|
|
}
|
|
|
|
return
|
|
}
|
|
}
|
|
|
|
type identifierKind uint8
|
|
|
|
const (
|
|
normalIdentifier identifierKind = iota
|
|
privateIdentifier
|
|
)
|
|
|
|
// This is an edge case that doesn't really exist in the wild, so it doesn't
|
|
// need to be as fast as possible.
|
|
func (lexer *Lexer) scanIdentifierWithEscapes(kind identifierKind) (MaybeSubstring, T) {
|
|
// First pass: scan over the identifier to see how long it is
|
|
for {
|
|
// Scan a unicode escape sequence. There is at least one because that's
|
|
// what caused us to get on this slow path in the first place.
|
|
if lexer.codePoint == '\\' {
|
|
lexer.step()
|
|
if lexer.codePoint != 'u' {
|
|
lexer.SyntaxError()
|
|
}
|
|
lexer.step()
|
|
if lexer.codePoint == '{' {
|
|
// Variable-length
|
|
lexer.step()
|
|
for lexer.codePoint != '}' {
|
|
switch lexer.codePoint {
|
|
case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
|
|
'a', 'b', 'c', 'd', 'e', 'f',
|
|
'A', 'B', 'C', 'D', 'E', 'F':
|
|
lexer.step()
|
|
default:
|
|
lexer.SyntaxError()
|
|
}
|
|
}
|
|
lexer.step()
|
|
} else {
|
|
// Fixed-length
|
|
for j := 0; j < 4; j++ {
|
|
switch lexer.codePoint {
|
|
case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
|
|
'a', 'b', 'c', 'd', 'e', 'f',
|
|
'A', 'B', 'C', 'D', 'E', 'F':
|
|
lexer.step()
|
|
default:
|
|
lexer.SyntaxError()
|
|
}
|
|
}
|
|
}
|
|
continue
|
|
}
|
|
|
|
// Stop when we reach the end of the identifier
|
|
if !js_ast.IsIdentifierContinue(lexer.codePoint) {
|
|
break
|
|
}
|
|
lexer.step()
|
|
}
|
|
|
|
// Second pass: re-use our existing escape sequence parser
|
|
decoded, ok, end := lexer.tryToDecodeEscapeSequences(lexer.start, lexer.Raw(), true /* reportErrors */)
|
|
if !ok {
|
|
lexer.end = end
|
|
lexer.SyntaxError()
|
|
}
|
|
text := string(helpers.UTF16ToString(decoded))
|
|
|
|
// Even though it was escaped, it must still be a valid identifier
|
|
identifier := text
|
|
if kind == privateIdentifier {
|
|
identifier = identifier[1:] // Skip over the "#"
|
|
}
|
|
if !js_ast.IsIdentifier(identifier) {
|
|
lexer.addRangeError(logger.Range{Loc: logger.Loc{Start: int32(lexer.start)}, Len: int32(lexer.end - lexer.start)},
|
|
fmt.Sprintf("Invalid identifier: %q", text))
|
|
}
|
|
|
|
// Escaped keywords are not allowed to work as actual keywords, but they are
|
|
// allowed wherever we allow identifiers or keywords. For example:
|
|
//
|
|
// // This is an error (equivalent to "var var;")
|
|
// var \u0076\u0061\u0072;
|
|
//
|
|
// // This is an error (equivalent to "var foo;" except for this rule)
|
|
// \u0076\u0061\u0072 foo;
|
|
//
|
|
// // This is a fine (equivalent to "foo.var;")
|
|
// foo.\u0076\u0061\u0072;
|
|
//
|
|
if Keywords[text] != 0 {
|
|
return MaybeSubstring{String: text}, TEscapedKeyword
|
|
} else {
|
|
return MaybeSubstring{String: text}, TIdentifier
|
|
}
|
|
}
|
|
|
|
func (lexer *Lexer) parseNumericLiteralOrDot() {
|
|
// Number or dot
|
|
first := lexer.codePoint
|
|
lexer.step()
|
|
|
|
// Dot without a digit after it
|
|
if first == '.' && (lexer.codePoint < '0' || lexer.codePoint > '9') {
|
|
// "..."
|
|
if lexer.codePoint == '.' &&
|
|
lexer.current < len(lexer.source.Contents) &&
|
|
lexer.source.Contents[lexer.current] == '.' {
|
|
lexer.step()
|
|
lexer.step()
|
|
lexer.Token = TDotDotDot
|
|
return
|
|
}
|
|
|
|
// "."
|
|
lexer.Token = TDot
|
|
return
|
|
}
|
|
|
|
underscoreCount := 0
|
|
lastUnderscoreEnd := 0
|
|
hasDotOrExponent := first == '.'
|
|
isMissingDigitAfterDot := false
|
|
base := 0.0
|
|
lexer.IsLegacyOctalLiteral = false
|
|
|
|
// Assume this is a number, but potentially change to a bigint later
|
|
lexer.Token = TNumericLiteral
|
|
|
|
// Check for binary, octal, or hexadecimal literal
|
|
if first == '0' {
|
|
switch lexer.codePoint {
|
|
case 'b', 'B':
|
|
base = 2
|
|
|
|
case 'o', 'O':
|
|
base = 8
|
|
|
|
case 'x', 'X':
|
|
base = 16
|
|
|
|
case '0', '1', '2', '3', '4', '5', '6', '7', '_':
|
|
base = 8
|
|
lexer.IsLegacyOctalLiteral = true
|
|
|
|
case '8', '9':
|
|
lexer.IsLegacyOctalLiteral = true
|
|
}
|
|
}
|
|
|
|
if base != 0 {
|
|
// Integer literal
|
|
isFirst := true
|
|
isInvalidLegacyOctalLiteral := false
|
|
lexer.Number = 0
|
|
if !lexer.IsLegacyOctalLiteral {
|
|
lexer.step()
|
|
}
|
|
|
|
integerLiteral:
|
|
for {
|
|
switch lexer.codePoint {
|
|
case '_':
|
|
// Cannot have multiple underscores in a row
|
|
if lastUnderscoreEnd > 0 && lexer.end == lastUnderscoreEnd+1 {
|
|
lexer.SyntaxError()
|
|
}
|
|
|
|
// The first digit must exist
|
|
if isFirst || lexer.IsLegacyOctalLiteral {
|
|
lexer.SyntaxError()
|
|
}
|
|
|
|
lastUnderscoreEnd = lexer.end
|
|
underscoreCount++
|
|
|
|
case '0', '1':
|
|
lexer.Number = lexer.Number*base + float64(lexer.codePoint-'0')
|
|
|
|
case '2', '3', '4', '5', '6', '7':
|
|
if base == 2 {
|
|
lexer.SyntaxError()
|
|
}
|
|
lexer.Number = lexer.Number*base + float64(lexer.codePoint-'0')
|
|
|
|
case '8', '9':
|
|
if lexer.IsLegacyOctalLiteral {
|
|
isInvalidLegacyOctalLiteral = true
|
|
} else if base < 10 {
|
|
lexer.SyntaxError()
|
|
}
|
|
lexer.Number = lexer.Number*base + float64(lexer.codePoint-'0')
|
|
|
|
case 'A', 'B', 'C', 'D', 'E', 'F':
|
|
if base != 16 {
|
|
lexer.SyntaxError()
|
|
}
|
|
lexer.Number = lexer.Number*base + float64(lexer.codePoint+10-'A')
|
|
|
|
case 'a', 'b', 'c', 'd', 'e', 'f':
|
|
if base != 16 {
|
|
lexer.SyntaxError()
|
|
}
|
|
lexer.Number = lexer.Number*base + float64(lexer.codePoint+10-'a')
|
|
|
|
default:
|
|
// The first digit must exist
|
|
if isFirst {
|
|
lexer.SyntaxError()
|
|
}
|
|
|
|
break integerLiteral
|
|
}
|
|
|
|
lexer.step()
|
|
isFirst = false
|
|
}
|
|
|
|
isBigIntegerLiteral := lexer.codePoint == 'n' && !hasDotOrExponent
|
|
|
|
// Slow path: do we need to re-scan the input as text?
|
|
if isBigIntegerLiteral || isInvalidLegacyOctalLiteral {
|
|
text := lexer.rawIdentifier()
|
|
|
|
// Can't use a leading zero for bigint literals
|
|
if isBigIntegerLiteral && lexer.IsLegacyOctalLiteral {
|
|
lexer.SyntaxError()
|
|
}
|
|
|
|
// Filter out underscores
|
|
if underscoreCount > 0 {
|
|
bytes := make([]byte, 0, len(text.String)-underscoreCount)
|
|
for i := 0; i < len(text.String); i++ {
|
|
c := text.String[i]
|
|
if c != '_' {
|
|
bytes = append(bytes, c)
|
|
}
|
|
}
|
|
text = MaybeSubstring{String: string(bytes)}
|
|
}
|
|
|
|
// Store bigints as text to avoid precision loss
|
|
if isBigIntegerLiteral {
|
|
lexer.Identifier = text
|
|
} else if isInvalidLegacyOctalLiteral {
|
|
// Legacy octal literals may turn out to be a base 10 literal after all
|
|
value, _ := strconv.ParseFloat(text.String, 64)
|
|
lexer.Number = value
|
|
}
|
|
}
|
|
} else {
|
|
// Floating-point literal
|
|
isInvalidLegacyOctalLiteral := first == '0' && (lexer.codePoint == '8' || lexer.codePoint == '9')
|
|
|
|
// Initial digits
|
|
for {
|
|
if lexer.codePoint < '0' || lexer.codePoint > '9' {
|
|
if lexer.codePoint != '_' {
|
|
break
|
|
}
|
|
|
|
// Cannot have multiple underscores in a row
|
|
if lastUnderscoreEnd > 0 && lexer.end == lastUnderscoreEnd+1 {
|
|
lexer.SyntaxError()
|
|
}
|
|
|
|
// The specification forbids underscores in this case
|
|
if isInvalidLegacyOctalLiteral {
|
|
lexer.SyntaxError()
|
|
}
|
|
|
|
lastUnderscoreEnd = lexer.end
|
|
underscoreCount++
|
|
}
|
|
lexer.step()
|
|
}
|
|
|
|
// Fractional digits
|
|
if first != '.' && lexer.codePoint == '.' {
|
|
// An underscore must not come last
|
|
if lastUnderscoreEnd > 0 && lexer.end == lastUnderscoreEnd+1 {
|
|
lexer.end--
|
|
lexer.SyntaxError()
|
|
}
|
|
|
|
hasDotOrExponent = true
|
|
lexer.step()
|
|
if lexer.codePoint == '_' {
|
|
lexer.SyntaxError()
|
|
}
|
|
isMissingDigitAfterDot = true
|
|
for {
|
|
if lexer.codePoint >= '0' && lexer.codePoint <= '9' {
|
|
isMissingDigitAfterDot = false
|
|
} else {
|
|
if lexer.codePoint != '_' {
|
|
break
|
|
}
|
|
|
|
// Cannot have multiple underscores in a row
|
|
if lastUnderscoreEnd > 0 && lexer.end == lastUnderscoreEnd+1 {
|
|
lexer.SyntaxError()
|
|
}
|
|
|
|
lastUnderscoreEnd = lexer.end
|
|
underscoreCount++
|
|
}
|
|
lexer.step()
|
|
}
|
|
}
|
|
|
|
// Exponent
|
|
if lexer.codePoint == 'e' || lexer.codePoint == 'E' {
|
|
// An underscore must not come last
|
|
if lastUnderscoreEnd > 0 && lexer.end == lastUnderscoreEnd+1 {
|
|
lexer.end--
|
|
lexer.SyntaxError()
|
|
}
|
|
|
|
hasDotOrExponent = true
|
|
lexer.step()
|
|
if lexer.codePoint == '+' || lexer.codePoint == '-' {
|
|
lexer.step()
|
|
}
|
|
if lexer.codePoint < '0' || lexer.codePoint > '9' {
|
|
lexer.SyntaxError()
|
|
}
|
|
for {
|
|
if lexer.codePoint < '0' || lexer.codePoint > '9' {
|
|
if lexer.codePoint != '_' {
|
|
break
|
|
}
|
|
|
|
// Cannot have multiple underscores in a row
|
|
if lastUnderscoreEnd > 0 && lexer.end == lastUnderscoreEnd+1 {
|
|
lexer.SyntaxError()
|
|
}
|
|
|
|
lastUnderscoreEnd = lexer.end
|
|
underscoreCount++
|
|
}
|
|
lexer.step()
|
|
}
|
|
}
|
|
|
|
// Take a slice of the text to parse
|
|
text := lexer.rawIdentifier()
|
|
|
|
// Filter out underscores
|
|
if underscoreCount > 0 {
|
|
bytes := make([]byte, 0, len(text.String)-underscoreCount)
|
|
for i := 0; i < len(text.String); i++ {
|
|
c := text.String[i]
|
|
if c != '_' {
|
|
bytes = append(bytes, c)
|
|
}
|
|
}
|
|
text = MaybeSubstring{String: string(bytes)}
|
|
}
|
|
|
|
if lexer.codePoint == 'n' && !hasDotOrExponent {
|
|
// The only bigint literal that can start with 0 is "0n"
|
|
if len(text.String) > 1 && first == '0' {
|
|
lexer.SyntaxError()
|
|
}
|
|
|
|
// Store bigints as text to avoid precision loss
|
|
lexer.Identifier = text
|
|
} else if !hasDotOrExponent && lexer.end-lexer.start < 10 {
|
|
// Parse a 32-bit integer (very fast path)
|
|
var number uint32 = 0
|
|
for _, c := range text.String {
|
|
number = number*10 + uint32(c-'0')
|
|
}
|
|
lexer.Number = float64(number)
|
|
} else {
|
|
// Parse a double-precision floating-point number
|
|
value, _ := strconv.ParseFloat(text.String, 64)
|
|
lexer.Number = value
|
|
}
|
|
}
|
|
|
|
// An underscore must not come last
|
|
if lastUnderscoreEnd > 0 && lexer.end == lastUnderscoreEnd+1 {
|
|
lexer.end--
|
|
lexer.SyntaxError()
|
|
}
|
|
|
|
// Handle bigint literals after the underscore-at-end check above
|
|
if lexer.codePoint == 'n' && !hasDotOrExponent {
|
|
lexer.Token = TBigIntegerLiteral
|
|
lexer.step()
|
|
}
|
|
|
|
// Identifiers can't occur immediately after numbers
|
|
if js_ast.IsIdentifierStart(lexer.codePoint) {
|
|
lexer.SyntaxError()
|
|
}
|
|
|
|
// None of these are allowed in JSON
|
|
if lexer.json == JSON && (first == '.' || base != 0 || underscoreCount > 0 || isMissingDigitAfterDot) {
|
|
lexer.Unexpected()
|
|
}
|
|
}
|
|
|
|
func (lexer *Lexer) ScanRegExp() {
|
|
validateAndStep := func() {
|
|
if lexer.codePoint == '\\' {
|
|
lexer.step()
|
|
}
|
|
|
|
switch lexer.codePoint {
|
|
case -1, // This indicates the end of the file
|
|
'\r', '\n', 0x2028, 0x2029: // Newlines aren't allowed in regular expressions
|
|
lexer.addRangeError(logger.Range{Loc: logger.Loc{Start: int32(lexer.end)}}, "Unterminated regular expression")
|
|
panic(LexerPanic{})
|
|
|
|
default:
|
|
lexer.step()
|
|
}
|
|
}
|
|
|
|
for {
|
|
switch lexer.codePoint {
|
|
case '/':
|
|
lexer.step()
|
|
bits := uint32(0)
|
|
for js_ast.IsIdentifierContinue(lexer.codePoint) {
|
|
switch lexer.codePoint {
|
|
case 'd', 'g', 'i', 'm', 's', 'u', 'v', 'y':
|
|
bit := uint32(1) << uint32(lexer.codePoint-'a')
|
|
if (bit & bits) != 0 {
|
|
// Reject duplicate flags
|
|
r1 := logger.Range{Loc: logger.Loc{Start: int32(lexer.start)}, Len: 1}
|
|
r2 := logger.Range{Loc: logger.Loc{Start: int32(lexer.end)}, Len: 1}
|
|
for r1.Loc.Start < r2.Loc.Start && lexer.source.Contents[r1.Loc.Start] != byte(lexer.codePoint) {
|
|
r1.Loc.Start++
|
|
}
|
|
lexer.log.AddErrorWithNotes(&lexer.tracker, r2,
|
|
fmt.Sprintf("Duplicate flag \"%c\" in regular expression", lexer.codePoint),
|
|
[]logger.MsgData{lexer.tracker.MsgData(r1,
|
|
fmt.Sprintf("The first \"%c\" was here:", lexer.codePoint))})
|
|
} else {
|
|
bits |= bit
|
|
}
|
|
lexer.step()
|
|
|
|
default:
|
|
lexer.SyntaxError()
|
|
}
|
|
}
|
|
return
|
|
|
|
case '[':
|
|
lexer.step()
|
|
for lexer.codePoint != ']' {
|
|
validateAndStep()
|
|
}
|
|
lexer.step()
|
|
|
|
default:
|
|
validateAndStep()
|
|
}
|
|
}
|
|
}
|
|
|
|
func decodeJSXEntities(decoded []uint16, text string) []uint16 {
|
|
i := 0
|
|
|
|
for i < len(text) {
|
|
c, width := utf8.DecodeRuneInString(text[i:])
|
|
i += width
|
|
|
|
if c == '&' {
|
|
length := strings.IndexByte(text[i:], ';')
|
|
if length > 0 {
|
|
entity := text[i : i+length]
|
|
if entity[0] == '#' {
|
|
number := entity[1:]
|
|
base := 10
|
|
if len(number) > 1 && number[0] == 'x' {
|
|
number = number[1:]
|
|
base = 16
|
|
}
|
|
if value, err := strconv.ParseInt(number, base, 32); err == nil {
|
|
c = rune(value)
|
|
i += length + 1
|
|
}
|
|
} else if value, ok := jsxEntity[entity]; ok {
|
|
c = value
|
|
i += length + 1
|
|
}
|
|
}
|
|
}
|
|
|
|
if c <= 0xFFFF {
|
|
decoded = append(decoded, uint16(c))
|
|
} else {
|
|
c -= 0x10000
|
|
decoded = append(decoded, uint16(0xD800+((c>>10)&0x3FF)), uint16(0xDC00+(c&0x3FF)))
|
|
}
|
|
}
|
|
|
|
return decoded
|
|
}
|
|
|
|
func fixWhitespaceAndDecodeJSXEntities(text string) []uint16 {
|
|
afterLastNonWhitespace := -1
|
|
decoded := []uint16{}
|
|
i := 0
|
|
|
|
// Trim whitespace off the end of the first line
|
|
firstNonWhitespace := 0
|
|
|
|
// Split into lines
|
|
for i < len(text) {
|
|
c, width := utf8.DecodeRuneInString(text[i:])
|
|
|
|
switch c {
|
|
case '\r', '\n', '\u2028', '\u2029':
|
|
// Newline
|
|
if firstNonWhitespace != -1 && afterLastNonWhitespace != -1 {
|
|
if len(decoded) > 0 {
|
|
decoded = append(decoded, ' ')
|
|
}
|
|
|
|
// Trim whitespace off the start and end of lines in the middle
|
|
decoded = decodeJSXEntities(decoded, text[firstNonWhitespace:afterLastNonWhitespace])
|
|
}
|
|
|
|
// Reset for the next line
|
|
firstNonWhitespace = -1
|
|
|
|
case '\t', ' ':
|
|
// Whitespace
|
|
|
|
default:
|
|
// Check for unusual whitespace characters
|
|
if !js_ast.IsWhitespace(c) {
|
|
afterLastNonWhitespace = i + width
|
|
if firstNonWhitespace == -1 {
|
|
firstNonWhitespace = i
|
|
}
|
|
}
|
|
}
|
|
|
|
i += width
|
|
}
|
|
|
|
if firstNonWhitespace != -1 {
|
|
if len(decoded) > 0 {
|
|
decoded = append(decoded, ' ')
|
|
}
|
|
|
|
// Trim whitespace off the start of the last line
|
|
decoded = decodeJSXEntities(decoded, text[firstNonWhitespace:])
|
|
}
|
|
|
|
return decoded
|
|
}
|
|
|
|
// If this fails, this returns "nil, false, end" where "end" is the value to
|
|
// store to "lexer.end" before calling "lexer.SyntaxError()" if relevant
|
|
func (lexer *Lexer) tryToDecodeEscapeSequences(start int, text string, reportErrors bool) ([]uint16, bool, int) {
|
|
decoded := []uint16{}
|
|
i := 0
|
|
|
|
for i < len(text) {
|
|
c, width := utf8.DecodeRuneInString(text[i:])
|
|
i += width
|
|
|
|
switch c {
|
|
case '\r':
|
|
// From the specification:
|
|
//
|
|
// 11.8.6.1 Static Semantics: TV and TRV
|
|
//
|
|
// TV excludes the code units of LineContinuation while TRV includes
|
|
// them. <CR><LF> and <CR> LineTerminatorSequences are normalized to
|
|
// <LF> for both TV and TRV. An explicit EscapeSequence is needed to
|
|
// include a <CR> or <CR><LF> sequence.
|
|
|
|
// Convert '\r\n' into '\n'
|
|
if i < len(text) && text[i] == '\n' {
|
|
i++
|
|
}
|
|
|
|
// Convert '\r' into '\n'
|
|
decoded = append(decoded, '\n')
|
|
continue
|
|
|
|
case '\\':
|
|
c2, width2 := utf8.DecodeRuneInString(text[i:])
|
|
i += width2
|
|
|
|
switch c2 {
|
|
case 'b':
|
|
decoded = append(decoded, '\b')
|
|
continue
|
|
|
|
case 'f':
|
|
decoded = append(decoded, '\f')
|
|
continue
|
|
|
|
case 'n':
|
|
decoded = append(decoded, '\n')
|
|
continue
|
|
|
|
case 'r':
|
|
decoded = append(decoded, '\r')
|
|
continue
|
|
|
|
case 't':
|
|
decoded = append(decoded, '\t')
|
|
continue
|
|
|
|
case 'v':
|
|
if lexer.json == JSON {
|
|
return nil, false, start + i - width2
|
|
}
|
|
|
|
decoded = append(decoded, '\v')
|
|
continue
|
|
|
|
case '0', '1', '2', '3', '4', '5', '6', '7':
|
|
octalStart := i - 2
|
|
if lexer.json == JSON {
|
|
return nil, false, start + i - width2
|
|
}
|
|
|
|
// 1-3 digit octal
|
|
isBad := false
|
|
value := c2 - '0'
|
|
c3, width3 := utf8.DecodeRuneInString(text[i:])
|
|
switch c3 {
|
|
case '0', '1', '2', '3', '4', '5', '6', '7':
|
|
value = value*8 + c3 - '0'
|
|
i += width3
|
|
c4, width4 := utf8.DecodeRuneInString(text[i:])
|
|
switch c4 {
|
|
case '0', '1', '2', '3', '4', '5', '6', '7':
|
|
temp := value*8 + c4 - '0'
|
|
if temp < 256 {
|
|
value = temp
|
|
i += width4
|
|
}
|
|
case '8', '9':
|
|
isBad = true
|
|
}
|
|
case '8', '9':
|
|
isBad = true
|
|
}
|
|
c = value
|
|
|
|
// Forbid the use of octal literals other than "\0"
|
|
if isBad || text[octalStart:i] != "\\0" {
|
|
lexer.LegacyOctalLoc = logger.Loc{Start: int32(start + octalStart)}
|
|
}
|
|
|
|
case '8', '9':
|
|
c = c2
|
|
|
|
// Forbid the invalid octal literals "\8" and "\9"
|
|
lexer.LegacyOctalLoc = logger.Loc{Start: int32(start + i - 2)}
|
|
|
|
case 'x':
|
|
if lexer.json == JSON {
|
|
return nil, false, start + i - width2
|
|
}
|
|
|
|
// 2-digit hexadecimal
|
|
value := '\000'
|
|
for j := 0; j < 2; j++ {
|
|
c3, width3 := utf8.DecodeRuneInString(text[i:])
|
|
i += width3
|
|
switch c3 {
|
|
case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
|
|
value = value*16 | (c3 - '0')
|
|
case 'a', 'b', 'c', 'd', 'e', 'f':
|
|
value = value*16 | (c3 + 10 - 'a')
|
|
case 'A', 'B', 'C', 'D', 'E', 'F':
|
|
value = value*16 | (c3 + 10 - 'A')
|
|
default:
|
|
return nil, false, start + i - width3
|
|
}
|
|
}
|
|
c = value
|
|
|
|
case 'u':
|
|
// Unicode
|
|
value := '\000'
|
|
|
|
// Check the first character
|
|
c3, width3 := utf8.DecodeRuneInString(text[i:])
|
|
i += width3
|
|
|
|
if c3 == '{' {
|
|
if lexer.json == JSON {
|
|
return nil, false, start + i - width2
|
|
}
|
|
|
|
// Variable-length
|
|
hexStart := i - width - width2 - width3
|
|
isFirst := true
|
|
isOutOfRange := false
|
|
variableLength:
|
|
for {
|
|
c3, width3 = utf8.DecodeRuneInString(text[i:])
|
|
i += width3
|
|
|
|
switch c3 {
|
|
case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
|
|
value = value*16 | (c3 - '0')
|
|
case 'a', 'b', 'c', 'd', 'e', 'f':
|
|
value = value*16 | (c3 + 10 - 'a')
|
|
case 'A', 'B', 'C', 'D', 'E', 'F':
|
|
value = value*16 | (c3 + 10 - 'A')
|
|
case '}':
|
|
if isFirst {
|
|
return nil, false, start + i - width3
|
|
}
|
|
break variableLength
|
|
default:
|
|
return nil, false, start + i - width3
|
|
}
|
|
|
|
if value > utf8.MaxRune {
|
|
isOutOfRange = true
|
|
}
|
|
|
|
isFirst = false
|
|
}
|
|
|
|
if isOutOfRange && reportErrors {
|
|
lexer.addRangeError(logger.Range{Loc: logger.Loc{Start: int32(start + hexStart)}, Len: int32(i - hexStart)},
|
|
"Unicode escape sequence is out of range")
|
|
panic(LexerPanic{})
|
|
}
|
|
} else {
|
|
// Fixed-length
|
|
for j := 0; j < 4; j++ {
|
|
switch c3 {
|
|
case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
|
|
value = value*16 | (c3 - '0')
|
|
case 'a', 'b', 'c', 'd', 'e', 'f':
|
|
value = value*16 | (c3 + 10 - 'a')
|
|
case 'A', 'B', 'C', 'D', 'E', 'F':
|
|
value = value*16 | (c3 + 10 - 'A')
|
|
default:
|
|
return nil, false, start + i - width3
|
|
}
|
|
|
|
if j < 3 {
|
|
c3, width3 = utf8.DecodeRuneInString(text[i:])
|
|
i += width3
|
|
}
|
|
}
|
|
}
|
|
c = value
|
|
|
|
case '\r':
|
|
if lexer.json == JSON {
|
|
return nil, false, start + i - width2
|
|
}
|
|
|
|
// Ignore line continuations. A line continuation is not an escaped newline.
|
|
if i < len(text) && text[i] == '\n' {
|
|
// Make sure Windows CRLF counts as a single newline
|
|
i++
|
|
}
|
|
continue
|
|
|
|
case '\n', '\u2028', '\u2029':
|
|
if lexer.json == JSON {
|
|
return nil, false, start + i - width2
|
|
}
|
|
|
|
// Ignore line continuations. A line continuation is not an escaped newline.
|
|
continue
|
|
|
|
default:
|
|
if lexer.json == JSON {
|
|
switch c2 {
|
|
case '"', '\\', '/':
|
|
|
|
default:
|
|
return nil, false, start + i - width2
|
|
}
|
|
}
|
|
|
|
c = c2
|
|
}
|
|
}
|
|
|
|
if c <= 0xFFFF {
|
|
decoded = append(decoded, uint16(c))
|
|
} else {
|
|
c -= 0x10000
|
|
decoded = append(decoded, uint16(0xD800+((c>>10)&0x3FF)), uint16(0xDC00+(c&0x3FF)))
|
|
}
|
|
}
|
|
|
|
return decoded, true, 0
|
|
}
|
|
|
|
func (lexer *Lexer) RescanCloseBraceAsTemplateToken() {
|
|
if lexer.Token != TCloseBrace {
|
|
lexer.Expected(TCloseBrace)
|
|
}
|
|
|
|
lexer.rescanCloseBraceAsTemplateToken = true
|
|
lexer.codePoint = '`'
|
|
lexer.current = lexer.end
|
|
lexer.end -= 1
|
|
lexer.Next()
|
|
lexer.rescanCloseBraceAsTemplateToken = false
|
|
}
|
|
|
|
func (lexer *Lexer) step() {
|
|
codePoint, width := utf8.DecodeRuneInString(lexer.source.Contents[lexer.current:])
|
|
|
|
// Use -1 to indicate the end of the file
|
|
if width == 0 {
|
|
codePoint = -1
|
|
}
|
|
|
|
// Track the approximate number of newlines in the file so we can preallocate
|
|
// the line offset table in the printer for source maps. The line offset table
|
|
// is the #1 highest allocation in the heap profile, so this is worth doing.
|
|
// This count is approximate because it handles "\n" and "\r\n" (the common
|
|
// cases) but not "\r" or "\u2028" or "\u2029". Getting this wrong is harmless
|
|
// because it's only a preallocation. The array will just grow if it's too small.
|
|
if codePoint == '\n' {
|
|
lexer.ApproximateNewlineCount++
|
|
}
|
|
|
|
lexer.codePoint = codePoint
|
|
lexer.end = lexer.current
|
|
lexer.current += width
|
|
}
|
|
|
|
func (lexer *Lexer) addRangeError(r logger.Range, text string) {
|
|
// Don't report multiple errors in the same spot
|
|
if r.Loc == lexer.prevErrorLoc {
|
|
return
|
|
}
|
|
lexer.prevErrorLoc = r.Loc
|
|
|
|
if !lexer.IsLogDisabled {
|
|
lexer.log.AddError(&lexer.tracker, r, text)
|
|
}
|
|
}
|
|
|
|
func (lexer *Lexer) addRangeErrorWithSuggestion(r logger.Range, text string, suggestion string) {
|
|
// Don't report multiple errors in the same spot
|
|
if r.Loc == lexer.prevErrorLoc {
|
|
return
|
|
}
|
|
lexer.prevErrorLoc = r.Loc
|
|
|
|
if !lexer.IsLogDisabled {
|
|
data := lexer.tracker.MsgData(r, text)
|
|
data.Location.Suggestion = suggestion
|
|
lexer.log.AddMsg(logger.Msg{Kind: logger.Error, Data: data})
|
|
}
|
|
}
|
|
|
|
func (lexer *Lexer) AddRangeErrorWithNotes(r logger.Range, text string, notes []logger.MsgData) {
|
|
// Don't report multiple errors in the same spot
|
|
if r.Loc == lexer.prevErrorLoc {
|
|
return
|
|
}
|
|
lexer.prevErrorLoc = r.Loc
|
|
|
|
if !lexer.IsLogDisabled {
|
|
lexer.log.AddErrorWithNotes(&lexer.tracker, r, text, notes)
|
|
}
|
|
}
|
|
|
|
func hasPrefixWithWordBoundary(text string, prefix string) bool {
|
|
t := len(text)
|
|
p := len(prefix)
|
|
if t >= p && text[0:p] == prefix {
|
|
if t == p {
|
|
return true
|
|
}
|
|
c, _ := utf8.DecodeRuneInString(text[p:])
|
|
if !js_ast.IsIdentifierContinue(c) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
type pragmaArg uint8
|
|
|
|
const (
|
|
pragmaNoSpaceFirst pragmaArg = iota
|
|
pragmaSkipSpaceFirst
|
|
)
|
|
|
|
func scanForPragmaArg(kind pragmaArg, start int, pragma string, text string) (logger.Span, bool) {
|
|
text = text[len(pragma):]
|
|
start += len(pragma)
|
|
|
|
if text == "" {
|
|
return logger.Span{}, false
|
|
}
|
|
|
|
// One or more whitespace characters
|
|
c, width := utf8.DecodeRuneInString(text)
|
|
if kind == pragmaSkipSpaceFirst {
|
|
if !js_ast.IsWhitespace(c) {
|
|
return logger.Span{}, false
|
|
}
|
|
for js_ast.IsWhitespace(c) {
|
|
text = text[width:]
|
|
start += width
|
|
if text == "" {
|
|
return logger.Span{}, false
|
|
}
|
|
c, width = utf8.DecodeRuneInString(text)
|
|
}
|
|
}
|
|
|
|
// One or more non-whitespace characters
|
|
i := 0
|
|
for !js_ast.IsWhitespace(c) {
|
|
i += width
|
|
if i >= len(text) {
|
|
break
|
|
}
|
|
c, width = utf8.DecodeRuneInString(text[i:])
|
|
if js_ast.IsWhitespace(c) {
|
|
break
|
|
}
|
|
}
|
|
|
|
return logger.Span{
|
|
Text: text[:i],
|
|
Range: logger.Range{
|
|
Loc: logger.Loc{Start: int32(start)},
|
|
Len: int32(i),
|
|
},
|
|
}, true
|
|
}
|
|
|
|
func isUpperASCII(c byte) bool {
|
|
return c >= 'A' && c <= 'Z'
|
|
}
|
|
|
|
func isLetterASCII(c byte) bool {
|
|
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
|
|
}
|
|
|
|
func (lexer *Lexer) scanCommentText() {
|
|
text := lexer.source.Contents[lexer.start:lexer.end]
|
|
hasLegalAnnotation := len(text) > 2 && text[2] == '!'
|
|
isMultiLineComment := text[1] == '*'
|
|
omitFromGeneralCommentPreservation := false
|
|
|
|
// Save the original comment text so we can subtract comments from the
|
|
// character frequency analysis used by symbol minification
|
|
lexer.AllComments = append(lexer.AllComments, lexer.Range())
|
|
|
|
// Omit the trailing "*/" from the checks below
|
|
endOfCommentText := len(text)
|
|
if isMultiLineComment {
|
|
endOfCommentText -= 2
|
|
}
|
|
|
|
for i, n := 0, len(text); i < n; i++ {
|
|
switch text[i] {
|
|
case '#':
|
|
rest := text[i+1 : endOfCommentText]
|
|
if hasPrefixWithWordBoundary(rest, "__PURE__") {
|
|
omitFromGeneralCommentPreservation = true
|
|
lexer.HasCommentBefore |= PureCommentBefore
|
|
} else if hasPrefixWithWordBoundary(rest, "__KEY__") {
|
|
omitFromGeneralCommentPreservation = true
|
|
lexer.HasCommentBefore |= KeyCommentBefore
|
|
} else if hasPrefixWithWordBoundary(rest, "__NO_SIDE_EFFECTS__") {
|
|
omitFromGeneralCommentPreservation = true
|
|
lexer.HasCommentBefore |= NoSideEffectsCommentBefore
|
|
} else if i == 2 && strings.HasPrefix(rest, " sourceMappingURL=") {
|
|
if arg, ok := scanForPragmaArg(pragmaNoSpaceFirst, lexer.start+i+1, " sourceMappingURL=", rest); ok {
|
|
omitFromGeneralCommentPreservation = true
|
|
lexer.SourceMappingURL = arg
|
|
}
|
|
}
|
|
|
|
case '@':
|
|
rest := text[i+1 : endOfCommentText]
|
|
if hasPrefixWithWordBoundary(rest, "__PURE__") {
|
|
omitFromGeneralCommentPreservation = true
|
|
lexer.HasCommentBefore |= PureCommentBefore
|
|
} else if hasPrefixWithWordBoundary(rest, "__KEY__") {
|
|
omitFromGeneralCommentPreservation = true
|
|
lexer.HasCommentBefore |= KeyCommentBefore
|
|
} else if hasPrefixWithWordBoundary(rest, "__NO_SIDE_EFFECTS__") {
|
|
omitFromGeneralCommentPreservation = true
|
|
lexer.HasCommentBefore |= NoSideEffectsCommentBefore
|
|
} else if hasPrefixWithWordBoundary(rest, "preserve") || hasPrefixWithWordBoundary(rest, "license") {
|
|
hasLegalAnnotation = true
|
|
} else if hasPrefixWithWordBoundary(rest, "jsx") {
|
|
if arg, ok := scanForPragmaArg(pragmaSkipSpaceFirst, lexer.start+i+1, "jsx", rest); ok {
|
|
lexer.JSXFactoryPragmaComment = arg
|
|
}
|
|
} else if hasPrefixWithWordBoundary(rest, "jsxFrag") {
|
|
if arg, ok := scanForPragmaArg(pragmaSkipSpaceFirst, lexer.start+i+1, "jsxFrag", rest); ok {
|
|
lexer.JSXFragmentPragmaComment = arg
|
|
}
|
|
} else if hasPrefixWithWordBoundary(rest, "jsxRuntime") {
|
|
if arg, ok := scanForPragmaArg(pragmaSkipSpaceFirst, lexer.start+i+1, "jsxRuntime", rest); ok {
|
|
lexer.JSXRuntimePragmaComment = arg
|
|
}
|
|
} else if hasPrefixWithWordBoundary(rest, "jsxImportSource") {
|
|
if arg, ok := scanForPragmaArg(pragmaSkipSpaceFirst, lexer.start+i+1, "jsxImportSource", rest); ok {
|
|
lexer.JSXImportSourcePragmaComment = arg
|
|
}
|
|
} else if i == 2 && strings.HasPrefix(rest, " sourceMappingURL=") {
|
|
if arg, ok := scanForPragmaArg(pragmaNoSpaceFirst, lexer.start+i+1, " sourceMappingURL=", rest); ok {
|
|
omitFromGeneralCommentPreservation = true
|
|
lexer.SourceMappingURL = arg
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if hasLegalAnnotation {
|
|
lexer.LegalCommentsBeforeToken = append(lexer.LegalCommentsBeforeToken, lexer.Range())
|
|
}
|
|
|
|
if !omitFromGeneralCommentPreservation {
|
|
lexer.CommentsBeforeToken = append(lexer.CommentsBeforeToken, lexer.Range())
|
|
}
|
|
}
|