The Checker Framework Manual:
Custom pluggable types for Java

Chapter 14 Regex Checker for regular expression syntax

The Regex Checker prevents, at compile time, use of syntactically invalid regular expressions and access of invalid capturing groups.

A regular expression, or regex, is a pattern for matching certain strings of text. In Java, a programmer writes a regular expression as a string. The syntax of regular expressions is complex, so it is easy to make a mistake. It is also easy to accidentally use a regex feature from another language that is not supported by Java (see section “Comparison to Perl 5” in the Pattern Javadoc). These problems cause run-time errors.

Regular expressions in Java also have capturing groups, which are delimited by parentheses and allow for extraction from text. If a programmer uses an incorrect index (larger than the number of capturing groups), an IndexOutOfBoundsException is thrown.

The Regex Checker warns about these problems at compile time, guaranteeing that your program does not crash due to incorrect use of regular expressions.

For further details, including case studies, see the paper “A type system for regular expressions” [SDE12] (FTfJP 2012, https://homes.cs.washington.edu/~mernst/pubs/regex-types-ftfjp2012-abstract.html).

To run the Regex Checker, supply the -processor org.checkerframework.checker.regex.RegexChecker command-line option to javac.

14.1 Regex annotations

These qualifiers make up the Regex type system:

@Regex

indicates that the run-time value is a valid regular expression String. If the optional parameter is supplied to the qualifier, then the number of capturing groups in the regular expression is at least that many. If not provided, the parameter defaults to 0. For example, if an expression’s type is @Regex(1) String, then its run-time value could be "colo(u?)r" or "(brown|beige)" but not "colou?r" nor a non-regex string such as "1) first point".

@PolyRegex

indicates qualifier polymorphism. For a description of qualifier polymorphism, see Section 32.2.

The subtyping hierarchy of the Regex Checker’s qualifiers is shown in Figure 14.1.

(image)

Figure 14.1: The subtyping relationship of the Regex Checker’s qualifiers. The type qualifiers are applicable to CharSequence and its subtypes. Because the parameter to a @Regex qualifier is at least the number of capturing groups in a regular expression, a @Regex qualifier with more capturing groups is a subtype of a @Regex qualifier with fewer capturing groups. Qualifiers in gray are used internally by the type system but should never be written by a programmer.

14.2 Annotating your code with @Regex

14.2.1 Implicit qualifiers

The Regex Checker adds implicit qualifiers, reducing the number of annotations that must appear in your code (see Section 33.4). If a String literal is a valid regex, the checker implicitly adds the @Regex qualifier with the argument set to the correct number of capturing groups. The Regex Checker allows the null literal to be assigned to any type qualified with the Regex qualifier.

14.2.2 Capturing groups

The Regex Checker validates that a legal capturing group number is passed to Matcher’s group, start, and end methods. To do this, the type of Matcher must be qualified with a @Regex annotation with the number of capturing groups in the regular expression. This is handled implicitly by the Regex Checker for local variables (see Section 33.7), but you may need to add @Regex annotations with a capturing group count to Pattern and Matcher fields and parameters.

14.2.3 Concatenation of partial regular expressions
public @Regex String parenthesize(@Regex String regex) {
    return "(" + regex + ")"; // Even though the parentheses are not @Regex Strings,
                              // the whole expression is a @Regex String
}

Figure 14.2: An example of the Regex Checker’s support for concatenation of non-regular-expression Strings to produce valid regular expression Strings.

In general, concatenating a non-regular-expression String with any other string yields a non-regular-expression String. The Regex Checker can sometimes determine that concatenation of non-regular-expression Strings will produce valid regular expression Strings. For an example, see Figure 14.2.

14.2.4 Testing whether a string is a regular expression

Sometimes, the Regex Checker cannot infer whether a particular expression is a regular expression — and sometimes your code cannot either! In these cases, you can use the isRegex method to perform such a test, and other helper methods to provide useful error messages. A common use is for user-provided regular expressions (such as ones passed on the command-line). Figure 14.3 gives an example of the intended use of the RegexUtil methods.

RegexUtil.isRegex

returns true if its argument is a valid regular expression.

RegexUtil.regexError

returns a String error message if its argument is not a valid regular expression, or null if its argument is a valid regular expression.

RegexUtil.regexException

returns the PatternSyntaxException that Pattern.compile(String) throws when compiling an invalid regular expression. It returns null if its argument is a valid regular expression.

A version of each of these methods is also provided that takes an additional group count parameter. The RegexUtil.isRegex method verifies that the argument has at least the given number of groups. The RegexUtil.regexError and RegexUtil.regexException methods return a String error message and a PatternSyntaxException, respectively, detailing why the given String is not a syntactically valid regular expression with at least the given number of capturing groups.

If you detect that a String is not a valid regular expression but would like to report the error higher up the call stack (potentially where you can provide a more detailed error message), you can throw a RegexUtil.CheckedPatternSyntaxException. This exception is functionally the same as a PatternSyntaxException except that it is checked to guarantee that the error will be handled up the call stack. For more details, see the Javadoc for RegexUtil.CheckedPatternSyntaxException.

To use the RegexUtil class, the checker-util.jar file must be on the classpath at run time.

String regex = getRegexFromUser();
if (! RegexUtil.isRegex(regex)) {
   throw new RuntimeException("Error parsing regex " + regex, RegexUtil.regexException(regex));
}
Pattern p = Pattern.compile(regex);

Figure 14.3: Example use of RegexUtil methods.

14.2.5 Suppressing warnings

If you are positive that a particular string that is being used as a regular expression is syntactically valid, but the Regex Checker cannot conclude this and issues a warning about possible use of an invalid regular expression, then you can use the RegexUtil.asRegex method to suppress the warning.

You can think of this method as a cast: it returns its argument unchanged, but with the type @Regex String if it is a valid regular expression. It throws an error if its argument is not a valid regular expression, but you should only use it when you are sure it will not throw an error.

There is an additional RegexUtil.asRegex method that takes a capturing group parameter. This method works the same as described above, but returns a @Regex String with the parameter on the annotation set to the value of the capturing group parameter passed to the method.

The use case shown in Figure 14.3 should support most cases, so the asRegex method should be used rarely.