languagetool-org/languagetool · error · RuntimeException

Contradicting regex contains both '?i' (case-insensitive) an

Error message

Contradicting regex contains both '?i' (case-insensitive) and \p{Lu}/\p{Ll} (case-sensitive): + s + in rule + id

What it means

XMLRuleHandler.hasPosixCharacterClass throws this RuntimeException when a regex in a rule combines the case-insensitive flag '(?i)' with POSIX-style case-sensitive character classes \p{Lu} or \p{Ll}. To be compatible with Java 15+, LanguageTool treats \p{Lu}/\p{Ll} as implying case sensitivity, so mixing them with (?i) is contradictory and rejected at rule load time.

Source

Thrown at languagetool-core/src/main/java/org/languagetool/rules/patterns/XMLRuleHandler.java:502

      PatternToken exception = new PatternToken(exceptionStringInflected, internMatcher(exceptions.toString().trim(), exceptionStringRegExp, caseSensitive));
      exception.setNegation(exceptionStringNegation);
      exception.setPosToken(obtainPosToken(exceptionPosToken, exceptionPosRegExp, exceptionPosNegation));
      patternToken.addException(exceptionValidNext, exceptionValidPrev, exception);
      exceptionPosToken = null;
      exceptionLevelCaseSensitive = null;
    }
    if (exceptionSpaceBeforeSet) {
      patternToken.setExceptionSpaceBefore(exceptionSpaceBefore);
    }
    resetException();
  }

  // To be compatible with Java 15, we make p{Lu} and p{Ll} imply case-sensitivity,
  // see https://github.com/languagetool-org/languagetool/issues/4061
  private boolean hasPosixCharacterClass(String s) {
    boolean res = s.contains("\\p{Lu}") || s.contains("\\p{Ll}");
    if (res && s.contains("(?i")) {
      throw new RuntimeException("Contradicting regex contains both '?i' (case-insensitive) and \\p{Lu}/\\p{Ll} (case-sensitive): "
              + s + " in rule " + id);
    }
    return res;
  }

  protected void setToken(Attributes attrs) throws SAXException {
    inToken = true;
    if (lastPhrase) {
      patternTokens.clear();
    }
    lastPhrase = false;
    tokenNegated = YES.equals(attrs.getValue(NEGATE));
    tokenInflected = YES.equals(attrs.getValue(INFLECTED));
    if (attrs.getValue(SKIP) != null) {
      skipPos = Integer.parseInt(attrs.getValue(SKIP));
    }
    if (attrs.getValue(MIN) != null) {
      minOccurrence = Integer.parseInt(attrs.getValue(MIN));

View on GitHub (pinned to 2e990059ce)

Solutions

  1. Remove the '(?i)' flag and spell out both cases explicitly (e.g. [\p{Lu}\p{Ll}]) or keep \p{Lu}/\p{Ll} only
  2. Alternatively drop \p{Lu}/\p{Ll} and rely on (?i) if case does not matter
  3. Test the regex in isolation with a small Java snippet or regex101 to confirm intended case behavior

Example fix

// before
<token regex="(?i)\p{Lu}\p{Lu}+"/>
// after
<token regex="\p{Lu}\p{Lu}+"/>
Defensive patterns

Strategy: validation

Validate before calling

if (regex.contains("(?i") && (regex.contains("\\p{Lu}") || regex.contains("\\p{Ll}"))) {
  throw new IllegalArgumentException("Case-insensitive flag combined with \\p{Lu}/\\p{Ll}: " + regex);
}

Try / catch

try {
  loader.getRules(is, filename);
} catch (RuntimeException e) {
  if (e.getMessage().contains("Contradicting regex")) {
    log.error("Remove (?i) or the POSIX case classes in the regex", e);
  } else throw e;
}

Prevention

When it happens

Trigger: A token with regex="...(?i)...\p{Lu}..." (or postag/exception regex containing both) is parsed in setToken/hasPosixCharacterClass for a rule with the given id.

Common situations: Upgrading LanguageTool/Java where regex semantics changed (issue #4061); combining previously working (?i) regexes with case-class tokens; reusing regexes from other tools.

Related errors


AI-assisted analysis of languagetool-org/languagetool@2e990059ce (2026-09-06). Data as JSON: /api/errors/07d6bf9d90da19cb. Report an issue: GitHub.