SonarPHP Regex Rules
SonarPHP checks the regular expressions of PHP code with 25 rules. This page names, for each of them, the PHPRegex identifier that reports the same defect, says whether PHPRegex covers it fully or in part, and why a rule is left out. A team moving from SonarPHP, or running both, can find every Sonar finding here and silence it once. Each rule carries its Sonar S-id, so its compliance examples can be pulled up in the Sonar product the team runs.
Lint identifiers (regex.lint.*) are reported by regex lint, the PHPStan rule with lint
enabled, the language server and the framework commands; each is described in the
Lint Rule Reference. A rule marked off by default runs once
checks.lint.rules in regex.json turns it on. The PHPStan rule never runs those: it
lints with no rule turned on beyond the defaults, so S5857, S6326 and S6397 are reported
by regex lint, the language server and the Symfony and Laravel commands only.
| Sonar rule | What it reports | PHPRegex | Coverage |
|---|---|---|---|
| S5361 | preg_replace() where str_replace() does |
the Rector rules (Rector guide); PHPStan regex.trivialMatch for preg_match() |
covered |
| S5842 | a repeated part that can match the empty string | regex.lint.quantifier.emptyRepeat |
covered; silent where an empty alternative, a quantified lookaround or a nested quantifier already reports the repeat, even when the configuration turns that rule off |
| S5843 | an overly complex expression | regex.lint.complexity |
partial: PHPRegex keeps its own complexity score |
| S5850 | anchors that bind to one alternative only | regex.lint.anchor.alternationPrecedence |
covered; silent, as in SonarPHP, when an anchor sits anywhere but the start of the first alternative and the end of the last, and when every alternative is anchored on one side |
| S5855 | redundant alternatives | regex.lint.alternation.duplicateDisjunction |
partial: identical alternatives only |
| S5856 | an invalid pattern | the validation every entry point runs (regex lint errors, PHPStan core and regex.invalidForTarget) |
covered |
| S5857 | a reluctant quantifier a negated class would replace | regex.lint.quantifier.lazyToClass (off by default) |
covered for .*? and .+? before one character, when the automata prove the rewrite writes the same $matches |
| S5867 | [a-zA-Z] where a Unicode class would serve |
none | out of scope: \p{L} matches other text than [a-zA-Z], so the advice changes what the pattern matches |
| S5868 | a grapheme cluster inside a class | regex.lint.unicode.multibyteInClassWithoutU |
partial: a multibyte character without /u only; under /u a combining-mark table would be needed |
| S5869 | a character twice in a class | regex.lint.charclass.redundant, regex.lint.charclass.duplicateChars |
covered |
| S5994 | an atom a possessive quantifier always starves | regex.lint.quantifier.possessiveImpossible |
covered for atoms that read one character |
| S5996 | a boundary that can never match | regex.lint.anchor.impossible.start, regex.lint.anchor.impossible.end, regex.lint.anchor.impossible.boundary |
covered; \b and \B between atoms that read one character |
| S6001 | a back reference to a group not matched before it | regex.lint.backref.undefined, regex.lint.backref.useless |
covered |
| S6002 | a contradictory lookahead | regex.lint.lookaround.impossible |
covered on the regular subset the automata read; silent past it |
| S6019 | a reluctant quantifier followed only by what can match nothing | regex.lint.quantifier.lazyEnd |
covered |
| S6035 | single-character alternatives a class would replace | the optimization suggestions of regex lint and PHPStan regex.optimization |
out of scope as a lint rule: the optimizer reports the shorter pattern |
| S6323 | an empty alternative | regex.lint.alternation.empty |
covered |
| S6326 | several spaces in a row | regex.lint.literal.multipleSpaces (off by default) |
covered |
| S6328 | a replacement that refers to a group the pattern does not have | PHPStan regex.replacement.undefinedGroup |
covered in PHPStan only: regex lint reads patterns, not the replacement argument |
| S6331 | an empty group | regex.lint.group.empty |
partial: (?:) and (?>); an empty capturing group () is a placeholder that numbers a group and is not reported, nor is a group a quantifier repeats (a(?:)? is not a?) or one that keeps an escape from a digit (\1(?:)0) or braces from a count (a{(?:)2}) |
| S6353 | a quantifier or class written longer than needed | the optimization suggestions of regex lint and PHPStan regex.optimization |
out of scope as a lint rule: the optimizer reports the shorter pattern |
| S6393 | a pattern without valid delimiters | the validation every entry point runs | covered |
| S6395 | a non-capturing group without a quantifier | regex.lint.group.redundant |
partial: a group around one atom only |
| S6396 | a superfluous curly-brace quantifier | regex.lint.quantifier.useless, regex.lint.quantifier.zero |
covered |
| S6397 | a class of a single character | regex.lint.charclass.single (off by default) |
covered; a class of one metacharacter, the delimiter, a multibyte character without /u, or a character that reads otherwise bare ([\1], the [0] of \1[0]) is left alone |