Skip to content

Commit

Permalink
fix: no-misleading-character-class edge cases with granular errors (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
mdjermanovic committed Jan 8, 2024
1 parent 287c4b7 commit 806f708
Show file tree
Hide file tree
Showing 2 changed files with 291 additions and 112 deletions.
213 changes: 103 additions & 110 deletions lib/rules/no-misleading-character-class.js
Expand Up @@ -73,119 +73,125 @@ function isUnicodeCodePointEscape(char) {

/**
* Each function returns matched characters if it detects that kind of problem.
* @type {Record<string, (char: Character, index: number, chars: Character[]) => Character[] | null>}
* @type {Record<string, (chars: Character[]) => IterableIterator<Character[]>>}
*/
const characterSequenceIndexFilters = {
surrogatePairWithoutUFlag(char, index, chars) {
if (index === 0) {
return null;
}

const previous = chars[index - 1];

if (
isSurrogatePair(previous.value, char.value) &&
!isUnicodeCodePointEscape(previous) &&
!isUnicodeCodePointEscape(char)
) {
return [previous, char];
const findCharacterSequences = {
*surrogatePairWithoutUFlag(chars) {
for (const [index, char] of chars.entries()) {
if (index === 0) {
continue;
}
const previous = chars[index - 1];

if (
isSurrogatePair(previous.value, char.value) &&
!isUnicodeCodePointEscape(previous) &&
!isUnicodeCodePointEscape(char)
) {
yield [previous, char];
}
}

return null;
},

surrogatePair(char, index, chars) {
if (index === 0) {
return null;
*surrogatePair(chars) {
for (const [index, char] of chars.entries()) {
if (index === 0) {
continue;
}
const previous = chars[index - 1];

if (
isSurrogatePair(previous.value, char.value) &&
(
isUnicodeCodePointEscape(previous) ||
isUnicodeCodePointEscape(char)
)
) {
yield [previous, char];
}
}
},

const previous = chars[index - 1];
*combiningClass(chars) {
for (const [index, char] of chars.entries()) {
if (index === 0) {
continue;
}
const previous = chars[index - 1];

if (
isSurrogatePair(previous.value, char.value) &&
(
isUnicodeCodePointEscape(previous) ||
isUnicodeCodePointEscape(char)
)
) {
return [previous, char];
if (
isCombiningCharacter(char.value) &&
!isCombiningCharacter(previous.value)
) {
yield [previous, char];
}
}

return null;
},

combiningClass(char, index, chars) {
if (
index !== 0 &&
isCombiningCharacter(char.value) &&
!isCombiningCharacter(chars[index - 1].value)
) {
return [chars[index - 1], char];
}

return null;
},
*emojiModifier(chars) {
for (const [index, char] of chars.entries()) {
if (index === 0) {
continue;
}
const previous = chars[index - 1];

emojiModifier(char, index, chars) {
if (
index !== 0 &&
isEmojiModifier(char.value) &&
!isEmojiModifier(chars[index - 1].value)
) {
return [chars[index - 1], char];
if (
isEmojiModifier(char.value) &&
!isEmojiModifier(previous.value)
) {
yield [previous, char];
}
}

return null;
},

regionalIndicatorSymbol(char, index, chars) {
if (
index !== 0 &&
isRegionalIndicatorSymbol(char.value) &&
isRegionalIndicatorSymbol(chars[index - 1].value)
) {
return [chars[index - 1], char];
}
*regionalIndicatorSymbol(chars) {
for (const [index, char] of chars.entries()) {
if (index === 0) {
continue;
}
const previous = chars[index - 1];

return null;
if (
isRegionalIndicatorSymbol(char.value) &&
isRegionalIndicatorSymbol(previous.value)
) {
yield [previous, char];
}
}
},

zwj(char, index, chars) {
if (
index !== 0 &&
index !== chars.length - 1 &&
char.value === 0x200d &&
chars[index - 1].value !== 0x200d &&
chars[index + 1].value !== 0x200d
) {
return chars.slice(index - 1, index + 2);
*zwj(chars) {
let sequence = null;

for (const [index, char] of chars.entries()) {
if (index === 0 || index === chars.length - 1) {
continue;
}
if (
char.value === 0x200d &&
chars[index - 1].value !== 0x200d &&
chars[index + 1].value !== 0x200d
) {
if (sequence) {
if (sequence.at(-1) === chars[index - 1]) {
sequence.push(char, chars[index + 1]); // append to the sequence
} else {
yield sequence;
sequence = chars.slice(index - 1, index + 2);
}
} else {
sequence = chars.slice(index - 1, index + 2);
}
}
}

return null;
if (sequence) {
yield sequence;
}
}
};

const kinds = Object.keys(characterSequenceIndexFilters);

/**
* Collects the indices where the filter returns an array.
* @param {Character[]} chars Characters to run the filter on.
* @param {(char: Character, index: number, chars: Character[]) => Character[] | null} filter Finds matches for an index.
* @returns {Character[][]} Indices where the filter returned true.
*/
function accumulate(chars, filter) {
const matchingChars = [];

chars.forEach((char, index) => {
const matches = filter(char, index, chars);

if (matches) {
matchingChars.push(matches);
}
});

return matchingChars;
}
const kinds = Object.keys(findCharacterSequences);

//------------------------------------------------------------------------------
// Rule Definition
Expand Down Expand Up @@ -232,7 +238,7 @@ module.exports = {
// Limit to to literals and expression-less templates with raw values === their value.
switch (node.type) {
case "TemplateLiteral":
if (node.expressions.length || node.quasis[0].value.raw !== node.quasis[0].value.cooked) {
if (node.expressions.length || sourceCode.getText(node).slice(1, -1) !== node.quasis[0].value.cooked) {
return null;
}
break;
Expand Down Expand Up @@ -309,12 +315,10 @@ module.exports = {
onCharacterClassEnter(ccNode) {
for (const chars of iterateCharacterSequence(ccNode.elements)) {
for (const kind of kinds) {
const matches = accumulate(chars, characterSequenceIndexFilters[kind]);

if (foundKindMatches.has(kind)) {
foundKindMatches.get(kind).push(...matches);
foundKindMatches.get(kind).push(...findCharacterSequences[kind](chars));
} else {
foundKindMatches.set(kind, matches);
foundKindMatches.set(kind, [...findCharacterSequences[kind](chars)]);
}

}
Expand All @@ -334,24 +338,13 @@ module.exports = {

const locs = getNodeReportLocations(matches, node);

// Grapheme zero-width joiners (e.g. in 👨‍👩‍👦) visually show as one emoji
if (kind === "zwj" && locs.length > 1) {
for (const loc of locs) {
context.report({
loc: {
start: locs[0].start,
end: locs[1].end
},
node,
loc,
messageId: kind,
suggest
});
} else {
for (const loc of locs) {
context.report({
loc,
messageId: kind,
suggest
});
}
}
}
}
Expand Down

0 comments on commit 806f708

Please sign in to comment.