eslint-plugin-regexp
Version:
ESLint plugin for finding RegExp mistakes and RegExp style guide violations.
229 lines (228 loc) • 10.6 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const refa_1 = require("refa");
const regexp_ast_analysis_1 = require("regexp-ast-analysis");
const utils_1 = require("../utils");
const mention_1 = require("../utils/mention");
const refa_2 = require("../utils/refa");
const util_1 = require("../utils/util");
function groupElements(elements, flags) {
const duplicates = [];
const characters = new Map();
const characterRanges = new Map();
const characterSetAndClasses = new Map();
function addToGroup(group, key, element) {
const current = group.get(key);
if (current !== undefined) {
duplicates.push({ element: current, duplicate: element });
}
else {
group.set(key, element);
}
}
for (const e of elements) {
if (e.type === "Character") {
const charSet = (0, regexp_ast_analysis_1.toCharSet)(e, flags);
const key = charSet.ranges[0].min;
addToGroup(characters, key, e);
}
else if (e.type === "CharacterClassRange") {
const charSet = (0, regexp_ast_analysis_1.toCharSet)(e, flags);
const key = buildRangeKey(charSet);
addToGroup(characterRanges, key, e);
}
else if (e.type === "CharacterSet" ||
e.type === "CharacterClass" ||
e.type === "ClassStringDisjunction" ||
e.type === "ExpressionCharacterClass") {
const key = e.raw;
addToGroup(characterSetAndClasses, key, e);
}
else {
(0, util_1.assertNever)(e);
}
}
return {
duplicates,
characters: [...characters.values()],
characterRanges: [...characterRanges.values()],
characterSetAndClasses: [...characterSetAndClasses.values()],
};
function buildRangeKey(rangeCharSet) {
return rangeCharSet.ranges
.map((r) => String.fromCodePoint(r.min, r.max))
.join(",");
}
}
function inRange({ min, max }, char) {
return min <= char && char <= max;
}
exports.default = (0, utils_1.createRule)("no-dupe-characters-character-class", {
meta: {
type: "suggestion",
docs: {
description: "disallow duplicate characters in the RegExp character class",
category: "Best Practices",
recommended: true,
},
fixable: "code",
schema: [],
messages: {
duplicate: "Unexpected duplicate {{duplicate}}.",
duplicateNonObvious: "Unexpected duplicate. {{duplicate}} is a duplicate of {{element}}.",
subset: "{{subsetElement}} is already included in {{element}}.",
subsetOfMany: "{{subsetElement}} is already included by the elements {{elements}}.",
overlap: "Unexpected overlap of {{elementA}} and {{elementB}} was found '{{overlap}}'.",
},
},
create(context) {
function reportDuplicate(regexpContext, duplicate, element) {
const { node, getRegexpLocation } = regexpContext;
if (duplicate.raw === element.raw) {
context.report({
node,
loc: getRegexpLocation(duplicate),
messageId: "duplicate",
data: {
duplicate: (0, mention_1.mentionChar)(duplicate),
},
fix: (0, utils_1.fixRemoveCharacterClassElement)(regexpContext, duplicate),
});
}
else {
context.report({
node,
loc: getRegexpLocation(duplicate),
messageId: "duplicateNonObvious",
data: {
duplicate: (0, mention_1.mentionChar)(duplicate),
element: (0, mention_1.mentionChar)(element),
},
fix: (0, utils_1.fixRemoveCharacterClassElement)(regexpContext, duplicate),
});
}
}
function reportOverlap({ node, getRegexpLocation }, element, intersectElement, overlap) {
context.report({
node,
loc: getRegexpLocation(element),
messageId: "overlap",
data: {
elementA: (0, mention_1.mentionChar)(element),
elementB: (0, mention_1.mentionChar)(intersectElement),
overlap,
},
});
}
function reportSubset(regexpContext, subsetElement, element) {
const { node, getRegexpLocation } = regexpContext;
context.report({
node,
loc: getRegexpLocation(subsetElement),
messageId: "subset",
data: {
subsetElement: (0, mention_1.mentionChar)(subsetElement),
element: (0, mention_1.mentionChar)(element),
},
fix: (0, utils_1.fixRemoveCharacterClassElement)(regexpContext, subsetElement),
});
}
function reportSubsetOfMany(regexpContext, subsetElement, elements) {
const { node, getRegexpLocation } = regexpContext;
context.report({
node,
loc: getRegexpLocation(subsetElement),
messageId: "subsetOfMany",
data: {
subsetElement: (0, mention_1.mentionChar)(subsetElement),
elements: `'${elements
.map((e) => e.raw)
.join("")}' (${elements.map(mention_1.mentionChar).join(", ")})`,
},
fix: (0, utils_1.fixRemoveCharacterClassElement)(regexpContext, subsetElement),
});
}
function createVisitor(regexpContext) {
const { flags } = regexpContext;
return {
onCharacterClassEnter(ccNode) {
const { duplicates, characters, characterRanges, characterSetAndClasses, } = groupElements(ccNode.elements, flags);
const elementsOtherThanCharacter = [
...characterRanges,
...characterSetAndClasses,
];
const subsets = new Set();
for (const { element, duplicate } of duplicates) {
reportDuplicate(regexpContext, duplicate, element);
subsets.add(duplicate);
}
for (const char of characters) {
for (const other of elementsOtherThanCharacter) {
if ((0, regexp_ast_analysis_1.toUnicodeSet)(other, flags).chars.has(char.value)) {
reportSubset(regexpContext, char, other);
subsets.add(char);
break;
}
}
}
for (const element of elementsOtherThanCharacter) {
for (const other of elementsOtherThanCharacter) {
if (element === other || subsets.has(other)) {
continue;
}
if ((0, regexp_ast_analysis_1.toUnicodeSet)(element, flags).isSubsetOf((0, regexp_ast_analysis_1.toUnicodeSet)(other, flags))) {
reportSubset(regexpContext, element, other);
subsets.add(element);
break;
}
}
}
const characterTotal = (0, regexp_ast_analysis_1.toUnicodeSet)(characters.filter((c) => !subsets.has(c)), flags);
for (const element of elementsOtherThanCharacter) {
if (subsets.has(element)) {
continue;
}
const totalOthers = characterTotal.union(...elementsOtherThanCharacter
.filter((e) => !subsets.has(e) && e !== element)
.map((e) => (0, regexp_ast_analysis_1.toUnicodeSet)(e, flags)));
const elementCharSet = (0, regexp_ast_analysis_1.toUnicodeSet)(element, flags);
if (elementCharSet.isSubsetOf(totalOthers)) {
const superSetElements = ccNode.elements
.filter((e) => !subsets.has(e) && e !== element)
.filter((e) => !(0, regexp_ast_analysis_1.toUnicodeSet)(e, flags).isDisjointWith(elementCharSet));
reportSubsetOfMany(regexpContext, element, superSetElements);
subsets.add(element);
}
}
for (let i = 0; i < characterRanges.length; i++) {
const range = characterRanges[i];
if (subsets.has(range)) {
continue;
}
for (let j = i + 1; j < elementsOtherThanCharacter.length; j++) {
const other = elementsOtherThanCharacter[j];
if (range === other || subsets.has(other)) {
continue;
}
const intersection = (0, regexp_ast_analysis_1.toUnicodeSet)(range, flags).intersect((0, regexp_ast_analysis_1.toUnicodeSet)(other, flags));
if (intersection.isEmpty) {
continue;
}
const interestingRanges = intersection.chars.ranges.filter((r) => inRange(r, range.min.value) ||
inRange(r, range.max.value));
(0, refa_2.assertValidFlags)(flags);
const interest = refa_1.JS.createCharSet(interestingRanges, flags);
if (!interest.isEmpty) {
reportOverlap(regexpContext, range, other, (0, refa_2.toCharSetSource)(interest, flags));
break;
}
}
}
},
};
}
return (0, utils_1.defineRegexpVisitor)(context, {
createVisitor,
});
},
});