diff --git a/cpp/ql/lib/change-notes/2026-07-21-cpp-regex-tree-view.md b/cpp/ql/lib/change-notes/2026-07-21-cpp-regex-tree-view.md new file mode 100644 index 000000000000..624c9e50c2de --- /dev/null +++ b/cpp/ql/lib/change-notes/2026-07-21-cpp-regex-tree-view.md @@ -0,0 +1,4 @@ +--- +category: minorAnalysis +--- +* Added an internal C++ regular-expression parse library and tree view targeting the ECMAScript grammar (the default for `std::regex`). No queries are added in this change. diff --git a/cpp/ql/lib/qlpack.yml b/cpp/ql/lib/qlpack.yml index 5f4b92a191fc..775e8c717291 100644 --- a/cpp/ql/lib/qlpack.yml +++ b/cpp/ql/lib/qlpack.yml @@ -10,6 +10,7 @@ dependencies: codeql/mad: ${workspace} codeql/quantum: ${workspace} codeql/rangeanalysis: ${workspace} + codeql/regex: ${workspace} codeql/ssa: ${workspace} codeql/typeflow: ${workspace} codeql/tutorial: ${workspace} diff --git a/cpp/ql/lib/semmle/code/cpp/regex/RegexTreeView.qll b/cpp/ql/lib/semmle/code/cpp/regex/RegexTreeView.qll new file mode 100644 index 000000000000..cec907476a7d --- /dev/null +++ b/cpp/ql/lib/semmle/code/cpp/regex/RegexTreeView.qll @@ -0,0 +1,1258 @@ +/** + * Provides a class hierarchy corresponding to a parse tree of C++ regular expressions. + */ + +private import internal.ParseRegExp +private import codeql.util.Numbers +private import semmle.code.cpp.exprs.Literal +private import codeql.regex.RegexTreeView +// exporting as RegexTreeView, and in the top-level scope. +import Impl as RegexTreeView +import Impl + +/** Gets the parse tree resulting from parsing `re`, if such has been constructed. */ +RegExpTerm getParsedRegExp(StringLiteral re) { + result.getRegExp() = re and result.isRootTerm() +} + +/** + * An element containing a regular expression term, that is, either + * a string literal (parsed as a regular expression) + * or another regular expression term. + * + * For sequences and alternations, we require at least one child. + * Otherwise, we wish to represent the term differently. + * This avoids multiple representations of the same term. + */ +private newtype TRegExpParent = + /** A string literal used as a regular expression */ + TRegExpLiteral(RegExp re) or + /** A quantified term */ + TRegExpQuantifier(RegExp re, int start, int end) { re.qualifiedItem(start, end, _, _) } or + /** A sequence term */ + TRegExpSequence(RegExp re, int start, int end) { re.sequence(start, end) } or + /** An alternation term */ + TRegExpAlt(RegExp re, int start, int end) { re.alternation(start, end) } or + /** A character class term */ + TRegExpCharacterClass(RegExp re, int start, int end) { re.charSet(start, end) } or + /** A character range term */ + TRegExpCharacterRange(RegExp re, int start, int end) { re.charRange(_, start, _, _, end) } or + /** A group term */ + TRegExpGroup(RegExp re, int start, int end) { re.group(start, end) } or + /** A special character */ + TRegExpSpecialChar(RegExp re, int start, int end) { re.specialCharacter(start, end, _) } or + /** A normal character */ + TRegExpNormalChar(RegExp re, int start, int end) { + re.normalCharacterSequence(start, end) + or + re.escapedCharacter(start, end) and + not re.specialCharacter(start, end, _) + } or + /** A back reference */ + TRegExpBackRef(RegExp re, int start, int end) { re.backreference(start, end) } or + /** A named character property */ + TRegExpNamedCharacterProperty(RegExp re, int start, int end) { + re.namedCharacterProperty(start, end, _) + } + +/** An implementation that satisfies the RegexTreeView signature. */ +private module Impl implements RegexTreeViewSig { + /** + * An element containing a regular expression term, that is, either + * a string literal (parsed as a regular expression) + * or another regular expression term. + */ + class RegExpParent extends TRegExpParent { + /** Gets a textual representation of this element. */ + string toString() { result = "RegExpParent" } + + /** Gets the `i`th child term. */ + RegExpTerm getChild(int i) { none() } + + /** Gets a child term . */ + final RegExpTerm getAChild() { result = this.getChild(_) } + + /** Gets the number of child terms. */ + int getNumChild() { result = count(this.getAChild()) } + + /** Gets the last child term of this element. */ + RegExpTerm getLastChild() { result = this.getChild(this.getNumChild() - 1) } + + /** + * Gets the name of a primary CodeQL class to which this regular + * expression term belongs. + */ + string getAPrimaryQlClass() { result = "RegExpParent" } + + /** + * Gets a comma-separated list of the names of the primary CodeQL classes to + * which this regular expression term belongs. + */ + final string getPrimaryQlClasses() { result = concat(this.getAPrimaryQlClass(), ",") } + } + + /** A string literal used as a regular expression */ + class RegExpLiteral extends TRegExpLiteral, RegExpParent { + RegExp re; + + RegExpLiteral() { this = TRegExpLiteral(re) } + + override RegExpTerm getChild(int i) { + i = 0 and result.getRegExp() = re and result.isRootTerm() + } + + /** Holds if dot, `.`, matches all characters, including newlines. */ + predicate isDotAll() { re.isDotAll() } + + /** Holds if this regex matching is case-insensitive for this regex. */ + predicate isIgnoreCase() { re.isIgnoreCase() } + + /** Get a string representing all modes for this regex. */ + string getFlags() { result = re.getFlags() } + + /** Gets the primary QL class for this regex. */ + override string getAPrimaryQlClass() { result = "RegExpLiteral" } + } + + /** + * Gets the number of source characters from the start of the string literal `re` + * to the first content character (i.e., past the opening delimiter). + * + * This is derived from the raw source spelling via `StringLiteral.getValueText()`: + * - Plain `"..."`: offset 1 (just the `"`) + * - Encoding prefix `L"..."`: offset 2 (`L"`) + * - Encoding prefix `u8"..."`: offset 3 (`u8"`) + * - Raw `R"(...)"` offset 3 (`R"(`) + * - Custom-delim raw `R"x(...)x"` offset 4 (`R"x(` — delim length 1) + * - Combined `LR"(...)"` offset 4 (`LR"(`) + * + * For non-raw strings, the value-offset maps 1:1 to source columns only when + * there are no escape sequences; escaped non-raw strings are approximate + * (mirroring Java/Python regex libraries). + */ + private int regexpContentOffset(RegExp re) { + exists(string vt | vt = re.getValueText() | + // Raw string: find the '(' that opens the raw content. + // getValueText() for raw strings looks like: [prefix]R"[delim](...)[delim]" + // The opening '(' is after the optional prefix, 'R', '"', and custom delimiter. + vt.matches("%R\"%(%") and + result = 1 + min(int i | vt.charAt(i) = "(" and i >= 1) + or + // Non-raw string: find the opening '"'. + not vt.matches("%R\"%(%") and + result = 1 + min(int i | vt.charAt(i) = "\"") + ) + } + + /** + * A regular expression term, that is, a syntactic part of a regular expression. + */ + class RegExpTerm extends RegExpParent { + RegExp re; + int start; + int end; + + RegExpTerm() { + this = TRegExpAlt(re, start, end) + or + this = TRegExpBackRef(re, start, end) + or + this = TRegExpCharacterClass(re, start, end) + or + this = TRegExpCharacterRange(re, start, end) + or + this = TRegExpNormalChar(re, start, end) + or + this = TRegExpGroup(re, start, end) + or + this = TRegExpQuantifier(re, start, end) + or + this = TRegExpSequence(re, start, end) and + exists(seqChild(re, start, end, 1)) // if a sequence does not have more than one element, it should be treated as that element instead. + or + this = TRegExpSpecialChar(re, start, end) + or + this = TRegExpNamedCharacterProperty(re, start, end) + } + + /** + * Gets the outermost term of this regular expression. + */ + RegExpTerm getRootTerm() { + this.isRootTerm() and result = this + or + result = this.getParent().(RegExpTerm).getRootTerm() + } + + /** + * Holds if this term is part of a string literal + * that is interpreted as a regular expression. + */ + predicate isUsedAsRegExp() { any() } + + /** + * Holds if this is the root term of a regular expression. + */ + predicate isRootTerm() { start = 0 and end = re.getText().length() } + + override RegExpTerm getChild(int i) { + result = this.(RegExpAlt).getChild(i) + or + result = this.(RegExpBackRef).getChild(i) + or + result = this.(RegExpCharacterClass).getChild(i) + or + result = this.(RegExpCharacterRange).getChild(i) + or + result = this.(RegExpNormalChar).getChild(i) + or + result = this.(RegExpGroup).getChild(i) + or + result = this.(RegExpQuantifier).getChild(i) + or + result = this.(RegExpSequence).getChild(i) + or + result = this.(RegExpSpecialChar).getChild(i) + or + result = this.(RegExpNamedCharacterProperty).getChild(i) + } + + /** + * Gets the parent term of this regular expression term, or the + * regular expression literal if this is the root term. + */ + RegExpParent getParent() { result.getAChild() = this } + + /** Gets the associated `RegExp`. */ + RegExp getRegExp() { result = re } + + /** Gets the offset at which this term starts. */ + int getStart() { result = start } + + /** Gets the offset at which this term ends. */ + int getEnd() { result = end } + + override string toString() { result = re.getText().substring(start, end) } + + /** + * Gets the location of the surrounding regex, as locations inside the regex do not exist. + * To get location information corresponding to the term inside the regex, + * use `hasLocationInfo`. + */ + Location getLocation() { result = re.getLocation() } + + /** + * Holds if this term is found at the specified location offsets. + * + * The content offset is computed from `StringLiteral.getValueText()` (the raw source + * spelling including delimiters), so it is correct for: + * - Plain `"..."`: offset 1 (`"`) + * - Encoding prefixes `L"..."`: offset 2 (`L"`) + * - Encoding prefix `u8"..."`: offset 3 (`u8"`) + * - Raw `R"(...)"` offset 3 (`R"(`) + * - Custom-delim raw `R"x(...)x"`: offset 4+len(delim) (`R"` + delim + `(`) + * - Combined, e.g. `LR"(...)"` offset 4 (`LR"(`) + * + * Note: for non-raw strings, the value-string offset equals the source-column offset + * only when there are no escape sequences. Escaped characters are an approximation, + * mirroring the Java/Python regex libraries. + */ + predicate hasLocationInfo( + string filepath, int startline, int startcolumn, int endline, int endcolumn + ) { + exists(int re_start, int offset | + re.getLocation().hasLocationInfo(filepath, startline, re_start, endline, _) and + offset = regexpContentOffset(re) and + startcolumn = re_start + offset + start and + endcolumn = re_start + offset + end - 1 + ) + } + + /** Gets the file in which this term is found. */ + File getFile() { result = this.getLocation().getFile() } + + /** Gets the raw source text of this term. */ + string getRawValue() { result = this.toString() } + + /** Gets the string literal in which this term is found. */ + RegExpLiteral getLiteral() { result = TRegExpLiteral(re) } + + /** Gets the regular expression term that is matched (textually) before this one, if any. */ + RegExpTerm getPredecessor() { + exists(RegExpTerm parent | parent = this.getParent() | + result = parent.(RegExpSequence).previousElement(this) + or + not exists(parent.(RegExpSequence).previousElement(this)) and + not parent instanceof RegExpSubPattern and + result = parent.getPredecessor() + ) + } + + /** Gets the regular expression term that is matched (textually) after this one, if any. */ + RegExpTerm getSuccessor() { + exists(RegExpTerm parent | parent = this.getParent() | + result = parent.(RegExpSequence).nextElement(this) + or + not exists(parent.(RegExpSequence).nextElement(this)) and + not parent instanceof RegExpSubPattern and + result = parent.getSuccessor() + ) + } + + /** + * Gets the single string this regular-expression term matches. + * + * This predicate is only defined for (sequences/groups of) constant regular + * expressions. In particular, terms involving zero-width assertions like `^` + * or `\b` are not considered to have a constant value. + * + * Note that this predicate does not take flags of the enclosing + * regular-expression literal into account. + */ + string getConstantValue() { none() } + + /** + * Gets a string that is matched by this regular-expression term. + */ + string getAMatchedString() { result = this.getConstantValue() } + + /** Gets the primary QL class for this term. */ + override string getAPrimaryQlClass() { result = "RegExpTerm" } + + /** Holds if this regular expression term can match the empty string. */ + predicate isNullable() { none() } + } + + /** + * A quantified regular expression term. + * + * Example: + * + * ``` + * ((ECMA|Java)[sS]cript)* + * ``` + */ + class RegExpQuantifier extends RegExpTerm, TRegExpQuantifier { + int part_end; + boolean may_repeat_forever; + + RegExpQuantifier() { + this = TRegExpQuantifier(re, start, end) and + re.qualifiedPart(start, part_end, end, _, may_repeat_forever) + } + + override RegExpTerm getChild(int i) { + i = 0 and + result.getRegExp() = re and + result.getStart() = start and + result.getEnd() = part_end + } + + /** Holds if this term may match an unlimited number of times. */ + predicate mayRepeatForever() { may_repeat_forever = true } + + /** Gets the qualifier for this term. That is e.g "?" for "a?". */ + string getQualifier() { result = re.getText().substring(part_end, end) } + + override string getAPrimaryQlClass() { result = "RegExpQuantifier" } + } + + /** + * A regular expression term that permits unlimited repetitions. + */ + class InfiniteRepetitionQuantifier extends RegExpQuantifier { + InfiniteRepetitionQuantifier() { this.mayRepeatForever() } + + override string getAPrimaryQlClass() { result = "InfiniteRepetitionQuantifier" } + } + + /** + * A star-quantified term. + * + * Example: + * + * ``` + * \w* + * ``` + */ + class RegExpStar extends InfiniteRepetitionQuantifier { + RegExpStar() { this.getQualifier().charAt(0) = "*" } + + override string getAPrimaryQlClass() { result = "RegExpStar" } + + override predicate isNullable() { any() } + } + + /** + * A plus-quantified term. + * + * Example: + * + * ``` + * \w+ + * ``` + */ + class RegExpPlus extends InfiniteRepetitionQuantifier { + RegExpPlus() { this.getQualifier().charAt(0) = "+" } + + override string getAPrimaryQlClass() { result = "RegExpPlus" } + + override predicate isNullable() { this.getAChild().isNullable() } + } + + /** + * An optional term. + * + * Example: + * + * ``` + * ;? + * ``` + */ + class RegExpOpt extends RegExpQuantifier { + RegExpOpt() { this.getQualifier().charAt(0) = "?" } + + override string getAPrimaryQlClass() { result = "RegExpOpt" } + + override predicate isNullable() { any() } + } + + /** + * A range-quantified term + * + * Examples: + * + * ``` + * \w{2,4} + * \w{2,} + * \w{2} + * ``` + */ + class RegExpRange extends RegExpQuantifier { + string upper; + string lower; + + RegExpRange() { re.multiples(part_end, end, lower, upper) } + + override string getAPrimaryQlClass() { result = "RegExpRange" } + + /** Gets the string defining the upper bound of this range, if any. */ + string getUpper() { result = upper } + + /** Gets the string defining the lower bound of this range, if any. */ + string getLower() { result = lower } + + /** + * Gets the upper bound of the range, if any. + * + * If there is no upper bound, any number of repetitions is allowed. + * For a term of the form `r{lo}`, both the lower and the upper bound + * are `lo`. + */ + int getUpperBound() { result = this.getUpper().toInt() } + + /** Gets the lower bound of the range. */ + int getLowerBound() { result = this.getLower().toInt() } + + override predicate isNullable() { this.getAChild().isNullable() or this.getLowerBound() = 0 } + } + + /** + * A sequence term. + * + * Example: + * + * ``` + * (ECMA|Java)Script + * ``` + * + * This is a sequence with the elements `(ECMA|Java)` and `Script`. + */ + class RegExpSequence extends RegExpTerm, TRegExpSequence { + RegExpSequence() { + this = TRegExpSequence(re, start, end) and + exists(seqChild(re, start, end, 1)) // if a sequence does not have more than one element, it should be treated as that element instead. + } + + override RegExpTerm getChild(int i) { result = seqChild(re, start, end, i) } + + /** Gets the element preceding `element` in this sequence. */ + RegExpTerm previousElement(RegExpTerm element) { element = this.nextElement(result) } + + /** Gets the element following `element` in this sequence. */ + RegExpTerm nextElement(RegExpTerm element) { + exists(int i | + element = this.getChild(i) and + result = this.getChild(i + 1) + ) + } + + override string getConstantValue() { result = this.getConstantValue(0) } + + /** + * Gets the single string matched by the `i`th child and all following + * children of this sequence, if any. + */ + private string getConstantValue(int i) { + i = this.getNumChild() and + result = "" + or + result = this.getChild(i).getConstantValue() + this.getConstantValue(i + 1) + } + + override string getAPrimaryQlClass() { result = "RegExpSequence" } + + override predicate isNullable() { + forall(RegExpTerm child | child = this.getAChild() | child.isNullable()) + } + } + + pragma[nomagic] + private int seqChildEnd(RegExp re, int start, int end, int i) { + result = seqChild(re, start, end, i).getEnd() + } + + // moved out so we can use it in the charpred + private RegExpTerm seqChild(RegExp re, int start, int end, int i) { + re.sequence(start, end) and + ( + i = 0 and + result.getRegExp() = re and + result.getStart() = start and + exists(int itemEnd | + re.item(start, itemEnd) and + result.getEnd() = itemEnd + ) + or + i > 0 and + result.getRegExp() = re and + exists(int itemStart | itemStart = seqChildEnd(re, start, end, i - 1) | + result.getStart() = itemStart and + re.item(itemStart, result.getEnd()) + ) + ) + } + + /** + * An alternative term, that is, a term of the form `a|b`. + * + * Example: + * + * ``` + * ECMA|Java + * ``` + */ + class RegExpAlt extends RegExpTerm, TRegExpAlt { + RegExpAlt() { this = TRegExpAlt(re, start, end) } + + override RegExpTerm getChild(int i) { + i = 0 and + result.getRegExp() = re and + result.getStart() = start and + exists(int part_end | + re.alternationOption(start, end, start, part_end) and + result.getEnd() = part_end + ) + or + i > 0 and + result.getRegExp() = re and + exists(int part_start | + part_start = this.getChild(i - 1).getEnd() + 1 // allow for the | + | + result.getStart() = part_start and + re.alternationOption(start, end, part_start, result.getEnd()) + ) + } + + /** Gets an alternative of this term. */ + RegExpTerm getAlternative() { result = this.getAChild() } + + override string getAMatchedString() { result = this.getAlternative().getAMatchedString() } + + override string getAPrimaryQlClass() { result = "RegExpAlt" } + + override predicate isNullable() { this.getAChild().isNullable() } + } + + /** + * A character escape in a regular expression. + * + * Example: + * + * ``` + * \. + * ``` + */ + class RegExpCharEscape = RegExpEscape; + + /** + * An escaped regular expression term, that is, a regular expression + * term starting with a backslash, which is not a backreference. + * + * Example: + * + * ``` + * \. + * \w + * ``` + */ + class RegExpEscape extends RegExpNormalChar { + RegExpEscape() { re.escapedCharacter(start, end) } + + /** + * Gets the name of the escaped; for example, `w` for `\w`. + * TODO: Handle named escapes. + */ + override string getValue() { + not this.isUnicode() and + this.isIdentityEscape() and + result = this.getUnescaped() + or + this.getUnescaped() = "n" and result = "\n" + or + this.getUnescaped() = "r" and result = "\r" + or + this.getUnescaped() = "t" and result = "\t" + or + this.getUnescaped() = "f" and result = 12.toUnicode() + or + this.getUnescaped() = "v" and result = 11.toUnicode() + or + this.isUnicode() and + result = this.getUnicode() + } + + /** Holds if this terms name is given by the part following the escape character. */ + predicate isIdentityEscape() { + not this.getUnescaped() in ["n", "r", "t", "f", "v"] and not this.isUnicode() + } + + override string getAPrimaryQlClass() { result = "RegExpEscape" } + + /** Gets the part of the term following the escape character. That is e.g. "w" if the term is "\w". */ + string getUnescaped() { result = this.getText().suffix(1) } + + /** + * Gets the text for this escape. That is e.g. "\w". + */ + private string getText() { result = re.getText().substring(start, end) } + + /** + * Holds if this is a unicode escape. + */ + private predicate isUnicode() { this.getText().prefix(2) = "\\u" } + + /** + * Gets the unicode char for this escape. + * E.g. for `\u0061` this returns "a". + */ + private string getUnicode() { + this.isUnicode() and + result = parseHexInt(this.getText().suffix(2)).toUnicode() + } + } + + /** + * A word boundary, that is, a regular expression term of the form `\b`. + */ + class RegExpWordBoundary extends RegExpSpecialChar { + RegExpWordBoundary() { this.getChar() = "\\b" } + + override predicate isNullable() { none() } + } + + /** + * A non-word boundary, that is, a regular expression term of the form `\B`. + */ + class RegExpNonWordBoundary extends RegExpSpecialChar { + RegExpNonWordBoundary() { this.getChar() = "\\B" } + + override string getAPrimaryQlClass() { result = "RegExpNonWordBoundary" } + } + + /** + * A character class escape in a regular expression. + * That is, an escaped character that denotes multiple characters. + * ECMAScript supports: \d, \D, \s, \S, \w, \W + * (Ruby-only \h, \H are removed) + * + * Examples: + * + * ``` + * \w + * \S + * ``` + */ + class RegExpCharacterClassEscape extends RegExpEscape { + RegExpCharacterClassEscape() { this.getValue() in ["d", "D", "s", "S", "w", "W"] } + + override RegExpTerm getChild(int i) { none() } + + override string getAPrimaryQlClass() { result = "RegExpCharacterClassEscape" } + + override predicate isNullable() { none() } + } + + /** + * A character class in a regular expression. + * + * Examples: + * + * ```cpp + * "[a-fA-F0-9]" + * "[^abc]" + * ``` + */ + class RegExpCharacterClass extends RegExpTerm, TRegExpCharacterClass { + RegExpCharacterClass() { this = TRegExpCharacterClass(re, start, end) } + + /** Holds if this character class is inverted, matching the opposite of its content. */ + predicate isInverted() { re.getChar(start + 1) = "^" } + + /** Holds if this character class can match anything. */ + predicate isUniversalClass() { + // [^] + this.isInverted() and not exists(this.getAChild()) + or + // [\w\W] and similar + not this.isInverted() and + exists(string cce1, string cce2 | + cce1 = this.getAChild().(RegExpCharacterClassEscape).getValue() and + cce2 = this.getAChild().(RegExpCharacterClassEscape).getValue() + | + cce1 != cce2 and cce1.toLowerCase() = cce2.toLowerCase() + ) + } + + override RegExpTerm getChild(int i) { + i = 0 and + result.getRegExp() = re and + exists(int itemStart, int itemEnd | + result.getStart() = itemStart and + re.charSetStart(start, itemStart) and + re.charSetChild(start, itemStart, itemEnd) and + result.getEnd() = itemEnd + ) + or + i > 0 and + result.getRegExp() = re and + exists(int itemStart | itemStart = this.getChild(i - 1).getEnd() | + result.getStart() = itemStart and + re.charSetChild(start, itemStart, result.getEnd()) + ) + } + + override string getAMatchedString() { + not this.isInverted() and result = this.getAChild().getAMatchedString() + } + + override string getAPrimaryQlClass() { result = "RegExpCharacterClass" } + + override predicate isNullable() { none() } + } + + /** + * A character range in a character class in a regular expression. + * + * Example: + * + * ``` + * a-z + * ``` + */ + class RegExpCharacterRange extends RegExpTerm, TRegExpCharacterRange { + int lower_end; + int upper_start; + + RegExpCharacterRange() { + this = TRegExpCharacterRange(re, start, end) and + re.charRange(_, start, lower_end, upper_start, end) + } + + /** Holds if this range goes from `lo` to `hi`, in effect is `lo-hi`. */ + predicate isRange(string lo, string hi) { + lo = re.getText().substring(start, lower_end) and + hi = re.getText().substring(upper_start, end) + } + + override RegExpTerm getChild(int i) { + i = 0 and + result.getRegExp() = re and + result.getStart() = start and + result.getEnd() = lower_end + or + i = 1 and + result.getRegExp() = re and + result.getStart() = upper_start and + result.getEnd() = end + } + + override string getAPrimaryQlClass() { result = "RegExpCharacterRange" } + + override predicate isNullable() { none() } + } + + /** + * A normal character in a regular expression, that is, a character + * without special meaning. This includes escaped characters. + * + * Examples: + * ``` + * t + * \t + * ``` + */ + additional class RegExpNormalChar extends RegExpTerm, TRegExpNormalChar { + RegExpNormalChar() { this = TRegExpNormalChar(re, start, end) } + + /** + * Holds if this constant represents a valid Unicode character (as opposed + * to a surrogate code point that does not correspond to a character by itself.) + */ + predicate isCharacter() { any() } + + /** Gets the string representation of the char matched by this term. */ + string getValue() { result = re.getText().substring(start, end) } + + override RegExpTerm getChild(int i) { none() } + + override string getAPrimaryQlClass() { result = "RegExpNormalChar" } + } + + /** + * A constant regular expression term, that is, a regular expression + * term matching a single string. Currently, this will always be a single character. + * + * Example: + * + * ``` + * a + * ``` + */ + class RegExpConstant extends RegExpTerm { + string value; + + RegExpConstant() { + this = TRegExpNormalChar(re, start, end) and + not this instanceof RegExpCharacterClassEscape and + // exclude chars in qualifiers + // TODO: push this into regex library + not exists(int qstart, int qend | re.qualifiedPart(_, qstart, qend, _, _) | + qstart <= start and end <= qend + ) and + value = this.(RegExpNormalChar).getValue() + or + this = TRegExpSpecialChar(re, start, end) and + re.inCharSet(start) and + value = this.(RegExpSpecialChar).getChar() + } + + /** + * Holds if this constant represents a valid Unicode character (as opposed + * to a surrogate code point that does not correspond to a character by itself.) + */ + predicate isCharacter() { any() } + + /** Gets the string matched by this constant term. */ + string getValue() { result = value } + + override RegExpTerm getChild(int i) { none() } + + override string getConstantValue() { result = this.getValue() } + + override string getAPrimaryQlClass() { result = "RegExpConstant" } + + override predicate isNullable() { none() } + } + + /** + * A grouped regular expression. + * + * Examples: + * + * ``` + * (ECMA|Java) + * (?:ECMA|Java) + * ``` + */ + class RegExpGroup extends RegExpTerm, TRegExpGroup { + RegExpGroup() { this = TRegExpGroup(re, start, end) } + + /** + * Gets the index of this capture group within the enclosing regular + * expression literal. + * + * For example, in the regular expression `/((a?).)(?:b)/`, the + * group `((a?).)` has index 1, the group `(a?)` nested inside it + * has index 2, and the group `(?:b)` has no index, since it is + * not a capture group. + */ + int getNumber() { result = re.getGroupNumber(start, end) } + + /** Holds if this is a capture group. */ + predicate isCapture() { exists(this.getNumber()) } + + override RegExpTerm getChild(int i) { + result.getRegExp() = re and + i = 0 and + re.groupContents(start, end, result.getStart(), result.getEnd()) + } + + override string getConstantValue() { result = this.getAChild().getConstantValue() } + + override string getAMatchedString() { result = this.getAChild().getAMatchedString() } + + override string getAPrimaryQlClass() { result = "RegExpGroup" } + + override predicate isNullable() { this.getAChild().isNullable() } + } + + /** + * A special character in a regular expression. + * + * Examples: + * ``` + * ^ + * $ + * . + * ``` + */ + additional class RegExpSpecialChar extends RegExpTerm, TRegExpSpecialChar { + string char; + + RegExpSpecialChar() { + this = TRegExpSpecialChar(re, start, end) and + re.specialCharacter(start, end, char) + } + + /** + * Holds if this constant represents a valid Unicode character (as opposed + * to a surrogate code point that does not correspond to a character by itself.) + */ + predicate isCharacter() { any() } + + /** Gets the char for this term. */ + string getChar() { result = char } + + override RegExpTerm getChild(int i) { none() } + + override string getAPrimaryQlClass() { result = "RegExpSpecialChar" } + } + + /** + * A dot regular expression. + * + * Example: + * + * ``` + * . + * ``` + */ + class RegExpDot extends RegExpSpecialChar { + RegExpDot() { this.getChar() = "." } + + override string getAPrimaryQlClass() { result = "RegExpDot" } + + override predicate isNullable() { none() } + } + + /** + * A term that matches a specific position between characters in the string. + * ECMAScript anchors: `^` and `$` only. + * (Ruby-only `\A`, `\Z`, `\z` removed) + * + * In the default `std::regex` ECMAScript grammar, `^` and `$` anchor to the + * start and end of the input string. Multiline mode (in which they would + * also match at line boundaries) is a construction-site flag that this + * parser cannot observe, and is therefore not modelled. + */ + class RegExpAnchor extends RegExpSpecialChar { + RegExpAnchor() { this.getChar() = ["^", "$"] } + + override string getAPrimaryQlClass() { result = "RegExpAnchor" } + } + + /** + * A dollar assertion `$` matching the end of the input string + * (multiline mode is not modelled; see `RegExpAnchor`). + * + * Example: + * + * ``` + * $ + * ``` + */ + class RegExpDollar extends RegExpAnchor { + RegExpDollar() { this.getChar() = "$" } + + override string getAPrimaryQlClass() { result = "RegExpDollar" } + + override predicate isNullable() { any() } + } + + /** + * A caret assertion `^` matching the start of the input string + * (multiline mode is not modelled; see `RegExpAnchor`). + * + * Example: + * + * ``` + * ^ + * ``` + */ + class RegExpCaret extends RegExpAnchor { + RegExpCaret() { this.getChar() = "^" } + + override string getAPrimaryQlClass() { result = "RegExpCaret" } + + override predicate isNullable() { any() } + } + + /** + * A zero-width match, that is, either an empty group or an assertion. + * + * Examples: + * ``` + * () + * (?=\w) + * ``` + */ + additional class RegExpZeroWidthMatch extends RegExpGroup { + RegExpZeroWidthMatch() { re.zeroWidthMatch(start, end) } + + override RegExpTerm getChild(int i) { none() } + + override string getAPrimaryQlClass() { result = "RegExpZeroWidthMatch" } + + override predicate isNullable() { any() } + } + + /** + * A zero-width lookahead or lookbehind assertion. + * + * Examples: + * + * ``` + * (?=\w) + * (?!\n) + * (?<=\.) + * (? 1 and + not this.charSetDelimiter(index - 1, _) = false and + result = false + or + // special handling of cases such as `[][]` (the character-set of the characters `]` and `[`). + exists(int prevClosingBracketPos | + // previous bracket is a closing bracket + this.charSetDelimiter(index - 1, prevClosingBracketPos) = false and + if + // check if the character that comes before the previous closing bracket + // is an opening bracket (taking `^` into account) + // check if the character that comes before the previous closing bracket + // is an opening bracket (taking `^` into account) + exists(int posBeforePrevClosingBracket | + if this.getChar(prevClosingBracketPos - 1) = "^" + then posBeforePrevClosingBracket = prevClosingBracketPos - 2 + else posBeforePrevClosingBracket = prevClosingBracketPos - 1 + | + this.charSetDelimiter(index - 2, posBeforePrevClosingBracket) = true + ) + then + // brackets without anything in between is not valid character ranges, so + // the first closing bracket in `[]]` and `[^]]` does not count, + // + // and we should _not_ mark the second opening bracket in `[][]` and `[^][]` + // as starting a new char set. ^ ^ + exists(int posBeforePrevClosingBracket | + this.charSetDelimiter(index - 2, posBeforePrevClosingBracket) = true + | + result = this.charSetStart(posBeforePrevClosingBracket).booleanNot() + ) + else + // if not, `pos` does in fact mark a real start of a character range + result = true + ) + ) + ) + } + + /** + * Helper predicate for chars that could be character-set delimiters. + * Holds if the (non-escaped) char at `pos` in the string, is the (one-based) `index` occurrence of a bracket (`[` or `]`) in the string. + * Result if `true` is the char is `[`, and `false` if the char is `]`. + */ + boolean charSetDelimiter(int index, int pos) { + pos = + rank[index](int p | + (this.nonEscapedCharAt(p) = "[" or this.nonEscapedCharAt(p) = "]") and + // Brackets that are part of POSIX expressions should not count as + // char-set delimiters. + not this.inAnyPosixBracket(p) + ) and + ( + this.nonEscapedCharAt(pos) = "[" and result = true + or + this.nonEscapedCharAt(pos) = "]" and result = false + ) + } + + /** Holds if a character set starts between `start` and `end`. */ + predicate charSetStart(int start, int end) { + this.charSetStart(start) = true and + ( + this.getChar(start + 1) = "^" and end = start + 2 + or + not this.getChar(start + 1) = "^" and end = start + 1 + ) + } + + /** Whether there is a character class, between start (inclusive) and end (exclusive) */ + predicate charSet(int start, int end) { + exists(int innerStart, int innerEnd | + this.charSetStart(start, innerStart) and + not this.charSetStart(_, start) + | + end = innerEnd + 1 and + innerEnd = + min(int e | + e > innerStart and + this.nonEscapedCharAt(e) = "]" and + not this.inAnyPosixBracket(e) + | + e + ) + ) + } + + /** + * Holds if the character set starting at `charsetStart` contains either + * a character or a `-` found between `start` and `end`. + */ + private predicate charSetToken(int charsetStart, int index, int tokenStart, int tokenEnd) { + tokenStart = + rank[index](int start, int end | this.charSetToken(charsetStart, start, end) | start) and + this.charSetToken(charsetStart, tokenStart, tokenEnd) + } + + /** + * Holds if the character set starting at `charsetStart` contains either + * a character or a `-` found between `start` and `end`. + */ + private predicate charSetToken(int charsetStart, int start, int end) { + this.charSetStart(charsetStart, start) and + ( + this.escapedCharacter(start, end) + or + this.namedCharacterProperty(start, end, _) + or + exists(this.nonEscapedCharAt(start)) and + not this.inAnyPosixBracket(start) and + end = start + 1 + ) + or + this.charSetToken(charsetStart, _, start) and + ( + this.escapedCharacter(start, end) + or + this.namedCharacterProperty(start, end, _) + or + exists(this.nonEscapedCharAt(start)) and + not this.inAnyPosixBracket(start) and + end = start + 1 and + not this.getChar(start) = "]" + ) + } + + /** + * Holds if the character set starting at `charsetStart` contains either + * a character or a range found between `start` and `end`. + */ + predicate charSetChild(int charsetStart, int start, int end) { + this.charSetToken(charsetStart, start, end) and + not exists(int rangeStart, int rangeEnd | + this.charRange(charsetStart, rangeStart, _, _, rangeEnd) and + rangeStart <= start and + rangeEnd >= end + ) + or + this.charRange(charsetStart, start, _, _, end) + } + + /** + * Holds if the character set starting at `charsetStart` contains a character range + * with lower bound found between `start` and `lowerEnd` + * and upper bound found between `upperStart` and `end`. + */ + predicate charRange(int charsetStart, int start, int lowerEnd, int upperStart, int end) { + exists(int index | + this.charRangeEnd(charsetStart, index) = true and + this.charSetToken(charsetStart, index - 2, start, lowerEnd) and + this.charSetToken(charsetStart, index, upperStart, end) + ) + } + + /** + * Helper predicate for `charRange`. + * We can determine where character ranges end by a left to right sweep. + * + * To avoid negative recursion we return a boolean. See `escaping`, + * the helper for `escapingChar`, for a clean use of this pattern. + */ + private boolean charRangeEnd(int charsetStart, int index) { + this.charSetToken(charsetStart, index, _, _) and + ( + index in [1, 2] and result = false + or + index > 2 and + exists(int connectorStart | + this.charSetToken(charsetStart, index - 1, connectorStart, _) and + this.nonEscapedCharAt(connectorStart) = "-" and + result = + this.charRangeEnd(charsetStart, index - 2) + .booleanNot() + .booleanAnd(this.charRangeEnd(charsetStart, index - 1).booleanNot()) + ) + or + not exists(int connectorStart | + this.charSetToken(charsetStart, index - 1, connectorStart, _) and + this.nonEscapedCharAt(connectorStart) = "-" + ) and + result = false + ) + } + + /** Holds if the character at `pos` is a "\" that is actually escaping what comes after. */ + predicate escapingChar(int pos) { this.escaping(pos) = true } + + /** + * Helper predicate for `escapingChar`. + * In order to avoid negative recursion, we return a boolean. + * This way, we can refer to `escaping(pos - 1).booleanNot()` + * rather than to a negated version of `escaping(pos)`. + */ + private boolean escaping(int pos) { + pos = -1 and result = false + or + this.getChar(pos) = "\\" and result = this.escaping(pos - 1).booleanNot() + or + this.getChar(pos) != "\\" and result = false + } + + /** Gets the text of this regex (the value of the string literal). */ + string getText() { result = this.getValue() } + + /** Gets the `i`th character of this regex */ + string getChar(int i) { result = this.getText().charAt(i) } + + /** Gets the `i`th character of this regex, unless it is part of a character escape sequence. */ + string nonEscapedCharAt(int i) { + result = this.getText().charAt(i) and + not exists(int x, int y | this.escapedCharacter(x, y) and i in [x .. y - 1]) + } + + private predicate isOptionDivider(int i) { this.nonEscapedCharAt(i) = "|" } + + private predicate isGroupEnd(int i) { this.nonEscapedCharAt(i) = ")" and not this.inCharSet(i) } + + private predicate isGroupStart(int i) { this.nonEscapedCharAt(i) = "(" and not this.inCharSet(i) } + + /** + * Holds if the `i`th character could not be parsed. + */ + predicate failedToParse(int i) { + exists(this.getChar(i)) and + not exists(int start, int end | + this.topLevel(start, end) and + start <= i and + end > i + ) + } + + /** Matches named character properties such as `[[:digit:]]`, `[.a.]`, and `[=a=]`. */ + predicate namedCharacterProperty(int start, int end, string name) { + this.posixStyleNamedCharacterProperty(start, end, name) or + this.posixCollatingSymbol(start, end, name) or + this.posixEquivalenceClass(start, end, name) + } + + /** Gets the name of the character property in start,end */ + string getCharacterPropertyName(int start, int end) { + this.namedCharacterProperty(start, end, result) + } + + /** + * Holds if the position `pos` falls inside any POSIX bracket atom + * (`[:name:]`, `[.x.]`, or `[=x=]`). + * Used to prevent the inner brackets from being treated as charSet delimiters. + */ + private predicate inAnyPosixBracket(int pos) { + exists(int x, int y | + this.posixStyleNamedCharacterProperty(x, y, _) or + this.posixCollatingSymbol(x, y, _) or + this.posixEquivalenceClass(x, y, _) + | + pos in [x .. y - 1] + ) + } + + /** Matches a POSIX bracket expression such as `[:alnum:]` within a character class. */ + private predicate posixStyleNamedCharacterProperty(int start, int end, string name) { + this.getChar(start) = "[" and + this.getChar(start + 1) = ":" and + // POSIX bracket expressions are only valid nested inside another character + // class. Approximate this by requiring at least one more (non-escaped) + // opening `[` than closing `]` before `start`. A well-formed POSIX bracket + // contributes one `[` and one `]` at/after `start`, so this check is + // unaffected by other POSIX brackets earlier in the text. + count(int p | p < start and this.nonEscapedCharAt(p) = "[") > + count(int p | p < start and this.nonEscapedCharAt(p) = "]") and + end = + min(int e | + e > start and + this.getChar(e - 2) = ":" and + this.getChar(e - 1) = "]" + | + e + ) and + exists(int nameStart | + this.getChar(start + 2) = "^" and nameStart = start + 3 + or + not this.getChar(start + 2) = "^" and nameStart = start + 2 + | + name = this.getText().substring(nameStart, end - 2) + ) + } + + /** + * Matches a POSIX collating symbol such as `[.a.]` within a character class. + * Example: `[[.a.]]` — the `[.a.]` atom nested inside the outer bracket. + */ + private predicate posixCollatingSymbol(int start, int end, string name) { + this.getChar(start) = "[" and + this.getChar(start + 1) = "." and + count(int p | p < start and this.nonEscapedCharAt(p) = "[") > + count(int p | p < start and this.nonEscapedCharAt(p) = "]") and + end = + min(int e | + e > start and + this.getChar(e - 2) = "." and + this.getChar(e - 1) = "]" + | + e + ) and + name = this.getText().substring(start + 2, end - 2) + } + + /** + * Matches a POSIX equivalence class such as `[=a=]` within a character class. + * Example: `[[=a=]]` — the `[=a=]` atom nested inside the outer bracket. + */ + private predicate posixEquivalenceClass(int start, int end, string name) { + this.getChar(start) = "[" and + this.getChar(start + 1) = "=" and + count(int p | p < start and this.nonEscapedCharAt(p) = "[") > + count(int p | p < start and this.nonEscapedCharAt(p) = "]") and + end = + min(int e | + e > start and + this.getChar(e - 2) = "=" and + this.getChar(e - 1) = "]" + | + e + ) and + name = this.getText().substring(start + 2, end - 2) + } + + /** + * Holds if the named character property is inverted. Examples for which it holds: + * - `[[:^digit:]]` + * + * Examples for which it doesn't hold: + * - `[[:alnum:]]` + */ + predicate namedCharacterPropertyIsInverted(int start, int end) { + this.posixStyleNamedCharacterProperty(start, end, _) and + this.getChar(start + 2) = "^" + } + + /** + * Holds if an escaped character is found between `start` and `end`. + * Escaped characters include hex values, octal values and named escapes, + * but excludes backreferences. + */ + predicate escapedCharacter(int start, int end) { + this.escapingChar(start) and + not this.numberedBackreference(start, _, _) and + ( + // hex char \xhh + this.getChar(start + 1) = "x" and end = start + 4 + or + // wide hex char \uhhhh + this.getChar(start + 1) = "u" and end = start + 6 + or + // NUL escape \0 (not followed by another digit, which would make it an octal escape) + this.getChar(start + 1) = "0" and + not exists(this.getChar(start + 2).toInt()) and + end = start + 2 + or + // escape not handled above; update when adding a new case + not this.getChar(start + 1) in ["x", "u"] and + not exists(this.getChar(start + 1).toInt()) and + end = start + 2 + ) + } + + /** + * Holds if the character at `index` is inside a character set. + */ + predicate inCharSet(int index) { + exists(int x, int y | this.charSet(x, y) and index in [x + 1 .. y - 2]) + } + + /** + * Holds if the character at `index` is inside a posix bracket + * (`[:name:]`, `[.x.]`, or `[=x=]`). + */ + predicate inPosixBracket(int index) { + exists(int x, int y | + ( + this.posixStyleNamedCharacterProperty(x, y, _) or + this.posixCollatingSymbol(x, y, _) or + this.posixEquivalenceClass(x, y, _) + ) and + index in [x + 1 .. y - 2] + ) + } + + /** + * 'simple' characters are any that don't alter the parsing of the regex. + */ + private predicate simpleCharacter(int start, int end) { + end = start + 1 and + not this.charSet(start, _) and + not this.charSet(_, start + 1) and + not this.inAnyPosixBracket(start) and + exists(string c | c = this.getChar(start) | + exists(int x, int y, int z | + this.charSet(x, z) and + this.charSetStart(x, y) + | + start = y + or + start = z - 2 + or + start > y and start < z - 2 and not this.charRange(_, _, start, end, _) + ) + or + not this.inCharSet(start) and + not c = "(" and + not c = "[" and + not c = ")" and + not c = "|" and + not this.qualifier(start, _, _, _) + ) + } + + /** + * Holds if a simple or escaped character is found between `start` and `end`. + */ + predicate character(int start, int end) { + ( + this.simpleCharacter(start, end) and + not exists(int x, int y | this.escapedCharacter(x, y) and x <= start and y >= end) + or + this.escapedCharacter(start, end) + ) and + not exists(int x, int y | this.groupStart(x, y) and x <= start and y >= end) and + not exists(int x, int y | this.backreference(x, y) and x <= start and y >= end) and + not exists(int x, int y | this.multiples(x, y, _, _) and x <= start and y >= end) + } + + /** + * Holds if a normal character is found between `start` and `end`. + */ + predicate normalCharacter(int start, int end) { + end = start + 1 and + this.character(start, end) and + not this.specialCharacter(start, end, _) + } + + /** + * Holds if a special character is found between `start` and `end`. + * ECMAScript anchors: `^`, `$`, `.`, `\b`, `\B` only. + * Ruby-only anchors `\A`, `\Z`, `\z`, `\G` are not in ECMAScript. + */ + predicate specialCharacter(int start, int end, string char) { + this.character(start, end) and + not this.inCharSet(start) and + ( + end = start + 1 and + char = this.getChar(start) and + (char = "$" or char = "^" or char = ".") + or + end = start + 2 and + this.escapingChar(start) and + char = this.getText().substring(start, end) and + char = ["\\b", "\\B"] + ) + } + + /** + * Holds if the range [start:end) consists of only 'normal' characters. + */ + predicate normalCharacterSequence(int start, int end) { + // a normal character inside a character set is interpreted on its own + this.normalCharacter(start, end) and + this.inCharSet(start) + or + // a maximal run of normal characters is considered as one constant + exists(int s, int e | + e = max(int i | this.normalCharacterRun(s, i)) and + not this.inCharSet(s) + | + // 'abc' can be considered one constant, but + // 'abc+' has to be broken up into 'ab' and 'c+', + // as the qualifier only applies to 'c'. + if this.qualifier(e, _, _, _) + then + end = e and start = e - 1 + or + end = e - 1 and start = s and start < end + else ( + end = e and + start = s + ) + ) + } + + private predicate normalCharacterRun(int start, int end) { + ( + this.normalCharacterRun(start, end - 1) + or + start = end - 1 and not this.normalCharacter(start - 1, start) + ) and + this.normalCharacter(end - 1, end) + } + + private predicate characterItem(int start, int end) { + this.normalCharacterSequence(start, end) or + this.escapedCharacter(start, end) or + this.specialCharacter(start, end, _) + } + + /** Whether the text in the range `start,end` is a group */ + predicate group(int start, int end) { + this.groupContents(start, end, _, _) + or + this.emptyGroup(start, end) + } + + /** Gets the number of the group in start,end */ + int getGroupNumber(int start, int end) { + this.group(start, end) and + not this.nonCapturingGroupStart(start, _) and + result = + count(int i | this.group(i, _) and i < start and not this.nonCapturingGroupStart(i, _)) + 1 + } + + /** Whether the text in the range start, end is a group and can match the empty string. */ + predicate zeroWidthMatch(int start, int end) { + this.emptyGroup(start, end) + or + this.negativeAssertionGroup(start, end) + or + this.positiveLookaheadAssertionGroup(start, end) + or + this.positiveLookbehindAssertionGroup(start, end) + } + + /** Holds if an empty group is found between `start` and `end`. */ + predicate emptyGroup(int start, int end) { + exists(int endm1 | end = endm1 + 1 | + this.groupStart(start, endm1) and + this.isGroupEnd(endm1) + ) + } + + private predicate emptyMatchAtStartGroup(int start, int end) { + this.emptyGroup(start, end) + or + this.negativeAssertionGroup(start, end) + or + this.positiveLookaheadAssertionGroup(start, end) + } + + private predicate emptyMatchAtEndGroup(int start, int end) { + this.emptyGroup(start, end) + or + this.negativeAssertionGroup(start, end) + or + this.positiveLookbehindAssertionGroup(start, end) + } + + private predicate negativeAssertionGroup(int start, int end) { + exists(int inStart | + this.negativeLookaheadAssertionStart(start, inStart) + or + this.negativeLookbehindAssertionStart(start, inStart) + | + this.groupContents(start, end, inStart, _) + ) + } + + /** Holds if a negative lookahead is found between `start` and `end` */ + predicate negativeLookaheadAssertionGroup(int start, int end) { + exists(int inStart | this.negativeLookaheadAssertionStart(start, inStart) | + this.groupContents(start, end, inStart, _) + ) + } + + /** Holds if a negative lookbehind is found between `start` and `end` */ + predicate negativeLookbehindAssertionGroup(int start, int end) { + exists(int inStart | this.negativeLookbehindAssertionStart(start, inStart) | + this.groupContents(start, end, inStart, _) + ) + } + + /** Holds if a positive lookahead is found between `start` and `end` */ + predicate positiveLookaheadAssertionGroup(int start, int end) { + exists(int inStart | this.lookaheadAssertionStart(start, inStart) | + this.groupContents(start, end, inStart, _) + ) + } + + /** Holds if a positive lookbehind is found between `start` and `end` */ + predicate positiveLookbehindAssertionGroup(int start, int end) { + exists(int inStart | this.lookbehindAssertionStart(start, inStart) | + this.groupContents(start, end, inStart, _) + ) + } + + private predicate groupStart(int start, int end) { + this.nonCapturingGroupStart(start, end) + or + this.lookaheadAssertionStart(start, end) + or + this.negativeLookaheadAssertionStart(start, end) + or + this.lookbehindAssertionStart(start, end) + or + this.negativeLookbehindAssertionStart(start, end) + or + this.commentGroupStart(start, end) + or + this.simpleGroupStart(start, end) + } + + /** + * Matches the start of a non-capturing group. + * ECMAScript non-capturing groups: `(?:`, lookaheads `(?=`/`(?!`, comments `(?#`. + * Note: `(?<` is intentionally excluded here — it is handled by + * `lookbehindAssertionStart` and `negativeLookbehindAssertionStart`. + */ + private predicate nonCapturingGroupStart(int start, int end) { + this.isGroupStart(start) and + this.getChar(start + 1) = "?" and + this.getChar(start + 2) = [":", "=", "!", "#"] and + end = start + 3 + } + + /** Matches the start of a simple group, e.g. `(a+)`. */ + private predicate simpleGroupStart(int start, int end) { + this.isGroupStart(start) and + this.getChar(start + 1) != "?" and + end = start + 1 + } + + /** Matches the start of a positive lookahead assertion, i.e. `(?=`. */ + private predicate lookaheadAssertionStart(int start, int end) { + this.isGroupStart(start) and + this.getChar(start + 1) = "?" and + this.getChar(start + 2) = "=" and + end = start + 3 + } + + /** Matches the start of a negative lookahead assertion, i.e. `(?!`. */ + private predicate negativeLookaheadAssertionStart(int start, int end) { + this.isGroupStart(start) and + this.getChar(start + 1) = "?" and + this.getChar(start + 2) = "!" and + end = start + 3 + } + + /** Matches the start of a positive lookbehind assertion, i.e. `(?<=`. */ + private predicate lookbehindAssertionStart(int start, int end) { + this.isGroupStart(start) and + this.getChar(start + 1) = "?" and + this.getChar(start + 2) = "<" and + this.getChar(start + 3) = "=" and + end = start + 4 + } + + /** Matches the start of a negative lookbehind assertion, i.e. `(? 0 + ) + } + + /** Whether the text in the range `start,end` is a back reference */ + predicate backreference(int start, int end) { this.numberedBackreference(start, end, _) } + + /** Gets the number of the back reference in start,end */ + int getBackRefNumber(int start, int end) { this.numberedBackreference(start, end, result) } + + private predicate baseItem(int start, int end) { + this.characterItem(start, end) and + not exists(int x, int y | this.charSet(x, y) and x <= start and y >= end) + or + this.group(start, end) + or + this.charSet(start, end) + or + this.backreference(start, end) + } + + private predicate qualifier(int start, int end, boolean maybeEmpty, boolean mayRepeatForever) { + this.shortQualifier(start, end, maybeEmpty, mayRepeatForever) and + not this.getChar(end) = "?" + or + exists(int shortEnd | this.shortQualifier(start, shortEnd, maybeEmpty, mayRepeatForever) | + if this.getChar(shortEnd) = "?" then end = shortEnd + 1 else end = shortEnd + ) + } + + private predicate shortQualifier(int start, int end, boolean maybeEmpty, boolean mayRepeatForever) { + ( + this.getChar(start) = "+" and maybeEmpty = false and mayRepeatForever = true + or + this.getChar(start) = "*" and maybeEmpty = true and mayRepeatForever = true + or + this.getChar(start) = "?" and maybeEmpty = true and mayRepeatForever = false + ) and + end = start + 1 + or + exists(string lower, string upper | + this.multiples(start, end, lower, upper) and + (if lower = "" or lower.toInt() = 0 then maybeEmpty = true else maybeEmpty = false) and + if upper = "" then mayRepeatForever = true else mayRepeatForever = false + ) + } + + /** + * Holds if a repetition quantifier is found between `start` and `end`, + * with the given lower and upper bounds. If a bound is omitted, the corresponding + * string is empty. + */ + predicate multiples(int start, int end, string lower, string upper) { + exists(string text, string match, string inner | + text = this.getText() and + end = start + match.length() and + inner = match.substring(1, match.length() - 1) + | + match = text.regexpFind("\\{[0-9]+\\}", _, start) and + lower = inner and + upper = lower + or + match = text.regexpFind("\\{[0-9]*,[0-9]*\\}", _, start) and + exists(int commaIndex | + commaIndex = inner.indexOf(",") and + lower = inner.prefix(commaIndex) and + upper = inner.suffix(commaIndex + 1) + ) + ) + } + + /** + * Whether the text in the range start,end is a qualified item, where item is a character, + * a character set or a group. + */ + predicate qualifiedItem(int start, int end, boolean maybeEmpty, boolean mayRepeatForever) { + this.qualifiedPart(start, _, end, maybeEmpty, mayRepeatForever) + } + + /** + * Holds if a qualified part is found between `start` and `partEnd` and the qualifier is + * found between `partEnd` and `end`. + * + * `maybeEmpty` is true if the part is optional. + * `mayRepeatForever` is true if the part may be repeated unboundedly. + */ + predicate qualifiedPart( + int start, int partEnd, int end, boolean maybeEmpty, boolean mayRepeatForever + ) { + this.baseItem(start, partEnd) and + this.qualifier(partEnd, end, maybeEmpty, mayRepeatForever) + } + + /** Holds if the range `start`, `end` contains a character, a quantifier, a character set or a group. */ + predicate item(int start, int end) { + this.qualifiedItem(start, end, _, _) + or + this.baseItem(start, end) and not this.qualifier(end, _, _, _) + } + + private predicate subsequence(int start, int end) { + ( + start = 0 or + this.groupStart(_, start) or + this.isOptionDivider(start - 1) + ) and + this.item(start, end) + or + exists(int mid | + this.subsequence(start, mid) and + this.item(mid, end) + ) + } + + /** + * Whether the text in the range start,end is a sequence of 1 or more items, where an item is a character, + * a character set or a group. + */ + predicate sequence(int start, int end) { + this.sequenceOrQualified(start, end) and + not this.qualifiedItem(start, end, _, _) + } + + private predicate sequenceOrQualified(int start, int end) { + this.subsequence(start, end) and + not this.itemStart(end) + } + + private predicate itemStart(int start) { + this.characterItem(start, _) or + this.isGroupStart(start) or + this.charSet(start, _) or + this.backreference(start, _) or + this.namedCharacterProperty(start, _, _) + } + + private predicate itemEnd(int end) { + this.characterItem(_, end) + or + exists(int endm1 | this.isGroupEnd(endm1) and end = endm1 + 1) + or + this.charSet(_, end) + or + this.qualifier(_, end, _, _) + } + + private predicate topLevel(int start, int end) { + this.subalternation(start, end, _) and + not this.isOptionDivider(end) + } + + private predicate subalternation(int start, int end, int itemStart) { + this.sequenceOrQualified(start, end) and + not this.isOptionDivider(start - 1) and + itemStart = start + or + start = end and + not this.itemEnd(start) and + this.isOptionDivider(end) and + itemStart = start + or + exists(int mid | + this.subalternation(start, mid, _) and + this.isOptionDivider(mid) and + itemStart = mid + 1 + | + this.sequenceOrQualified(itemStart, end) + or + not this.itemStart(end) and end = itemStart + ) + } + + /** + * Whether the text in the range start,end is an alternation + */ + predicate alternation(int start, int end) { + not this.inCharSet(start) and + this.topLevel(start, end) and + exists(int less | this.subalternation(start, less, _) and less < end) + } + + /** + * Whether the text in the range start,end is an alternation and the text in partStart, partEnd is one of the + * options in that alternation. + */ + predicate alternationOption(int start, int end, int partStart, int partEnd) { + this.alternation(start, end) and + this.subalternation(start, partEnd, partStart) + } + + /** A part of the regex that may match the start of the string. */ + private predicate firstPart(int start, int end) { + start = 0 and end = this.getText().length() + or + exists(int x | this.firstPart(x, end) | + this.emptyMatchAtStartGroup(x, start) + or + this.qualifiedItem(x, start, true, _) + or + // ^ matches the start of the string (ECMAScript only) + this.specialCharacter(x, start, "^") + ) + or + exists(int y | this.firstPart(start, y) | + this.item(start, end) + or + this.qualifiedPart(start, end, y, _, _) + ) + or + exists(int x, int y | this.firstPart(x, y) | + this.groupContents(x, y, start, end) + or + this.alternationOption(x, y, start, end) + ) + } + + /** A part of the regex that may match the end of the string. */ + private predicate lastPart(int start, int end) { + start = 0 and end = this.getText().length() + or + exists(int y | this.lastPart(start, y) | + this.emptyMatchAtEndGroup(end, y) + or + this.qualifiedItem(end, y, true, _) + or + // $ matches the end of the string (ECMAScript only) + this.specialCharacter(end, y, "$") + ) + or + this.lastPart(_, end) and + this.item(start, end) + or + exists(int y | this.lastPart(start, y) | this.qualifiedPart(start, end, y, _, _)) + or + exists(int x, int y | this.lastPart(x, y) | + this.groupContents(x, y, start, end) + or + this.alternationOption(x, y, start, end) + ) + } + + /** + * Whether the item at [start, end) is one of the first items + * to be matched. + */ + predicate firstItem(int start, int end) { + ( + this.characterItem(start, end) + or + this.qualifiedItem(start, end, _, _) + or + this.charSet(start, end) + ) and + this.firstPart(start, end) + } + + /** + * Whether the item at [start, end) is one of the last items + * to be matched. + */ + predicate lastItem(int start, int end) { + ( + this.characterItem(start, end) + or + this.qualifiedItem(start, end, _, _) + or + this.charSet(start, end) + ) and + this.lastPart(start, end) + } +} diff --git a/cpp/ql/test/library-tests/regex/Consistency.expected b/cpp/ql/test/library-tests/regex/Consistency.expected new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/cpp/ql/test/library-tests/regex/Consistency.ql b/cpp/ql/test/library-tests/regex/Consistency.ql new file mode 100644 index 000000000000..c99a56fb90b9 --- /dev/null +++ b/cpp/ql/test/library-tests/regex/Consistency.ql @@ -0,0 +1,11 @@ +/** + * Regression guard: every corpus regex must parse without leaving any + * character unaccounted for. Expected output is empty. + */ + +import cpp +import semmle.code.cpp.regex.internal.ParseRegExp + +from RegExp re, int i +where re.failedToParse(i) +select re, i, re.getChar(i) diff --git a/cpp/ql/test/library-tests/regex/locations.expected b/cpp/ql/test/library-tests/regex/locations.expected new file mode 100644 index 000000000000..3730bc17f506 --- /dev/null +++ b/cpp/ql/test/library-tests/regex/locations.expected @@ -0,0 +1,104 @@ +| test.cpp:142:24:142:32 | [[:alpha] | 23 | 0 | 9 | 24 | 32 | +| test.cpp:142:25:142:25 | [ | 23 | 1 | 2 | 25 | 25 | +| test.cpp:142:26:142:26 | : | 23 | 2 | 3 | 26 | 26 | +| test.cpp:142:27:142:27 | a | 23 | 3 | 4 | 27 | 27 | +| test.cpp:142:28:142:28 | l | 23 | 4 | 5 | 28 | 28 | +| test.cpp:142:29:142:29 | p | 23 | 5 | 6 | 29 | 29 | +| test.cpp:142:30:142:30 | h | 23 | 6 | 7 | 30 | 30 | +| test.cpp:142:31:142:31 | a | 23 | 7 | 8 | 31 | 31 | +| test.cpp:145:24:145:35 | []a[:alpha:] | 23 | 0 | 12 | 24 | 35 | +| test.cpp:145:24:145:36 | []a[:alpha:]] | 23 | 0 | 13 | 24 | 36 | +| test.cpp:145:25:145:25 | ] | 23 | 1 | 2 | 25 | 25 | +| test.cpp:145:26:145:26 | a | 23 | 2 | 3 | 26 | 26 | +| test.cpp:145:27:145:27 | [ | 23 | 3 | 4 | 27 | 27 | +| test.cpp:145:28:145:28 | : | 23 | 4 | 5 | 28 | 28 | +| test.cpp:145:29:145:29 | a | 23 | 5 | 6 | 29 | 29 | +| test.cpp:145:30:145:30 | l | 23 | 6 | 7 | 30 | 30 | +| test.cpp:145:31:145:31 | p | 23 | 7 | 8 | 31 | 31 | +| test.cpp:145:32:145:32 | h | 23 | 8 | 9 | 32 | 32 | +| test.cpp:145:33:145:33 | a | 23 | 9 | 10 | 33 | 33 | +| test.cpp:145:34:145:34 | : | 23 | 10 | 11 | 34 | 34 | +| test.cpp:145:36:145:36 | ] | 23 | 12 | 13 | 36 | 36 | +| test.cpp:148:25:148:53 | [[:alpha:][:digit:][:space:]] | 24 | 0 | 29 | 25 | 53 | +| test.cpp:148:26:148:34 | [:alpha:] | 24 | 1 | 10 | 26 | 34 | +| test.cpp:148:35:148:43 | [:digit:] | 24 | 10 | 19 | 35 | 43 | +| test.cpp:148:44:148:52 | [:space:] | 24 | 19 | 28 | 44 | 52 | +| test.cpp:151:25:151:36 | [[:xdigit:]] | 24 | 0 | 12 | 25 | 36 | +| test.cpp:151:26:151:35 | [:xdigit:] | 24 | 1 | 11 | 26 | 35 | +| test.cpp:152:25:152:35 | [[:blank:]] | 24 | 0 | 11 | 25 | 35 | +| test.cpp:152:26:152:34 | [:blank:] | 24 | 1 | 10 | 26 | 34 | +| test.cpp:153:25:153:35 | [[:cntrl:]] | 24 | 0 | 11 | 25 | 35 | +| test.cpp:153:26:153:34 | [:cntrl:] | 24 | 1 | 10 | 26 | 34 | +| test.cpp:154:25:154:35 | [[:graph:]] | 24 | 0 | 11 | 25 | 35 | +| test.cpp:154:26:154:34 | [:graph:] | 24 | 1 | 10 | 26 | 34 | +| test.cpp:157:25:157:25 | ^ | 24 | 0 | 1 | 25 | 25 | +| test.cpp:157:25:157:92 | ^([[:alpha:]]+[[:digit:]]*[[:space:]]?)+([[:punct:]]\|[[:xdigit:]])+$ | 24 | 0 | 68 | 25 | 92 | +| test.cpp:157:26:157:63 | ([[:alpha:]]+[[:digit:]]*[[:space:]]?) | 24 | 1 | 39 | 26 | 63 | +| test.cpp:157:26:157:64 | ([[:alpha:]]+[[:digit:]]*[[:space:]]?)+ | 24 | 1 | 40 | 26 | 64 | +| test.cpp:157:27:157:37 | [[:alpha:]] | 24 | 2 | 13 | 27 | 37 | +| test.cpp:157:27:157:38 | [[:alpha:]]+ | 24 | 2 | 14 | 27 | 38 | +| test.cpp:157:27:157:62 | [[:alpha:]]+[[:digit:]]*[[:space:]]? | 24 | 2 | 38 | 27 | 62 | +| test.cpp:157:28:157:36 | [:alpha:] | 24 | 3 | 12 | 28 | 36 | +| test.cpp:157:39:157:49 | [[:digit:]] | 24 | 14 | 25 | 39 | 49 | +| test.cpp:157:39:157:50 | [[:digit:]]* | 24 | 14 | 26 | 39 | 50 | +| test.cpp:157:40:157:48 | [:digit:] | 24 | 15 | 24 | 40 | 48 | +| test.cpp:157:51:157:61 | [[:space:]] | 24 | 26 | 37 | 51 | 61 | +| test.cpp:157:51:157:62 | [[:space:]]? | 24 | 26 | 38 | 51 | 62 | +| test.cpp:157:52:157:60 | [:space:] | 24 | 27 | 36 | 52 | 60 | +| test.cpp:157:65:157:90 | ([[:punct:]]\|[[:xdigit:]]) | 24 | 40 | 66 | 65 | 90 | +| test.cpp:157:65:157:91 | ([[:punct:]]\|[[:xdigit:]])+ | 24 | 40 | 67 | 65 | 91 | +| test.cpp:157:66:157:76 | [[:punct:]] | 24 | 41 | 52 | 66 | 76 | +| test.cpp:157:66:157:89 | [[:punct:]]\|[[:xdigit:]] | 24 | 41 | 65 | 66 | 89 | +| test.cpp:157:67:157:75 | [:punct:] | 24 | 42 | 51 | 67 | 75 | +| test.cpp:157:78:157:89 | [[:xdigit:]] | 24 | 53 | 65 | 78 | 89 | +| test.cpp:157:79:157:88 | [:xdigit:] | 24 | 54 | 64 | 79 | 88 | +| test.cpp:157:92:157:92 | $ | 24 | 67 | 68 | 92 | 92 | +| test.cpp:162:21:162:26 | \\u9879 | 20 | 0 | 6 | 21 | 26 | +| test.cpp:170:23:170:23 | a | 22 | 0 | 1 | 23 | 23 | +| test.cpp:170:23:170:24 | a+ | 22 | 0 | 2 | 23 | 24 | +| test.cpp:170:23:170:25 | a+b | 22 | 0 | 3 | 23 | 25 | +| test.cpp:170:25:170:25 | b | 22 | 2 | 3 | 25 | 25 | +| test.cpp:173:23:173:23 | a | 20 | 0 | 1 | 23 | 23 | +| test.cpp:173:23:173:24 | a+ | 20 | 0 | 2 | 23 | 24 | +| test.cpp:173:23:173:25 | a+b | 20 | 0 | 3 | 23 | 25 | +| test.cpp:173:25:173:25 | b | 20 | 2 | 3 | 25 | 25 | +| test.cpp:176:24:176:25 | \\s | 21 | 0 | 2 | 24 | 25 | +| test.cpp:176:24:176:26 | \\s+ | 21 | 0 | 3 | 24 | 26 | +| test.cpp:176:24:176:27 | \\s+$ | 21 | 0 | 4 | 24 | 27 | +| test.cpp:176:27:176:27 | $ | 21 | 3 | 4 | 27 | 27 | +| test.cpp:179:24:179:25 | \\( | 21 | 0 | 2 | 24 | 25 | +| test.cpp:179:24:179:37 | \\(([,\\w]+)+\\)$ | 21 | 0 | 14 | 24 | 37 | +| test.cpp:179:26:179:33 | ([,\\w]+) | 21 | 2 | 10 | 26 | 33 | +| test.cpp:179:26:179:34 | ([,\\w]+)+ | 21 | 2 | 11 | 26 | 34 | +| test.cpp:179:27:179:31 | [,\\w] | 21 | 3 | 8 | 27 | 31 | +| test.cpp:179:27:179:32 | [,\\w]+ | 21 | 3 | 9 | 27 | 32 | +| test.cpp:179:28:179:28 | , | 21 | 4 | 5 | 28 | 28 | +| test.cpp:179:29:179:30 | \\w | 21 | 5 | 7 | 29 | 30 | +| test.cpp:179:35:179:36 | \\) | 21 | 11 | 13 | 35 | 36 | +| test.cpp:179:37:179:37 | $ | 21 | 13 | 14 | 37 | 37 | +| test.cpp:182:25:182:25 | a | 21 | 0 | 1 | 25 | 25 | +| test.cpp:182:25:182:26 | a+ | 21 | 0 | 2 | 25 | 26 | +| test.cpp:182:25:182:27 | a+b | 21 | 0 | 3 | 25 | 27 | +| test.cpp:182:27:182:27 | b | 21 | 2 | 3 | 27 | 27 | +| test.cpp:185:24:185:24 | a | 22 | 0 | 1 | 24 | 24 | +| test.cpp:185:24:185:25 | a+ | 22 | 0 | 2 | 24 | 25 | +| test.cpp:185:24:185:26 | a+b | 22 | 0 | 3 | 24 | 26 | +| test.cpp:185:26:185:26 | b | 22 | 2 | 3 | 26 | 26 | +| test.cpp:188:30:188:30 | a | 26 | 0 | 1 | 30 | 30 | +| test.cpp:188:30:188:31 | a+ | 26 | 0 | 2 | 30 | 31 | +| test.cpp:188:30:188:32 | a+b | 26 | 0 | 3 | 30 | 32 | +| test.cpp:188:32:188:32 | b | 26 | 2 | 3 | 32 | 32 | +| test.cpp:192:22:192:23 | \\s | 21 | 0 | 2 | 22 | 23 | +| test.cpp:192:22:192:24 | \\s+ | 21 | 0 | 3 | 22 | 24 | +| test.cpp:195:22:195:22 | a | 21 | 0 | 1 | 22 | 22 | +| test.cpp:195:22:195:25 | a\\.b | 21 | 0 | 4 | 22 | 25 | +| test.cpp:195:23:195:24 | \\. | 21 | 1 | 3 | 23 | 24 | +| test.cpp:195:25:195:25 | b | 21 | 3 | 4 | 25 | 25 | +| test.cpp:199:22:199:22 | a | 19 | 0 | 1 | 22 | 22 | +| test.cpp:199:22:199:23 | a+ | 19 | 0 | 2 | 22 | 23 | +| test.cpp:199:22:199:24 | a+b | 19 | 0 | 3 | 22 | 24 | +| test.cpp:199:24:199:24 | b | 19 | 2 | 3 | 24 | 24 | +| test.cpp:202:28:202:28 | a | 23 | 0 | 1 | 28 | 28 | +| test.cpp:202:28:202:29 | a+ | 23 | 0 | 2 | 28 | 29 | +| test.cpp:202:28:202:30 | a+b | 23 | 0 | 3 | 28 | 30 | +| test.cpp:202:30:202:30 | b | 23 | 2 | 3 | 30 | 30 | diff --git a/cpp/ql/test/library-tests/regex/locations.ql b/cpp/ql/test/library-tests/regex/locations.ql new file mode 100644 index 000000000000..54f0ebd2a867 --- /dev/null +++ b/cpp/ql/test/library-tests/regex/locations.ql @@ -0,0 +1,26 @@ +/** + * Reports per-term location info for each regex term, showing the literal's + * start column, the term's value offsets, and the computed start/end columns. + * Used to verify that `hasLocationInfo` produces correct columns for plain, + * raw, and encoding-prefixed C++ string literals. + */ + +import cpp +import semmle.code.cpp.regex.RegexTreeView as RE + +query predicate locations( + RE::RegExpTerm t, + int litStartCol, + int valueStart, + int valueEnd, + int termStartCol, + int termEndCol +) { + // Only report terms from the location test function (test_locations) + // to keep the output focused. + t.getRegExp().getLocation().getStartLine() >= 142 and // test_locations starts here + t.getRegExp().getLocation().hasLocationInfo(_, _, litStartCol, _, _) and + valueStart = t.getStart() and + valueEnd = t.getEnd() and + t.hasLocationInfo(_, _, termStartCol, _, termEndCol) +} diff --git a/cpp/ql/test/library-tests/regex/options b/cpp/ql/test/library-tests/regex/options new file mode 100644 index 000000000000..2ebedec9a4cc --- /dev/null +++ b/cpp/ql/test/library-tests/regex/options @@ -0,0 +1 @@ +semmle-extractor-options: -std=c++17 diff --git a/cpp/ql/test/library-tests/regex/parse.expected b/cpp/ql/test/library-tests/regex/parse.expected new file mode 100644 index 000000000000..3f414bb46cab --- /dev/null +++ b/cpp/ql/test/library-tests/regex/parse.expected @@ -0,0 +1,854 @@ +test.cpp: +# 33| [RegExpConstant, RegExpNormalChar] abc + +# 36| [RegExpConstant, RegExpNormalChar] a + +# 36| [RegExpStar] a* +#-----| 0 -> [RegExpConstant, RegExpNormalChar] a + +# 36| [RegExpSequence] a*b+c?d +#-----| 0 -> [RegExpStar] a* +#-----| 1 -> [RegExpPlus] b+ +#-----| 2 -> [RegExpOpt] c? +#-----| 3 -> [RegExpConstant, RegExpNormalChar] d + +# 36| [RegExpConstant, RegExpNormalChar] b + +# 36| [RegExpPlus] b+ +#-----| 0 -> [RegExpConstant, RegExpNormalChar] b + +# 36| [RegExpConstant, RegExpNormalChar] c + +# 36| [RegExpOpt] c? +#-----| 0 -> [RegExpConstant, RegExpNormalChar] c + +# 36| [RegExpConstant, RegExpNormalChar] d + +# 37| [RegExpConstant, RegExpNormalChar] a + +# 37| [RegExpRange] a{4,8} +#-----| 0 -> [RegExpConstant, RegExpNormalChar] a + +# 39| [RegExpConstant, RegExpNormalChar] a + +# 39| [InfiniteRepetitionQuantifier, RegExpRange] a{3,} +#-----| 0 -> [RegExpConstant, RegExpNormalChar] a + +# 40| [RegExpConstant, RegExpNormalChar] a + +# 40| [RegExpRange] a{7} +#-----| 0 -> [RegExpConstant, RegExpNormalChar] a + +# 43| [RegExpConstant, RegExpNormalChar] foo + +# 43| [RegExpAlt] foo|bar +#-----| 0 -> [RegExpConstant, RegExpNormalChar] foo +#-----| 1 -> [RegExpConstant, RegExpNormalChar] bar + +# 43| [RegExpConstant, RegExpNormalChar] bar + +# 46| [RegExpCharacterClass] [abc] +#-----| 0 -> [RegExpConstant, RegExpNormalChar] a +#-----| 1 -> [RegExpConstant, RegExpNormalChar] b +#-----| 2 -> [RegExpConstant, RegExpNormalChar] c + +# 46| [RegExpConstant, RegExpNormalChar] a + +# 46| [RegExpConstant, RegExpNormalChar] b + +# 46| [RegExpConstant, RegExpNormalChar] c + +# 47| [RegExpCharacterClass] [a-fA-F0-9_] +#-----| 0 -> [RegExpCharacterRange] a-f +#-----| 1 -> [RegExpCharacterRange] A-F +#-----| 2 -> [RegExpCharacterRange] 0-9 +#-----| 3 -> [RegExpConstant, RegExpNormalChar] _ + +# 47| [RegExpConstant, RegExpNormalChar] a + +# 47| [RegExpCharacterRange] a-f +#-----| 0 -> [RegExpConstant, RegExpNormalChar] a +#-----| 1 -> [RegExpConstant, RegExpNormalChar] f + +# 47| [RegExpConstant, RegExpNormalChar] f + +# 47| [RegExpConstant, RegExpNormalChar] A + +# 47| [RegExpCharacterRange] A-F +#-----| 0 -> [RegExpConstant, RegExpNormalChar] A +#-----| 1 -> [RegExpConstant, RegExpNormalChar] F + +# 47| [RegExpConstant, RegExpNormalChar] F + +# 47| [RegExpConstant, RegExpNormalChar] 0 + +# 47| [RegExpCharacterRange] 0-9 +#-----| 0 -> [RegExpConstant, RegExpNormalChar] 0 +#-----| 1 -> [RegExpConstant, RegExpNormalChar] 9 + +# 47| [RegExpConstant, RegExpNormalChar] 9 + +# 47| [RegExpConstant, RegExpNormalChar] _ + +# 49| [RegExpCaret] ^ + +# 49| [RegExpSequence] ^[+-]?\d+ +#-----| 0 -> [RegExpCaret] ^ +#-----| 1 -> [RegExpOpt] [+-]? +#-----| 2 -> [RegExpPlus] \d+ + +# 49| [RegExpCharacterClass] [+-] +#-----| 0 -> [RegExpConstant, RegExpNormalChar] + +#-----| 1 -> [RegExpConstant, RegExpNormalChar] - + +# 49| [RegExpOpt] [+-]? +#-----| 0 -> [RegExpCharacterClass] [+-] + +# 49| [RegExpConstant, RegExpNormalChar] + + +# 49| [RegExpConstant, RegExpNormalChar] - + +# 49| [RegExpCharacterClassEscape] \d + +# 49| [RegExpPlus] \d+ +#-----| 0 -> [RegExpCharacterClassEscape] \d + +# 50| [RegExpCharacterClass] [\w] +#-----| 0 -> [RegExpCharacterClassEscape] \w + +# 50| [RegExpPlus] [\w]+ +#-----| 0 -> [RegExpCharacterClass] [\w] + +# 50| [RegExpCharacterClassEscape] \w + +# 51| [RegExpConstant, RegExpEscape] \[ + +# 51| [RegExpSequence] \[\][123] +#-----| 0 -> [RegExpConstant, RegExpEscape] \[ +#-----| 1 -> [RegExpConstant, RegExpEscape] \] +#-----| 2 -> [RegExpCharacterClass] [123] + +# 51| [RegExpConstant, RegExpEscape] \] + +# 51| [RegExpCharacterClass] [123] +#-----| 0 -> [RegExpConstant, RegExpNormalChar] 1 +#-----| 1 -> [RegExpConstant, RegExpNormalChar] 2 +#-----| 2 -> [RegExpConstant, RegExpNormalChar] 3 + +# 51| [RegExpConstant, RegExpNormalChar] 1 + +# 51| [RegExpConstant, RegExpNormalChar] 2 + +# 51| [RegExpConstant, RegExpNormalChar] 3 + +# 52| [RegExpCharacterClass] [^A-Z] +#-----| 0 -> [RegExpCharacterRange] A-Z + +# 52| [RegExpConstant, RegExpNormalChar] A + +# 52| [RegExpCharacterRange] A-Z +#-----| 0 -> [RegExpConstant, RegExpNormalChar] A +#-----| 1 -> [RegExpConstant, RegExpNormalChar] Z + +# 52| [RegExpConstant, RegExpNormalChar] Z + +# 53| [RegExpCharacterClass] []] +#-----| 0 -> [RegExpConstant, RegExpNormalChar] ] + +# 53| [RegExpConstant, RegExpNormalChar] ] + +# 54| [RegExpCharacterClass] [^]] +#-----| 0 -> [RegExpConstant, RegExpNormalChar] ] + +# 54| [RegExpConstant, RegExpNormalChar] ] + +# 55| [RegExpCharacterClass] [^-] +#-----| 0 -> [RegExpConstant, RegExpNormalChar] - + +# 55| [RegExpConstant, RegExpNormalChar] - + +# 56| [RegExpCharacterClass] [|] +#-----| 0 -> [RegExpConstant, RegExpNormalChar] | + +# 56| [RegExpConstant, RegExpNormalChar] | + +# 59| [RegExpCharacterClass] [[a-f] +#-----| 0 -> [RegExpConstant, RegExpNormalChar] [ +#-----| 1 -> [RegExpCharacterRange] a-f + +# 59| [RegExpSequence] [[a-f]A-F] +#-----| 0 -> [RegExpCharacterClass] [[a-f] +#-----| 1 -> [RegExpConstant, RegExpNormalChar] A-F] + +# 59| [RegExpConstant, RegExpNormalChar] [ + +# 59| [RegExpConstant, RegExpNormalChar] a + +# 59| [RegExpCharacterRange] a-f +#-----| 0 -> [RegExpConstant, RegExpNormalChar] a +#-----| 1 -> [RegExpConstant, RegExpNormalChar] f + +# 59| [RegExpConstant, RegExpNormalChar] f + +# 59| [RegExpConstant, RegExpNormalChar] A-F] + +# 65| [RegExpCharacterClass] [[:alpha:]] +#-----| 0 -> [RegExpNamedCharacterProperty] [:alpha:] + +# 65| [RegExpNamedCharacterProperty] [:alpha:] + +# 66| [RegExpCharacterClass] [[:digit:]] +#-----| 0 -> [RegExpNamedCharacterProperty] [:digit:] + +# 66| [RegExpNamedCharacterProperty] [:digit:] + +# 67| [RegExpCharacterClass] [[:space:]] +#-----| 0 -> [RegExpNamedCharacterProperty] [:space:] + +# 67| [RegExpNamedCharacterProperty] [:space:] + +# 68| [RegExpCharacterClass] [[:upper:]] +#-----| 0 -> [RegExpNamedCharacterProperty] [:upper:] + +# 68| [RegExpNamedCharacterProperty] [:upper:] + +# 69| [RegExpCharacterClass] [[:lower:]] +#-----| 0 -> [RegExpNamedCharacterProperty] [:lower:] + +# 69| [RegExpNamedCharacterProperty] [:lower:] + +# 70| [RegExpCharacterClass] [[:alnum:]] +#-----| 0 -> [RegExpNamedCharacterProperty] [:alnum:] + +# 70| [RegExpNamedCharacterProperty] [:alnum:] + +# 71| [RegExpCharacterClass] [[:print:]] +#-----| 0 -> [RegExpNamedCharacterProperty] [:print:] + +# 71| [RegExpNamedCharacterProperty] [:print:] + +# 72| [RegExpCharacterClass] [[:punct:]] +#-----| 0 -> [RegExpNamedCharacterProperty] [:punct:] + +# 72| [RegExpNamedCharacterProperty] [:punct:] + +# 75| [RegExpCharacterClass] [^[:space:]] +#-----| 0 -> [RegExpNamedCharacterProperty] [:space:] + +# 75| [RegExpNamedCharacterProperty] [:space:] + +# 78| [RegExpCharacterClass] [a[:space:]] +#-----| 0 -> [RegExpConstant, RegExpNormalChar] a +#-----| 1 -> [RegExpNamedCharacterProperty] [:space:] + +# 78| [RegExpConstant, RegExpNormalChar] a + +# 78| [RegExpNamedCharacterProperty] [:space:] + +# 81| [RegExpCharacterClass] [[.a.]] +#-----| 0 -> [RegExpNamedCharacterProperty] [.a.] + +# 81| [RegExpNamedCharacterProperty] [.a.] + +# 84| [RegExpCharacterClass] [[=a=]] +#-----| 0 -> [RegExpNamedCharacterProperty] [=a=] + +# 84| [RegExpNamedCharacterProperty] [=a=] + +# 87| [RegExpCharacterClass] [[:alpha:]0-9] +#-----| 0 -> [RegExpNamedCharacterProperty] [:alpha:] +#-----| 1 -> [RegExpCharacterRange] 0-9 + +# 87| [RegExpNamedCharacterProperty] [:alpha:] + +# 87| [RegExpConstant, RegExpNormalChar] 0 + +# 87| [RegExpCharacterRange] 0-9 +#-----| 0 -> [RegExpConstant, RegExpNormalChar] 0 +#-----| 1 -> [RegExpConstant, RegExpNormalChar] 9 + +# 87| [RegExpConstant, RegExpNormalChar] 9 + +# 90| [RegExpDot] . + +# 90| [RegExpStar] .* +#-----| 0 -> [RegExpDot] . + +# 91| [RegExpCharacterClassEscape] \w + +# 91| [RegExpPlus] \w+ +#-----| 0 -> [RegExpCharacterClassEscape] \w + +# 91| [RegExpSequence] \w+\W +#-----| 0 -> [RegExpPlus] \w+ +#-----| 1 -> [RegExpCharacterClassEscape] \W + +# 91| [RegExpCharacterClassEscape] \W + +# 92| [RegExpCharacterClassEscape] \s + +# 92| [RegExpSequence] \s\S +#-----| 0 -> [RegExpCharacterClassEscape] \s +#-----| 1 -> [RegExpCharacterClassEscape] \S + +# 92| [RegExpCharacterClassEscape] \S + +# 93| [RegExpCharacterClassEscape] \d + +# 93| [RegExpSequence] \d\D +#-----| 0 -> [RegExpCharacterClassEscape] \d +#-----| 1 -> [RegExpCharacterClassEscape] \D + +# 93| [RegExpCharacterClassEscape] \D + +# 95| [RegExpConstant, RegExpEscape] \n + +# 95| [RegExpSequence] \n\r\t +#-----| 0 -> [RegExpConstant, RegExpEscape] \n +#-----| 1 -> [RegExpConstant, RegExpEscape] \r +#-----| 2 -> [RegExpConstant, RegExpEscape] \t + +# 95| [RegExpConstant, RegExpEscape] \r + +# 95| [RegExpConstant, RegExpEscape] \t + +# 98| [RegExpConstant, RegExpNormalChar] a + +# 98| [RegExpSequence] a\0b +#-----| 0 -> [RegExpConstant, RegExpNormalChar] a +#-----| 1 -> [RegExpConstant, RegExpEscape] \0 +#-----| 2 -> [RegExpConstant, RegExpNormalChar] b + +# 98| [RegExpConstant, RegExpEscape] \0 + +# 98| [RegExpConstant, RegExpNormalChar] b + +# 102| [RegExpSpecialChar] \b + +# 102| [RegExpSequence] \b!a\B +#-----| 0 -> [RegExpSpecialChar] \b +#-----| 1 -> [RegExpConstant, RegExpNormalChar] !a +#-----| 2 -> [RegExpNonWordBoundary] \B + +# 102| [RegExpConstant, RegExpNormalChar] !a + +# 102| [RegExpNonWordBoundary] \B + +# 105| [RegExpGroup] (foo) +#-----| 0 -> [RegExpConstant, RegExpNormalChar] foo + +# 105| [RegExpStar] (foo)* +#-----| 0 -> [RegExpGroup] (foo) + +# 105| [RegExpSequence] (foo)*bar +#-----| 0 -> [RegExpStar] (foo)* +#-----| 1 -> [RegExpConstant, RegExpNormalChar] bar + +# 105| [RegExpConstant, RegExpNormalChar] foo + +# 105| [RegExpConstant, RegExpNormalChar] bar + +# 106| [RegExpConstant, RegExpNormalChar] fo + +# 106| [RegExpSequence] fo(o|b)ar +#-----| 0 -> [RegExpConstant, RegExpNormalChar] fo +#-----| 1 -> [RegExpGroup] (o|b) +#-----| 2 -> [RegExpConstant, RegExpNormalChar] ar + +# 106| [RegExpGroup] (o|b) +#-----| 0 -> [RegExpAlt] o|b + +# 106| [RegExpConstant, RegExpNormalChar] o + +# 106| [RegExpAlt] o|b +#-----| 0 -> [RegExpConstant, RegExpNormalChar] o +#-----| 1 -> [RegExpConstant, RegExpNormalChar] b + +# 106| [RegExpConstant, RegExpNormalChar] b + +# 106| [RegExpConstant, RegExpNormalChar] ar + +# 107| [RegExpGroup] (a|b|cd) +#-----| 0 -> [RegExpAlt] a|b|cd + +# 107| [RegExpSequence] (a|b|cd)e +#-----| 0 -> [RegExpGroup] (a|b|cd) +#-----| 1 -> [RegExpConstant, RegExpNormalChar] e + +# 107| [RegExpConstant, RegExpNormalChar] a + +# 107| [RegExpAlt] a|b|cd +#-----| 0 -> [RegExpConstant, RegExpNormalChar] a +#-----| 1 -> [RegExpConstant, RegExpNormalChar] b +#-----| 2 -> [RegExpConstant, RegExpNormalChar] cd + +# 107| [RegExpConstant, RegExpNormalChar] b + +# 107| [RegExpConstant, RegExpNormalChar] cd + +# 107| [RegExpConstant, RegExpNormalChar] e + +# 108| [RegExpGroup] (?::+) +#-----| 0 -> [RegExpPlus] :+ + +# 108| [RegExpSequence] (?::+)\w +#-----| 0 -> [RegExpGroup] (?::+) +#-----| 1 -> [RegExpCharacterClassEscape] \w + +# 108| [RegExpConstant, RegExpNormalChar] : + +# 108| [RegExpPlus] :+ +#-----| 0 -> [RegExpConstant, RegExpNormalChar] : + +# 108| [RegExpCharacterClassEscape] \w + +# 115| [RegExpGroup] (a+) +#-----| 0 -> [RegExpPlus] a+ + +# 115| [RegExpSequence] (a+)b+\1 +#-----| 0 -> [RegExpGroup] (a+) +#-----| 1 -> [RegExpPlus] b+ +#-----| 2 -> [RegExpBackRef] \1 + +# 115| [RegExpConstant, RegExpNormalChar] a + +# 115| [RegExpPlus] a+ +#-----| 0 -> [RegExpConstant, RegExpNormalChar] a + +# 115| [RegExpConstant, RegExpNormalChar] b + +# 115| [RegExpPlus] b+ +#-----| 0 -> [RegExpConstant, RegExpNormalChar] b + +# 115| [RegExpBackRef] \1 + +# 118| [RegExpCharacterClass] [a-f\d] +#-----| 0 -> [RegExpCharacterRange] a-f +#-----| 1 -> [RegExpCharacterClassEscape] \d + +# 118| [RegExpPlus] [a-f\d]+ +#-----| 0 -> [RegExpCharacterClass] [a-f\d] + +# 118| [RegExpConstant, RegExpNormalChar] a + +# 118| [RegExpCharacterRange] a-f +#-----| 0 -> [RegExpConstant, RegExpNormalChar] a +#-----| 1 -> [RegExpConstant, RegExpNormalChar] f + +# 118| [RegExpConstant, RegExpNormalChar] f + +# 118| [RegExpCharacterClassEscape] \d + +# 121| [RegExpCharacterClass] [[:alpha:]] +#-----| 0 -> [RegExpNamedCharacterProperty] [:alpha:] + +# 121| [RegExpSequence] [[:alpha:]][[:digit:]] +#-----| 0 -> [RegExpCharacterClass] [[:alpha:]] +#-----| 1 -> [RegExpCharacterClass] [[:digit:]] + +# 121| [RegExpNamedCharacterProperty] [:alpha:] + +# 121| [RegExpCharacterClass] [[:digit:]] +#-----| 0 -> [RegExpNamedCharacterProperty] [:digit:] + +# 121| [RegExpNamedCharacterProperty] [:digit:] + +# 124| [RegExpCharacterClass] [[:alpha:][:digit:]] +#-----| 0 -> [RegExpNamedCharacterProperty] [:alpha:] +#-----| 1 -> [RegExpNamedCharacterProperty] [:digit:] + +# 124| [RegExpNamedCharacterProperty] [:alpha:] + +# 124| [RegExpNamedCharacterProperty] [:digit:] + +# 127| [RegExpCharacterClass] [A-F[:digit:]a-f] +#-----| 0 -> [RegExpCharacterRange] A-F +#-----| 1 -> [RegExpNamedCharacterProperty] [:digit:] +#-----| 2 -> [RegExpCharacterRange] a-f + +# 127| [RegExpConstant, RegExpNormalChar] A + +# 127| [RegExpCharacterRange] A-F +#-----| 0 -> [RegExpConstant, RegExpNormalChar] A +#-----| 1 -> [RegExpConstant, RegExpNormalChar] F + +# 127| [RegExpConstant, RegExpNormalChar] F + +# 127| [RegExpNamedCharacterProperty] [:digit:] + +# 127| [RegExpConstant, RegExpNormalChar] a + +# 127| [RegExpCharacterRange] a-f +#-----| 0 -> [RegExpConstant, RegExpNormalChar] a +#-----| 1 -> [RegExpConstant, RegExpNormalChar] f + +# 127| [RegExpConstant, RegExpNormalChar] f + +# 130| [RegExpCharacterClass] [:digit:] +#-----| 0 -> [RegExpConstant, RegExpNormalChar] : +#-----| 1 -> [RegExpConstant, RegExpNormalChar] d +#-----| 2 -> [RegExpConstant, RegExpNormalChar] i +#-----| 3 -> [RegExpConstant, RegExpNormalChar] g +#-----| 4 -> [RegExpConstant, RegExpNormalChar] i +#-----| 5 -> [RegExpConstant, RegExpNormalChar] t +#-----| 6 -> [RegExpConstant, RegExpNormalChar] : + +# 130| [RegExpConstant, RegExpNormalChar] : + +# 130| [RegExpConstant, RegExpNormalChar] d + +# 130| [RegExpConstant, RegExpNormalChar] i + +# 130| [RegExpConstant, RegExpNormalChar] g + +# 130| [RegExpConstant, RegExpNormalChar] i + +# 130| [RegExpConstant, RegExpNormalChar] t + +# 130| [RegExpConstant, RegExpNormalChar] : + +# 133| [RegExpCharacterClass] [:alpha:] +#-----| 0 -> [RegExpConstant, RegExpNormalChar] : +#-----| 1 -> [RegExpConstant, RegExpNormalChar] a +#-----| 2 -> [RegExpConstant, RegExpNormalChar] l +#-----| 3 -> [RegExpConstant, RegExpNormalChar] p +#-----| 4 -> [RegExpConstant, RegExpNormalChar] h +#-----| 5 -> [RegExpConstant, RegExpNormalChar] a +#-----| 6 -> [RegExpConstant, RegExpNormalChar] : + +# 133| [RegExpConstant, RegExpNormalChar] : + +# 133| [RegExpConstant, RegExpNormalChar] a + +# 133| [RegExpConstant, RegExpNormalChar] l + +# 133| [RegExpConstant, RegExpNormalChar] p + +# 133| [RegExpConstant, RegExpNormalChar] h + +# 133| [RegExpConstant, RegExpNormalChar] a + +# 133| [RegExpConstant, RegExpNormalChar] : + +# 136| [RegExpConstant, RegExpNormalChar] a + +# 136| [RegExpSequence] a[:b:]c +#-----| 0 -> [RegExpConstant, RegExpNormalChar] a +#-----| 1 -> [RegExpCharacterClass] [:b:] +#-----| 2 -> [RegExpConstant, RegExpNormalChar] c + +# 136| [RegExpCharacterClass] [:b:] +#-----| 0 -> [RegExpConstant, RegExpNormalChar] : +#-----| 1 -> [RegExpConstant, RegExpNormalChar] b +#-----| 2 -> [RegExpConstant, RegExpNormalChar] : + +# 136| [RegExpConstant, RegExpNormalChar] : + +# 136| [RegExpConstant, RegExpNormalChar] b + +# 136| [RegExpConstant, RegExpNormalChar] : + +# 136| [RegExpConstant, RegExpNormalChar] c + +# 139| [RegExpCharacterClass] [[:alpha:]-z] +#-----| 0 -> [RegExpCharacterRange] [:alpha:]-z + +# 139| [RegExpNamedCharacterProperty] [:alpha:] + +# 139| [RegExpCharacterRange] [:alpha:]-z +#-----| 0 -> [RegExpNamedCharacterProperty] [:alpha:] +#-----| 1 -> [RegExpConstant, RegExpNormalChar] z + +# 139| [RegExpConstant, RegExpNormalChar] z + +# 142| [RegExpCharacterClass] [[:alpha] +#-----| 0 -> [RegExpConstant, RegExpNormalChar] [ +#-----| 1 -> [RegExpConstant, RegExpNormalChar] : +#-----| 2 -> [RegExpConstant, RegExpNormalChar] a +#-----| 3 -> [RegExpConstant, RegExpNormalChar] l +#-----| 4 -> [RegExpConstant, RegExpNormalChar] p +#-----| 5 -> [RegExpConstant, RegExpNormalChar] h +#-----| 6 -> [RegExpConstant, RegExpNormalChar] a + +# 142| [RegExpConstant, RegExpNormalChar] [ + +# 142| [RegExpConstant, RegExpNormalChar] : + +# 142| [RegExpConstant, RegExpNormalChar] a + +# 142| [RegExpConstant, RegExpNormalChar] l + +# 142| [RegExpConstant, RegExpNormalChar] p + +# 142| [RegExpConstant, RegExpNormalChar] h + +# 142| [RegExpConstant, RegExpNormalChar] a + +# 145| [RegExpCharacterClass] []a[:alpha:] +#-----| 0 -> [RegExpConstant, RegExpNormalChar] ] +#-----| 1 -> [RegExpConstant, RegExpNormalChar] a +#-----| 2 -> [RegExpConstant, RegExpNormalChar] [ +#-----| 3 -> [RegExpConstant, RegExpNormalChar] : +#-----| 4 -> [RegExpConstant, RegExpNormalChar] a +#-----| 5 -> [RegExpConstant, RegExpNormalChar] l +#-----| 6 -> [RegExpConstant, RegExpNormalChar] p +#-----| 7 -> [RegExpConstant, RegExpNormalChar] h +#-----| 8 -> [RegExpConstant, RegExpNormalChar] a +#-----| 9 -> [RegExpConstant, RegExpNormalChar] : + +# 145| [RegExpSequence] []a[:alpha:]] +#-----| 0 -> [RegExpCharacterClass] []a[:alpha:] +#-----| 1 -> [RegExpConstant, RegExpNormalChar] ] + +# 145| [RegExpConstant, RegExpNormalChar] ] + +# 145| [RegExpConstant, RegExpNormalChar] a + +# 145| [RegExpConstant, RegExpNormalChar] [ + +# 145| [RegExpConstant, RegExpNormalChar] : + +# 145| [RegExpConstant, RegExpNormalChar] a + +# 145| [RegExpConstant, RegExpNormalChar] l + +# 145| [RegExpConstant, RegExpNormalChar] p + +# 145| [RegExpConstant, RegExpNormalChar] h + +# 145| [RegExpConstant, RegExpNormalChar] a + +# 145| [RegExpConstant, RegExpNormalChar] : + +# 145| [RegExpConstant, RegExpNormalChar] ] + +# 148| [RegExpCharacterClass] [[:alpha:][:digit:][:space:]] +#-----| 0 -> [RegExpNamedCharacterProperty] [:alpha:] +#-----| 1 -> [RegExpNamedCharacterProperty] [:digit:] +#-----| 2 -> [RegExpNamedCharacterProperty] [:space:] + +# 148| [RegExpNamedCharacterProperty] [:alpha:] + +# 148| [RegExpNamedCharacterProperty] [:digit:] + +# 148| [RegExpNamedCharacterProperty] [:space:] + +# 151| [RegExpCharacterClass] [[:xdigit:]] +#-----| 0 -> [RegExpNamedCharacterProperty] [:xdigit:] + +# 151| [RegExpNamedCharacterProperty] [:xdigit:] + +# 152| [RegExpCharacterClass] [[:blank:]] +#-----| 0 -> [RegExpNamedCharacterProperty] [:blank:] + +# 152| [RegExpNamedCharacterProperty] [:blank:] + +# 153| [RegExpCharacterClass] [[:cntrl:]] +#-----| 0 -> [RegExpNamedCharacterProperty] [:cntrl:] + +# 153| [RegExpNamedCharacterProperty] [:cntrl:] + +# 154| [RegExpCharacterClass] [[:graph:]] +#-----| 0 -> [RegExpNamedCharacterProperty] [:graph:] + +# 154| [RegExpNamedCharacterProperty] [:graph:] + +# 157| [RegExpCaret] ^ + +# 157| [RegExpSequence] ^([[:alpha:]]+[[:digit:]]*[[:space:]]?)+([[:punct:]]|[[:xdigit:]])+$ +#-----| 0 -> [RegExpCaret] ^ +#-----| 1 -> [RegExpPlus] ([[:alpha:]]+[[:digit:]]*[[:space:]]?)+ +#-----| 2 -> [RegExpPlus] ([[:punct:]]|[[:xdigit:]])+ +#-----| 3 -> [RegExpDollar] $ + +# 157| [RegExpGroup] ([[:alpha:]]+[[:digit:]]*[[:space:]]?) +#-----| 0 -> [RegExpSequence] [[:alpha:]]+[[:digit:]]*[[:space:]]? + +# 157| [RegExpPlus] ([[:alpha:]]+[[:digit:]]*[[:space:]]?)+ +#-----| 0 -> [RegExpGroup] ([[:alpha:]]+[[:digit:]]*[[:space:]]?) + +# 157| [RegExpCharacterClass] [[:alpha:]] +#-----| 0 -> [RegExpNamedCharacterProperty] [:alpha:] + +# 157| [RegExpPlus] [[:alpha:]]+ +#-----| 0 -> [RegExpCharacterClass] [[:alpha:]] + +# 157| [RegExpSequence] [[:alpha:]]+[[:digit:]]*[[:space:]]? +#-----| 0 -> [RegExpPlus] [[:alpha:]]+ +#-----| 1 -> [RegExpStar] [[:digit:]]* +#-----| 2 -> [RegExpOpt] [[:space:]]? + +# 157| [RegExpNamedCharacterProperty] [:alpha:] + +# 157| [RegExpCharacterClass] [[:digit:]] +#-----| 0 -> [RegExpNamedCharacterProperty] [:digit:] + +# 157| [RegExpStar] [[:digit:]]* +#-----| 0 -> [RegExpCharacterClass] [[:digit:]] + +# 157| [RegExpNamedCharacterProperty] [:digit:] + +# 157| [RegExpCharacterClass] [[:space:]] +#-----| 0 -> [RegExpNamedCharacterProperty] [:space:] + +# 157| [RegExpOpt] [[:space:]]? +#-----| 0 -> [RegExpCharacterClass] [[:space:]] + +# 157| [RegExpNamedCharacterProperty] [:space:] + +# 157| [RegExpGroup] ([[:punct:]]|[[:xdigit:]]) +#-----| 0 -> [RegExpAlt] [[:punct:]]|[[:xdigit:]] + +# 157| [RegExpPlus] ([[:punct:]]|[[:xdigit:]])+ +#-----| 0 -> [RegExpGroup] ([[:punct:]]|[[:xdigit:]]) + +# 157| [RegExpCharacterClass] [[:punct:]] +#-----| 0 -> [RegExpNamedCharacterProperty] [:punct:] + +# 157| [RegExpAlt] [[:punct:]]|[[:xdigit:]] +#-----| 0 -> [RegExpCharacterClass] [[:punct:]] +#-----| 1 -> [RegExpCharacterClass] [[:xdigit:]] + +# 157| [RegExpNamedCharacterProperty] [:punct:] + +# 157| [RegExpCharacterClass] [[:xdigit:]] +#-----| 0 -> [RegExpNamedCharacterProperty] [:xdigit:] + +# 157| [RegExpNamedCharacterProperty] [:xdigit:] + +# 157| [RegExpDollar] $ + +# 162| [RegExpConstant, RegExpEscape] \u9879 + +# 170| [RegExpConstant, RegExpNormalChar] a + +# 170| [RegExpPlus] a+ +#-----| 0 -> [RegExpConstant, RegExpNormalChar] a + +# 170| [RegExpSequence] a+b +#-----| 0 -> [RegExpPlus] a+ +#-----| 1 -> [RegExpConstant, RegExpNormalChar] b + +# 170| [RegExpConstant, RegExpNormalChar] b + +# 173| [RegExpConstant, RegExpNormalChar] a + +# 173| [RegExpPlus] a+ +#-----| 0 -> [RegExpConstant, RegExpNormalChar] a + +# 173| [RegExpSequence] a+b +#-----| 0 -> [RegExpPlus] a+ +#-----| 1 -> [RegExpConstant, RegExpNormalChar] b + +# 173| [RegExpConstant, RegExpNormalChar] b + +# 176| [RegExpCharacterClassEscape] \s + +# 176| [RegExpPlus] \s+ +#-----| 0 -> [RegExpCharacterClassEscape] \s + +# 176| [RegExpSequence] \s+$ +#-----| 0 -> [RegExpPlus] \s+ +#-----| 1 -> [RegExpDollar] $ + +# 176| [RegExpDollar] $ + +# 179| [RegExpConstant, RegExpEscape] \( + +# 179| [RegExpSequence] \(([,\w]+)+\)$ +#-----| 0 -> [RegExpConstant, RegExpEscape] \( +#-----| 1 -> [RegExpPlus] ([,\w]+)+ +#-----| 2 -> [RegExpConstant, RegExpEscape] \) +#-----| 3 -> [RegExpDollar] $ + +# 179| [RegExpGroup] ([,\w]+) +#-----| 0 -> [RegExpPlus] [,\w]+ + +# 179| [RegExpPlus] ([,\w]+)+ +#-----| 0 -> [RegExpGroup] ([,\w]+) + +# 179| [RegExpCharacterClass] [,\w] +#-----| 0 -> [RegExpConstant, RegExpNormalChar] , +#-----| 1 -> [RegExpCharacterClassEscape] \w + +# 179| [RegExpPlus] [,\w]+ +#-----| 0 -> [RegExpCharacterClass] [,\w] + +# 179| [RegExpConstant, RegExpNormalChar] , + +# 179| [RegExpCharacterClassEscape] \w + +# 179| [RegExpConstant, RegExpEscape] \) + +# 179| [RegExpDollar] $ + +# 182| [RegExpConstant, RegExpNormalChar] a + +# 182| [RegExpPlus] a+ +#-----| 0 -> [RegExpConstant, RegExpNormalChar] a + +# 182| [RegExpSequence] a+b +#-----| 0 -> [RegExpPlus] a+ +#-----| 1 -> [RegExpConstant, RegExpNormalChar] b + +# 182| [RegExpConstant, RegExpNormalChar] b + +# 185| [RegExpConstant, RegExpNormalChar] a + +# 185| [RegExpPlus] a+ +#-----| 0 -> [RegExpConstant, RegExpNormalChar] a + +# 185| [RegExpSequence] a+b +#-----| 0 -> [RegExpPlus] a+ +#-----| 1 -> [RegExpConstant, RegExpNormalChar] b + +# 185| [RegExpConstant, RegExpNormalChar] b + +# 188| [RegExpConstant, RegExpNormalChar] a + +# 188| [RegExpPlus] a+ +#-----| 0 -> [RegExpConstant, RegExpNormalChar] a + +# 188| [RegExpSequence] a+b +#-----| 0 -> [RegExpPlus] a+ +#-----| 1 -> [RegExpConstant, RegExpNormalChar] b + +# 188| [RegExpConstant, RegExpNormalChar] b + +# 192| [RegExpCharacterClassEscape] \s + +# 192| [RegExpPlus] \s+ +#-----| 0 -> [RegExpCharacterClassEscape] \s + +# 195| [RegExpConstant, RegExpNormalChar] a + +# 195| [RegExpSequence] a\.b +#-----| 0 -> [RegExpConstant, RegExpNormalChar] a +#-----| 1 -> [RegExpConstant, RegExpEscape] \. +#-----| 2 -> [RegExpConstant, RegExpNormalChar] b + +# 195| [RegExpConstant, RegExpEscape] \. + +# 195| [RegExpConstant, RegExpNormalChar] b + +# 199| [RegExpConstant, RegExpNormalChar] a + +# 199| [RegExpPlus] a+ +#-----| 0 -> [RegExpConstant, RegExpNormalChar] a + +# 199| [RegExpSequence] a+b +#-----| 0 -> [RegExpPlus] a+ +#-----| 1 -> [RegExpConstant, RegExpNormalChar] b + +# 199| [RegExpConstant, RegExpNormalChar] b + +# 202| [RegExpConstant, RegExpNormalChar] a + +# 202| [RegExpPlus] a+ +#-----| 0 -> [RegExpConstant, RegExpNormalChar] a + +# 202| [RegExpSequence] a+b +#-----| 0 -> [RegExpPlus] a+ +#-----| 1 -> [RegExpConstant, RegExpNormalChar] b + +# 202| [RegExpConstant, RegExpNormalChar] b diff --git a/cpp/ql/test/library-tests/regex/parse.ql b/cpp/ql/test/library-tests/regex/parse.ql new file mode 100644 index 000000000000..0f0ec1521322 --- /dev/null +++ b/cpp/ql/test/library-tests/regex/parse.ql @@ -0,0 +1,27 @@ +/** + * @kind graph + */ + +import cpp +import semmle.code.cpp.regex.RegexTreeView as RE + +query predicate nodes(RE::RegExpTerm n, string attr, string val) { + attr = "semmle.label" and + val = "[" + concat(n.getAPrimaryQlClass(), ", ") + "] " + n.toString() + or + attr = "semmle.order" and + val = + any(int i | + n = + rank[i](RE::RegExpTerm t, string fp, int sl, int sc, int el, int ec | + t.hasLocationInfo(fp, sl, sc, el, ec) + | + t order by fp, sl, sc, el, ec, t.toString() + ) + ).toString() +} + +query predicate edges(RE::RegExpTerm pred, RE::RegExpTerm succ, string attr, string val) { + attr in ["semmle.label", "semmle.order"] and + val = any(int i | succ = pred.getChild(i)).toString() +} diff --git a/cpp/ql/test/library-tests/regex/regexp.expected b/cpp/ql/test/library-tests/regex/regexp.expected new file mode 100644 index 000000000000..03b6bf257656 --- /dev/null +++ b/cpp/ql/test/library-tests/regex/regexp.expected @@ -0,0 +1,456 @@ +groupNumber +| test.cpp:105:22:105:26 | (foo) | 1 | +| test.cpp:106:24:106:28 | (o\|b) | 1 | +| test.cpp:107:22:107:29 | (a\|b\|cd) | 1 | +| test.cpp:115:23:115:26 | (a+) | 1 | +| test.cpp:157:26:157:63 | ([[:alpha:]]+[[:digit:]]*[[:space:]]?) | 1 | +| test.cpp:157:65:157:90 | ([[:punct:]]\|[[:xdigit:]]) | 2 | +| test.cpp:179:26:179:33 | ([,\\w]+) | 1 | +term +| test.cpp:33:21:33:23 | abc | RegExpConstant,RegExpNormalChar | +| test.cpp:36:22:36:22 | a | RegExpConstant,RegExpNormalChar | +| test.cpp:36:22:36:23 | a* | RegExpStar | +| test.cpp:36:22:36:28 | a*b+c?d | RegExpSequence | +| test.cpp:36:24:36:24 | b | RegExpConstant,RegExpNormalChar | +| test.cpp:36:24:36:25 | b+ | RegExpPlus | +| test.cpp:36:26:36:26 | c | RegExpConstant,RegExpNormalChar | +| test.cpp:36:26:36:27 | c? | RegExpOpt | +| test.cpp:36:28:36:28 | d | RegExpConstant,RegExpNormalChar | +| test.cpp:37:22:37:22 | a | RegExpConstant,RegExpNormalChar | +| test.cpp:37:22:37:27 | a{4,8} | RegExpRange | +| test.cpp:39:22:39:22 | a | RegExpConstant,RegExpNormalChar | +| test.cpp:39:22:39:26 | a{3,} | InfiniteRepetitionQuantifier,RegExpRange | +| test.cpp:40:22:40:22 | a | RegExpConstant,RegExpNormalChar | +| test.cpp:40:22:40:25 | a{7} | RegExpRange | +| test.cpp:43:21:43:23 | foo | RegExpConstant,RegExpNormalChar | +| test.cpp:43:21:43:27 | foo\|bar | RegExpAlt | +| test.cpp:43:25:43:27 | bar | RegExpConstant,RegExpNormalChar | +| test.cpp:46:21:46:25 | [abc] | RegExpCharacterClass | +| test.cpp:46:22:46:22 | a | RegExpConstant,RegExpNormalChar | +| test.cpp:46:23:46:23 | b | RegExpConstant,RegExpNormalChar | +| test.cpp:46:24:46:24 | c | RegExpConstant,RegExpNormalChar | +| test.cpp:47:21:47:32 | [a-fA-F0-9_] | RegExpCharacterClass | +| test.cpp:47:22:47:22 | a | RegExpConstant,RegExpNormalChar | +| test.cpp:47:22:47:24 | a-f | RegExpCharacterRange | +| test.cpp:47:24:47:24 | f | RegExpConstant,RegExpNormalChar | +| test.cpp:47:25:47:25 | A | RegExpConstant,RegExpNormalChar | +| test.cpp:47:25:47:27 | A-F | RegExpCharacterRange | +| test.cpp:47:27:47:27 | F | RegExpConstant,RegExpNormalChar | +| test.cpp:47:28:47:28 | 0 | RegExpConstant,RegExpNormalChar | +| test.cpp:47:28:47:30 | 0-9 | RegExpCharacterRange | +| test.cpp:47:30:47:30 | 9 | RegExpConstant,RegExpNormalChar | +| test.cpp:47:31:47:31 | _ | RegExpConstant,RegExpNormalChar | +| test.cpp:49:21:49:21 | ^ | RegExpCaret | +| test.cpp:49:21:49:29 | ^[+-]?\\d+ | RegExpSequence | +| test.cpp:49:22:49:25 | [+-] | RegExpCharacterClass | +| test.cpp:49:22:49:26 | [+-]? | RegExpOpt | +| test.cpp:49:23:49:23 | + | RegExpConstant,RegExpNormalChar | +| test.cpp:49:24:49:24 | - | RegExpConstant,RegExpNormalChar | +| test.cpp:49:27:49:28 | \\d | RegExpCharacterClassEscape | +| test.cpp:49:27:49:29 | \\d+ | RegExpPlus | +| test.cpp:50:21:50:24 | [\\w] | RegExpCharacterClass | +| test.cpp:50:21:50:25 | [\\w]+ | RegExpPlus | +| test.cpp:50:22:50:23 | \\w | RegExpCharacterClassEscape | +| test.cpp:51:21:51:22 | \\[ | RegExpConstant,RegExpEscape | +| test.cpp:51:21:51:29 | \\[\\][123] | RegExpSequence | +| test.cpp:51:23:51:24 | \\] | RegExpConstant,RegExpEscape | +| test.cpp:51:25:51:29 | [123] | RegExpCharacterClass | +| test.cpp:51:26:51:26 | 1 | RegExpConstant,RegExpNormalChar | +| test.cpp:51:27:51:27 | 2 | RegExpConstant,RegExpNormalChar | +| test.cpp:51:28:51:28 | 3 | RegExpConstant,RegExpNormalChar | +| test.cpp:52:21:52:26 | [^A-Z] | RegExpCharacterClass | +| test.cpp:52:23:52:23 | A | RegExpConstant,RegExpNormalChar | +| test.cpp:52:23:52:25 | A-Z | RegExpCharacterRange | +| test.cpp:52:25:52:25 | Z | RegExpConstant,RegExpNormalChar | +| test.cpp:53:21:53:23 | []] | RegExpCharacterClass | +| test.cpp:53:22:53:22 | ] | RegExpConstant,RegExpNormalChar | +| test.cpp:54:21:54:24 | [^]] | RegExpCharacterClass | +| test.cpp:54:23:54:23 | ] | RegExpConstant,RegExpNormalChar | +| test.cpp:55:21:55:24 | [^-] | RegExpCharacterClass | +| test.cpp:55:23:55:23 | - | RegExpConstant,RegExpNormalChar | +| test.cpp:56:22:56:24 | [\|] | RegExpCharacterClass | +| test.cpp:56:23:56:23 | \| | RegExpConstant,RegExpNormalChar | +| test.cpp:59:24:59:29 | [[a-f] | RegExpCharacterClass | +| test.cpp:59:24:59:33 | [[a-f]A-F] | RegExpSequence | +| test.cpp:59:25:59:25 | [ | RegExpConstant,RegExpNormalChar | +| test.cpp:59:26:59:26 | a | RegExpConstant,RegExpNormalChar | +| test.cpp:59:26:59:28 | a-f | RegExpCharacterRange | +| test.cpp:59:28:59:28 | f | RegExpConstant,RegExpNormalChar | +| test.cpp:59:30:59:33 | A-F] | RegExpConstant,RegExpNormalChar | +| test.cpp:65:29:65:39 | [[:alpha:]] | RegExpCharacterClass | +| test.cpp:65:30:65:38 | [:alpha:] | RegExpNamedCharacterProperty | +| test.cpp:66:29:66:39 | [[:digit:]] | RegExpCharacterClass | +| test.cpp:66:30:66:38 | [:digit:] | RegExpNamedCharacterProperty | +| test.cpp:67:29:67:39 | [[:space:]] | RegExpCharacterClass | +| test.cpp:67:30:67:38 | [:space:] | RegExpNamedCharacterProperty | +| test.cpp:68:29:68:39 | [[:upper:]] | RegExpCharacterClass | +| test.cpp:68:30:68:38 | [:upper:] | RegExpNamedCharacterProperty | +| test.cpp:69:29:69:39 | [[:lower:]] | RegExpCharacterClass | +| test.cpp:69:30:69:38 | [:lower:] | RegExpNamedCharacterProperty | +| test.cpp:70:29:70:39 | [[:alnum:]] | RegExpCharacterClass | +| test.cpp:70:30:70:38 | [:alnum:] | RegExpNamedCharacterProperty | +| test.cpp:71:29:71:39 | [[:print:]] | RegExpCharacterClass | +| test.cpp:71:30:71:38 | [:print:] | RegExpNamedCharacterProperty | +| test.cpp:72:29:72:39 | [[:punct:]] | RegExpCharacterClass | +| test.cpp:72:30:72:38 | [:punct:] | RegExpNamedCharacterProperty | +| test.cpp:75:27:75:38 | [^[:space:]] | RegExpCharacterClass | +| test.cpp:75:29:75:37 | [:space:] | RegExpNamedCharacterProperty | +| test.cpp:78:29:78:40 | [a[:space:]] | RegExpCharacterClass | +| test.cpp:78:30:78:30 | a | RegExpConstant,RegExpNormalChar | +| test.cpp:78:31:78:39 | [:space:] | RegExpNamedCharacterProperty | +| test.cpp:81:28:81:34 | [[.a.]] | RegExpCharacterClass | +| test.cpp:81:29:81:33 | [.a.] | RegExpNamedCharacterProperty | +| test.cpp:84:29:84:35 | [[=a=]] | RegExpCharacterClass | +| test.cpp:84:30:84:34 | [=a=] | RegExpNamedCharacterProperty | +| test.cpp:87:29:87:42 | [[:alpha:]0-9] | RegExpCharacterClass | +| test.cpp:87:30:87:38 | [:alpha:] | RegExpNamedCharacterProperty | +| test.cpp:87:39:87:39 | 0 | RegExpConstant,RegExpNormalChar | +| test.cpp:87:39:87:41 | 0-9 | RegExpCharacterRange | +| test.cpp:87:41:87:41 | 9 | RegExpConstant,RegExpNormalChar | +| test.cpp:90:23:90:23 | . | RegExpDot | +| test.cpp:90:23:90:24 | .* | RegExpStar | +| test.cpp:91:23:91:24 | \\w | RegExpCharacterClassEscape | +| test.cpp:91:23:91:25 | \\w+ | RegExpPlus | +| test.cpp:91:23:91:27 | \\w+\\W | RegExpSequence | +| test.cpp:91:26:91:27 | \\W | RegExpCharacterClassEscape | +| test.cpp:92:23:92:24 | \\s | RegExpCharacterClassEscape | +| test.cpp:92:23:92:26 | \\s\\S | RegExpSequence | +| test.cpp:92:25:92:26 | \\S | RegExpCharacterClassEscape | +| test.cpp:93:23:93:24 | \\d | RegExpCharacterClassEscape | +| test.cpp:93:23:93:26 | \\d\\D | RegExpSequence | +| test.cpp:93:25:93:26 | \\D | RegExpCharacterClassEscape | +| test.cpp:95:23:95:24 | \\n | RegExpConstant,RegExpEscape | +| test.cpp:95:23:95:28 | \\n\\r\\t | RegExpSequence | +| test.cpp:95:25:95:26 | \\r | RegExpConstant,RegExpEscape | +| test.cpp:95:27:95:28 | \\t | RegExpConstant,RegExpEscape | +| test.cpp:98:21:98:21 | a | RegExpConstant,RegExpNormalChar | +| test.cpp:98:21:98:24 | a\\0b | RegExpSequence | +| test.cpp:98:22:98:23 | \\0 | RegExpConstant,RegExpEscape | +| test.cpp:98:24:98:24 | b | RegExpConstant,RegExpNormalChar | +| test.cpp:102:22:102:23 | \\b | RegExpSpecialChar | +| test.cpp:102:22:102:27 | \\b!a\\B | RegExpSequence | +| test.cpp:102:24:102:25 | !a | RegExpConstant,RegExpNormalChar | +| test.cpp:102:26:102:27 | \\B | RegExpNonWordBoundary | +| test.cpp:105:22:105:26 | (foo) | RegExpGroup | +| test.cpp:105:22:105:27 | (foo)* | RegExpStar | +| test.cpp:105:22:105:30 | (foo)*bar | RegExpSequence | +| test.cpp:105:23:105:25 | foo | RegExpConstant,RegExpNormalChar | +| test.cpp:105:28:105:30 | bar | RegExpConstant,RegExpNormalChar | +| test.cpp:106:22:106:23 | fo | RegExpConstant,RegExpNormalChar | +| test.cpp:106:22:106:30 | fo(o\|b)ar | RegExpSequence | +| test.cpp:106:24:106:28 | (o\|b) | RegExpGroup | +| test.cpp:106:25:106:25 | o | RegExpConstant,RegExpNormalChar | +| test.cpp:106:25:106:27 | o\|b | RegExpAlt | +| test.cpp:106:27:106:27 | b | RegExpConstant,RegExpNormalChar | +| test.cpp:106:29:106:30 | ar | RegExpConstant,RegExpNormalChar | +| test.cpp:107:22:107:29 | (a\|b\|cd) | RegExpGroup | +| test.cpp:107:22:107:30 | (a\|b\|cd)e | RegExpSequence | +| test.cpp:107:23:107:23 | a | RegExpConstant,RegExpNormalChar | +| test.cpp:107:23:107:28 | a\|b\|cd | RegExpAlt | +| test.cpp:107:25:107:25 | b | RegExpConstant,RegExpNormalChar | +| test.cpp:107:27:107:28 | cd | RegExpConstant,RegExpNormalChar | +| test.cpp:107:30:107:30 | e | RegExpConstant,RegExpNormalChar | +| test.cpp:108:22:108:27 | (?::+) | RegExpGroup | +| test.cpp:108:22:108:29 | (?::+)\\w | RegExpSequence | +| test.cpp:108:25:108:25 | : | RegExpConstant,RegExpNormalChar | +| test.cpp:108:25:108:26 | :+ | RegExpPlus | +| test.cpp:108:28:108:29 | \\w | RegExpCharacterClassEscape | +| test.cpp:115:23:115:26 | (a+) | RegExpGroup | +| test.cpp:115:23:115:30 | (a+)b+\\1 | RegExpSequence | +| test.cpp:115:24:115:24 | a | RegExpConstant,RegExpNormalChar | +| test.cpp:115:24:115:25 | a+ | RegExpPlus | +| test.cpp:115:27:115:27 | b | RegExpConstant,RegExpNormalChar | +| test.cpp:115:27:115:28 | b+ | RegExpPlus | +| test.cpp:115:29:115:30 | \\1 | RegExpBackRef | +| test.cpp:118:23:118:29 | [a-f\\d] | RegExpCharacterClass | +| test.cpp:118:23:118:30 | [a-f\\d]+ | RegExpPlus | +| test.cpp:118:24:118:24 | a | RegExpConstant,RegExpNormalChar | +| test.cpp:118:24:118:26 | a-f | RegExpCharacterRange | +| test.cpp:118:26:118:26 | f | RegExpConstant,RegExpNormalChar | +| test.cpp:118:27:118:28 | \\d | RegExpCharacterClassEscape | +| test.cpp:121:24:121:34 | [[:alpha:]] | RegExpCharacterClass | +| test.cpp:121:24:121:45 | [[:alpha:]][[:digit:]] | RegExpSequence | +| test.cpp:121:25:121:33 | [:alpha:] | RegExpNamedCharacterProperty | +| test.cpp:121:35:121:45 | [[:digit:]] | RegExpCharacterClass | +| test.cpp:121:36:121:44 | [:digit:] | RegExpNamedCharacterProperty | +| test.cpp:124:24:124:43 | [[:alpha:][:digit:]] | RegExpCharacterClass | +| test.cpp:124:25:124:33 | [:alpha:] | RegExpNamedCharacterProperty | +| test.cpp:124:34:124:42 | [:digit:] | RegExpNamedCharacterProperty | +| test.cpp:127:24:127:40 | [A-F[:digit:]a-f] | RegExpCharacterClass | +| test.cpp:127:25:127:25 | A | RegExpConstant,RegExpNormalChar | +| test.cpp:127:25:127:27 | A-F | RegExpCharacterRange | +| test.cpp:127:27:127:27 | F | RegExpConstant,RegExpNormalChar | +| test.cpp:127:28:127:36 | [:digit:] | RegExpNamedCharacterProperty | +| test.cpp:127:37:127:37 | a | RegExpConstant,RegExpNormalChar | +| test.cpp:127:37:127:39 | a-f | RegExpCharacterRange | +| test.cpp:127:39:127:39 | f | RegExpConstant,RegExpNormalChar | +| test.cpp:130:24:130:32 | [:digit:] | RegExpCharacterClass | +| test.cpp:130:25:130:25 | : | RegExpConstant,RegExpNormalChar | +| test.cpp:130:26:130:26 | d | RegExpConstant,RegExpNormalChar | +| test.cpp:130:27:130:27 | i | RegExpConstant,RegExpNormalChar | +| test.cpp:130:28:130:28 | g | RegExpConstant,RegExpNormalChar | +| test.cpp:130:29:130:29 | i | RegExpConstant,RegExpNormalChar | +| test.cpp:130:30:130:30 | t | RegExpConstant,RegExpNormalChar | +| test.cpp:130:31:130:31 | : | RegExpConstant,RegExpNormalChar | +| test.cpp:133:24:133:32 | [:alpha:] | RegExpCharacterClass | +| test.cpp:133:25:133:25 | : | RegExpConstant,RegExpNormalChar | +| test.cpp:133:26:133:26 | a | RegExpConstant,RegExpNormalChar | +| test.cpp:133:27:133:27 | l | RegExpConstant,RegExpNormalChar | +| test.cpp:133:28:133:28 | p | RegExpConstant,RegExpNormalChar | +| test.cpp:133:29:133:29 | h | RegExpConstant,RegExpNormalChar | +| test.cpp:133:30:133:30 | a | RegExpConstant,RegExpNormalChar | +| test.cpp:133:31:133:31 | : | RegExpConstant,RegExpNormalChar | +| test.cpp:136:24:136:24 | a | RegExpConstant,RegExpNormalChar | +| test.cpp:136:24:136:30 | a[:b:]c | RegExpSequence | +| test.cpp:136:25:136:29 | [:b:] | RegExpCharacterClass | +| test.cpp:136:26:136:26 | : | RegExpConstant,RegExpNormalChar | +| test.cpp:136:27:136:27 | b | RegExpConstant,RegExpNormalChar | +| test.cpp:136:28:136:28 | : | RegExpConstant,RegExpNormalChar | +| test.cpp:136:30:136:30 | c | RegExpConstant,RegExpNormalChar | +| test.cpp:139:24:139:36 | [[:alpha:]-z] | RegExpCharacterClass | +| test.cpp:139:25:139:33 | [:alpha:] | RegExpNamedCharacterProperty | +| test.cpp:139:25:139:35 | [:alpha:]-z | RegExpCharacterRange | +| test.cpp:139:35:139:35 | z | RegExpConstant,RegExpNormalChar | +| test.cpp:142:24:142:32 | [[:alpha] | RegExpCharacterClass | +| test.cpp:142:25:142:25 | [ | RegExpConstant,RegExpNormalChar | +| test.cpp:142:26:142:26 | : | RegExpConstant,RegExpNormalChar | +| test.cpp:142:27:142:27 | a | RegExpConstant,RegExpNormalChar | +| test.cpp:142:28:142:28 | l | RegExpConstant,RegExpNormalChar | +| test.cpp:142:29:142:29 | p | RegExpConstant,RegExpNormalChar | +| test.cpp:142:30:142:30 | h | RegExpConstant,RegExpNormalChar | +| test.cpp:142:31:142:31 | a | RegExpConstant,RegExpNormalChar | +| test.cpp:145:24:145:35 | []a[:alpha:] | RegExpCharacterClass | +| test.cpp:145:24:145:36 | []a[:alpha:]] | RegExpSequence | +| test.cpp:145:25:145:25 | ] | RegExpConstant,RegExpNormalChar | +| test.cpp:145:26:145:26 | a | RegExpConstant,RegExpNormalChar | +| test.cpp:145:27:145:27 | [ | RegExpConstant,RegExpNormalChar | +| test.cpp:145:28:145:28 | : | RegExpConstant,RegExpNormalChar | +| test.cpp:145:29:145:29 | a | RegExpConstant,RegExpNormalChar | +| test.cpp:145:30:145:30 | l | RegExpConstant,RegExpNormalChar | +| test.cpp:145:31:145:31 | p | RegExpConstant,RegExpNormalChar | +| test.cpp:145:32:145:32 | h | RegExpConstant,RegExpNormalChar | +| test.cpp:145:33:145:33 | a | RegExpConstant,RegExpNormalChar | +| test.cpp:145:34:145:34 | : | RegExpConstant,RegExpNormalChar | +| test.cpp:145:36:145:36 | ] | RegExpConstant,RegExpNormalChar | +| test.cpp:148:25:148:53 | [[:alpha:][:digit:][:space:]] | RegExpCharacterClass | +| test.cpp:148:26:148:34 | [:alpha:] | RegExpNamedCharacterProperty | +| test.cpp:148:35:148:43 | [:digit:] | RegExpNamedCharacterProperty | +| test.cpp:148:44:148:52 | [:space:] | RegExpNamedCharacterProperty | +| test.cpp:151:25:151:36 | [[:xdigit:]] | RegExpCharacterClass | +| test.cpp:151:26:151:35 | [:xdigit:] | RegExpNamedCharacterProperty | +| test.cpp:152:25:152:35 | [[:blank:]] | RegExpCharacterClass | +| test.cpp:152:26:152:34 | [:blank:] | RegExpNamedCharacterProperty | +| test.cpp:153:25:153:35 | [[:cntrl:]] | RegExpCharacterClass | +| test.cpp:153:26:153:34 | [:cntrl:] | RegExpNamedCharacterProperty | +| test.cpp:154:25:154:35 | [[:graph:]] | RegExpCharacterClass | +| test.cpp:154:26:154:34 | [:graph:] | RegExpNamedCharacterProperty | +| test.cpp:157:25:157:25 | ^ | RegExpCaret | +| test.cpp:157:25:157:92 | ^([[:alpha:]]+[[:digit:]]*[[:space:]]?)+([[:punct:]]\|[[:xdigit:]])+$ | RegExpSequence | +| test.cpp:157:26:157:63 | ([[:alpha:]]+[[:digit:]]*[[:space:]]?) | RegExpGroup | +| test.cpp:157:26:157:64 | ([[:alpha:]]+[[:digit:]]*[[:space:]]?)+ | RegExpPlus | +| test.cpp:157:27:157:37 | [[:alpha:]] | RegExpCharacterClass | +| test.cpp:157:27:157:38 | [[:alpha:]]+ | RegExpPlus | +| test.cpp:157:27:157:62 | [[:alpha:]]+[[:digit:]]*[[:space:]]? | RegExpSequence | +| test.cpp:157:28:157:36 | [:alpha:] | RegExpNamedCharacterProperty | +| test.cpp:157:39:157:49 | [[:digit:]] | RegExpCharacterClass | +| test.cpp:157:39:157:50 | [[:digit:]]* | RegExpStar | +| test.cpp:157:40:157:48 | [:digit:] | RegExpNamedCharacterProperty | +| test.cpp:157:51:157:61 | [[:space:]] | RegExpCharacterClass | +| test.cpp:157:51:157:62 | [[:space:]]? | RegExpOpt | +| test.cpp:157:52:157:60 | [:space:] | RegExpNamedCharacterProperty | +| test.cpp:157:65:157:90 | ([[:punct:]]\|[[:xdigit:]]) | RegExpGroup | +| test.cpp:157:65:157:91 | ([[:punct:]]\|[[:xdigit:]])+ | RegExpPlus | +| test.cpp:157:66:157:76 | [[:punct:]] | RegExpCharacterClass | +| test.cpp:157:66:157:89 | [[:punct:]]\|[[:xdigit:]] | RegExpAlt | +| test.cpp:157:67:157:75 | [:punct:] | RegExpNamedCharacterProperty | +| test.cpp:157:78:157:89 | [[:xdigit:]] | RegExpCharacterClass | +| test.cpp:157:79:157:88 | [:xdigit:] | RegExpNamedCharacterProperty | +| test.cpp:157:92:157:92 | $ | RegExpDollar | +| test.cpp:162:21:162:26 | \\u9879 | RegExpConstant,RegExpEscape | +| test.cpp:170:23:170:23 | a | RegExpConstant,RegExpNormalChar | +| test.cpp:170:23:170:24 | a+ | RegExpPlus | +| test.cpp:170:23:170:25 | a+b | RegExpSequence | +| test.cpp:170:25:170:25 | b | RegExpConstant,RegExpNormalChar | +| test.cpp:173:23:173:23 | a | RegExpConstant,RegExpNormalChar | +| test.cpp:173:23:173:24 | a+ | RegExpPlus | +| test.cpp:173:23:173:25 | a+b | RegExpSequence | +| test.cpp:173:25:173:25 | b | RegExpConstant,RegExpNormalChar | +| test.cpp:176:24:176:25 | \\s | RegExpCharacterClassEscape | +| test.cpp:176:24:176:26 | \\s+ | RegExpPlus | +| test.cpp:176:24:176:27 | \\s+$ | RegExpSequence | +| test.cpp:176:27:176:27 | $ | RegExpDollar | +| test.cpp:179:24:179:25 | \\( | RegExpConstant,RegExpEscape | +| test.cpp:179:24:179:37 | \\(([,\\w]+)+\\)$ | RegExpSequence | +| test.cpp:179:26:179:33 | ([,\\w]+) | RegExpGroup | +| test.cpp:179:26:179:34 | ([,\\w]+)+ | RegExpPlus | +| test.cpp:179:27:179:31 | [,\\w] | RegExpCharacterClass | +| test.cpp:179:27:179:32 | [,\\w]+ | RegExpPlus | +| test.cpp:179:28:179:28 | , | RegExpConstant,RegExpNormalChar | +| test.cpp:179:29:179:30 | \\w | RegExpCharacterClassEscape | +| test.cpp:179:35:179:36 | \\) | RegExpConstant,RegExpEscape | +| test.cpp:179:37:179:37 | $ | RegExpDollar | +| test.cpp:182:25:182:25 | a | RegExpConstant,RegExpNormalChar | +| test.cpp:182:25:182:26 | a+ | RegExpPlus | +| test.cpp:182:25:182:27 | a+b | RegExpSequence | +| test.cpp:182:27:182:27 | b | RegExpConstant,RegExpNormalChar | +| test.cpp:185:24:185:24 | a | RegExpConstant,RegExpNormalChar | +| test.cpp:185:24:185:25 | a+ | RegExpPlus | +| test.cpp:185:24:185:26 | a+b | RegExpSequence | +| test.cpp:185:26:185:26 | b | RegExpConstant,RegExpNormalChar | +| test.cpp:188:30:188:30 | a | RegExpConstant,RegExpNormalChar | +| test.cpp:188:30:188:31 | a+ | RegExpPlus | +| test.cpp:188:30:188:32 | a+b | RegExpSequence | +| test.cpp:188:32:188:32 | b | RegExpConstant,RegExpNormalChar | +| test.cpp:192:22:192:23 | \\s | RegExpCharacterClassEscape | +| test.cpp:192:22:192:24 | \\s+ | RegExpPlus | +| test.cpp:195:22:195:22 | a | RegExpConstant,RegExpNormalChar | +| test.cpp:195:22:195:25 | a\\.b | RegExpSequence | +| test.cpp:195:23:195:24 | \\. | RegExpConstant,RegExpEscape | +| test.cpp:195:25:195:25 | b | RegExpConstant,RegExpNormalChar | +| test.cpp:199:22:199:22 | a | RegExpConstant,RegExpNormalChar | +| test.cpp:199:22:199:23 | a+ | RegExpPlus | +| test.cpp:199:22:199:24 | a+b | RegExpSequence | +| test.cpp:199:24:199:24 | b | RegExpConstant,RegExpNormalChar | +| test.cpp:202:28:202:28 | a | RegExpConstant,RegExpNormalChar | +| test.cpp:202:28:202:29 | a+ | RegExpPlus | +| test.cpp:202:28:202:30 | a+b | RegExpSequence | +| test.cpp:202:30:202:30 | b | RegExpConstant,RegExpNormalChar | +regExpNormalCharValue +| test.cpp:33:21:33:23 | abc | abc | +| test.cpp:36:22:36:22 | a | a | +| test.cpp:36:24:36:24 | b | b | +| test.cpp:36:26:36:26 | c | c | +| test.cpp:36:28:36:28 | d | d | +| test.cpp:37:22:37:22 | a | a | +| test.cpp:39:22:39:22 | a | a | +| test.cpp:40:22:40:22 | a | a | +| test.cpp:43:21:43:23 | foo | foo | +| test.cpp:43:25:43:27 | bar | bar | +| test.cpp:46:22:46:22 | a | a | +| test.cpp:46:23:46:23 | b | b | +| test.cpp:46:24:46:24 | c | c | +| test.cpp:47:22:47:22 | a | a | +| test.cpp:47:24:47:24 | f | f | +| test.cpp:47:25:47:25 | A | A | +| test.cpp:47:27:47:27 | F | F | +| test.cpp:47:28:47:28 | 0 | 0 | +| test.cpp:47:30:47:30 | 9 | 9 | +| test.cpp:47:31:47:31 | _ | _ | +| test.cpp:49:23:49:23 | + | + | +| test.cpp:49:24:49:24 | - | - | +| test.cpp:49:27:49:28 | \\d | d | +| test.cpp:50:22:50:23 | \\w | w | +| test.cpp:51:21:51:22 | \\[ | [ | +| test.cpp:51:23:51:24 | \\] | ] | +| test.cpp:51:26:51:26 | 1 | 1 | +| test.cpp:51:27:51:27 | 2 | 2 | +| test.cpp:51:28:51:28 | 3 | 3 | +| test.cpp:52:23:52:23 | A | A | +| test.cpp:52:25:52:25 | Z | Z | +| test.cpp:53:22:53:22 | ] | ] | +| test.cpp:54:23:54:23 | ] | ] | +| test.cpp:55:23:55:23 | - | - | +| test.cpp:56:23:56:23 | \| | \| | +| test.cpp:59:25:59:25 | [ | [ | +| test.cpp:59:26:59:26 | a | a | +| test.cpp:59:28:59:28 | f | f | +| test.cpp:59:30:59:33 | A-F] | A-F] | +| test.cpp:78:30:78:30 | a | a | +| test.cpp:87:39:87:39 | 0 | 0 | +| test.cpp:87:41:87:41 | 9 | 9 | +| test.cpp:91:23:91:24 | \\w | w | +| test.cpp:91:26:91:27 | \\W | W | +| test.cpp:92:23:92:24 | \\s | s | +| test.cpp:92:25:92:26 | \\S | S | +| test.cpp:93:23:93:24 | \\d | d | +| test.cpp:93:25:93:26 | \\D | D | +| test.cpp:95:23:95:24 | \\n | \n | +| test.cpp:95:25:95:26 | \\r | \r | +| test.cpp:95:27:95:28 | \\t | \t | +| test.cpp:98:21:98:21 | a | a | +| test.cpp:98:22:98:23 | \\0 | 0 | +| test.cpp:98:24:98:24 | b | b | +| test.cpp:102:24:102:25 | !a | !a | +| test.cpp:105:23:105:25 | foo | foo | +| test.cpp:105:28:105:30 | bar | bar | +| test.cpp:106:22:106:23 | fo | fo | +| test.cpp:106:25:106:25 | o | o | +| test.cpp:106:27:106:27 | b | b | +| test.cpp:106:29:106:30 | ar | ar | +| test.cpp:107:23:107:23 | a | a | +| test.cpp:107:25:107:25 | b | b | +| test.cpp:107:27:107:28 | cd | cd | +| test.cpp:107:30:107:30 | e | e | +| test.cpp:108:25:108:25 | : | : | +| test.cpp:108:28:108:29 | \\w | w | +| test.cpp:115:24:115:24 | a | a | +| test.cpp:115:27:115:27 | b | b | +| test.cpp:118:24:118:24 | a | a | +| test.cpp:118:26:118:26 | f | f | +| test.cpp:118:27:118:28 | \\d | d | +| test.cpp:127:25:127:25 | A | A | +| test.cpp:127:27:127:27 | F | F | +| test.cpp:127:37:127:37 | a | a | +| test.cpp:127:39:127:39 | f | f | +| test.cpp:130:25:130:25 | : | : | +| test.cpp:130:26:130:26 | d | d | +| test.cpp:130:27:130:27 | i | i | +| test.cpp:130:28:130:28 | g | g | +| test.cpp:130:29:130:29 | i | i | +| test.cpp:130:30:130:30 | t | t | +| test.cpp:130:31:130:31 | : | : | +| test.cpp:133:25:133:25 | : | : | +| test.cpp:133:26:133:26 | a | a | +| test.cpp:133:27:133:27 | l | l | +| test.cpp:133:28:133:28 | p | p | +| test.cpp:133:29:133:29 | h | h | +| test.cpp:133:30:133:30 | a | a | +| test.cpp:133:31:133:31 | : | : | +| test.cpp:136:24:136:24 | a | a | +| test.cpp:136:26:136:26 | : | : | +| test.cpp:136:27:136:27 | b | b | +| test.cpp:136:28:136:28 | : | : | +| test.cpp:136:30:136:30 | c | c | +| test.cpp:139:35:139:35 | z | z | +| test.cpp:142:25:142:25 | [ | [ | +| test.cpp:142:26:142:26 | : | : | +| test.cpp:142:27:142:27 | a | a | +| test.cpp:142:28:142:28 | l | l | +| test.cpp:142:29:142:29 | p | p | +| test.cpp:142:30:142:30 | h | h | +| test.cpp:142:31:142:31 | a | a | +| test.cpp:145:25:145:25 | ] | ] | +| test.cpp:145:26:145:26 | a | a | +| test.cpp:145:27:145:27 | [ | [ | +| test.cpp:145:28:145:28 | : | : | +| test.cpp:145:29:145:29 | a | a | +| test.cpp:145:30:145:30 | l | l | +| test.cpp:145:31:145:31 | p | p | +| test.cpp:145:32:145:32 | h | h | +| test.cpp:145:33:145:33 | a | a | +| test.cpp:145:34:145:34 | : | : | +| test.cpp:145:36:145:36 | ] | ] | +| test.cpp:162:21:162:26 | \\u9879 | \u9879 | +| test.cpp:170:23:170:23 | a | a | +| test.cpp:170:25:170:25 | b | b | +| test.cpp:173:23:173:23 | a | a | +| test.cpp:173:25:173:25 | b | b | +| test.cpp:176:24:176:25 | \\s | s | +| test.cpp:179:24:179:25 | \\( | ( | +| test.cpp:179:28:179:28 | , | , | +| test.cpp:179:29:179:30 | \\w | w | +| test.cpp:179:35:179:36 | \\) | ) | +| test.cpp:182:25:182:25 | a | a | +| test.cpp:182:27:182:27 | b | b | +| test.cpp:185:24:185:24 | a | a | +| test.cpp:185:26:185:26 | b | b | +| test.cpp:188:30:188:30 | a | a | +| test.cpp:188:32:188:32 | b | b | +| test.cpp:192:22:192:23 | \\s | s | +| test.cpp:195:22:195:22 | a | a | +| test.cpp:195:23:195:24 | \\. | . | +| test.cpp:195:25:195:25 | b | b | +| test.cpp:199:22:199:22 | a | a | +| test.cpp:199:24:199:24 | b | b | +| test.cpp:202:28:202:28 | a | a | +| test.cpp:202:30:202:30 | b | b | diff --git a/cpp/ql/test/library-tests/regex/regexp.ql b/cpp/ql/test/library-tests/regex/regexp.ql new file mode 100644 index 000000000000..119a4b28c3cb --- /dev/null +++ b/cpp/ql/test/library-tests/regex/regexp.ql @@ -0,0 +1,10 @@ +import cpp +import semmle.code.cpp.regex.RegexTreeView as RE + +query predicate groupNumber(RE::RegExpGroup g, int number) { number = g.getNumber() } + +query predicate term(RE::RegExpTerm term, string c) { c = term.getPrimaryQlClasses() } + +query predicate regExpNormalCharValue(RE::RegExpNormalChar term, string value) { + value = term.getValue() +} diff --git a/cpp/ql/test/library-tests/regex/test.cpp b/cpp/ql/test/library-tests/regex/test.cpp new file mode 100644 index 000000000000..ea81639a4c88 --- /dev/null +++ b/cpp/ql/test/library-tests/regex/test.cpp @@ -0,0 +1,204 @@ +// Minimal stubs for std::regex surface — no #include of any external header. +// Provides just enough of the std:: regex API that the C++ extractor +// creates StringLiteral nodes for each regex pattern, using real qualified +// names so a future flow-config PR can identify them unchanged. +namespace std { + template + class basic_regex { + public: + basic_regex(const char *s) {} + basic_regex(const char *s, int flags) {} + basic_regex(const wchar_t *s) {} + basic_regex &assign(const char *s) { return *this; } + }; + typedef basic_regex regex; + typedef basic_regex wregex; + + template + bool regex_match(const char *s, const basic_regex &re) { return false; } + + template + bool regex_search(const char *s, const basic_regex &re) { return false; } + + template + const char *regex_replace(const char *s, const basic_regex &re, + const char *fmt) { return s; } +} // namespace std + +void test() { + // Empty + std::regex r_empty(""); + + // Basic sequence + std::regex r_abc("abc"); + + // Repetition + std::regex r_rep1("a*b+c?d"); + std::regex r_rep2("a{4,8}"); + + std::regex r_rep4("a{3,}"); + std::regex r_rep5("a{7}"); + + // Alternation + std::regex r_alt("foo|bar"); + + // Character classes + std::regex r_cc1("[abc]"); + std::regex r_cc2("[a-fA-F0-9_]"); + + std::regex r_cc3("^[+-]?\\d+"); + std::regex r_cc4("[\\w]+"); + std::regex r_cc5("\\[\\][123]"); + std::regex r_cc6("[^A-Z]"); + std::regex r_cc7("[]]"); // MRI gives a warning, but accepts this as matching ']' + std::regex r_cc8("[^]]"); // MRI gives a warning, but accepts this as matching anything except ']' + std::regex r_cc9("[^-]"); + std::regex r_cc10("[|]"); + + // Nested character classes (BAD - not parsed correctly) + std::regex r_nested("[[a-f]A-F]"); + + // POSIX bracket extensions (std::regex ECMAScript mode supports these inside [...]) + + + // Single POSIX classes (single outer bracket around a single POSIX expression) + std::regex r_posix_alpha("[[:alpha:]]"); // naïve parser closes at inner ] of [:alpha:] + std::regex r_posix_digit("[[:digit:]]"); + std::regex r_posix_space("[[:space:]]"); + std::regex r_posix_upper("[[:upper:]]"); + std::regex r_posix_lower("[[:lower:]]"); + std::regex r_posix_alnum("[[:alnum:]]"); + std::regex r_posix_print("[[:print:]]"); + std::regex r_posix_punct("[[:punct:]]"); + + // POSIX class negated (outer bracket negated) + std::regex r_posix_neg("[^[:space:]]"); + + // Mixed: regular char + POSIX class inside outer bracket + std::regex r_posix_mixed("[a[:space:]]"); + + // POSIX collating symbol + std::regex r_posix_coll("[[.a.]]"); + + // POSIX equivalence class + std::regex r_posix_equiv("[[=a=]]"); + + // Mixed: POSIX class + range inside outer bracket (mis-tokenization-prone) + std::regex r_posix_range("[[:alpha:]0-9]"); + + // Meta-character classes + std::regex r_meta1(".*"); + std::regex r_meta2("\\w+\\W"); + std::regex r_meta3("\\s\\S"); + std::regex r_meta4("\\d\\D"); + + std::regex r_meta6("\\n\\r\\t"); + + // NUL escape \0 (ECMAScript: valid when not followed by another digit) + std::regex r_nul("a\\0b"); + + // Anchors (ECMAScript: \b, \B only) + + std::regex r_anc2("\\b!a\\B"); + + // Groups + std::regex r_grp1("(foo)*bar"); + std::regex r_grp2("fo(o|b)ar"); + std::regex r_grp3("(a|b|cd)e"); + std::regex r_grp4("(?::+)\\w"); // Non-capturing group matching colons + + // Named groups are not supported by the ECMAScript grammar used by + // `std::regex`, so the parser does not recognise `(?...)`. + + + // Backreferences + std::regex r_bref1("(a+)b+\\1"); + + // A character class combining a range with an escape class + std::regex r_prop1("[a-f\\d]+"); + + // Two separate character classes, each containing a single POSIX bracket expression + std::regex r_posix1("[[:alpha:]][[:digit:]]"); + + // A single character class containing two POSIX bracket expressions + std::regex r_posix2("[[:alpha:][:digit:]]"); + + // A single character class containing two ranges and one POSIX bracket expression + std::regex r_posix3("[A-F[:digit:]a-f]"); + + // *Not* a POSIX bracket expression; just a regular character class. + std::regex r_posix4("[:digit:]"); + + // POSIX-looking but not nested — a plain class with `:`, `a`, `l`, `p`, `h` + std::regex r_posix5("[:alpha:]"); + + // POSIX-looking tokens mid-pattern, not a real POSIX class + std::regex r_posix6("a[:b:]c"); + + // POSIX class as a range endpoint + std::regex r_posix7("[[:alpha:]-z]"); + + // Malformed — missing closing colon + std::regex r_posix8("[[:alpha]"); + + // Leading literal `]` combined with a POSIX class + std::regex r_posix9("[]a[:alpha:]]"); + + // Three POSIX classes in one class + std::regex r_posix10("[[:alpha:][:digit:][:space:]]"); + + // Additional POSIX class names + std::regex r_posix11("[[:xdigit:]]"); + std::regex r_posix12("[[:blank:]]"); + std::regex r_posix13("[[:cntrl:]]"); + std::regex r_posix14("[[:graph:]]"); + + // Integration case + std::regex r_posix15("^([[:alpha:]]+[[:digit:]]*[[:space:]]?)+([[:punct:]]|[[:xdigit:]])+$"); + + + + // unicode: \uHHHH 4-digit hex form (valid ECMAScript \u escape in std::regex) + std::regex r_uni("\\u9879"); +} + +// Per-form location tests for C++ string-literal spellings. Each literal is +// wrapped in an appropriate std::regex use so it is extracted, and each uses +// a simple "a+b" body so term columns are predictable. +void test_locations() { + // Plain "..." — content offset 1. + std::regex r_plain("a+b"); + + // Raw R"(...)" — content offset 3 (`R"(` = 3 chars). + std::regex r_raw(R"(a+b)"); + + // Raw with regex metacharacters — content offset 3. + std::regex r_raw2(R"(\s+$)"); + + // Complex raw — content offset 3. + std::regex r_raw3(R"(\(([,\w]+)+\)$)"); + + // Custom-delimiter raw R"x(...)x" — content offset 4 (`R"x(` = 4 chars). + std::regex r_raw4(R"x(a+b)x"); + + // Wide-char prefix L"..." — content offset 2 (`L"` = 2 chars). + std::wregex r_wide(L"a+b"); + + // Wide raw LR"(...)" — content offset 4 (`LR"(` = 4 chars). + std::wregex r_wide_raw(LR"(a+b)"); + + // Escape-containing plain — value is \s+ (2 chars); offset 1. + // (Within-content column mapping is approximate for escaped strings.) + std::regex r_esc1("\\s+"); + + // Escape with dot — value is a\.b; offset 1. + std::regex r_esc2("a\\.b"); + + // u8 encoding prefix — content offset 3 (u8" = 3 chars). Requires C++17+. +#if __cplusplus >= 201703L + std::regex r_u8(u8"a+b"); + + // u8R raw — content offset 5 (u8R"( = 5 chars). + std::regex r_u8_raw(u8R"(a+b)"); +#endif +}