Grapheme Cluster In Character Class

ID

kotlin.regex_grapheme_cluster_in_class

Severity

high

Remediation Complexity

trivial

Remediation Risk

low

Remediation Effort

low

Resource

Reliability

Language

Kotlin

Tags

code-style

Description

Reports a regex character class that combines a base character with a following Unicode combining mark. What looks like one accented letter is encoded as two code points — the base letter, then the mark — and a character class is an enumeration of its members, never a sequence.

Rationale

// Bad — written as if it held one accented letter
val re = Regex("[c̈]")
re.containsMatchIn("c̈")   // false — the class never matches the pair
re.containsMatchIn("c")     // true  — it matches the bare base letter
re.containsMatchIn("̈")    // true  — or the bare combining mark
  • The class does not mean what it looks like. [c̈] is read exactly like [c + mark + ]: two independent members, so it matches either code point alone and never the grapheme the source appears to name.

  • The defect is invisible in review. Most editors render the base and its mark as a single glyph, so the class looks like it holds one character and the diff shows nothing unusual.

  • A validation or sanitising class built this way silently accepts a lone combining mark, and silently rejects the accented character it was meant to allow.

Remediation

Match the grapheme as a unit — with an alternation, or with a group when it has to sit inside a larger class-like choice. Using the precomposed single code point (é, ñ) where one exists is simpler still.

// Good
val re = Regex("c̈|ö")        // an alternation matches each grapheme as a unit
val precomposed = Regex("[éñ]")   // one code point each, no combining mark

If the individual code point really was intended, say so explicitly by writing the mark as an escape in a raw string:

val re = Regex("""[n\u0303]""")   // deliberately the base letter or that code point

What is not reported

An alternation or a group is never reported — only a character class, because only a class turns a sequence into a choice. A precomposed character is a single code point and carries no mark. A combining mark alone in a class has no base character to combine with. A code-point escape inside a raw string breaks the pair and is treated as deliberate.

That last carve-out does not extend to an ordinary string literal, where "\u0303" is a Kotlin escape: it is resolved while the literal’s value is reconstructed, so an escaped mark and a typed one are the same text by the time the pattern is analysed, and both are reported. The pattern behaves identically either way, so the finding is accurate — write the escape in a raw string to mark it as intentional.

A pattern that is not statically known — built by concatenation, interpolated, or held in a variable — is not reported. Neither is String.replace("[c̈]", "-"), which takes a plain string and replaces it literally.

© 2026 Xygeni. All rights reserved