1077 lines
34 KiB
Go
1077 lines
34 KiB
Go
package scanner
|
|
|
|
import (
|
|
"maps"
|
|
"math"
|
|
"strconv"
|
|
"strings"
|
|
"unicode/utf16"
|
|
"unicode/utf8"
|
|
|
|
"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/stringutil"
|
|
)
|
|
|
|
type regularExpressionFlags int32
|
|
|
|
const (
|
|
regularExpressionFlagsNone regularExpressionFlags = 0
|
|
regularExpressionFlagsHasIndices regularExpressionFlags = 1 << 0 // d
|
|
regularExpressionFlagsGlobal regularExpressionFlags = 1 << 1 // g
|
|
regularExpressionFlagsIgnoreCase regularExpressionFlags = 1 << 2 // i
|
|
regularExpressionFlagsMultiline regularExpressionFlags = 1 << 3 // m
|
|
regularExpressionFlagsDotAll regularExpressionFlags = 1 << 4 // s
|
|
regularExpressionFlagsUnicode regularExpressionFlags = 1 << 5 // u
|
|
regularExpressionFlagsUnicodeSets regularExpressionFlags = 1 << 6 // v
|
|
regularExpressionFlagsSticky regularExpressionFlags = 1 << 7 // y
|
|
regularExpressionFlagsAnyUnicodeMode regularExpressionFlags = regularExpressionFlagsUnicode | regularExpressionFlagsUnicodeSets
|
|
regularExpressionFlagsModifiers regularExpressionFlags = regularExpressionFlagsIgnoreCase | regularExpressionFlagsMultiline | regularExpressionFlagsDotAll
|
|
)
|
|
|
|
var charCodeToRegExpFlag = map[rune]regularExpressionFlags{
|
|
'd': regularExpressionFlagsHasIndices,
|
|
'g': regularExpressionFlagsGlobal,
|
|
'i': regularExpressionFlagsIgnoreCase,
|
|
'm': regularExpressionFlagsMultiline,
|
|
's': regularExpressionFlagsDotAll,
|
|
'u': regularExpressionFlagsUnicode,
|
|
'v': regularExpressionFlagsUnicodeSets,
|
|
'y': regularExpressionFlagsSticky,
|
|
}
|
|
|
|
var regExpFlagToFirstAvailableLanguageVersion = map[regularExpressionFlags]core.ScriptTarget{
|
|
regularExpressionFlagsHasIndices: core.ScriptTargetES2022,
|
|
regularExpressionFlagsDotAll: core.ScriptTargetES2018,
|
|
regularExpressionFlagsUnicodeSets: core.ScriptTargetES2024,
|
|
}
|
|
|
|
func (s *Scanner) checkRegularExpressionFlagAvailability(flag regularExpressionFlags, pos int, size int) {
|
|
if availableFrom, ok := regExpFlagToFirstAvailableLanguageVersion[flag]; ok && s.languageVersion() < availableFrom {
|
|
s.errorAt(diagnostics.This_regular_expression_flag_is_only_available_when_targeting_0_or_later, pos, size, strings.ToLower(availableFrom.String()))
|
|
}
|
|
}
|
|
|
|
type classSetExpressionType int
|
|
|
|
const (
|
|
classSetExpressionTypeUnknown classSetExpressionType = iota
|
|
classSetExpressionTypeClassUnion
|
|
classSetExpressionTypeClassIntersection
|
|
classSetExpressionTypeClassSubtraction
|
|
)
|
|
|
|
type groupNameReference struct {
|
|
pos int
|
|
end int
|
|
name string
|
|
}
|
|
|
|
type decimalEscapeValue struct {
|
|
pos int
|
|
end int
|
|
value int
|
|
}
|
|
|
|
type regExpParser struct {
|
|
scanner *Scanner
|
|
end int
|
|
regExpFlags regularExpressionFlags
|
|
anyUnicodeMode bool
|
|
unicodeSetsMode bool
|
|
annexB bool
|
|
|
|
anyUnicodeModeOrNonAnnexB bool
|
|
namedCaptureGroups bool
|
|
|
|
// See scanClassSetExpression.
|
|
mayContainStrings bool
|
|
// The number of all (named and unnamed) capturing groups defined in the regex.
|
|
numberOfCapturingGroups int
|
|
// All named capturing groups defined in the regex.
|
|
groupSpecifiers map[string]bool
|
|
// All references to named capturing groups in the regex.
|
|
groupNameReferences []groupNameReference
|
|
// All numeric backreferences within the regex.
|
|
decimalEscapes []decimalEscapeValue
|
|
// A stack of scopes for named capturing groups. See scanGroupName.
|
|
namedCapturingGroups []map[string]bool
|
|
|
|
// pendingLowSurrogate holds the low surrogate to emit on the next
|
|
// scanSourceCharacter call when Corsa has to split a non-BMP rune into
|
|
// UTF-16 surrogate code units in non-unicode mode. Strada did not need
|
|
// this bookkeeping because its source text was already indexed as UTF-16.
|
|
pendingLowSurrogate rune
|
|
}
|
|
|
|
func (p *regExpParser) pos() int {
|
|
return p.scanner.pos
|
|
}
|
|
|
|
func (p *regExpParser) setPos(v int) {
|
|
p.scanner.pos = v
|
|
}
|
|
|
|
func (p *regExpParser) incPos(n int) {
|
|
p.scanner.pos += n
|
|
}
|
|
|
|
func (p *regExpParser) char() rune {
|
|
return p.scanner.char()
|
|
}
|
|
|
|
func (p *regExpParser) charAt(pos int) rune {
|
|
return p.scanner.charAt(pos - p.pos())
|
|
}
|
|
|
|
func (p *regExpParser) error(msg *diagnostics.Message, pos int, length int, args ...any) {
|
|
p.scanner.errorAt(msg, pos, length, args...)
|
|
}
|
|
|
|
func (p *regExpParser) text() string {
|
|
return p.scanner.text
|
|
}
|
|
|
|
func compareDecimalStrings(a string, b string) int {
|
|
a = strings.TrimLeft(a, "0")
|
|
b = strings.TrimLeft(b, "0")
|
|
if a == "" {
|
|
a = "0"
|
|
}
|
|
if b == "" {
|
|
b = "0"
|
|
}
|
|
if len(a) != len(b) {
|
|
if len(a) < len(b) {
|
|
return -1
|
|
}
|
|
return 1
|
|
}
|
|
return strings.Compare(a, b)
|
|
}
|
|
|
|
// Disjunction ::= Alternative ('|' Alternative)*
|
|
func (p *regExpParser) scanDisjunction(isInGroup bool) {
|
|
for {
|
|
p.namedCapturingGroups = append(p.namedCapturingGroups, make(map[string]bool))
|
|
p.scanAlternative(isInGroup)
|
|
p.namedCapturingGroups = p.namedCapturingGroups[:len(p.namedCapturingGroups)-1]
|
|
if p.char() != '|' {
|
|
return
|
|
}
|
|
p.incPos(1)
|
|
}
|
|
}
|
|
|
|
// Alternative ::= Term*
|
|
// Term ::=
|
|
//
|
|
// | Assertion
|
|
// | Atom Quantifier?
|
|
//
|
|
// Assertion ::=
|
|
//
|
|
// | '^'
|
|
// | '$'
|
|
// | '\b'
|
|
// | '\B'
|
|
// | '(?=' Disjunction ')'
|
|
// | '(?!' Disjunction ')'
|
|
// | '(?<=' Disjunction ')'
|
|
// | '(?<!' Disjunction ')'
|
|
//
|
|
// Quantifier ::= QuantifierPrefix '?'?
|
|
// QuantifierPrefix ::=
|
|
//
|
|
// | '*'
|
|
// | '+'
|
|
// | '?'
|
|
// | '{' DecimalDigits (',' DecimalDigits?)? '}'
|
|
//
|
|
// Atom ::=
|
|
//
|
|
// | PatternCharacter
|
|
// | '.'
|
|
// | '\' AtomEscape
|
|
// | CharacterClass
|
|
// | '(?<' RegExpIdentifierName '>' Disjunction ')'
|
|
// | '(?' RegularExpressionFlags ('-' RegularExpressionFlags)? ':' Disjunction ')'
|
|
//
|
|
// CharacterClass ::= unicodeMode
|
|
//
|
|
// ? '[' ClassRanges ']'
|
|
// : '[' ClassSetExpression ']'
|
|
func (p *regExpParser) scanAlternative(isInGroup bool) {
|
|
isPreviousTermQuantifiable := false
|
|
for p.pos() < p.end {
|
|
start := p.pos()
|
|
ch := p.char()
|
|
switch ch {
|
|
case '^', '$':
|
|
p.incPos(1)
|
|
isPreviousTermQuantifiable = false
|
|
case '\\':
|
|
p.incPos(1)
|
|
switch p.char() {
|
|
case 'b', 'B':
|
|
p.incPos(1)
|
|
isPreviousTermQuantifiable = false
|
|
default:
|
|
p.scanAtomEscape()
|
|
isPreviousTermQuantifiable = true
|
|
}
|
|
case '(':
|
|
p.incPos(1)
|
|
if p.char() == '?' {
|
|
p.incPos(1)
|
|
switch p.char() {
|
|
case '=', '!':
|
|
p.incPos(1)
|
|
// In Annex B, `(?=Disjunction)` and `(?!Disjunction)` are quantifiable
|
|
isPreviousTermQuantifiable = !p.anyUnicodeModeOrNonAnnexB
|
|
case '<':
|
|
groupNameStart := p.pos()
|
|
p.incPos(1)
|
|
switch p.char() {
|
|
case '=', '!':
|
|
p.incPos(1)
|
|
isPreviousTermQuantifiable = false
|
|
default:
|
|
p.scanGroupName(false /*isReference*/)
|
|
p.scanExpectedChar('>')
|
|
if p.scanner.languageVersion() < core.ScriptTargetES2018 {
|
|
p.error(diagnostics.Named_capturing_groups_are_only_available_when_targeting_ES2018_or_later, groupNameStart, p.pos()-groupNameStart)
|
|
}
|
|
p.numberOfCapturingGroups++
|
|
isPreviousTermQuantifiable = true
|
|
}
|
|
default:
|
|
flagsStart := p.pos()
|
|
setFlags := p.scanPatternModifiers(regularExpressionFlagsNone)
|
|
if p.char() == '-' {
|
|
p.incPos(1)
|
|
p.scanPatternModifiers(setFlags)
|
|
if p.pos() == flagsStart+1 {
|
|
p.error(diagnostics.Subpattern_flags_must_be_present_when_there_is_a_minus_sign, flagsStart, p.pos()-flagsStart)
|
|
}
|
|
}
|
|
p.scanExpectedChar(':')
|
|
isPreviousTermQuantifiable = true
|
|
}
|
|
} else {
|
|
p.numberOfCapturingGroups++
|
|
isPreviousTermQuantifiable = true
|
|
}
|
|
p.scanDisjunction(true /*isInGroup*/)
|
|
p.scanExpectedChar(')')
|
|
case '{':
|
|
p.incPos(1)
|
|
digitsStart := p.pos()
|
|
p.scanDigits()
|
|
minStr := p.scanner.tokenValue
|
|
if !p.anyUnicodeModeOrNonAnnexB && minStr == "" {
|
|
isPreviousTermQuantifiable = true
|
|
continue
|
|
}
|
|
if p.char() == ',' {
|
|
p.incPos(1)
|
|
p.scanDigits()
|
|
maxStr := p.scanner.tokenValue
|
|
if minStr == "" {
|
|
if maxStr != "" || p.char() == '}' {
|
|
p.error(diagnostics.Incomplete_quantifier_Digit_expected, digitsStart, 0)
|
|
} else {
|
|
p.error(diagnostics.Unexpected_0_Did_you_mean_to_escape_it_with_backslash, start, 1, string(ch))
|
|
isPreviousTermQuantifiable = true
|
|
continue
|
|
}
|
|
} else if maxStr != "" {
|
|
if compareDecimalStrings(minStr, maxStr) > 0 && (p.anyUnicodeModeOrNonAnnexB || p.char() == '}') {
|
|
p.error(diagnostics.Numbers_out_of_order_in_quantifier, digitsStart, p.pos()-digitsStart)
|
|
}
|
|
}
|
|
} else if minStr == "" {
|
|
if p.anyUnicodeModeOrNonAnnexB {
|
|
p.error(diagnostics.Unexpected_0_Did_you_mean_to_escape_it_with_backslash, start, 1, string(ch))
|
|
}
|
|
isPreviousTermQuantifiable = true
|
|
continue
|
|
}
|
|
if p.char() != '}' {
|
|
if p.anyUnicodeModeOrNonAnnexB {
|
|
p.error(diagnostics.X_0_expected, p.pos(), 0, "}")
|
|
p.incPos(-1)
|
|
} else {
|
|
isPreviousTermQuantifiable = true
|
|
continue
|
|
}
|
|
}
|
|
fallthrough
|
|
case '*', '+', '?':
|
|
p.incPos(1)
|
|
if p.char() == '?' {
|
|
// Non-greedy
|
|
p.incPos(1)
|
|
}
|
|
if !isPreviousTermQuantifiable {
|
|
p.error(diagnostics.There_is_nothing_available_for_repetition, start, p.pos()-start)
|
|
}
|
|
isPreviousTermQuantifiable = false
|
|
case '.':
|
|
p.incPos(1)
|
|
isPreviousTermQuantifiable = true
|
|
case '[':
|
|
p.incPos(1)
|
|
if p.unicodeSetsMode {
|
|
p.scanClassSetExpression()
|
|
} else {
|
|
p.scanClassRanges()
|
|
p.pendingLowSurrogate = 0
|
|
}
|
|
p.scanExpectedChar(']')
|
|
isPreviousTermQuantifiable = true
|
|
case ')':
|
|
if isInGroup {
|
|
return
|
|
}
|
|
fallthrough
|
|
case ']', '}':
|
|
if p.anyUnicodeModeOrNonAnnexB || ch == ')' {
|
|
p.error(diagnostics.Unexpected_0_Did_you_mean_to_escape_it_with_backslash, p.pos(), 1, string(ch))
|
|
}
|
|
p.incPos(1)
|
|
isPreviousTermQuantifiable = true
|
|
case '/', '|':
|
|
return
|
|
default:
|
|
p.scanSourceCharacter()
|
|
isPreviousTermQuantifiable = true
|
|
}
|
|
}
|
|
}
|
|
|
|
func (p *regExpParser) scanPatternModifiers(currFlags regularExpressionFlags) regularExpressionFlags {
|
|
for p.pos() < p.end {
|
|
ch, size := utf8.DecodeRuneInString(p.text()[p.pos():])
|
|
if ch == utf8.RuneError || !IsIdentifierPart(ch) {
|
|
break
|
|
}
|
|
flag, ok := charCodeToRegExpFlag[ch]
|
|
if !ok {
|
|
p.error(diagnostics.Unknown_regular_expression_flag, p.pos(), size)
|
|
} else if currFlags&flag != 0 {
|
|
p.error(diagnostics.Duplicate_regular_expression_flag, p.pos(), size)
|
|
} else if flag®ularExpressionFlagsModifiers == 0 {
|
|
p.error(diagnostics.This_regular_expression_flag_cannot_be_toggled_within_a_subpattern, p.pos(), size)
|
|
} else {
|
|
currFlags |= flag
|
|
p.scanner.checkRegularExpressionFlagAvailability(flag, p.pos(), size)
|
|
}
|
|
p.incPos(size)
|
|
}
|
|
return currFlags
|
|
}
|
|
|
|
// AtomEscape ::=
|
|
//
|
|
// | DecimalEscape
|
|
// | CharacterClassEscape
|
|
// | CharacterEscape
|
|
// | 'k<' RegExpIdentifierName '>'
|
|
func (p *regExpParser) scanAtomEscape() {
|
|
debug.Assert(p.pos() > 0 && p.text()[p.pos()-1] == '\\')
|
|
switch p.char() {
|
|
case 'k':
|
|
p.incPos(1)
|
|
if p.char() == '<' {
|
|
p.incPos(1)
|
|
p.scanGroupName(true /*isReference*/)
|
|
p.scanExpectedChar('>')
|
|
} else if p.anyUnicodeModeOrNonAnnexB || p.namedCaptureGroups {
|
|
p.error(diagnostics.X_k_must_be_followed_by_a_capturing_group_name_enclosed_in_angle_brackets, p.pos()-2, 2)
|
|
}
|
|
case 'q':
|
|
if p.unicodeSetsMode {
|
|
p.incPos(1)
|
|
p.error(diagnostics.X_q_is_only_available_inside_character_class, p.pos()-2, 2)
|
|
return
|
|
}
|
|
fallthrough
|
|
default:
|
|
if !p.scanCharacterClassEscape() && !p.scanDecimalEscape() {
|
|
// Regex literals cannot contain line breaks here, so a character escape must consume something.
|
|
debug.Assert(p.scanCharacterEscape(true /*atomEscape*/) != "")
|
|
}
|
|
}
|
|
}
|
|
|
|
// DecimalEscape ::= [1-9] [0-9]*
|
|
func (p *regExpParser) scanDecimalEscape() bool {
|
|
debug.Assert(p.pos() > 0 && p.text()[p.pos()-1] == '\\')
|
|
ch := p.char()
|
|
if ch >= '1' && ch <= '9' {
|
|
start := p.pos()
|
|
p.scanDigits()
|
|
val, err := strconv.Atoi(p.scanner.tokenValue)
|
|
if err != nil {
|
|
val = math.MaxInt
|
|
}
|
|
p.decimalEscapes = append(p.decimalEscapes, decimalEscapeValue{pos: start, end: p.pos(), value: val})
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// CharacterEscape ::=
|
|
//
|
|
// | `c` ControlLetter
|
|
// | IdentityEscape
|
|
// | (Other sequences handled by `scanEscapeSequence`)
|
|
//
|
|
// IdentityEscape ::=
|
|
//
|
|
// | '^' | '$' | '/' | '\' | '.' | '*' | '+' | '?' | '(' | ')' | '[' | ']' | '{' | '}' | '|'
|
|
// | [~AnyUnicodeMode] (any other non-identifier characters)
|
|
func (p *regExpParser) scanCharacterEscape(atomEscape bool) string {
|
|
debug.Assert(p.pos() > 0 && p.text()[p.pos()-1] == '\\')
|
|
ch := p.char()
|
|
switch ch {
|
|
case -1:
|
|
p.error(diagnostics.Undetermined_character_escape, p.pos()-1, 1)
|
|
return "\\"
|
|
case 'c':
|
|
p.incPos(1)
|
|
ch = p.char()
|
|
if stringutil.IsASCIILetter(ch) {
|
|
p.incPos(1)
|
|
return string(ch & 0x1f)
|
|
}
|
|
if p.anyUnicodeModeOrNonAnnexB {
|
|
p.error(diagnostics.X_c_must_be_followed_by_an_ASCII_letter, p.pos()-2, 2)
|
|
} else if atomEscape {
|
|
p.incPos(-1)
|
|
return "\\"
|
|
}
|
|
return string(ch)
|
|
case '^', '$', '/', '\\', '.', '*', '+', '?', '(', ')', '[', ']', '{', '}', '|':
|
|
p.incPos(1)
|
|
return string(ch)
|
|
default:
|
|
p.incPos(-1) // back up to include the backslash for scanEscapeSequence
|
|
flags := EscapeSequenceScanningFlagsRegularExpression
|
|
if p.annexB {
|
|
flags |= EscapeSequenceScanningFlagsAnnexB
|
|
}
|
|
if p.anyUnicodeMode {
|
|
flags |= EscapeSequenceScanningFlagsAnyUnicodeMode
|
|
}
|
|
if atomEscape {
|
|
flags |= EscapeSequenceScanningFlagsAtomEscape
|
|
}
|
|
return p.scanner.scanEscapeSequence(flags)
|
|
}
|
|
}
|
|
|
|
func (p *regExpParser) scanGroupName(isReference bool) {
|
|
debug.Assert(p.pos() > 0 && p.text()[p.pos()-1] == '<')
|
|
p.scanner.tokenStart = p.pos()
|
|
p.scanner.scanIdentifier(0)
|
|
if p.pos() == p.scanner.tokenStart {
|
|
p.error(diagnostics.Expected_a_capturing_group_name, p.pos(), 0)
|
|
} else if isReference {
|
|
p.groupNameReferences = append(p.groupNameReferences, groupNameReference{pos: p.scanner.tokenStart, end: p.pos(), name: p.scanner.tokenValue})
|
|
} else if p.namedCapturingGroupsContains(p.scanner.tokenValue) {
|
|
p.error(diagnostics.Named_capturing_groups_with_the_same_name_must_be_mutually_exclusive_to_each_other, p.scanner.tokenStart, p.pos()-p.scanner.tokenStart)
|
|
} else {
|
|
if len(p.namedCapturingGroups) > 0 {
|
|
p.namedCapturingGroups[len(p.namedCapturingGroups)-1][p.scanner.tokenValue] = true
|
|
}
|
|
p.groupSpecifiers[p.scanner.tokenValue] = true
|
|
}
|
|
}
|
|
|
|
func (p *regExpParser) namedCapturingGroupsContains(name string) bool {
|
|
for _, group := range p.namedCapturingGroups {
|
|
if group[name] {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (p *regExpParser) isClassContentExit(ch rune) bool {
|
|
return ch == ']' || p.pos() >= p.end
|
|
}
|
|
|
|
// ClassRanges ::= '^'? (ClassAtom ('-' ClassAtom)?)*
|
|
func (p *regExpParser) scanClassRanges() {
|
|
debug.Assert(p.pos() > 0 && p.text()[p.pos()-1] == '[')
|
|
p.pendingLowSurrogate = 0
|
|
if p.char() == '^' {
|
|
p.incPos(1)
|
|
}
|
|
for p.pos() < p.end {
|
|
ch := p.char()
|
|
if p.isClassContentExit(ch) {
|
|
return
|
|
}
|
|
minStart := p.pos()
|
|
minCharacter := p.scanClassAtom()
|
|
if p.char() == '-' {
|
|
p.incPos(1)
|
|
ch = p.char()
|
|
if p.isClassContentExit(ch) {
|
|
return
|
|
}
|
|
if minCharacter == "" && p.anyUnicodeModeOrNonAnnexB {
|
|
p.error(diagnostics.A_character_class_range_must_not_be_bounded_by_another_character_class, minStart, p.pos()-1-minStart)
|
|
}
|
|
maxStart := p.pos()
|
|
maxCharacter := p.scanClassAtom()
|
|
if maxCharacter == "" && p.anyUnicodeModeOrNonAnnexB {
|
|
p.error(diagnostics.A_character_class_range_must_not_be_bounded_by_another_character_class, maxStart, p.pos()-maxStart)
|
|
continue
|
|
}
|
|
if minCharacter == "" {
|
|
continue
|
|
}
|
|
minCharacterValue, minSize := stringutil.DecodeJSStringRune(minCharacter)
|
|
maxCharacterValue, maxSize := stringutil.DecodeJSStringRune(maxCharacter)
|
|
if len(minCharacter) == minSize && len(maxCharacter) == maxSize && minCharacterValue > maxCharacterValue {
|
|
p.error(diagnostics.Range_out_of_order_in_character_class, minStart, p.pos()-minStart)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Static Semantics: MayContainStrings
|
|
// ClassUnion: ClassSetOperands.some(ClassSetOperand => ClassSetOperand.MayContainStrings)
|
|
// ClassIntersection: ClassSetOperands.every(ClassSetOperand => ClassSetOperand.MayContainStrings)
|
|
// ClassSubtraction: ClassSetOperands[0].MayContainStrings
|
|
// ClassSetOperand:
|
|
// || ClassStringDisjunctionContents.MayContainStrings
|
|
// || CharacterClassEscape.UnicodePropertyValueExpression.LoneUnicodePropertyNameOrValue.MayContainStrings
|
|
// ClassStringDisjunctionContents: ClassStrings.some(ClassString => ClassString.ClassSetCharacters.length !== 1)
|
|
// LoneUnicodePropertyNameOrValue: isBinaryUnicodePropertyOfStrings(LoneUnicodePropertyNameOrValue)
|
|
|
|
// ClassSetExpression ::= '^'? (ClassUnion | ClassIntersection | ClassSubtraction)
|
|
// ClassUnion ::= (ClassSetRange | ClassSetOperand)*
|
|
// ClassIntersection ::= ClassSetOperand ('&&' ClassSetOperand)+
|
|
// ClassSubtraction ::= ClassSetOperand ('--' ClassSetOperand)+
|
|
// ClassSetRange ::= ClassSetCharacter '-' ClassSetCharacter
|
|
func (p *regExpParser) scanClassSetExpression() {
|
|
debug.Assert(p.pos() > 0 && p.text()[p.pos()-1] == '[')
|
|
isCharacterComplement := false
|
|
if p.char() == '^' {
|
|
p.incPos(1)
|
|
isCharacterComplement = true
|
|
}
|
|
expressionMayContainStrings := false
|
|
ch := p.char()
|
|
if p.isClassContentExit(ch) {
|
|
return
|
|
}
|
|
start := p.pos()
|
|
var operand string
|
|
twoChars := ""
|
|
if p.pos()+1 < p.end {
|
|
twoChars = p.text()[p.pos() : p.pos()+2]
|
|
}
|
|
switch twoChars {
|
|
case "--", "&&":
|
|
p.error(diagnostics.Expected_a_class_set_operand, p.pos(), 0)
|
|
p.mayContainStrings = false
|
|
default:
|
|
operand = p.scanClassSetOperand()
|
|
}
|
|
switch p.char() {
|
|
case '-':
|
|
if p.pos()+1 < p.end && p.charAt(p.pos()+1) == '-' {
|
|
if isCharacterComplement && p.mayContainStrings {
|
|
p.error(diagnostics.Anything_that_would_possibly_match_more_than_a_single_character_is_invalid_inside_a_negated_character_class, start, p.pos()-start)
|
|
}
|
|
expressionMayContainStrings = p.mayContainStrings
|
|
p.scanClassSetSubExpression(classSetExpressionTypeClassSubtraction)
|
|
p.mayContainStrings = !isCharacterComplement && expressionMayContainStrings
|
|
return
|
|
}
|
|
case '&':
|
|
if p.pos()+1 < p.end && p.charAt(p.pos()+1) == '&' {
|
|
p.scanClassSetSubExpression(classSetExpressionTypeClassIntersection)
|
|
if isCharacterComplement && p.mayContainStrings {
|
|
p.error(diagnostics.Anything_that_would_possibly_match_more_than_a_single_character_is_invalid_inside_a_negated_character_class, start, p.pos()-start)
|
|
}
|
|
expressionMayContainStrings = p.mayContainStrings
|
|
p.mayContainStrings = !isCharacterComplement && expressionMayContainStrings
|
|
return
|
|
} else {
|
|
p.error(diagnostics.Unexpected_0_Did_you_mean_to_escape_it_with_backslash, p.pos(), 1, string(ch))
|
|
}
|
|
default:
|
|
if isCharacterComplement && p.mayContainStrings {
|
|
p.error(diagnostics.Anything_that_would_possibly_match_more_than_a_single_character_is_invalid_inside_a_negated_character_class, start, p.pos()-start)
|
|
}
|
|
expressionMayContainStrings = p.mayContainStrings
|
|
}
|
|
for p.pos() < p.end {
|
|
ch = p.char()
|
|
switch ch {
|
|
case '-':
|
|
p.incPos(1)
|
|
ch = p.char()
|
|
if p.isClassContentExit(ch) {
|
|
p.mayContainStrings = !isCharacterComplement && expressionMayContainStrings
|
|
return
|
|
}
|
|
if ch == '-' {
|
|
p.incPos(1)
|
|
p.error(diagnostics.Operators_must_not_be_mixed_within_a_character_class_Wrap_it_in_a_nested_class_instead, p.pos()-2, 2)
|
|
start = p.pos() - 2
|
|
operand = p.text()[start:p.pos()]
|
|
continue
|
|
} else {
|
|
if operand == "" {
|
|
p.error(diagnostics.A_character_class_range_must_not_be_bounded_by_another_character_class, start, p.pos()-1-start)
|
|
}
|
|
secondStart := p.pos()
|
|
secondOperand := p.scanClassSetOperand()
|
|
if isCharacterComplement && p.mayContainStrings {
|
|
p.error(diagnostics.Anything_that_would_possibly_match_more_than_a_single_character_is_invalid_inside_a_negated_character_class, secondStart, p.pos()-secondStart)
|
|
}
|
|
expressionMayContainStrings = expressionMayContainStrings || p.mayContainStrings
|
|
if secondOperand == "" {
|
|
p.error(diagnostics.A_character_class_range_must_not_be_bounded_by_another_character_class, secondStart, p.pos()-secondStart)
|
|
} else if operand != "" {
|
|
minCharacterValue, minSize := stringutil.DecodeJSStringRune(operand)
|
|
maxCharacterValue, maxSize := stringutil.DecodeJSStringRune(secondOperand)
|
|
if len(operand) == minSize && len(secondOperand) == maxSize && minCharacterValue > maxCharacterValue {
|
|
p.error(diagnostics.Range_out_of_order_in_character_class, start, p.pos()-start)
|
|
}
|
|
}
|
|
}
|
|
case '&':
|
|
start = p.pos()
|
|
p.incPos(1)
|
|
if p.char() == '&' {
|
|
p.incPos(1)
|
|
p.error(diagnostics.Operators_must_not_be_mixed_within_a_character_class_Wrap_it_in_a_nested_class_instead, p.pos()-2, 2)
|
|
if p.char() == '&' {
|
|
p.error(diagnostics.Unexpected_0_Did_you_mean_to_escape_it_with_backslash, p.pos(), 1, string(ch))
|
|
p.incPos(1)
|
|
}
|
|
} else {
|
|
p.error(diagnostics.Unexpected_0_Did_you_mean_to_escape_it_with_backslash, p.pos()-1, 1, string(ch))
|
|
}
|
|
operand = p.text()[start:p.pos()]
|
|
continue
|
|
}
|
|
if p.isClassContentExit(p.char()) {
|
|
break
|
|
}
|
|
start = p.pos()
|
|
twoChars = ""
|
|
if p.pos()+1 < p.end {
|
|
twoChars = p.text()[p.pos() : p.pos()+2]
|
|
}
|
|
switch twoChars {
|
|
case "--", "&&":
|
|
p.error(diagnostics.Operators_must_not_be_mixed_within_a_character_class_Wrap_it_in_a_nested_class_instead, p.pos(), 2)
|
|
p.incPos(2)
|
|
operand = p.text()[start:p.pos()]
|
|
default:
|
|
operand = p.scanClassSetOperand()
|
|
}
|
|
}
|
|
p.mayContainStrings = !isCharacterComplement && expressionMayContainStrings
|
|
}
|
|
|
|
func (p *regExpParser) scanClassSetSubExpression(expressionType classSetExpressionType) {
|
|
expressionMayContainStrings := p.mayContainStrings
|
|
for p.pos() < p.end {
|
|
ch := p.char()
|
|
if p.isClassContentExit(ch) {
|
|
break
|
|
}
|
|
switch ch {
|
|
case '-':
|
|
p.incPos(1)
|
|
if p.char() == '-' {
|
|
p.incPos(1)
|
|
if expressionType != classSetExpressionTypeClassSubtraction {
|
|
p.error(diagnostics.Operators_must_not_be_mixed_within_a_character_class_Wrap_it_in_a_nested_class_instead, p.pos()-2, 2)
|
|
}
|
|
} else {
|
|
p.error(diagnostics.Operators_must_not_be_mixed_within_a_character_class_Wrap_it_in_a_nested_class_instead, p.pos()-1, 1)
|
|
}
|
|
case '&':
|
|
p.incPos(1)
|
|
if p.char() == '&' {
|
|
p.incPos(1)
|
|
if expressionType != classSetExpressionTypeClassIntersection {
|
|
p.error(diagnostics.Operators_must_not_be_mixed_within_a_character_class_Wrap_it_in_a_nested_class_instead, p.pos()-2, 2)
|
|
}
|
|
if p.char() == '&' {
|
|
p.error(diagnostics.Unexpected_0_Did_you_mean_to_escape_it_with_backslash, p.pos(), 1, string(ch))
|
|
p.incPos(1)
|
|
}
|
|
} else {
|
|
p.error(diagnostics.Unexpected_0_Did_you_mean_to_escape_it_with_backslash, p.pos()-1, 1, string(ch))
|
|
}
|
|
default:
|
|
switch expressionType {
|
|
case classSetExpressionTypeClassSubtraction:
|
|
p.error(diagnostics.X_0_expected, p.pos(), 0, "--")
|
|
case classSetExpressionTypeClassIntersection:
|
|
p.error(diagnostics.X_0_expected, p.pos(), 0, "&&")
|
|
}
|
|
}
|
|
ch = p.char()
|
|
if p.isClassContentExit(ch) {
|
|
p.error(diagnostics.Expected_a_class_set_operand, p.pos(), 0)
|
|
break
|
|
}
|
|
p.scanClassSetOperand()
|
|
if expressionType == classSetExpressionTypeClassIntersection {
|
|
expressionMayContainStrings = expressionMayContainStrings && p.mayContainStrings
|
|
}
|
|
}
|
|
p.mayContainStrings = expressionMayContainStrings
|
|
}
|
|
|
|
// ClassSetOperand ::=
|
|
//
|
|
// | '[' ClassSetExpression ']'
|
|
// | '\' CharacterClassEscape
|
|
// | '\q{' ClassStringDisjunctionContents '}'
|
|
// | ClassSetCharacter
|
|
func (p *regExpParser) scanClassSetOperand() string {
|
|
p.mayContainStrings = false
|
|
switch p.char() {
|
|
case '[':
|
|
p.incPos(1)
|
|
p.scanClassSetExpression()
|
|
p.scanExpectedChar(']')
|
|
return ""
|
|
case '\\':
|
|
p.incPos(1)
|
|
if p.scanCharacterClassEscape() {
|
|
return ""
|
|
} else if p.char() == 'q' {
|
|
p.incPos(1)
|
|
if p.char() == '{' {
|
|
p.incPos(1)
|
|
p.scanClassStringDisjunctionContents()
|
|
p.scanExpectedChar('}')
|
|
return ""
|
|
} else {
|
|
p.error(diagnostics.X_q_must_be_followed_by_string_alternatives_enclosed_in_braces, p.pos()-2, 2)
|
|
return "q"
|
|
}
|
|
}
|
|
p.incPos(-1)
|
|
fallthrough
|
|
default:
|
|
return p.scanClassSetCharacter()
|
|
}
|
|
}
|
|
|
|
// ClassStringDisjunctionContents ::= ClassSetCharacter* ('|' ClassSetCharacter*)*
|
|
func (p *regExpParser) scanClassStringDisjunctionContents() {
|
|
debug.Assert(p.pos() > 0 && p.text()[p.pos()-1] == '{')
|
|
characterCount := 0
|
|
for p.pos() < p.end {
|
|
ch := p.char()
|
|
switch ch {
|
|
case '}':
|
|
if characterCount != 1 {
|
|
p.mayContainStrings = true
|
|
}
|
|
return
|
|
case '|':
|
|
if characterCount != 1 {
|
|
p.mayContainStrings = true
|
|
}
|
|
p.incPos(1)
|
|
characterCount = 0
|
|
default:
|
|
p.scanClassSetCharacter()
|
|
characterCount++
|
|
}
|
|
}
|
|
}
|
|
|
|
// ClassSetCharacter ::=
|
|
//
|
|
// | SourceCharacter -- ClassSetSyntaxCharacter -- ClassSetReservedDoublePunctuator
|
|
// | '\' (CharacterEscape | ClassSetReservedPunctuator | 'b')
|
|
func (p *regExpParser) scanClassSetCharacter() string {
|
|
ch := p.char()
|
|
if ch == '\\' {
|
|
p.incPos(1)
|
|
innerCh := p.char()
|
|
switch innerCh {
|
|
case 'b':
|
|
p.incPos(1)
|
|
return "\b"
|
|
case '&', '-', '!', '#', '%', ',', ':', ';', '<', '=', '>', '@', '`', '~':
|
|
p.incPos(1)
|
|
return string(innerCh)
|
|
default:
|
|
return p.scanCharacterEscape(false /*atomEscape*/)
|
|
}
|
|
} else if p.pos()+1 < p.end && ch == p.charAt(p.pos()+1) {
|
|
switch ch {
|
|
case '&', '!', '#', '%', '*', '+', ',', '.', ':', ';', '<', '=', '>', '?', '@', '`', '~':
|
|
p.error(diagnostics.A_character_class_must_not_contain_a_reserved_double_punctuator_Did_you_mean_to_escape_it_with_backslash, p.pos(), 2)
|
|
p.incPos(2)
|
|
return p.text()[p.pos()-2 : p.pos()]
|
|
}
|
|
}
|
|
switch ch {
|
|
case '/', '(', ')', '[', ']', '{', '}', '-', '|':
|
|
p.error(diagnostics.Unexpected_0_Did_you_mean_to_escape_it_with_backslash, p.pos(), 1, string(ch))
|
|
p.incPos(1)
|
|
return string(ch)
|
|
}
|
|
return p.scanSourceCharacter()
|
|
}
|
|
|
|
// ClassAtom ::=
|
|
//
|
|
// | SourceCharacter but not one of '\' or ']'
|
|
// | '\' ClassEscape
|
|
//
|
|
// ClassEscape ::=
|
|
//
|
|
// | 'b'
|
|
// | '-'
|
|
// | CharacterClassEscape
|
|
// | CharacterEscape
|
|
func (p *regExpParser) scanClassAtom() string {
|
|
if p.char() == '\\' {
|
|
p.incPos(1)
|
|
ch := p.char()
|
|
switch ch {
|
|
case 'b':
|
|
p.incPos(1)
|
|
return "\b"
|
|
case '-':
|
|
p.incPos(1)
|
|
return string(ch)
|
|
default:
|
|
if p.scanCharacterClassEscape() {
|
|
return ""
|
|
}
|
|
return p.scanCharacterEscape(false /*atomEscape*/)
|
|
}
|
|
} else {
|
|
return p.scanSourceCharacter()
|
|
}
|
|
}
|
|
|
|
// CharacterClassEscape ::=
|
|
//
|
|
// | 'd' | 'D' | 's' | 'S' | 'w' | 'W'
|
|
// | [+AnyUnicodeMode] ('P' | 'p') '{' UnicodePropertyValueExpression '}'
|
|
func (p *regExpParser) scanCharacterClassEscape() bool {
|
|
debug.Assert(p.pos() > 0 && p.text()[p.pos()-1] == '\\')
|
|
isCharacterComplement := false
|
|
start := p.pos() - 1
|
|
ch := p.char()
|
|
switch ch {
|
|
case 'd', 'D', 's', 'S', 'w', 'W':
|
|
p.incPos(1)
|
|
return true
|
|
case 'P':
|
|
isCharacterComplement = true
|
|
fallthrough
|
|
case 'p':
|
|
p.incPos(1)
|
|
if p.char() == '{' {
|
|
p.incPos(1)
|
|
propertyNameOrValueStart := p.pos()
|
|
propertyNameOrValue := p.scanWordCharacters()
|
|
if p.char() == '=' {
|
|
propertyName := nonBinaryUnicodeProperties[propertyNameOrValue]
|
|
if p.pos() == propertyNameOrValueStart {
|
|
p.error(diagnostics.Expected_a_Unicode_property_name, p.pos(), 0)
|
|
} else if propertyName == "" {
|
|
p.error(diagnostics.Unknown_Unicode_property_name, propertyNameOrValueStart, p.pos()-propertyNameOrValueStart)
|
|
suggestion := p.getSpellingSuggestionForUnicodePropertyName(propertyNameOrValue)
|
|
if suggestion != "" {
|
|
p.error(diagnostics.Did_you_mean_0, propertyNameOrValueStart, p.pos()-propertyNameOrValueStart, suggestion)
|
|
}
|
|
}
|
|
p.incPos(1)
|
|
propertyValueStart := p.pos()
|
|
propertyValue := p.scanWordCharacters()
|
|
if p.pos() == propertyValueStart {
|
|
p.error(diagnostics.Expected_a_Unicode_property_value, p.pos(), 0)
|
|
} else if propertyName != "" {
|
|
values := valuesOfNonBinaryUnicodeProperties[propertyName]
|
|
if values != nil && !values.Has(propertyValue) {
|
|
p.error(diagnostics.Unknown_Unicode_property_value, propertyValueStart, p.pos()-propertyValueStart)
|
|
suggestion := p.getSpellingSuggestionForUnicodePropertyValue(propertyName, propertyValue)
|
|
if suggestion != "" {
|
|
p.error(diagnostics.Did_you_mean_0, propertyValueStart, p.pos()-propertyValueStart, suggestion)
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
if p.pos() == propertyNameOrValueStart {
|
|
p.error(diagnostics.Expected_a_Unicode_property_name_or_value, p.pos(), 0)
|
|
} else if binaryUnicodePropertiesOfStrings.Has(propertyNameOrValue) {
|
|
if !p.unicodeSetsMode {
|
|
p.error(diagnostics.Any_Unicode_property_that_would_possibly_match_more_than_a_single_character_is_only_available_when_the_Unicode_Sets_v_flag_is_set, propertyNameOrValueStart, p.pos()-propertyNameOrValueStart)
|
|
} else if isCharacterComplement {
|
|
p.error(diagnostics.Anything_that_would_possibly_match_more_than_a_single_character_is_invalid_inside_a_negated_character_class, propertyNameOrValueStart, p.pos()-propertyNameOrValueStart)
|
|
} else {
|
|
p.mayContainStrings = true
|
|
}
|
|
} else if !valuesOfNonBinaryUnicodeProperties["General_Category"].Has(propertyNameOrValue) && !binaryUnicodeProperties.Has(propertyNameOrValue) {
|
|
p.error(diagnostics.Unknown_Unicode_property_name_or_value, propertyNameOrValueStart, p.pos()-propertyNameOrValueStart)
|
|
suggestion := p.getSpellingSuggestionForUnicodePropertyNameOrValue(propertyNameOrValue)
|
|
if suggestion != "" {
|
|
p.error(diagnostics.Did_you_mean_0, propertyNameOrValueStart, p.pos()-propertyNameOrValueStart, suggestion)
|
|
}
|
|
}
|
|
}
|
|
p.scanExpectedChar('}')
|
|
if !p.anyUnicodeMode {
|
|
p.error(diagnostics.Unicode_property_value_expressions_are_only_available_when_the_Unicode_u_flag_or_the_Unicode_Sets_v_flag_is_set, start, p.pos()-start)
|
|
}
|
|
} else if p.anyUnicodeModeOrNonAnnexB {
|
|
p.error(diagnostics.X_0_must_be_followed_by_a_Unicode_property_value_expression_enclosed_in_braces, p.pos()-2, 2, string(ch))
|
|
} else {
|
|
p.incPos(-1)
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (p *regExpParser) getSpellingSuggestionForUnicodePropertyName(name string) string {
|
|
return core.GetSpellingSuggestionForStrings(name, maps.Keys(nonBinaryUnicodeProperties))
|
|
}
|
|
|
|
func (p *regExpParser) getSpellingSuggestionForUnicodePropertyValue(propertyName string, value string) string {
|
|
values := valuesOfNonBinaryUnicodeProperties[propertyName]
|
|
if values == nil {
|
|
return ""
|
|
}
|
|
return core.GetSpellingSuggestionForStrings(value, maps.Keys(values.Keys()))
|
|
}
|
|
|
|
func (p *regExpParser) getSpellingSuggestionForUnicodePropertyNameOrValue(name string) string {
|
|
return core.GetSpellingSuggestionForStrings(name, core.ConcatenateSeq(
|
|
maps.Keys(valuesOfNonBinaryUnicodeProperties["General_Category"].Keys()),
|
|
maps.Keys(binaryUnicodeProperties.Keys()),
|
|
maps.Keys(binaryUnicodePropertiesOfStrings.Keys()),
|
|
))
|
|
}
|
|
|
|
func (p *regExpParser) scanWordCharacters() string {
|
|
start := p.pos()
|
|
for p.pos() < p.end {
|
|
ch := p.char()
|
|
if !isWordCharacter(ch) {
|
|
break
|
|
}
|
|
p.incPos(1)
|
|
}
|
|
return p.text()[start:p.pos()]
|
|
}
|
|
|
|
func (p *regExpParser) scanSourceCharacter() string {
|
|
if p.pos() >= p.end {
|
|
return ""
|
|
}
|
|
if !p.anyUnicodeMode {
|
|
if p.pendingLowSurrogate != 0 {
|
|
// Second of two surrogate code units for the same non-BMP character.
|
|
// Now advance past the full UTF-8 sequence (the high surrogate call did not advance).
|
|
_, size := utf8.DecodeRuneInString(p.text()[p.pos():])
|
|
p.incPos(size)
|
|
low := p.pendingLowSurrogate
|
|
p.pendingLowSurrogate = 0
|
|
return stringutil.EncodeJSStringRune(low)
|
|
}
|
|
ch, size := utf8.DecodeRuneInString(p.text()[p.pos():])
|
|
if ch == utf8.RuneError || size == 0 {
|
|
// Not a valid rune; consume one raw byte.
|
|
p.incPos(1)
|
|
return string(p.text()[p.pos()-1])
|
|
}
|
|
if utf16.RuneLen(ch) == 2 {
|
|
// Non-BMP character: emit the high surrogate first WITHOUT advancing.
|
|
// The low surrogate will be emitted on the next call, which also advances.
|
|
high, low := stringutil.CodePointToSurrogatePair(ch)
|
|
p.pendingLowSurrogate = low
|
|
return stringutil.EncodeJSStringRune(high)
|
|
}
|
|
p.incPos(size)
|
|
return string(ch)
|
|
}
|
|
ch, size := utf8.DecodeRuneInString(p.text()[p.pos():])
|
|
if size == 0 {
|
|
return ""
|
|
}
|
|
if ch == utf8.RuneError {
|
|
// Invalid UTF-8; consume the byte to avoid infinite loops.
|
|
p.incPos(size)
|
|
return ""
|
|
}
|
|
p.incPos(size)
|
|
return string(ch)
|
|
}
|
|
|
|
func (p *regExpParser) scanExpectedChar(ch rune) {
|
|
if p.char() == ch {
|
|
p.incPos(1)
|
|
} else {
|
|
p.error(diagnostics.X_0_expected, p.pos(), 0, string(ch))
|
|
}
|
|
}
|
|
|
|
func (p *regExpParser) scanDigits() {
|
|
start := p.pos()
|
|
for p.pos() < p.end && stringutil.IsDigit(p.char()) {
|
|
p.incPos(1)
|
|
}
|
|
p.scanner.tokenValue = p.text()[start:p.pos()]
|
|
}
|
|
|
|
func (p *regExpParser) run() {
|
|
// Regular expressions are checked more strictly when either in 'u' or 'v' mode, or
|
|
// when not using the looser interpretation of the syntax from ECMA-262 Annex B.
|
|
p.anyUnicodeModeOrNonAnnexB = p.anyUnicodeMode || !p.annexB
|
|
|
|
p.scanDisjunction(false /*isInGroup*/)
|
|
|
|
for _, reference := range p.groupNameReferences {
|
|
if !p.groupSpecifiers[reference.name] {
|
|
p.error(diagnostics.There_is_no_capturing_group_named_0_in_this_regular_expression, reference.pos, reference.end-reference.pos, reference.name)
|
|
if len(p.groupSpecifiers) > 0 {
|
|
suggestion := core.GetSpellingSuggestionForStrings(reference.name, maps.Keys(p.groupSpecifiers))
|
|
if suggestion != "" {
|
|
p.error(diagnostics.Did_you_mean_0, reference.pos, reference.end-reference.pos, suggestion)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
for _, escape := range p.decimalEscapes {
|
|
// Although a DecimalEscape with a value greater than the number of capturing groups
|
|
// is treated as either a LegacyOctalEscapeSequence or an IdentityEscape in Annex B,
|
|
// an error is nevertheless reported since it's most likely a mistake.
|
|
if escape.value > p.numberOfCapturingGroups {
|
|
if p.numberOfCapturingGroups > 0 {
|
|
p.error(diagnostics.This_backreference_refers_to_a_group_that_does_not_exist_There_are_only_0_capturing_groups_in_this_regular_expression, escape.pos, escape.end-escape.pos, p.numberOfCapturingGroups)
|
|
} else {
|
|
p.error(diagnostics.This_backreference_refers_to_a_group_that_does_not_exist_There_are_no_capturing_groups_in_this_regular_expression, escape.pos, escape.end-escape.pos)
|
|
}
|
|
}
|
|
}
|
|
}
|