{"record":{"id":"2a4b033325f94648","repo":"oracle/graal","slug":"possessive-quantifiers-are-not-supported","errorCode":null,"errorMessage":"possessive quantifiers are not supported","messagePattern":"possessive quantifiers are not supported","errorType":"exception","errorClass":"UnsupportedRegexException","httpStatus":null,"severity":"error","filePath":"regex/src/com.oracle.truffle.regex/src/com/oracle/truffle/regex/flavor/java/JavaRegexParser.java","lineNumber":164,"sourceCode":"                case nonWordBoundary:\n                    if (lexer.getLocalFlags().isUnicodeCharacterClass()) {\n                        buildWordNonBoundaryAssertion(lexer.unicode.word, lexer.unicode.nonWord);\n                    } else {\n                        buildWordNonBoundaryAssertion(Constants.WORD_CHARS, Constants.NON_WORD_CHARS);\n                    }\n                    break;\n                case backReference:\n                    astBuilder.addBackReference((Token.BackReference) token, getFlags().isCaseInsensitive(), getFlags().isUnicodeCase() || getFlags().isUnicodeCharacterClass());\n                    break;\n                case quantifier:\n                    Token.Quantifier quantifier = (Token.Quantifier) token;\n                    // quantifiers of type *, + or ? cannot directly follow another quantifier\n                    if (last instanceof Token.Quantifier && quantifier.isSingleChar()) {\n                        throw syntaxErrorHere(JavaErrorMessages.danglingMetaCharacter(quantifier), ErrorCode.InvalidQuantifier);\n                    }\n                    if (astBuilder.getCurTerm() != null) {\n                        if (quantifier.isPossessive()) {\n                            throw new UnsupportedRegexException(\"possessive quantifiers are not supported\");\n                        }\n                        addQuantifier((Token.Quantifier) token);\n                    } else {\n                        if (quantifier.isSingleChar()) {\n                            throw syntaxErrorHere(JavaErrorMessages.danglingMetaCharacter(quantifier), ErrorCode.InvalidQuantifier);\n                        }\n                    }\n                    break;\n                case alternation:\n                    astBuilder.nextSequence();\n                    break;\n                case inlineFlags:\n                    // flagStack push is handled in the lexer\n                    if (!((Token.InlineFlags) token).isGlobal()) {\n                        astBuilder.pushGroup(token);\n                        lexer.pushLocalFlags();\n                    }\n                    lexer.setCurrentFlags((JavaFlags) ((Token.InlineFlags) token).getFlags());","sourceCodeStart":146,"sourceCodeEnd":182,"githubUrl":"https://github.com/oracle/graal/blob/a66e9ccd1d7bf2552883939aa0788dfd0e294aab/regex/src/com.oracle.truffle.regex/src/com/oracle/truffle/regex/flavor/java/JavaRegexParser.java#L146-L182","documentation":"Thrown by the Java-flavor parser when a quantifier is marked possessive (*+, ++, ?+, {n,m}+). Possessive quantifiers swallow the input without ever giving it back, which requires cut semantics in the matcher; TRegex's NFA/DFA executors cannot express that, so JavaRegexParser checks quantifier.isPossessive() on the parsed token and throws UnsupportedRegexException.","triggerScenarios":"Compiling a Java-flavor pattern containing a possessive quantifier, e.g. \"a*+\", \"\\\\d++\\\\.\", or \"[a-z]{2,5}+\". The lexer emits a Quantifier token flagged possessive (the extra '+' after the quantifier), and the parser's quantifier case throws when the current term is non-null.","commonSituations":"Performance-tuned patterns from java.util.regex (possessive quantifiers are a standard JDK feature); ReDoS mitigation advice that suggests *+ or ++; copy-paste from PCRE-centric documentation into a TRegex-backed engine.","solutions":["Drop the trailing '+' to make the quantifier greedy: 'a*+' -> 'a*', '\\\\d++' -> '\\\\d+'","If the possessive '+' prevented catastrophic backtracking, redesign the pattern (anchor earlier, use negated classes, unroll the loop) rather than relying on the cut","Add a pattern pre-check that rejects/maps possessive suffixes when patterns come from users"],"exampleFix":"// before\nString p = \"\\\\d++\\\\.\"; // possessive\n\n// after\nString p = \"\\\\d+\\\\.\"; // greedy","handlingStrategy":"validation","validationCode":"private static final java.util.regex.Pattern POSSESSIVE =\n    java.util.regex.Pattern.compile(\"([*+?]|\\\\{[0-9]+(,[0-9]*)?\\\\})\\\\+\");\n\nboolean hasPossessiveQuantifier(String pattern) {\n    return POSSESSIVE.matcher(pattern).find();\n}","typeGuard":null,"tryCatchPattern":"try {\n    RegexObject re = compileJavaFlavor(pattern);\n} catch (UnsupportedRegexException e) {\n    if (e.getReason().contains(\"possessive\")) {\n        pattern = POSSESSIVE.matcher(pattern).replaceAll(\"$1\"); // strip possessive '+'\n        re = compileJavaFlavor(pattern);\n    } else { throw e; }\n}","preventionTips":["Strip possessive '+' suffixes when translating JDK/PCRE patterns for TRegex","Test patterns against the target engine in CI, not only against java.util.regex"],"tags":["regex","java-flavor","quantifier","possessive","unsupported-feature"],"backgroundTag":null,"analyzedSha":"a66e9ccd1d7bf2552883939aa0788dfd0e294aab","analyzedAt":"2026-08-14T13:58:47.161Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}