fix(linter/eslint/no-misleading-character-class): detect more combining characters (#28733)

fixes https://github.com/oxc-project/oxc/issues/28729

Generated by Claude Sonnet 5.5 🤖

The upstream rules uses regex to detect if it is a combining character with the `\p` shorthand:
https://github.com/eslint/eslint/blob/main/lib/rules/utils/unicode/is-combining-character.js#L11

Because there is no similar utility in rust, we currently have a self-managed list of character detection.
This list now get extended to detect more characters for other languages like Arabic, Hebrew, Thai ...

Verified manually that the failing test cases also fails upstream.
Did not verfify if the list is really valid and covers all possible combining characters.
I hope that the AI did no mistakes and included characters which are still valid.
This commit is contained in:
Sysix committed 2026-11-03 09:47:30 +00:00
1 parent 74d50c1f32
commit b5b3f3fc88
2 files changed
+310 -3

No files matched your search

@@ -265,9 +265,15 @@ fn regional_indicator_symbol_sequences(chars: &[&Character], ctx: &LintContext<'
// - Combining Diacritical Marks for Symbols: U+20D0–U+20FF
// - Combining Half Marks: U+FE20–U+FE2F
//
// Additional combining characters exist within script-specific blocks
// (e.g., Devanagari signs, Hiragana/Katakana marks) and are not confined to a block,
// but share the Mn/Mc/Me categories
// Additional combining characters exist within script-specific blocks and are not
// confined to a dedicated block, but share the Mn/Mc/Me categories. These are listed
// per script in the `matches!` below, e.g.:
// - Hebrew, Arabic, Syriac, Thaana
// - Devanagari, Bengali, Gurmukhi, Gujarati, Oriya, Tamil, Telugu, Kannada, Malayalam, Sinhala
// - Thai, Lao, Tibetan, Myanmar, Khmer, Mongolian
// - Balinese, Javanese, Cham, Meetei Mayek, Hiragana/Katakana marks, and others
//
// Supplementary-plane scripts are only partially covered (Brahmi, Kharoshthi, Grantha).
//
// B. Variation Selectors (also Combining)
//
@@ -283,6 +289,50 @@ fn is_combining_character(value: u32) -> bool {
0x1DC0..=0x1DFF | // Combining Diacritical Marks Supplement
0x20D0..=0x20FF | // Combining Diacritical Marks for Symbols
0xFE20..=0xFE2F | // Combining Half Marks
0x0483..=0x0489 | // Cyrillic
0x0591..=0x05BD | 0x05BF | 0x05C1..=0x05C2 | 0x05C4..=0x05C5 | 0x05C7 | // Hebrew
0x0610..=0x061A | 0x064B..=0x065F | 0x0670 | 0x06D6..=0x06DC | 0x06DF..=0x06E4 | 0x06E7..=0x06E8 | 0x06EA..=0x06ED | // Arabic
0x0711 | 0x0730..=0x074A | // Syriac
0x07A6..=0x07B0 | // Thaana
0x07EB..=0x07F3 | 0x07FD | // NKo
0x0816..=0x0819 | 0x081B..=0x0823 | 0x0825..=0x0827 | 0x0829..=0x082D | // Samaritan
0x0859..=0x085B | // Mandaic
0x0898..=0x089F | 0x08CA..=0x08E1 | // Arabic Extended
0x08E3..=0x0903 | 0x093A..=0x093C | 0x093E..=0x094F | 0x0951..=0x0957 | 0x0962..=0x0963 | // Devanagari
0x0981..=0x0983 | 0x09BC | 0x09BE..=0x09C4 | 0x09C7..=0x09C8 | 0x09CB..=0x09CD | 0x09D7 | 0x09E2..=0x09E3 | 0x09FE | // Bengali
0x0A01..=0x0A03 | 0x0A3C | 0x0A3E..=0x0A42 | 0x0A47..=0x0A48 | 0x0A4B..=0x0A4D | 0x0A51 | 0x0A70..=0x0A71 | 0x0A75 | // Gurmukhi
0x0A81..=0x0A83 | 0x0ABC | 0x0ABE..=0x0AC5 | 0x0AC7..=0x0AC9 | 0x0ACB..=0x0ACD | 0x0AE2..=0x0AE3 | 0x0AFA..=0x0AFF | // Gujarati
0x0B01..=0x0B03 | 0x0B3C | 0x0B3E..=0x0B44 | 0x0B47..=0x0B48 | 0x0B4B..=0x0B4D | 0x0B55..=0x0B57 | 0x0B62..=0x0B63 | // Oriya
0x0B82 | 0x0BBE..=0x0BC2 | 0x0BC6..=0x0BC8 | 0x0BCA..=0x0BCD | 0x0BD7 | // Tamil
0x0C00..=0x0C04 | 0x0C3C | 0x0C3E..=0x0C44 | 0x0C46..=0x0C48 | 0x0C4A..=0x0C4D | 0x0C55..=0x0C56 | 0x0C62..=0x0C63 | // Telugu
0x0C81..=0x0C83 | 0x0CBC | 0x0CBE..=0x0CC4 | 0x0CC6..=0x0CC8 | 0x0CCA..=0x0CCD | 0x0CD5..=0x0CD6 | 0x0CE2..=0x0CE3 | 0x0CF3 | // Kannada
0x0D00..=0x0D03 | 0x0D3B..=0x0D3C | 0x0D3E..=0x0D44 | 0x0D46..=0x0D48 | 0x0D4A..=0x0D4D | 0x0D57 | 0x0D62..=0x0D63 | // Malayalam
0x0D81..=0x0D83 | 0x0DCA | 0x0DCF..=0x0DD4 | 0x0DD6 | 0x0DD8..=0x0DDF | 0x0DF2..=0x0DF3 | // Sinhala
0x0E31 | 0x0E34..=0x0E3A | 0x0E47..=0x0E4E | // Thai
0x0EB1 | 0x0EB4..=0x0EBC | 0x0EC8..=0x0ECE | // Lao
0x0F18..=0x0F19 | 0x0F35 | 0x0F37 | 0x0F39 | 0x0F3E..=0x0F3F | 0x0F71..=0x0F84 | 0x0F86..=0x0F87 | 0x0F8D..=0x0FBC | 0x0FC6 | // Tibetan
0x102B..=0x103E | 0x1056..=0x1059 | 0x105E..=0x1060 | 0x1062..=0x1064 | 0x1067..=0x106D | 0x1071..=0x1074 | 0x1082..=0x108D | 0x108F | 0x109A..=0x109D | // Myanmar
0x135D..=0x135F | // Ethiopic
0x1712..=0x1715 | 0x1732..=0x1734 | 0x1752..=0x1753 | 0x1772..=0x1773 | // Tagalog, Hanunoo, Buhid, Tagbanwa
0x17B4..=0x17D3 | 0x17DD | // Khmer
0x180B..=0x180D | 0x180F | 0x1885..=0x1886 | 0x18A9 | // Mongolian
0x1920..=0x192B | 0x1930..=0x193B | // Limbu
// spellchecker:disable-next-line
0x1A17..=0x1A1B | 0x1A55..=0x1A7F | // Buginese, Tai Tham
0x1B00..=0x1B04 | 0x1B34..=0x1B44 | 0x1B6B..=0x1B73 | // Balinese
0x1B80..=0x1B82 | 0x1BA1..=0x1BAD | 0x1BE6..=0x1BF3 | // Sundanese, Batak
0x1C24..=0x1C37 | // Lepcha
0x1CD0..=0x1CD2 | 0x1CD4..=0x1CE8 | 0x1CED | 0x1CF4 | 0x1CF7..=0x1CF9 | // Vedic Extensions
0x302A..=0x302F | 0x3099..=0x309A | // CJK / Kana combining marks
0xA66F..=0xA672 | 0xA674..=0xA67D | 0xA69E..=0xA69F | 0xA6F0..=0xA6F1 | // Cyrillic Extended-B, Bamum
0xA802 | 0xA806 | 0xA80B | 0xA823..=0xA827 | // Syloti Nagri
0xA880..=0xA881 | 0xA8B4..=0xA8C5 | 0xA8E0..=0xA8F1 | // Saurashtra, Devanagari Extended
0xA926..=0xA92D | 0xA947..=0xA953 | 0xA980..=0xA983 | 0xA9B3..=0xA9C0 | // Kayah Li, Rejang, Javanese
0xAA29..=0xAA36 | 0xAAEB..=0xAAEF | 0xABE3..=0xABEA | 0xABEC..=0xABED | // Cham, Meetei Mayek
0xFB1E | // Hebrew point judeo-spanish varika
0x10A01..=0x10A0F | 0x10A38..=0x10A3F | // Kharoshthi
0x11000..=0x11002 | 0x11038..=0x11046 | 0x11070 | 0x11073..=0x11074 | 0x1107F..=0x11082 | // Brahmi
0x11300..=0x11303 | 0x1133B..=0x1133C | 0x1133E..=0x11344 | 0x11347..=0x11348 | 0x1134B..=0x1134D | 0x11357 | 0x11362..=0x11363 | 0x11366..=0x1136C | 0x11370..=0x11374 | // Grantha
0xFE00..=0xFE0F | // Variation Selectors (Basic)
0xE0100..=0xE01EF // Variation Selectors (Supplement)
)
@@ -824,6 +874,39 @@ fn test() {
// https://github.com/oxc-project/oxc/issues/19090 -- without u flag it should fail
// this should not be a `UnicodePropertyEscape`
(r"/[\u200c\u200d\p{ID_Continue}.]/", None),
// https://github.com/oxc-project/oxc/issues/28729
("/[ગુ]/", None),
("/[कि]/", None),
("/[ક્]/", None),
("/[नमस्ते]/", None),
("new RegExp('[ગુ]', 'u');", None),
// combining marks of other scripts (base letter + mark)
("/[а҃]/u", None), // Cyrillic
("/[אָ]/u", None), // Hebrew
("/[بَ]/u", None), // Arabic
("/[ܐܰ]/u", None), // Syriac
("/[ހަ]/u", None), // Thaana
("/[ߊ߫]/u", None), // NKo
("/[কা]/u", None), // Bengali
("/[ਕਾ]/u", None), // Gurmukhi
("/[କା]/u", None), // Oriya
("/[கா]/u", None), // Tamil
("/[కా]/u", None), // Telugu
("/[ಕಾ]/u", None), // Kannada
("/[കാ]/u", None), // Malayalam
("/[කා]/u", None), // Sinhala
("/[กั]/u", None), // Thai
("/[ກັ]/u", None), // Lao
("/[ཀཱ]/u", None), // Tibetan
("/[ကါ]/u", None), // Myanmar
("/[កា]/u", None), // Khmer
("/[ᠠ᠋]/u", None), // Mongolian
("/[ᬆ]/u", None), // Balinese
("/[が]/u", None), // Kana
("/[ꦏꦴ]/u", None), // Javanese
("/[𑀓𑀸]/u", None), // Brahmi
("/[𐨕𐨁]/u", None), // Kharoshthi
("/[𑌕𑌾]/u", None), // Grantha
];
let fix = vec![
@@ -1222,3 +1222,227 @@ source: crates/oxc_linter/src/tester.rs
· ──────────────
╰────
help: Use Unicode code point escapes (e.g., \u{1F468}\u200D\u{1F469} for '👨‍👩') instead of zero-width joiner sequences in character classes.
⚠ eslint(no-misleading-character-class): Unexpected combining class in character class.
╭─[no_misleading_character_class.tsx:1:3]
1 │ /[ગુ]/
· ─
╰────
help: Replace the character with its normalized form (NFC) or use Unicode code point escapes instead of combining sequences.
⚠ eslint(no-misleading-character-class): Unexpected combining class in character class.
╭─[no_misleading_character_class.tsx:1:3]
1 │ /[कि]/
· ──
╰────
help: Replace the character with its normalized form (NFC) or use Unicode code point escapes instead of combining sequences.
⚠ eslint(no-misleading-character-class): Unexpected combining class in character class.
╭─[no_misleading_character_class.tsx:1:3]
1 │ /[ક્]/
· ─
╰────
help: Replace the character with its normalized form (NFC) or use Unicode code point escapes instead of combining sequences.
⚠ eslint(no-misleading-character-class): Unexpected combining class in character class.
╭─[no_misleading_character_class.tsx:1:9]
1 │ /[नमस्ते]/
· ─
╰────
help: Replace the character with its normalized form (NFC) or use Unicode code point escapes instead of combining sequences.
⚠ eslint(no-misleading-character-class): Unexpected combining class in character class.
╭─[no_misleading_character_class.tsx:1:15]
1 │ /[नमस्ते]/
· ─
╰────
help: Replace the character with its normalized form (NFC) or use Unicode code point escapes instead of combining sequences.
⚠ eslint(no-misleading-character-class): Unexpected combining class in character class.
╭─[no_misleading_character_class.tsx:1:14]
1 │ new RegExp('[ગુ]', 'u');
· ─
╰────
help: Replace the character with its normalized form (NFC) or use Unicode code point escapes instead of combining sequences.
⚠ eslint(no-misleading-character-class): Unexpected combining class in character class.
╭─[no_misleading_character_class.tsx:1:3]
1 │ /[а҃]/u
· ─
╰────
help: Replace the character with its normalized form (NFC) or use Unicode code point escapes instead of combining sequences.
⚠ eslint(no-misleading-character-class): Unexpected combining class in character class.
╭─[no_misleading_character_class.tsx:1:3]
1 │ /[אָ]/u
· ─
╰────
help: Replace the character with its normalized form (NFC) or use Unicode code point escapes instead of combining sequences.
⚠ eslint(no-misleading-character-class): Unexpected combining class in character class.
╭─[no_misleading_character_class.tsx:1:3]
1 │ /[بَ]/u
· ─
╰────
help: Replace the character with its normalized form (NFC) or use Unicode code point escapes instead of combining sequences.
⚠ eslint(no-misleading-character-class): Unexpected combining class in character class.
╭─[no_misleading_character_class.tsx:1:3]
1 │ /[ܐܰ]/u
· ─
╰────
help: Replace the character with its normalized form (NFC) or use Unicode code point escapes instead of combining sequences.
⚠ eslint(no-misleading-character-class): Unexpected combining class in character class.
╭─[no_misleading_character_class.tsx:1:3]
1 │ /[ހަ]/u
· ─
╰────
help: Replace the character with its normalized form (NFC) or use Unicode code point escapes instead of combining sequences.
⚠ eslint(no-misleading-character-class): Unexpected combining class in character class.
╭─[no_misleading_character_class.tsx:1:3]
1 │ /[ߊ߫]/u
· ─
╰────
help: Replace the character with its normalized form (NFC) or use Unicode code point escapes instead of combining sequences.
⚠ eslint(no-misleading-character-class): Unexpected combining class in character class.
╭─[no_misleading_character_class.tsx:1:3]
1 │ /[কা]/u
· ─
╰────
help: Replace the character with its normalized form (NFC) or use Unicode code point escapes instead of combining sequences.
⚠ eslint(no-misleading-character-class): Unexpected combining class in character class.
╭─[no_misleading_character_class.tsx:1:3]
1 │ /[ਕਾ]/u
· ──
╰────
help: Replace the character with its normalized form (NFC) or use Unicode code point escapes instead of combining sequences.
⚠ eslint(no-misleading-character-class): Unexpected combining class in character class.
╭─[no_misleading_character_class.tsx:1:3]
1 │ /[କା]/u
· ─
╰────
help: Replace the character with its normalized form (NFC) or use Unicode code point escapes instead of combining sequences.
⚠ eslint(no-misleading-character-class): Unexpected combining class in character class.
╭─[no_misleading_character_class.tsx:1:3]
1 │ /[கா]/u
· ─
╰────
help: Replace the character with its normalized form (NFC) or use Unicode code point escapes instead of combining sequences.
⚠ eslint(no-misleading-character-class): Unexpected combining class in character class.
╭─[no_misleading_character_class.tsx:1:3]
1 │ /[కా]/u
· ─
╰────
help: Replace the character with its normalized form (NFC) or use Unicode code point escapes instead of combining sequences.
⚠ eslint(no-misleading-character-class): Unexpected combining class in character class.
╭─[no_misleading_character_class.tsx:1:3]
1 │ /[ಕಾ]/u
· ──
╰────
help: Replace the character with its normalized form (NFC) or use Unicode code point escapes instead of combining sequences.
⚠ eslint(no-misleading-character-class): Unexpected combining class in character class.
╭─[no_misleading_character_class.tsx:1:3]
1 │ /[കാ]/u
· ─
╰────
help: Replace the character with its normalized form (NFC) or use Unicode code point escapes instead of combining sequences.
⚠ eslint(no-misleading-character-class): Unexpected combining class in character class.
╭─[no_misleading_character_class.tsx:1:3]
1 │ /[කා]/u
· ─
╰────
help: Replace the character with its normalized form (NFC) or use Unicode code point escapes instead of combining sequences.
⚠ eslint(no-misleading-character-class): Unexpected combining class in character class.
╭─[no_misleading_character_class.tsx:1:3]
1 │ /[กั]/u
· ─
╰────
help: Replace the character with its normalized form (NFC) or use Unicode code point escapes instead of combining sequences.
⚠ eslint(no-misleading-character-class): Unexpected combining class in character class.
╭─[no_misleading_character_class.tsx:1:3]
1 │ /[ກັ]/u
· ─
╰────
help: Replace the character with its normalized form (NFC) or use Unicode code point escapes instead of combining sequences.
⚠ eslint(no-misleading-character-class): Unexpected combining class in character class.
╭─[no_misleading_character_class.tsx:1:3]
1 │ /[ཀཱ]/u
· ─
╰────
help: Replace the character with its normalized form (NFC) or use Unicode code point escapes instead of combining sequences.
⚠ eslint(no-misleading-character-class): Unexpected combining class in character class.
╭─[no_misleading_character_class.tsx:1:3]
1 │ /[ကါ]/u
· ──
╰────
help: Replace the character with its normalized form (NFC) or use Unicode code point escapes instead of combining sequences.
⚠ eslint(no-misleading-character-class): Unexpected combining class in character class.
╭─[no_misleading_character_class.tsx:1:3]
1 │ /[កា]/u
· ──
╰────
help: Replace the character with its normalized form (NFC) or use Unicode code point escapes instead of combining sequences.
⚠ eslint(no-misleading-character-class): Unexpected combining class in character class.
╭─[no_misleading_character_class.tsx:1:3]
1 │ /[ᠠ᠋]/u
· ─
╰────
help: Replace the character with its normalized form (NFC) or use Unicode code point escapes instead of combining sequences.
⚠ eslint(no-misleading-character-class): Unexpected combining class in character class.
╭─[no_misleading_character_class.tsx:1:3]
1 │ /[ᬆ]/u
· ─
╰────
help: Replace the character with its normalized form (NFC) or use Unicode code point escapes instead of combining sequences.
⚠ eslint(no-misleading-character-class): Unexpected combining class in character class.
╭─[no_misleading_character_class.tsx:1:3]
1 │ /[が]/u
· ──
╰────
help: Replace the character with its normalized form (NFC) or use Unicode code point escapes instead of combining sequences.
⚠ eslint(no-misleading-character-class): Unexpected combining class in character class.
╭─[no_misleading_character_class.tsx:1:3]
1 │ /[ꦏꦴ]/u
· ──
╰────
help: Replace the character with its normalized form (NFC) or use Unicode code point escapes instead of combining sequences.
⚠ eslint(no-misleading-character-class): Unexpected combining class in character class.
╭─[no_misleading_character_class.tsx:1:3]
1 │ /[𑀓𑀸]/u
· ─
╰────
help: Replace the character with its normalized form (NFC) or use Unicode code point escapes instead of combining sequences.
⚠ eslint(no-misleading-character-class): Unexpected combining class in character class.
╭─[no_misleading_character_class.tsx:1:3]
1 │ /[𐨕𐨁]/u
· ─
╰────
help: Replace the character with its normalized form (NFC) or use Unicode code point escapes instead of combining sequences.
⚠ eslint(no-misleading-character-class): Unexpected combining class in character class.
╭─[no_misleading_character_class.tsx:1:3]
1 │ /[𑌕𑌾]/u
· ─
╰────
help: Replace the character with its normalized form (NFC) or use Unicode code point escapes instead of combining sequences.