C++: Add ECMAScript std::regex parser - #22300
Conversation
Note that we are currently still implementing what Ruby thinks a regex is. This is not correct as C++ by default uses a variant of ECMAScript regexes. We will address this in the follow-up commits.
Strings can have a prefix in C++, which affects the location.
Clean up test source while here
There was a problem hiding this comment.
Pull request overview
Adds an ECMAScript-compatible std::regex parser and parse-tree model for future C++ data-flow integration.
Changes:
- Adds parser and regex tree-view libraries.
- Supports raw-string source locations and shared regex utilities.
- Adds comprehensive parsing, value, and location tests.
Show a summary per file
| File | Description |
|---|---|
cpp/ql/lib/semmle/code/cpp/regex/internal/ParseRegExp.qll |
Implements regex parsing. |
cpp/ql/lib/semmle/code/cpp/regex/RegexTreeView.qll |
Exposes parsed regex trees. |
cpp/ql/lib/semmle/code/cpp/exprs/Literal.qll |
Models raw string literals. |
cpp/ql/lib/qlpack.yml |
Adds the regex dependency. |
cpp/ql/lib/change-notes/2026-07-23-std-regex-ecmascript-parser.md |
Documents the feature. |
cpp/ql/test/library-tests/regex/regexp.cpp |
Provides regex test inputs. |
cpp/ql/test/library-tests/regex/regexp.ql |
Tests parsed terms and values. |
cpp/ql/test/library-tests/regex/regexp.expected |
Records expected query results. |
cpp/ql/test/library-tests/regex/parse.ql |
Produces parse-tree graphs. |
cpp/ql/test/library-tests/regex/parse.expected |
Records expected parse trees. |
cpp/ql/test/library-tests/regex/locations.ql |
Tests source locations. |
cpp/ql/test/library-tests/regex/locations.expected |
Records expected locations. |
Review details
Tip
Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Suppressed comments (3)
cpp/ql/lib/semmle/code/cpp/regex/RegexTreeView.qll:834
- The single-character claim contradicts this implementation and the new
abctest result, where oneRegExpConstantrepresents the complete three-character constant. Remove that claim to keep the public class documentation accurate.
* A constant regular expression term, that is, a regular expression
* term matching a single string. Currently, this will always be a single character.
cpp/ql/lib/semmle/code/cpp/regex/internal/ParseRegExp.qll:65
- Remove the accidentally duplicated explanation.
// 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)
cpp/ql/lib/semmle/code/cpp/regex/RegexTreeView.qll:604
- Add the missing possessive apostrophe.
/** Holds if this terms name is given by the part following the escape character. */
- Files reviewed: 12/12 changed files
- Comments generated: 5
- Review effort level: Balanced
| result.getEnd() = part_end | ||
| } | ||
|
|
||
| /** Hodls if this term may match an unlimited number of times. */ |
| * A normal character in a regular expression, that is, a character | ||
| * without special meaning. This includes escaped characters. |
There was a problem hiding this comment.
Rename to RegExpNormalChars???
| } | ||
|
|
||
| /** | ||
| * Holds if `root` has the `s` flag for multi-line matching. |
There was a problem hiding this comment.
Yep, this appears to be a stale comment.
There was a problem hiding this comment.
I'm not actually sure about that. The predicate name seems very specific to one specific regex version parsed in one of the languages where we have a similar regex library. I'll double check though.
| */ | ||
| abstract class RegExp extends StringLiteral { | ||
| /** | ||
| * Holds if this `RegExp` has the `s` flag for multi-line matching. |
geoffw0
left a comment
There was a problem hiding this comment.
Review part 1. Thank you for breaking this PR into logical commits, with commit comments explaining the changes - this was extremely helpful for making sense of the changes you've made from the Ruby implementation I'm already somewhat familiar with.
I've also created a small PR jketema#33 onto this PR, addressing an edge case with string locations that I investigated, and figured I might as well trivially fix and share my code changes for.
| std::regex r_cc4("\\[\\][123]"); | ||
| std::regex r_cc5("[^A-Z]"); | ||
| std::regex r_cc6("[]]"); // MRI gives a warning, but accepts this as matching ']' | ||
| std::regex r_cc7("[^]]"); // MRI gives a warning, but accepts this as matching anything except ']' |
There was a problem hiding this comment.
https://en.wikipedia.org/wiki/Ruby_MRI
This is a left-over from Ruby tests that I copied. We should probably just remove the comments.
| std::basic_regex<char16_t> r_loc_uR(uR"(a\nc)"); | ||
| std::basic_regex<char32_t> r_loc_UR(UR"(a\nc)"); | ||
| std::regex r_loc_Rx(R"x(a\nc)x"); | ||
| std::regex r_loc_Rfoo(R"foo(a\nc)foo"); |
There was a problem hiding this comment.
Should we have test cases for invalid regexs to ensure nothing too strange happens there - and for that matter string literals that are not remotely regexs, to ensure they are not misidentified as such.
There was a problem hiding this comment.
Is there anything specific you're thinking about?
There was a problem hiding this comment.
Nothing in particular, just (1) sometimes people write invalid regexs, and sometimes libraries even accept them so they don't get spotted; (2) real world databases will contain a lot of string literals that aren't regexs, so it's important we don't do anything silly (or computationally expensive for that matter) with them. I can't remember how this worked in Ruby / Python / Swift.
There was a problem hiding this comment.
I can't remember how this worked in Ruby / Python / Swift.
I think the idea is that we only look at strings that actually flow into the correct std::regex function arguments. That is clearly missing at this point.
| * const wchar_t *s2 = LR"x(123456)x"; | ||
| * ``` | ||
| */ | ||
| class RawStringLiteral extends StringLiteral { |
There was a problem hiding this comment.
It might be nice to have a test for RawStringLiteral itself, even if it's just a very basic test as part of the regex tests e.g.
from RawStringLiteral rsl
select rsl
There was a problem hiding this comment.
Makes sense. Will add.
Commented on that here: https://github.com/jketema/codeql/pull/33/files#r3871704321 |
geoffw0
left a comment
There was a problem hiding this comment.
Review part 2. I've now reviewed all of the code and tests.
I'm very happy with what I see, you've clearly put considerably effort into getting this right ... though experience suggests we should be prepared to find bugs and missing features when we start to implement queries on top of the library, as doing so will test it more deeply.
I'm going to do a few quick tests locally, but I'm leaning towards getting this merged quickly so we can build on it.
👍
| basic_regex(const CharT *s, int flags) {} | ||
| basic_regex &assign(const CharT *s) { return *this; } | ||
| }; | ||
| typedef basic_regex<char> regex; |
There was a problem hiding this comment.
Do we want to test std::wregex as well?
And initialization constants such as std::regex_constants::icase? (which I think we can support fairly easily, when we're ready to add that)
And non-ECMA regexs? (which I guess, realistically, we plan to either ignore for the time being???)
There was a problem hiding this comment.
icase requires dataflow.
non-ECMA regexes are on my todo list but only after I've rigged up whatever else I need to get to a sensible Redos query. These additional regex formats seem relatively easy to add, but again some dataflow will be required to determine which regex format is being used.
There was a problem hiding this comment.
Do we want to test
std::wregexas well?
I'm not sure that's adding much at this stage, but let me add the needed prototypes and at least one test.
|
|
||
| /** | ||
| * Holds if the property is inverted. For example, it holds for `[[:^digit:]]`, | ||
| * which matches non-digits. |
There was a problem hiding this comment.
Again, I'm a bit surprised this isn't supported.
There was a problem hiding this comment.
I think this is pretty Ruby specific?
There was a problem hiding this comment.
On https://en.cppreference.com/cpp/regex/ecmascript it has an example [^[:digit:]], but that's slightly different, and looking at it now I think it's just a regular use of [^...].
There was a problem hiding this comment.
Indeed. [^[:digit:]] should be covered by the regular [^...] case. I'll add a test case of that if there isn't one already.
| std::regex r_posix3("[A-F[:digit:]a-f]"); | ||
|
|
||
| // POSIX collating symbols | ||
| std::regex r_posix_coll1("[[.a.]]"); |
There was a problem hiding this comment.
If I'm understanding correctly (I'm not confident I am) this is a somewhat strange example - since the collating symbol is intended to join several characters as though they are one?
There was a problem hiding this comment.
That might be. I'll have a look and update the test if appropriate.
| not this.isUnicode() and | ||
| not this.isHex() and | ||
| not this.isControl() and |
There was a problem hiding this comment.
I believe these are redundant, as they're checked inside isIdentityEscape.
| not this.isUnicode() and | |
| not this.isHex() and | |
| not this.isControl() and |
There was a problem hiding this comment.
You might be right. I'll check.
| * A normal character in a regular expression, that is, a character | ||
| * without special meaning. This includes escaped characters. |
There was a problem hiding this comment.
Rename to RegExpNormalChars???
| } | ||
|
|
||
| /** | ||
| * Holds if `root` has the `s` flag for multi-line matching. |
There was a problem hiding this comment.
Yep, this appears to be a stale comment.
I've now done my local testing. I noticed locations of regex terms where the regex includes one or more escape sequences ( Nothing else to review, I'm happy to approve this when everything above has been answered. I don't think anything I've asked for is critical at this stage (as no queries use the library yet), so feel free to write up a TODO list issue instead of fixing every detail now. It will be good to have some code in |
I saw that. I tried fixing that, but that turned out to be non-trivial. I'll add a note in the |
Commit-by-commit review recommended. This is not hooked into anything yet, as this is getting quite big already, so I didn't want to add the flow config here, or other regex alternatives that can be used with
std::regex.Approach: