Regex Anchor Precedence

ID

kotlin.regex_anchor_precedence

Severity

high

Remediation Complexity

trivial

Remediation Risk

low

Remediation Effort

low

Resource

Reliability

Language

Kotlin

Tags

code-style

Description

Reports an alternation whose first branch starts with ^/\A or whose last branch ends with $/\Z/\z, while at least one other branch of the same alternation carries no such anchor.

The pattern text is read from the Regex(…​) constructor, from the receiver of String.toRegex(), and from Pattern.compile(…​) / Pattern.matches(…​) reached through Java interop. A pattern built at run time — a concatenation, an interpolated string, a variable — is not reported, since the string the engine compiles is not known here.

Rationale

Anchors bind tighter than |, so ^a|b|c$ is not "match a, b or c, anchored at both ends" — it parses as (^a)|b|(c$):

val re = Regex("^a|b|c$")
// "a" only matches at the very start, "c" only at the very end,
// but "b" matches anywhere in the input

The layout suggests every branch is bounded; only the outermost ones actually are. For a validation regex that means it accepts input it was written to reject.

Kotlin’s Regex compiles to the same engine as java.util.regex, so \A, \Z and \z are input-boundary assertions here and count as anchors alongside ^ and $.

Remediation

Wrap the alternatives in a non-capturing group so the anchors apply to the whole thing, or repeat the anchors on every branch if each was really meant to be anchored on its own:

val re = Regex("^(?:a|b|c)$")
// or
val re = Regex("^a$|^b$|^c$")

What is not reported

An alternation where every branch carries the same anchor on its own (a$|b$|^c$) is not reported — there is no gap between branches to be misled by. An alternation already wrapped in a group (^(?:a|b|c)$), or one whose anchored branches are each explicitly scoped in their own group ((?:^a)|b|(?:c$)), reads exactly as intended and is not reported either. An alternation with no anchor at either end is out of scope.

Two idioms that look like the defect are also exempt: a branch consisting of nothing but the anchor — Regex("(?:,|$)"), the deliberate "this or the boundary" form — and the two-branch trim shape ^A|B$, as in text.replace(Regex("^,|,$"), ""), where each branch is already anchored at the edge it is meant to strip.

Finally, String.replace("^a|b$", "-") is not reported: that overload takes a plain string and replaces it literally, so no regular expression is involved. The same holds for String.split(vararg String) and String.replaceFirst(String, String), and for Regex.fromLiteral(…​), which matches its argument as plain text.

© 2026 Xygeni. All rights reserved