Skip to content

C++: Add ECMAScript std::regex parser - #22300

Open
jketema wants to merge 40 commits into
github:mainfrom
jketema:jketema/ecma-regex
Open

C++: Add ECMAScript std::regex parser#22300
jketema wants to merge 40 commits into
github:mainfrom
jketema:jketema/ecma-regex

Conversation

@jketema

@jketema jketema commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

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:

  1. Import the Ruby parser, make QL and tests compile, fix position issue with raw strings
  2. Remove unsupported features
  3. Add features that are not supported by the Ruby parser
  4. Add missing test cases
  5. Some after-the-fact fixes.

Copilot AI and others added 30 commits August 7, 2026 14:26
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
Comment thread cpp/ql/lib/semmle/code/cpp/regex/RegexTreeView.qll Fixed
Comment thread cpp/ql/lib/semmle/code/cpp/regex/RegexTreeView.qll Fixed
Comment thread cpp/ql/lib/semmle/code/cpp/regex/RegexTreeView.qll Fixed
@jketema
jketema marked this pull request as ready for review August 10, 2026 09:25
@jketema
jketema requested a review from a team as a code owner August 10, 2026 09:25
Copilot AI balanced review requested due to automatic review settings August 10, 2026 09:25

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 abc test result, where one RegExpConstant represents 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. */
Comment on lines +806 to +807
* A normal character in a regular expression, that is, a character
* without special meaning. This includes escaped characters.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rename to RegExpNormalChars???

}

/**
* Holds if `root` has the `s` flag for multi-line matching.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, this appears to be a stale comment.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
Comment thread cpp/ql/lib/semmle/code/cpp/regex/internal/ParseRegExp.qll Outdated
jketema and others added 2 commits August 10, 2026 11:30
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

@geoffw0 geoffw0 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 ']'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is MRI?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there anything specific you're thinking about?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense. Will add.

@jketema

jketema commented Aug 27, 2026

Copy link
Copy Markdown
Contributor Author

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.

Commented on that here: https://github.com/jketema/codeql/pull/33/files#r3871704321

@geoffw0 geoffw0 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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???)

@jketema jketema Aug 27, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to test std::wregex as well?

I'm not sure that's adding much at this stage, but let me add the needed prototypes and at least one test.

Comment thread cpp/ql/lib/semmle/code/cpp/regex/RegexTreeView.qll Outdated

/**
* Holds if the property is inverted. For example, it holds for `[[:^digit:]]`,
* which matches non-digits.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, I'm a bit surprised this isn't supported.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is pretty Ruby specific?

@geoffw0 geoffw0 Aug 27, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 [^...].

@jketema jketema Aug 27, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.]]");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That might be. I'll have a look and update the test if appropriate.

Comment on lines +576 to +578
not this.isUnicode() and
not this.isHex() and
not this.isControl() and

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe these are redundant, as they're checked inside isIdentityEscape.

Suggested change
not this.isUnicode() and
not this.isHex() and
not this.isControl() and

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You might be right. I'll check.

Comment on lines +806 to +807
* A normal character in a regular expression, that is, a character
* without special meaning. This includes escaped characters.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rename to RegExpNormalChars???

}

/**
* Holds if `root` has the `s` flag for multi-line matching.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, this appears to be a stale comment.

@geoffw0

geoffw0 commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

I'm going to do a few quick tests locally

I've now done my local testing. I noticed locations of regex terms where the regex includes one or more escape sequences (\\) are a little bit off. It doesn't need fixing now, but it's worth noting I think. Other than that I'm happy.


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 main. 🚀

@jketema

jketema commented Aug 27, 2026

Copy link
Copy Markdown
Contributor Author

I noticed locations of regex terms where the regex includes one or more escape sequences (\\) are a little bit off.

I saw that. I tried fixing that, but that turned out to be non-trivial. I'll add a note in the qll file on this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants