af5fc2d4d2
See https://github.com/tc39/proposal-regexp-named-groups for a high-level description of the feature and examples. This is one of the features requested in https://github.com/dart-lang/sdk/issues/34935. This is a partial implementation because while there is a way to retrieve groups via Dart by name, it requires casting the returned Match to the new RegExpMatch interface to avoid changing the RegExp interface. Changing the RegExp interface will happen in a future update, since there are other planned changes to the RegExp interface coming soon and that way we only change it once. See https://github.com/dart-lang/sdk/issues/36171 for more details on the planned changes. Also, since only BMP regular expressions are supported, not full Unicode ones (i.e., those with the /u flag in ECMAscript), \k<NAME> will only be parsed as a named back reference if there are named captures in the string. Otherwise, the \k will be parsed as the identity escape for backwards compatibility. The new tests illustrate this difference. Change-Id: Ieeb0374813db78924c9aa8ac3e652dfb6d4a5934 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/95461 Commit-Queue: Stevie Strickland <sstrickl@google.com> Reviewed-by: Lasse R.H. Nielsen <lrn@google.com> Reviewed-by: Martin Kustermann <kustermann@google.com> Reviewed-by: Jenny Messerly <jmesserly@google.com> Reviewed-by: Johnni Winther <johnniwinther@google.com>
1333 lines
37 KiB
C++
1333 lines
37 KiB
C++
// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
|
|
// for details. All rights reserved. Use of this source code is governed by a
|
|
// BSD-style license that can be found in the LICENSE file.
|
|
|
|
#include "vm/regexp_parser.h"
|
|
#include "vm/longjump.h"
|
|
#include "vm/object_store.h"
|
|
|
|
namespace dart {
|
|
|
|
#define Z zone()
|
|
|
|
// Enables possessive quantifier syntax for testing.
|
|
static const bool FLAG_regexp_possessive_quantifier = false;
|
|
|
|
RegExpBuilder::RegExpBuilder()
|
|
: zone_(Thread::Current()->zone()),
|
|
pending_empty_(false),
|
|
characters_(NULL),
|
|
terms_(),
|
|
text_(),
|
|
alternatives_()
|
|
#ifdef DEBUG
|
|
,
|
|
last_added_(ADD_NONE)
|
|
#endif
|
|
{
|
|
}
|
|
|
|
void RegExpBuilder::FlushCharacters() {
|
|
pending_empty_ = false;
|
|
if (characters_ != NULL) {
|
|
RegExpTree* atom = new (Z) RegExpAtom(characters_);
|
|
characters_ = NULL;
|
|
text_.Add(atom);
|
|
LAST(ADD_ATOM);
|
|
}
|
|
}
|
|
|
|
void RegExpBuilder::FlushText() {
|
|
FlushCharacters();
|
|
intptr_t num_text = text_.length();
|
|
if (num_text == 0) {
|
|
return;
|
|
} else if (num_text == 1) {
|
|
terms_.Add(text_.Last());
|
|
} else {
|
|
RegExpText* text = new (Z) RegExpText();
|
|
for (intptr_t i = 0; i < num_text; i++)
|
|
text_[i]->AppendToText(text);
|
|
terms_.Add(text);
|
|
}
|
|
text_.Clear();
|
|
}
|
|
|
|
void RegExpBuilder::AddCharacter(uint16_t c) {
|
|
pending_empty_ = false;
|
|
if (characters_ == NULL) {
|
|
characters_ = new (Z) ZoneGrowableArray<uint16_t>(4);
|
|
}
|
|
characters_->Add(c);
|
|
LAST(ADD_CHAR);
|
|
}
|
|
|
|
void RegExpBuilder::AddEmpty() {
|
|
pending_empty_ = true;
|
|
}
|
|
|
|
void RegExpBuilder::AddAtom(RegExpTree* term) {
|
|
if (term->IsEmpty()) {
|
|
AddEmpty();
|
|
return;
|
|
}
|
|
if (term->IsTextElement()) {
|
|
FlushCharacters();
|
|
text_.Add(term);
|
|
} else {
|
|
FlushText();
|
|
terms_.Add(term);
|
|
}
|
|
LAST(ADD_ATOM);
|
|
}
|
|
|
|
void RegExpBuilder::AddAssertion(RegExpTree* assert) {
|
|
FlushText();
|
|
terms_.Add(assert);
|
|
LAST(ADD_ASSERT);
|
|
}
|
|
|
|
void RegExpBuilder::NewAlternative() {
|
|
FlushTerms();
|
|
}
|
|
|
|
void RegExpBuilder::FlushTerms() {
|
|
FlushText();
|
|
intptr_t num_terms = terms_.length();
|
|
RegExpTree* alternative;
|
|
if (num_terms == 0) {
|
|
alternative = RegExpEmpty::GetInstance();
|
|
} else if (num_terms == 1) {
|
|
alternative = terms_.Last();
|
|
} else {
|
|
ZoneGrowableArray<RegExpTree*>* terms =
|
|
new (Z) ZoneGrowableArray<RegExpTree*>();
|
|
for (intptr_t i = 0; i < terms_.length(); i++) {
|
|
terms->Add(terms_[i]);
|
|
}
|
|
alternative = new (Z) RegExpAlternative(terms);
|
|
}
|
|
alternatives_.Add(alternative);
|
|
terms_.Clear();
|
|
LAST(ADD_NONE);
|
|
}
|
|
|
|
RegExpTree* RegExpBuilder::ToRegExp() {
|
|
FlushTerms();
|
|
intptr_t num_alternatives = alternatives_.length();
|
|
if (num_alternatives == 0) {
|
|
return RegExpEmpty::GetInstance();
|
|
}
|
|
if (num_alternatives == 1) {
|
|
return alternatives_.Last();
|
|
}
|
|
ZoneGrowableArray<RegExpTree*>* alternatives =
|
|
new (Z) ZoneGrowableArray<RegExpTree*>();
|
|
for (intptr_t i = 0; i < alternatives_.length(); i++) {
|
|
alternatives->Add(alternatives_[i]);
|
|
}
|
|
return new (Z) RegExpDisjunction(alternatives);
|
|
}
|
|
|
|
bool RegExpBuilder::AddQuantifierToAtom(
|
|
intptr_t min,
|
|
intptr_t max,
|
|
RegExpQuantifier::QuantifierType quantifier_type) {
|
|
if (pending_empty_) {
|
|
pending_empty_ = false;
|
|
return true;
|
|
}
|
|
RegExpTree* atom;
|
|
if (characters_ != NULL) {
|
|
DEBUG_ASSERT(last_added_ == ADD_CHAR);
|
|
// Last atom was character.
|
|
|
|
ZoneGrowableArray<uint16_t>* char_vector =
|
|
new (Z) ZoneGrowableArray<uint16_t>();
|
|
char_vector->AddArray(*characters_);
|
|
intptr_t num_chars = char_vector->length();
|
|
if (num_chars > 1) {
|
|
ZoneGrowableArray<uint16_t>* prefix =
|
|
new (Z) ZoneGrowableArray<uint16_t>();
|
|
for (intptr_t i = 0; i < num_chars - 1; i++) {
|
|
prefix->Add(char_vector->At(i));
|
|
}
|
|
text_.Add(new (Z) RegExpAtom(prefix));
|
|
ZoneGrowableArray<uint16_t>* tail = new (Z) ZoneGrowableArray<uint16_t>();
|
|
tail->Add(char_vector->At(num_chars - 1));
|
|
char_vector = tail;
|
|
}
|
|
characters_ = NULL;
|
|
atom = new (Z) RegExpAtom(char_vector);
|
|
FlushText();
|
|
} else if (text_.length() > 0) {
|
|
DEBUG_ASSERT(last_added_ == ADD_ATOM);
|
|
atom = text_.RemoveLast();
|
|
FlushText();
|
|
} else if (terms_.length() > 0) {
|
|
DEBUG_ASSERT(last_added_ == ADD_ATOM);
|
|
atom = terms_.RemoveLast();
|
|
if (auto lookaround = atom->AsLookaround()) {
|
|
// Lookbehinds are not quantifiable.
|
|
if (lookaround->type() == RegExpLookaround::LOOKBEHIND) {
|
|
return false;
|
|
}
|
|
}
|
|
if (atom->max_match() == 0) {
|
|
// Guaranteed to only match an empty string.
|
|
LAST(ADD_TERM);
|
|
if (min == 0) {
|
|
return true;
|
|
}
|
|
terms_.Add(atom);
|
|
return true;
|
|
}
|
|
} else {
|
|
// Only call immediately after adding an atom or character!
|
|
UNREACHABLE();
|
|
}
|
|
terms_.Add(new (Z) RegExpQuantifier(min, max, quantifier_type, atom));
|
|
LAST(ADD_TERM);
|
|
return true;
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// Implementation of Parser
|
|
|
|
RegExpParser::RegExpParser(const String& in, String* error, bool multiline)
|
|
: zone_(Thread::Current()->zone()),
|
|
captures_(nullptr),
|
|
named_captures_(nullptr),
|
|
named_back_references_(nullptr),
|
|
in_(in),
|
|
current_(kEndMarker),
|
|
next_pos_(0),
|
|
captures_started_(0),
|
|
capture_count_(0),
|
|
has_more_(true),
|
|
multiline_(multiline),
|
|
simple_(false),
|
|
contains_anchor_(false),
|
|
is_scanned_for_captures_(false),
|
|
has_named_captures_(false) {
|
|
Advance();
|
|
}
|
|
|
|
uint32_t RegExpParser::Next() {
|
|
if (has_next()) {
|
|
return in().CharAt(next_pos_);
|
|
} else {
|
|
return kEndMarker;
|
|
}
|
|
}
|
|
|
|
void RegExpParser::Advance() {
|
|
if (next_pos_ < in().Length()) {
|
|
current_ = in().CharAt(next_pos_);
|
|
next_pos_++;
|
|
} else {
|
|
current_ = kEndMarker;
|
|
has_more_ = false;
|
|
}
|
|
}
|
|
|
|
void RegExpParser::Reset(intptr_t pos) {
|
|
next_pos_ = pos;
|
|
has_more_ = (pos < in().Length());
|
|
Advance();
|
|
}
|
|
|
|
void RegExpParser::Advance(intptr_t dist) {
|
|
next_pos_ += dist - 1;
|
|
Advance();
|
|
}
|
|
|
|
bool RegExpParser::simple() {
|
|
return simple_;
|
|
}
|
|
|
|
void RegExpParser::ReportError(const char* message) {
|
|
// Zip to the end to make sure the no more input is read.
|
|
current_ = kEndMarker;
|
|
next_pos_ = in().Length();
|
|
|
|
// Throw a FormatException on parsing failures.
|
|
const String& msg = String::Handle(
|
|
String::Concat(String::Handle(String::New(message)), in()));
|
|
const Array& args = Array::Handle(Array::New(1));
|
|
args.SetAt(0, msg);
|
|
Exceptions::ThrowByType(Exceptions::kFormat, args);
|
|
UNREACHABLE();
|
|
}
|
|
|
|
// Pattern ::
|
|
// Disjunction
|
|
RegExpTree* RegExpParser::ParsePattern() {
|
|
RegExpTree* result = ParseDisjunction();
|
|
PatchNamedBackReferences();
|
|
ASSERT(!has_more());
|
|
// If the result of parsing is a literal string atom, and it has the
|
|
// same length as the input, then the atom is identical to the input.
|
|
if (result->IsAtom() && result->AsAtom()->length() == in().Length()) {
|
|
simple_ = true;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
// Disjunction ::
|
|
// Alternative
|
|
// Alternative | Disjunction
|
|
// Alternative ::
|
|
// [empty]
|
|
// Term Alternative
|
|
// Term ::
|
|
// Assertion
|
|
// Atom
|
|
// Atom Quantifier
|
|
RegExpTree* RegExpParser::ParseDisjunction() {
|
|
// Used to store current state while parsing subexpressions.
|
|
RegExpParserState initial_state(nullptr, INITIAL, RegExpLookaround::LOOKAHEAD,
|
|
0, nullptr, Z);
|
|
RegExpParserState* stored_state = &initial_state;
|
|
// Cache the builder in a local variable for quick access.
|
|
RegExpBuilder* builder = initial_state.builder();
|
|
while (true) {
|
|
switch (current()) {
|
|
case kEndMarker:
|
|
if (stored_state->IsSubexpression()) {
|
|
// Inside a parenthesized group when hitting end of input.
|
|
ReportError("Unterminated group");
|
|
UNREACHABLE();
|
|
}
|
|
ASSERT(INITIAL == stored_state->group_type());
|
|
// Parsing completed successfully.
|
|
return builder->ToRegExp();
|
|
case ')': {
|
|
if (!stored_state->IsSubexpression()) {
|
|
ReportError("Unmatched ')'");
|
|
UNREACHABLE();
|
|
}
|
|
ASSERT(INITIAL != stored_state->group_type());
|
|
|
|
Advance();
|
|
// End disjunction parsing and convert builder content to new single
|
|
// regexp atom.
|
|
RegExpTree* body = builder->ToRegExp();
|
|
|
|
intptr_t end_capture_index = captures_started();
|
|
|
|
intptr_t capture_index = stored_state->capture_index();
|
|
SubexpressionType group_type = stored_state->group_type();
|
|
|
|
// Build result of subexpression.
|
|
if (group_type == CAPTURE) {
|
|
if (stored_state->IsNamedCapture()) {
|
|
CreateNamedCaptureAtIndex(stored_state->capture_name(),
|
|
capture_index);
|
|
}
|
|
RegExpCapture* capture = GetCapture(capture_index);
|
|
capture->set_body(body);
|
|
body = capture;
|
|
} else if (group_type != GROUPING) {
|
|
ASSERT(group_type == POSITIVE_LOOKAROUND ||
|
|
group_type == NEGATIVE_LOOKAROUND);
|
|
bool is_positive = (group_type == POSITIVE_LOOKAROUND);
|
|
body = new (Z) RegExpLookaround(
|
|
body, is_positive, end_capture_index - capture_index,
|
|
capture_index, stored_state->lookaround_type());
|
|
}
|
|
|
|
// Restore previous state.
|
|
stored_state = stored_state->previous_state();
|
|
builder = stored_state->builder();
|
|
|
|
builder->AddAtom(body);
|
|
// For compatibility with JSC and ES3, we allow quantifiers after
|
|
// lookaheads, and break in all cases.
|
|
break;
|
|
}
|
|
case '|': {
|
|
Advance();
|
|
builder->NewAlternative();
|
|
continue;
|
|
}
|
|
case '*':
|
|
case '+':
|
|
case '?':
|
|
ReportError("Nothing to repeat");
|
|
UNREACHABLE();
|
|
case '^': {
|
|
Advance();
|
|
if (multiline_) {
|
|
builder->AddAssertion(
|
|
new (Z) RegExpAssertion(RegExpAssertion::START_OF_LINE));
|
|
} else {
|
|
builder->AddAssertion(
|
|
new (Z) RegExpAssertion(RegExpAssertion::START_OF_INPUT));
|
|
set_contains_anchor();
|
|
}
|
|
continue;
|
|
}
|
|
case '$': {
|
|
Advance();
|
|
RegExpAssertion::AssertionType assertion_type =
|
|
multiline_ ? RegExpAssertion::END_OF_LINE
|
|
: RegExpAssertion::END_OF_INPUT;
|
|
builder->AddAssertion(new RegExpAssertion(assertion_type));
|
|
continue;
|
|
}
|
|
case '.': {
|
|
Advance();
|
|
// everything except \x0a, \x0d, \u2028 and \u2029
|
|
ZoneGrowableArray<CharacterRange>* ranges =
|
|
new ZoneGrowableArray<CharacterRange>(2);
|
|
CharacterRange::AddClassEscape('.', ranges);
|
|
RegExpTree* atom = new RegExpCharacterClass(ranges, false);
|
|
builder->AddAtom(atom);
|
|
break;
|
|
}
|
|
case '(': {
|
|
stored_state = ParseOpenParenthesis(stored_state);
|
|
builder = stored_state->builder();
|
|
continue;
|
|
}
|
|
case '[': {
|
|
RegExpTree* atom = ParseCharacterClass();
|
|
builder->AddAtom(atom);
|
|
break;
|
|
}
|
|
// Atom ::
|
|
// \ AtomEscape
|
|
case '\\':
|
|
switch (Next()) {
|
|
case kEndMarker:
|
|
ReportError("\\ at end of pattern");
|
|
UNREACHABLE();
|
|
case 'b':
|
|
Advance(2);
|
|
builder->AddAssertion(
|
|
new RegExpAssertion(RegExpAssertion::BOUNDARY));
|
|
continue;
|
|
case 'B':
|
|
Advance(2);
|
|
builder->AddAssertion(
|
|
new RegExpAssertion(RegExpAssertion::NON_BOUNDARY));
|
|
continue;
|
|
// AtomEscape ::
|
|
// CharacterClassEscape
|
|
//
|
|
// CharacterClassEscape :: one of
|
|
// d D s S w W
|
|
case 'd':
|
|
case 'D':
|
|
case 's':
|
|
case 'S':
|
|
case 'w':
|
|
case 'W': {
|
|
uint32_t c = Next();
|
|
Advance(2);
|
|
ZoneGrowableArray<CharacterRange>* ranges =
|
|
new ZoneGrowableArray<CharacterRange>(2);
|
|
CharacterRange::AddClassEscape(c, ranges);
|
|
RegExpTree* atom = new RegExpCharacterClass(ranges, false);
|
|
builder->AddAtom(atom);
|
|
break;
|
|
}
|
|
case '1':
|
|
case '2':
|
|
case '3':
|
|
case '4':
|
|
case '5':
|
|
case '6':
|
|
case '7':
|
|
case '8':
|
|
case '9': {
|
|
intptr_t index = 0;
|
|
if (ParseBackReferenceIndex(&index)) {
|
|
if (stored_state->IsInsideCaptureGroup(index)) {
|
|
// The back reference is inside the capture group it refers to.
|
|
// Nothing can possibly have been captured yet, so we use empty
|
|
// instead. This ensures that, when checking a back reference,
|
|
// the capture registers of the referenced capture are either
|
|
// both set or both cleared.
|
|
builder->AddEmpty();
|
|
} else {
|
|
RegExpCapture* capture = GetCapture(index);
|
|
RegExpTree* atom = new RegExpBackReference(capture);
|
|
builder->AddAtom(atom);
|
|
}
|
|
break;
|
|
}
|
|
uint32_t first_digit = Next();
|
|
if (first_digit == '8' || first_digit == '9') {
|
|
// Treat as identity escape
|
|
builder->AddCharacter(first_digit);
|
|
Advance(2);
|
|
break;
|
|
}
|
|
FALL_THROUGH;
|
|
}
|
|
case '0': {
|
|
Advance();
|
|
uint32_t octal = ParseOctalLiteral();
|
|
builder->AddCharacter(octal);
|
|
break;
|
|
}
|
|
// ControlEscape :: one of
|
|
// f n r t v
|
|
case 'f':
|
|
Advance(2);
|
|
builder->AddCharacter('\f');
|
|
break;
|
|
case 'n':
|
|
Advance(2);
|
|
builder->AddCharacter('\n');
|
|
break;
|
|
case 'r':
|
|
Advance(2);
|
|
builder->AddCharacter('\r');
|
|
break;
|
|
case 't':
|
|
Advance(2);
|
|
builder->AddCharacter('\t');
|
|
break;
|
|
case 'v':
|
|
Advance(2);
|
|
builder->AddCharacter('\v');
|
|
break;
|
|
case 'c': {
|
|
Advance();
|
|
uint32_t controlLetter = Next();
|
|
// Special case if it is an ASCII letter.
|
|
// Convert lower case letters to uppercase.
|
|
uint32_t letter = controlLetter & ~('a' ^ 'A');
|
|
if (letter < 'A' || 'Z' < letter) {
|
|
// controlLetter is not in range 'A'-'Z' or 'a'-'z'.
|
|
// This is outside the specification. We match JSC in
|
|
// reading the backslash as a literal character instead
|
|
// of as starting an escape.
|
|
builder->AddCharacter('\\');
|
|
} else {
|
|
Advance(2);
|
|
builder->AddCharacter(controlLetter & 0x1f);
|
|
}
|
|
break;
|
|
}
|
|
case 'x': {
|
|
Advance(2);
|
|
uint32_t value;
|
|
if (ParseHexEscape(2, &value)) {
|
|
builder->AddCharacter(value);
|
|
} else {
|
|
builder->AddCharacter('x');
|
|
}
|
|
break;
|
|
}
|
|
case 'u': {
|
|
Advance(2);
|
|
uint32_t value;
|
|
if (ParseHexEscape(4, &value)) {
|
|
builder->AddCharacter(value);
|
|
} else {
|
|
builder->AddCharacter('u');
|
|
}
|
|
break;
|
|
}
|
|
case 'k':
|
|
// Either an identity escape or a named back-reference. The two
|
|
// interpretations are mutually exclusive: '\k' is interpreted as
|
|
// an identity escape for non-Unicode patterns without named
|
|
// capture groups, and as the beginning of a named back-reference
|
|
// in all other cases.
|
|
if (HasNamedCaptures()) {
|
|
Advance(2);
|
|
ParseNamedBackReference(builder, stored_state);
|
|
break;
|
|
}
|
|
FALL_THROUGH;
|
|
default:
|
|
// Identity escape.
|
|
builder->AddCharacter(Next());
|
|
Advance(2);
|
|
break;
|
|
}
|
|
break;
|
|
case '{': {
|
|
intptr_t dummy;
|
|
if (ParseIntervalQuantifier(&dummy, &dummy)) {
|
|
ReportError("Nothing to repeat");
|
|
UNREACHABLE();
|
|
}
|
|
FALL_THROUGH;
|
|
}
|
|
default:
|
|
builder->AddCharacter(current());
|
|
Advance();
|
|
break;
|
|
} // end switch(current())
|
|
|
|
intptr_t min;
|
|
intptr_t max;
|
|
switch (current()) {
|
|
// QuantifierPrefix ::
|
|
// *
|
|
// +
|
|
// ?
|
|
// {
|
|
case '*':
|
|
min = 0;
|
|
max = RegExpTree::kInfinity;
|
|
Advance();
|
|
break;
|
|
case '+':
|
|
min = 1;
|
|
max = RegExpTree::kInfinity;
|
|
Advance();
|
|
break;
|
|
case '?':
|
|
min = 0;
|
|
max = 1;
|
|
Advance();
|
|
break;
|
|
case '{':
|
|
if (ParseIntervalQuantifier(&min, &max)) {
|
|
if (max < min) {
|
|
ReportError("numbers out of order in {} quantifier.");
|
|
UNREACHABLE();
|
|
}
|
|
break;
|
|
} else {
|
|
continue;
|
|
}
|
|
default:
|
|
continue;
|
|
}
|
|
RegExpQuantifier::QuantifierType quantifier_type = RegExpQuantifier::GREEDY;
|
|
if (current() == '?') {
|
|
quantifier_type = RegExpQuantifier::NON_GREEDY;
|
|
Advance();
|
|
} else if (FLAG_regexp_possessive_quantifier && current() == '+') {
|
|
// FLAG_regexp_possessive_quantifier is a debug-only flag.
|
|
quantifier_type = RegExpQuantifier::POSSESSIVE;
|
|
Advance();
|
|
}
|
|
if (!builder->AddQuantifierToAtom(min, max, quantifier_type)) {
|
|
ReportError("invalid quantifier.");
|
|
UNREACHABLE();
|
|
}
|
|
}
|
|
}
|
|
|
|
#ifdef DEBUG
|
|
// Currently only used in an ASSERT.
|
|
static bool IsSpecialClassEscape(uint32_t c) {
|
|
switch (c) {
|
|
case 'd':
|
|
case 'D':
|
|
case 's':
|
|
case 'S':
|
|
case 'w':
|
|
case 'W':
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
#endif
|
|
|
|
RegExpParser::RegExpParserState* RegExpParser::ParseOpenParenthesis(
|
|
RegExpParserState* state) {
|
|
RegExpLookaround::Type lookaround_type = state->lookaround_type();
|
|
bool is_named_capture = false;
|
|
const RegExpCaptureName* capture_name = nullptr;
|
|
SubexpressionType subexpr_type = CAPTURE;
|
|
Advance();
|
|
if (current() == '?') {
|
|
switch (Next()) {
|
|
case ':':
|
|
Advance(2);
|
|
subexpr_type = GROUPING;
|
|
break;
|
|
case '=':
|
|
Advance(2);
|
|
lookaround_type = RegExpLookaround::LOOKAHEAD;
|
|
subexpr_type = POSITIVE_LOOKAROUND;
|
|
break;
|
|
case '!':
|
|
Advance(2);
|
|
lookaround_type = RegExpLookaround::LOOKAHEAD;
|
|
subexpr_type = NEGATIVE_LOOKAROUND;
|
|
break;
|
|
case '<':
|
|
Advance();
|
|
if (Next() == '=') {
|
|
Advance(2);
|
|
lookaround_type = RegExpLookaround::LOOKBEHIND;
|
|
subexpr_type = POSITIVE_LOOKAROUND;
|
|
break;
|
|
} else if (Next() == '!') {
|
|
Advance(2);
|
|
lookaround_type = RegExpLookaround::LOOKBEHIND;
|
|
subexpr_type = NEGATIVE_LOOKAROUND;
|
|
break;
|
|
}
|
|
is_named_capture = true;
|
|
has_named_captures_ = true;
|
|
Advance();
|
|
break;
|
|
default:
|
|
ReportError("Invalid group");
|
|
UNREACHABLE();
|
|
}
|
|
}
|
|
|
|
if (subexpr_type == CAPTURE) {
|
|
if (captures_started_ >= kMaxCaptures) {
|
|
ReportError("Too many captures");
|
|
UNREACHABLE();
|
|
}
|
|
captures_started_++;
|
|
|
|
if (is_named_capture) {
|
|
capture_name = ParseCaptureGroupName();
|
|
}
|
|
}
|
|
// Store current state and begin new disjunction parsing.
|
|
return new RegExpParserState(state, subexpr_type, lookaround_type,
|
|
captures_started_, capture_name, Z);
|
|
}
|
|
|
|
// In order to know whether an escape is a backreference or not we have to scan
|
|
// the entire regexp and find the number of capturing parentheses. However we
|
|
// don't want to scan the regexp twice unless it is necessary. This mini-parser
|
|
// is called when needed. It can see the difference between capturing and
|
|
// noncapturing parentheses and can skip character classes and backslash-escaped
|
|
// characters.
|
|
void RegExpParser::ScanForCaptures() {
|
|
ASSERT(!is_scanned_for_captures_);
|
|
const intptr_t saved_position = position();
|
|
// Start with captures started previous to current position
|
|
intptr_t capture_count = captures_started();
|
|
// Add count of captures after this position.
|
|
uintptr_t n;
|
|
while ((n = current()) != kEndMarker) {
|
|
Advance();
|
|
switch (n) {
|
|
case '\\':
|
|
Advance();
|
|
break;
|
|
case '[': {
|
|
uintptr_t c;
|
|
while ((c = current()) != kEndMarker) {
|
|
Advance();
|
|
if (c == '\\') {
|
|
Advance();
|
|
} else {
|
|
if (c == ']') break;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case '(':
|
|
// At this point we could be in
|
|
// * a non-capturing group '(:',
|
|
// * a lookbehind assertion '(?<=' '(?<!'
|
|
// * or a named capture '(?<'.
|
|
//
|
|
// Of these, only named captures are capturing groups.
|
|
if (current() == '?') {
|
|
Advance();
|
|
if (current() != '<') break;
|
|
|
|
Advance();
|
|
if (current() == '=' || current() == '!') break;
|
|
|
|
// Found a possible named capture. It could turn out to be a syntax
|
|
// error (e.g. an unterminated or invalid name), but that distinction
|
|
// does not matter for our purposes.
|
|
has_named_captures_ = true;
|
|
}
|
|
capture_count++;
|
|
break;
|
|
}
|
|
}
|
|
capture_count_ = capture_count;
|
|
is_scanned_for_captures_ = true;
|
|
Reset(saved_position);
|
|
}
|
|
|
|
static inline bool IsDecimalDigit(int32_t c) {
|
|
return '0' <= c && c <= '9';
|
|
}
|
|
|
|
bool RegExpParser::ParseBackReferenceIndex(intptr_t* index_out) {
|
|
ASSERT('\\' == current());
|
|
ASSERT('1' <= Next() && Next() <= '9');
|
|
// Try to parse a decimal literal that is no greater than the total number
|
|
// of left capturing parentheses in the input.
|
|
intptr_t start = position();
|
|
intptr_t value = Next() - '0';
|
|
Advance(2);
|
|
while (true) {
|
|
uint32_t c = current();
|
|
if (IsDecimalDigit(c)) {
|
|
value = 10 * value + (c - '0');
|
|
if (value > kMaxCaptures) {
|
|
Reset(start);
|
|
return false;
|
|
}
|
|
Advance();
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
if (value > captures_started()) {
|
|
if (!is_scanned_for_captures_) ScanForCaptures();
|
|
if (value > capture_count_) {
|
|
Reset(start);
|
|
return false;
|
|
}
|
|
}
|
|
*index_out = value;
|
|
return true;
|
|
}
|
|
|
|
namespace {
|
|
|
|
inline constexpr bool IsIdentifierStart(uint16_t ch) {
|
|
return (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') || ch == '_' ||
|
|
ch == '$';
|
|
}
|
|
|
|
inline constexpr bool IsIdentifierPart(uint16_t ch) {
|
|
return IsIdentifierStart(ch) || (ch >= '0' && ch <= '9');
|
|
}
|
|
|
|
bool IsSameName(const RegExpCaptureName* name1,
|
|
const RegExpCaptureName* name2) {
|
|
if (name1->length() != name2->length()) return false;
|
|
for (intptr_t i = 0; i < name1->length(); i++) {
|
|
if (name1->At(i) != name2->At(i)) return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
} // end namespace
|
|
|
|
const RegExpCaptureName* RegExpParser::ParseCaptureGroupName() {
|
|
auto name = new (Z) RegExpCaptureName();
|
|
|
|
bool at_start = true;
|
|
while (true) {
|
|
const uint16_t c = current();
|
|
Advance();
|
|
|
|
// The backslash char is misclassified as both ID_Start and ID_Continue.
|
|
if (c == '\\') {
|
|
ReportError("Invalid capture group name");
|
|
UNREACHABLE();
|
|
}
|
|
|
|
if (at_start) {
|
|
if (!IsIdentifierStart(c)) {
|
|
ReportError("Invalid capture group name");
|
|
UNREACHABLE();
|
|
}
|
|
name->Add(c);
|
|
at_start = false;
|
|
} else {
|
|
if (c == '>') {
|
|
break;
|
|
} else if (IsIdentifierPart(c)) {
|
|
name->Add(c);
|
|
} else {
|
|
ReportError("Invalid capture group name");
|
|
UNREACHABLE();
|
|
}
|
|
}
|
|
}
|
|
|
|
return name;
|
|
}
|
|
|
|
intptr_t RegExpParser::GetNamedCaptureIndex(const RegExpCaptureName* name) {
|
|
for (const auto& capture : *named_captures_) {
|
|
if (IsSameName(name, capture->name())) return capture->index();
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
void RegExpParser::CreateNamedCaptureAtIndex(const RegExpCaptureName* name,
|
|
intptr_t index) {
|
|
ASSERT(0 < index && index <= captures_started_);
|
|
ASSERT(name != nullptr);
|
|
|
|
if (named_captures_ == nullptr) {
|
|
named_captures_ = new (Z) ZoneGrowableArray<RegExpCapture*>(1);
|
|
} else {
|
|
// Check for duplicates and bail if we find any. Currently O(n^2).
|
|
if (GetNamedCaptureIndex(name) >= 0) {
|
|
ReportError("Duplicate capture group name");
|
|
UNREACHABLE();
|
|
}
|
|
}
|
|
|
|
RegExpCapture* capture = GetCapture(index);
|
|
ASSERT(capture->name() == nullptr);
|
|
|
|
capture->set_name(name);
|
|
named_captures_->Add(capture);
|
|
}
|
|
|
|
bool RegExpParser::ParseNamedBackReference(RegExpBuilder* builder,
|
|
RegExpParserState* state) {
|
|
// The parser is assumed to be on the '<' in \k<name>.
|
|
if (current() != '<') {
|
|
ReportError("Invalid named reference");
|
|
UNREACHABLE();
|
|
}
|
|
|
|
Advance();
|
|
const RegExpCaptureName* name = ParseCaptureGroupName();
|
|
if (name == nullptr) {
|
|
return false;
|
|
}
|
|
|
|
if (state->IsInsideCaptureGroup(name)) {
|
|
builder->AddEmpty();
|
|
} else {
|
|
RegExpBackReference* atom = new (Z) RegExpBackReference();
|
|
atom->set_name(name);
|
|
|
|
builder->AddAtom(atom);
|
|
|
|
if (named_back_references_ == nullptr) {
|
|
named_back_references_ =
|
|
new (Z) ZoneGrowableArray<RegExpBackReference*>(1);
|
|
}
|
|
named_back_references_->Add(atom);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void RegExpParser::PatchNamedBackReferences() {
|
|
if (named_back_references_ == nullptr) return;
|
|
|
|
if (named_captures_ == nullptr) {
|
|
ReportError("Invalid named capture referenced");
|
|
return;
|
|
}
|
|
|
|
// Look up and patch the actual capture for each named back reference.
|
|
// Currently O(n^2), optimize if necessary.
|
|
for (intptr_t i = 0; i < named_back_references_->length(); i++) {
|
|
RegExpBackReference* ref = named_back_references_->At(i);
|
|
intptr_t index = GetNamedCaptureIndex(ref->name());
|
|
|
|
if (index < 0) {
|
|
ReportError("Invalid named capture referenced");
|
|
UNREACHABLE();
|
|
}
|
|
ref->set_capture(GetCapture(index));
|
|
}
|
|
}
|
|
|
|
RegExpCapture* RegExpParser::GetCapture(intptr_t index) {
|
|
// The index for the capture groups are one-based. Its index in the list is
|
|
// zero-based.
|
|
const intptr_t know_captures =
|
|
is_scanned_for_captures_ ? capture_count_ : captures_started_;
|
|
ASSERT(index <= know_captures);
|
|
if (captures_ == nullptr) {
|
|
captures_ = new (Z) ZoneGrowableArray<RegExpCapture*>(know_captures);
|
|
}
|
|
while (captures_->length() < know_captures) {
|
|
captures_->Add(new (Z) RegExpCapture(captures_->length() + 1));
|
|
}
|
|
return captures_->At(index - 1);
|
|
}
|
|
|
|
RawArray* RegExpParser::CreateCaptureNameMap() {
|
|
if (named_captures_ == nullptr || named_captures_->is_empty()) {
|
|
return Array::null();
|
|
}
|
|
|
|
const intptr_t len = named_captures_->length() * 2;
|
|
|
|
const Array& array = Array::Handle(Array::New(len));
|
|
|
|
auto& name = String::Handle();
|
|
auto& smi = Smi::Handle();
|
|
for (intptr_t i = 0; i < named_captures_->length(); i++) {
|
|
RegExpCapture* capture = named_captures_->At(i);
|
|
name =
|
|
String::FromUTF16(capture->name()->data(), capture->name()->length());
|
|
smi = Smi::New(capture->index());
|
|
array.SetAt(i * 2, name);
|
|
array.SetAt(i * 2 + 1, smi);
|
|
}
|
|
|
|
return array.raw();
|
|
}
|
|
|
|
bool RegExpParser::HasNamedCaptures() {
|
|
if (has_named_captures_ || is_scanned_for_captures_) {
|
|
return has_named_captures_;
|
|
}
|
|
|
|
ScanForCaptures();
|
|
ASSERT(is_scanned_for_captures_);
|
|
return has_named_captures_;
|
|
}
|
|
|
|
bool RegExpParser::RegExpParserState::IsInsideCaptureGroup(intptr_t index) {
|
|
for (RegExpParserState* s = this; s != nullptr; s = s->previous_state()) {
|
|
if (s->group_type() != CAPTURE) continue;
|
|
// Return true if we found the matching capture index.
|
|
if (index == s->capture_index()) return true;
|
|
// Abort if index is larger than what has been parsed up till this state.
|
|
if (index > s->capture_index()) return false;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool RegExpParser::RegExpParserState::IsInsideCaptureGroup(
|
|
const RegExpCaptureName* name) {
|
|
ASSERT(name != nullptr);
|
|
for (RegExpParserState* s = this; s != nullptr; s = s->previous_state()) {
|
|
if (s->capture_name() == nullptr) continue;
|
|
if (IsSameName(s->capture_name(), name)) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// QuantifierPrefix ::
|
|
// { DecimalDigits }
|
|
// { DecimalDigits , }
|
|
// { DecimalDigits , DecimalDigits }
|
|
//
|
|
// Returns true if parsing succeeds, and set the min_out and max_out
|
|
// values. Values are truncated to RegExpTree::kInfinity if they overflow.
|
|
bool RegExpParser::ParseIntervalQuantifier(intptr_t* min_out,
|
|
intptr_t* max_out) {
|
|
ASSERT(current() == '{');
|
|
intptr_t start = position();
|
|
Advance();
|
|
intptr_t min = 0;
|
|
if (!IsDecimalDigit(current())) {
|
|
Reset(start);
|
|
return false;
|
|
}
|
|
while (IsDecimalDigit(current())) {
|
|
intptr_t next = current() - '0';
|
|
if (min > (RegExpTree::kInfinity - next) / 10) {
|
|
// Overflow. Skip past remaining decimal digits and return -1.
|
|
do {
|
|
Advance();
|
|
} while (IsDecimalDigit(current()));
|
|
min = RegExpTree::kInfinity;
|
|
break;
|
|
}
|
|
min = 10 * min + next;
|
|
Advance();
|
|
}
|
|
intptr_t max = 0;
|
|
if (current() == '}') {
|
|
max = min;
|
|
Advance();
|
|
} else if (current() == ',') {
|
|
Advance();
|
|
if (current() == '}') {
|
|
max = RegExpTree::kInfinity;
|
|
Advance();
|
|
} else {
|
|
while (IsDecimalDigit(current())) {
|
|
intptr_t next = current() - '0';
|
|
if (max > (RegExpTree::kInfinity - next) / 10) {
|
|
do {
|
|
Advance();
|
|
} while (IsDecimalDigit(current()));
|
|
max = RegExpTree::kInfinity;
|
|
break;
|
|
}
|
|
max = 10 * max + next;
|
|
Advance();
|
|
}
|
|
if (current() != '}') {
|
|
Reset(start);
|
|
return false;
|
|
}
|
|
Advance();
|
|
}
|
|
} else {
|
|
Reset(start);
|
|
return false;
|
|
}
|
|
*min_out = min;
|
|
*max_out = max;
|
|
return true;
|
|
}
|
|
|
|
uint32_t RegExpParser::ParseOctalLiteral() {
|
|
ASSERT(('0' <= current() && current() <= '7') || current() == kEndMarker);
|
|
// For compatibility with some other browsers (not all), we parse
|
|
// up to three octal digits with a value below 256.
|
|
uint32_t value = current() - '0';
|
|
Advance();
|
|
if ('0' <= current() && current() <= '7') {
|
|
value = value * 8 + current() - '0';
|
|
Advance();
|
|
if (value < 32 && '0' <= current() && current() <= '7') {
|
|
value = value * 8 + current() - '0';
|
|
Advance();
|
|
}
|
|
}
|
|
return value;
|
|
}
|
|
|
|
// Returns the value (0 .. 15) of a hexadecimal character c.
|
|
// If c is not a legal hexadecimal character, returns a value < 0.
|
|
static inline intptr_t HexValue(uint32_t c) {
|
|
c -= '0';
|
|
if (static_cast<unsigned>(c) <= 9) return c;
|
|
c = (c | 0x20) - ('a' - '0'); // detect 0x11..0x16 and 0x31..0x36.
|
|
if (static_cast<unsigned>(c) <= 5) return c + 10;
|
|
return -1;
|
|
}
|
|
|
|
bool RegExpParser::ParseHexEscape(intptr_t length, uint32_t* value) {
|
|
intptr_t start = position();
|
|
uint32_t val = 0;
|
|
bool done = false;
|
|
for (intptr_t i = 0; !done; i++) {
|
|
uint32_t c = current();
|
|
intptr_t d = HexValue(c);
|
|
if (d < 0) {
|
|
Reset(start);
|
|
return false;
|
|
}
|
|
val = val * 16 + d;
|
|
Advance();
|
|
if (i == length - 1) {
|
|
done = true;
|
|
}
|
|
}
|
|
*value = val;
|
|
return true;
|
|
}
|
|
|
|
uint32_t RegExpParser::ParseClassCharacterEscape() {
|
|
ASSERT(current() == '\\');
|
|
DEBUG_ASSERT(has_next() && !IsSpecialClassEscape(Next()));
|
|
Advance();
|
|
switch (current()) {
|
|
case 'b':
|
|
Advance();
|
|
return '\b';
|
|
// ControlEscape :: one of
|
|
// f n r t v
|
|
case 'f':
|
|
Advance();
|
|
return '\f';
|
|
case 'n':
|
|
Advance();
|
|
return '\n';
|
|
case 'r':
|
|
Advance();
|
|
return '\r';
|
|
case 't':
|
|
Advance();
|
|
return '\t';
|
|
case 'v':
|
|
Advance();
|
|
return '\v';
|
|
case 'c': {
|
|
uint32_t controlLetter = Next();
|
|
uint32_t letter = controlLetter & ~('A' ^ 'a');
|
|
// For compatibility with JSC, inside a character class
|
|
// we also accept digits and underscore as control characters.
|
|
if ((controlLetter >= '0' && controlLetter <= '9') ||
|
|
controlLetter == '_' || (letter >= 'A' && letter <= 'Z')) {
|
|
Advance(2);
|
|
// Control letters mapped to ASCII control characters in the range
|
|
// 0x00-0x1f.
|
|
return controlLetter & 0x1f;
|
|
}
|
|
// We match JSC in reading the backslash as a literal
|
|
// character instead of as starting an escape.
|
|
return '\\';
|
|
}
|
|
case '0':
|
|
FALL_THROUGH;
|
|
case '1':
|
|
FALL_THROUGH;
|
|
case '2':
|
|
FALL_THROUGH;
|
|
case '3':
|
|
FALL_THROUGH;
|
|
case '4':
|
|
FALL_THROUGH;
|
|
case '5':
|
|
FALL_THROUGH;
|
|
case '6':
|
|
FALL_THROUGH;
|
|
case '7':
|
|
// For compatibility, we interpret a decimal escape that isn't
|
|
// a back reference (and therefore either \0 or not valid according
|
|
// to the specification) as a 1..3 digit octal character code.
|
|
return ParseOctalLiteral();
|
|
case 'x': {
|
|
Advance();
|
|
uint32_t value;
|
|
if (ParseHexEscape(2, &value)) {
|
|
return value;
|
|
}
|
|
// If \x is not followed by a two-digit hexadecimal, treat it
|
|
// as an identity escape.
|
|
return 'x';
|
|
}
|
|
case 'u': {
|
|
Advance();
|
|
uint32_t value;
|
|
if (ParseHexEscape(4, &value)) {
|
|
return value;
|
|
}
|
|
// If \u is not followed by a four-digit hexadecimal, treat it
|
|
// as an identity escape.
|
|
return 'u';
|
|
}
|
|
default: {
|
|
// Extended identity escape. We accept any character that hasn't
|
|
// been matched by a more specific case, not just the subset required
|
|
// by the ECMAScript specification.
|
|
uint32_t result = current();
|
|
Advance();
|
|
return result;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
CharacterRange RegExpParser::ParseClassAtom(uint16_t* char_class) {
|
|
ASSERT(0 == *char_class);
|
|
uint32_t first = current();
|
|
if (first == '\\') {
|
|
switch (Next()) {
|
|
case 'w':
|
|
case 'W':
|
|
case 'd':
|
|
case 'D':
|
|
case 's':
|
|
case 'S': {
|
|
*char_class = Next();
|
|
Advance(2);
|
|
return CharacterRange::Singleton(0); // Return dummy value.
|
|
}
|
|
case kEndMarker:
|
|
ReportError("\\ at end of pattern");
|
|
UNREACHABLE();
|
|
default:
|
|
uint32_t c = ParseClassCharacterEscape();
|
|
return CharacterRange::Singleton(c);
|
|
}
|
|
} else {
|
|
Advance();
|
|
return CharacterRange::Singleton(first);
|
|
}
|
|
}
|
|
|
|
static const uint16_t kNoCharClass = 0;
|
|
|
|
// Adds range or pre-defined character class to character ranges.
|
|
// If char_class is not kInvalidClass, it's interpreted as a class
|
|
// escape (i.e., 's' means whitespace, from '\s').
|
|
static inline void AddRangeOrEscape(ZoneGrowableArray<CharacterRange>* ranges,
|
|
uint16_t char_class,
|
|
CharacterRange range) {
|
|
if (char_class != kNoCharClass) {
|
|
CharacterRange::AddClassEscape(char_class, ranges);
|
|
} else {
|
|
ranges->Add(range);
|
|
}
|
|
}
|
|
|
|
RegExpTree* RegExpParser::ParseCharacterClass() {
|
|
static const char* kUnterminated = "Unterminated character class";
|
|
static const char* kRangeOutOfOrder = "Range out of order in character class";
|
|
|
|
ASSERT(current() == '[');
|
|
Advance();
|
|
bool is_negated = false;
|
|
if (current() == '^') {
|
|
is_negated = true;
|
|
Advance();
|
|
}
|
|
ZoneGrowableArray<CharacterRange>* ranges =
|
|
new (Z) ZoneGrowableArray<CharacterRange>(2);
|
|
while (has_more() && current() != ']') {
|
|
uint16_t char_class = kNoCharClass;
|
|
CharacterRange first = ParseClassAtom(&char_class);
|
|
if (current() == '-') {
|
|
Advance();
|
|
if (current() == kEndMarker) {
|
|
// If we reach the end we break out of the loop and let the
|
|
// following code report an error.
|
|
break;
|
|
} else if (current() == ']') {
|
|
AddRangeOrEscape(ranges, char_class, first);
|
|
ranges->Add(CharacterRange::Singleton('-'));
|
|
break;
|
|
}
|
|
uint16_t char_class_2 = kNoCharClass;
|
|
CharacterRange next = ParseClassAtom(&char_class_2);
|
|
if (char_class != kNoCharClass || char_class_2 != kNoCharClass) {
|
|
// Either end is an escaped character class. Treat the '-' verbatim.
|
|
AddRangeOrEscape(ranges, char_class, first);
|
|
ranges->Add(CharacterRange::Singleton('-'));
|
|
AddRangeOrEscape(ranges, char_class_2, next);
|
|
continue;
|
|
}
|
|
if (first.from() > next.to()) {
|
|
ReportError(kRangeOutOfOrder);
|
|
UNREACHABLE();
|
|
}
|
|
ranges->Add(CharacterRange::Range(first.from(), next.to()));
|
|
} else {
|
|
AddRangeOrEscape(ranges, char_class, first);
|
|
}
|
|
}
|
|
if (!has_more()) {
|
|
ReportError(kUnterminated);
|
|
UNREACHABLE();
|
|
}
|
|
Advance();
|
|
if (ranges->length() == 0) {
|
|
ranges->Add(CharacterRange::Everything());
|
|
is_negated = !is_negated;
|
|
}
|
|
return new (Z) RegExpCharacterClass(ranges, is_negated);
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// The Parser interface.
|
|
|
|
void RegExpParser::ParseRegExp(const String& input,
|
|
bool multiline,
|
|
RegExpCompileData* result) {
|
|
ASSERT(result != NULL);
|
|
RegExpParser parser(input, &result->error, multiline);
|
|
// Throws an exception if 'input' is not valid.
|
|
RegExpTree* tree = parser.ParsePattern();
|
|
ASSERT(tree != NULL);
|
|
ASSERT(result->error.IsNull());
|
|
result->tree = tree;
|
|
intptr_t capture_count = parser.captures_started();
|
|
result->simple = tree->IsAtom() && parser.simple() && capture_count == 0;
|
|
result->contains_anchor = parser.contains_anchor();
|
|
result->capture_name_map = parser.CreateCaptureNameMap();
|
|
result->capture_count = capture_count;
|
|
}
|
|
|
|
} // namespace dart
|