77 * classes, POSIX bracket sub-expressions, character/normal-character
88 * tokenization, and failure-to-parse reporting) purely in terms of a small
99 * set of **dialect hook** abstract predicates. Concrete grammar dialects
10- * (currently only ECMAScript, implemented by `EcmaRegExp`) supply the raw
11- * lexical decisions — "is this position an escape backslash ?", "is this a
12- * group open?", "is this a quantifier?", etc. — by overriding those hooks.
10+ * supply the raw lexical decisions — "is this position an escape
11+ * backslash?", "is this a group open ?", "is this a quantifier?", etc. — by
12+ * overriding those hooks.
1313 *
14- * The only grammar dialect implemented today is ECMAScript (the default
15- * used by `std::regex`, i.e. `std::regex_constants::ECMAScript`). The other
16- * `std::regex` grammars (`basic`, `extended`, `awk`, `grep`, `egrep`) are
14+ * The grammar dialects modelled today are:
15+ * - ECMAScript (`EcmaRegExp`), the default used by `std::regex` (i.e.
16+ * `std::regex_constants::ECMAScript`); and
17+ * - POSIX Extended Regular Expressions (`EreRegExp`), selected via the
18+ * `extended`, `egrep`, and `awk` flags.
19+ *
20+ * POSIX Basic Regular Expressions (BRE, selected via `basic`/`grep`) are
1721 * still excluded by the `RegExp` characteristic predicate; the hook-based
18- * split exists so a later phase can plug in POSIX BRE/ERE without touching
19- * the shared core.
22+ * split exists so a later phase can plug in BRE without touching the
23+ * shared core.
2024 *
21- * The single-grammar-per-literal assumption is baked into this phase:
22- * because every parsed literal is `Ecma()` today, `EcmaRegExp` is the sole
23- * concrete subclass and there is no overlap. The "same literal used under
24- * two different grammars" case is not yet handled and is deferred to the
25- * phase that actually introduces a second concrete grammar subclass; at
26- * that point this file will need to revisit how `TRegExpParent` identity
27- * relates to grammar.
25+ * The single-grammar-per-literal assumption is still baked into this phase:
26+ * because each parsed literal's grammar is uniquely determined by
27+ * `regexGrammar` (a functional classifier over the construction-site flag
28+ * argument), a literal is either an `EcmaRegExp` or an `EreRegExp`, never
29+ * both. The "same literal used under two different grammars" case is not
30+ * yet handled and is deferred to the phase that actually introduces
31+ * overlap; at that point this file will need to revisit how
32+ * `TRegExpParent` identity relates to grammar.
2833 */
2934
3035import cpp
@@ -40,19 +45,20 @@ private import semmle.code.cpp.regex.RegexFlowConfigs as RFC
4045 * A `StringLiteral` is treated as a regex only when dataflow indicates it
4146 * flows to a `std::basic_regex` construction/assignment or to a
4247 * `regex_match`/`regex_search`/`regex_replace`/iterator call. Regexes
43- * constructed with an explicit non-ECMAScript grammar flag are excluded,
44- * since the parser only models the ECMAScript dialect (see
45- * `EcmaRegExp`) .
48+ * constructed with a grammar flag the parser does not yet model (currently
49+ * POSIX Basic Regular Expressions — the `basic` and `grep` flags) are
50+ * excluded .
4651 *
4752 * This class is abstract: its structural predicates are expressed in terms
4853 * of dialect hooks (see below), and concrete grammar subclasses supply the
49- * dialect-specific token-recognition behavior. The sole concrete subclass
50- * today is `EcmaRegExp`.
54+ * dialect-specific token-recognition behavior. The concrete subclasses are
55+ * `EcmaRegExp` (for ECMAScript) and `EreRegExp` (for POSIX Extended
56+ * Regular Expressions).
5157 */
5258abstract class RegExp extends StringLiteral {
5359 RegExp ( ) {
5460 RFC:: usedAsRegex ( this ) and
55- not RFC:: hasNonEcmaScriptGrammarFlag ( this )
61+ ( RFC :: regexGrammar ( this ) = RFC:: Ecma ( ) or RFC :: regexGrammar ( this ) = RFC :: Ere ( ) )
5662 }
5763
5864 /** Gets the `i`th character of this regex string. */
@@ -912,11 +918,12 @@ abstract class RegExp extends StringLiteral {
912918 * The ECMAScript-grammar concrete `RegExp` implementation. Supplies all
913919 * dialect hooks with the ECMAScript token-recognition behavior.
914920 *
915- * Because the core `RegExp` class already excludes non-ECMAScript-grammar
916- * literals via `not hasNonEcmaScriptGrammarFlag`, and `regexGrammar(this)`
917- * returns `Ecma()` for anything not explicitly tagged as `basic`/`grep`/
918- * `extended`/`egrep`/`awk`, this class currently covers every `RegExp`
919- * instance in the database — `EcmaRegExp` is the sole concrete subclass.
921+ * Selected for regex literals whose construction-site flag argument does
922+ * not specify a POSIX grammar (i.e. anything not tagged as
923+ * `basic`/`grep`/`extended`/`egrep`/`awk`), matching the default `std::regex`
924+ * grammar. `EcmaRegExp` and `EreRegExp` are the two concrete subclasses of
925+ * `RegExp`; the grammar of a given literal is determined uniquely by
926+ * `regexGrammar`.
920927 */
921928class EcmaRegExp extends RegExp {
922929 EcmaRegExp ( ) { RFC:: regexGrammar ( this ) = RFC:: Ecma ( ) }
@@ -1262,3 +1269,220 @@ class EcmaRegExp extends RegExp {
12621269 this .named_backreference ( start , end , result )
12631270 }
12641271}
1272+
1273+ // ===========================================================================
1274+ // POSIX Extended Regular Expressions (ERE) dialect
1275+ // ===========================================================================
1276+
1277+ /**
1278+ * The POSIX Extended Regular Expressions concrete `RegExp` implementation.
1279+ * Supplies all dialect hooks with the ERE token-recognition behavior.
1280+ *
1281+ * ERE is selected via the `std::regex_constants` flags `extended`, `egrep`,
1282+ * and `awk` (all three parse the same grammar — they differ only in whether
1283+ * the matching engine is treated as backtracking; see
1284+ * `RegexFlowConfigs::isBacktrackingEngine`).
1285+ *
1286+ * ERE is largely subtractive relative to ECMAScript:
1287+ *
1288+ * - Same grouping (`(...)`), alternation (`|`), and quantifiers
1289+ * (`*`, `+`, `?`, `{n}`, `{n,}`, `{n,m}`).
1290+ * - Same character classes `[...]` (including the shared POSIX bracket
1291+ * sub-expressions handled entirely in the core: `[:class:]`, `[.a.]`,
1292+ * `[=a=]`).
1293+ * - Same anchors `^`, `$` and wildcard `.`.
1294+ *
1295+ * ERE has **no**:
1296+ * - Class-shorthand escapes `\d`, `\w`, `\s`, `\D`, `\W`, `\S`.
1297+ * - Word-boundary anchors `\b`, `\B`.
1298+ * - Numeric or hex/unicode/octal escapes (`\1`, `\xNN`, `\uNNNN`, `\0`).
1299+ * - Back-references (numbered or named).
1300+ * - Look-around (`(?=`, `(?!`, `(?<=`, `(?<!`).
1301+ * - Non-capturing groups (`(?:...)`).
1302+ * - Named capturing groups (`(?<name>...)`).
1303+ * - Lazy quantifier suffix (`*?`, `+?`, `??`, `{n,m}?`).
1304+ *
1305+ * A backslash in front of any character produces a literal escaped
1306+ * character (two-character span): so `\.` matches a literal `.`, `\(`
1307+ * matches a literal `(`, and so on. This is the standard interpretation of
1308+ * `\` in ERE for metacharacters; behavior for `\` in front of an ordinary
1309+ * character is implementation-defined but is uniformly treated here as a
1310+ * literal escape of the following character so the tokenizer is total.
1311+ */
1312+ class EreRegExp extends RegExp {
1313+ EreRegExp ( ) { RFC:: regexGrammar ( this ) = RFC:: Ere ( ) }
1314+
1315+ override RFC:: TRegexGrammar getGrammar ( ) { result = RFC:: Ere ( ) }
1316+
1317+ // ---------------------------------------------------------------------------
1318+ // Escaping
1319+ // ---------------------------------------------------------------------------
1320+
1321+ /**
1322+ * Helper predicate for `escapingChar`.
1323+ * Returns `true` if the character at position `pos` is an active backslash
1324+ * (i.e., it escapes the next character). Uses a boolean to avoid negation
1325+ * in recursive calls.
1326+ */
1327+ private boolean escaping ( int pos ) {
1328+ pos = - 1 and result = false
1329+ or
1330+ this .getChar ( pos ) = "\\" and result = this .escaping ( pos - 1 ) .booleanNot ( )
1331+ or
1332+ this .getChar ( pos ) != "\\" and result = false
1333+ }
1334+
1335+ override predicate escapingChar ( int pos ) { this .escaping ( pos ) = true }
1336+
1337+ // ---------------------------------------------------------------------------
1338+ // Escaped characters
1339+ //
1340+ // ERE has no numeric, hex, unicode, or octal escapes and no back-references.
1341+ // Every `\X` is a simple two-character escape yielding a literal X.
1342+ // ---------------------------------------------------------------------------
1343+
1344+ override predicate escapedCharacter ( int start , int end ) {
1345+ this .escapingChar ( start ) and
1346+ exists ( this .getChar ( start + 1 ) ) and
1347+ end = start + 2
1348+ }
1349+
1350+ // ---------------------------------------------------------------------------
1351+ // Special (meta) characters
1352+ //
1353+ // ERE has only the position-assertion / wildcard specials `^`, `$`, `.`.
1354+ // There are no word-boundary escapes `\b` / `\B`.
1355+ // ---------------------------------------------------------------------------
1356+
1357+ override predicate specialCharacter ( int start , int end , string char ) {
1358+ not this .inCharSet ( start ) and
1359+ this .character ( start , end ) and
1360+ end = start + 1 and
1361+ char = this .getChar ( start ) and
1362+ ( char = "$" or char = "^" or char = "." )
1363+ }
1364+
1365+ // ---------------------------------------------------------------------------
1366+ // Quantifiers
1367+ //
1368+ // ERE has no lazy suffix, so `qualifier` and `short_qualifier` coincide.
1369+ // ---------------------------------------------------------------------------
1370+
1371+ override predicate qualifier ( int start , int end , boolean maybe_empty , boolean may_repeat_forever ) {
1372+ this .short_qualifier ( start , end , maybe_empty , may_repeat_forever )
1373+ }
1374+
1375+ override predicate short_qualifier (
1376+ int start , int end , boolean maybe_empty , boolean may_repeat_forever
1377+ ) {
1378+ (
1379+ this .getChar ( start ) = "+" and maybe_empty = false and may_repeat_forever = true
1380+ or
1381+ this .getChar ( start ) = "*" and maybe_empty = true and may_repeat_forever = true
1382+ or
1383+ this .getChar ( start ) = "?" and maybe_empty = true and may_repeat_forever = false
1384+ ) and
1385+ end = start + 1 and
1386+ not this .escapingChar ( start - 1 )
1387+ or
1388+ exists ( string lower , string upper |
1389+ this .multiples ( start , end , lower , upper ) and
1390+ ( if lower = "" or lower .toInt ( ) = 0 then maybe_empty = true else maybe_empty = false ) and
1391+ if upper = "" then may_repeat_forever = true else may_repeat_forever = false
1392+ )
1393+ }
1394+
1395+ /**
1396+ * Holds if `[start, end)` is a `{n}`, `{n,m}`, or `{n,}` quantifier.
1397+ *
1398+ * In ERE, `{...}` is unconditionally a quantifier — there is no `\{...\}`
1399+ * literal-brace form (that would be BRE). A backslash-escaped `\{` is a
1400+ * literal `{`, so we must not treat it as a quantifier here.
1401+ */
1402+ override predicate multiples ( int start , int end , string lower , string upper ) {
1403+ not this .escapingChar ( start - 1 ) and
1404+ exists ( string text , string match , string inner |
1405+ text = this .getText ( ) and
1406+ end = start + match .length ( ) and
1407+ inner = match .substring ( 1 , match .length ( ) - 1 )
1408+ |
1409+ match = text .regexpFind ( "\\{[0-9]+\\}" , _, start ) and
1410+ lower = inner and
1411+ upper = lower
1412+ or
1413+ match = text .regexpFind ( "\\{[0-9]*,[0-9]*\\}" , _, start ) and
1414+ exists ( int commaIndex |
1415+ commaIndex = inner .indexOf ( "," ) and
1416+ lower = inner .prefix ( commaIndex ) and
1417+ upper = inner .suffix ( commaIndex + 1 )
1418+ )
1419+ )
1420+ }
1421+
1422+ // ---------------------------------------------------------------------------
1423+ // Groups
1424+ //
1425+ // ERE has only simple capturing groups `(...)`. It has no `(?:...)`,
1426+ // no `(?<name>...)`, and no look-around forms.
1427+ // ---------------------------------------------------------------------------
1428+
1429+ override predicate isOptionDivider ( int i ) { this .nonEscapedCharAt ( i ) = "|" }
1430+
1431+ override predicate isGroupEnd ( int i ) { this .nonEscapedCharAt ( i ) = ")" and not this .inCharSet ( i ) }
1432+
1433+ override predicate isGroupStart ( int i ) { this .nonEscapedCharAt ( i ) = "(" and not this .inCharSet ( i ) }
1434+
1435+ override predicate group_start ( int start , int end ) { this .simple_group_start ( start , end ) }
1436+
1437+ /** `(...)` – simple capturing group. */
1438+ override predicate simple_group_start ( int start , int end ) {
1439+ this .isGroupStart ( start ) and end = start + 1
1440+ }
1441+
1442+ /** ERE has no non-capturing group form. */
1443+ override predicate non_capturing_group_start ( int start , int end ) { none ( ) }
1444+
1445+ /** ERE has no named capturing group form. */
1446+ override predicate ecma_named_group_start ( int start , int end ) { none ( ) }
1447+
1448+ /** ERE has no look-around. */
1449+ override predicate lookahead_assertion_start ( int start , int end ) { none ( ) }
1450+
1451+ /** ERE has no look-around. */
1452+ override predicate negative_lookahead_assertion_start ( int start , int end ) { none ( ) }
1453+
1454+ /** ERE has no look-around. */
1455+ override predicate lookbehind_assertion_start ( int start , int end ) { none ( ) }
1456+
1457+ /** ERE has no look-around. */
1458+ override predicate negative_lookbehind_assertion_start ( int start , int end ) { none ( ) }
1459+
1460+ /**
1461+ * Gets the 1-based index of the capture group at `[start, end)`. In ERE
1462+ * every `(...)` group is a capturing group, numbered by left-to-right
1463+ * position of the opening `(`.
1464+ */
1465+ override int getGroupNumber ( int start , int end ) {
1466+ this .group ( start , end ) and
1467+ result = count ( int i | this .group ( i , _) and i < start ) + 1
1468+ }
1469+
1470+ /** ERE has no named groups. */
1471+ override string getGroupName ( int start , int end ) { none ( ) }
1472+
1473+ // ---------------------------------------------------------------------------
1474+ // Back-references
1475+ //
1476+ // ERE has no back-references (neither numbered nor named).
1477+ // ---------------------------------------------------------------------------
1478+
1479+ override predicate numbered_backreference ( int start , int end , int value ) { none ( ) }
1480+
1481+ override predicate named_backreference ( int start , int end , string name ) { none ( ) }
1482+
1483+ override predicate backreference ( int start , int end ) { none ( ) }
1484+
1485+ override int getBackrefNumber ( int start , int end ) { none ( ) }
1486+
1487+ override string getBackrefName ( int start , int end ) { none ( ) }
1488+ }
0 commit comments