eslint-plugin-regexp
Version:
ESLint plugin for finding RegExp mistakes and RegExp style guide violations.
198 lines (197 loc) • 8.12 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
const utils_1 = require("../utils");
const mention_1 = require("../utils/mention");
const regex_syntax_1 = require("../utils/regex-syntax");
const segmenter = new Intl.Segmenter();
function startsWithSurrogate(s) {
if (s.length < 2) {
return false;
}
const h = s.charCodeAt(0);
const l = s.charCodeAt(1);
return h >= 0xd800 && h <= 0xdbff && l >= 0xdc00 && l <= 0xdfff;
}
function getProblem(grapheme, flags) {
if (grapheme.length > 2 ||
(grapheme.length === 2 && !startsWithSurrogate(grapheme))) {
return "Multi";
}
else if (!flags.unicode &&
!flags.unicodeSets &&
startsWithSurrogate(grapheme)) {
return "Surrogate";
}
return null;
}
function getGraphemeBeforeQuant(quant) {
const alt = quant.parent;
let start = quant.start;
for (let i = alt.elements.indexOf(quant) - 1; i >= 0; i--) {
const e = alt.elements[i];
if (e.type === "Character" && !(0, regex_syntax_1.isEscapeSequence)(e.raw)) {
start = e.start;
}
else {
break;
}
}
const before = alt.raw.slice(start - alt.start, quant.element.end - alt.start);
const segments = [...segmenter.segment(before)];
const segment = segments[segments.length - 1];
return segment.segment;
}
function getGraphemeProblems(cc, flags) {
const offset = cc.negate ? 2 : 1;
const ignoreElements = cc.elements.filter((element) => element.type === "CharacterClass" ||
element.type === "ExpressionCharacterClass" ||
element.type === "ClassStringDisjunction");
const problems = [];
for (const { segment, index } of segmenter.segment(cc.raw.slice(offset, -1))) {
const problem = getProblem(segment, flags);
if (problem !== null) {
const start = offset + index + cc.start;
const end = start + segment.length;
if (ignoreElements.some((ignore) => ignore.start <= start && end <= ignore.end)) {
continue;
}
problems.push({
grapheme: segment,
problem,
start,
end,
elements: cc.elements.filter((e) => e.start < end && e.end > start),
});
}
}
return problems;
}
function getGraphemeProblemsFix(problems, cc, flags) {
if (cc.negate) {
return null;
}
if (!problems.every((p) => p.start === p.elements[0].start &&
p.end === p.elements[p.elements.length - 1].end)) {
return null;
}
const prefixGraphemes = problems.map((p) => p.grapheme);
let ccRaw = cc.raw;
for (let i = problems.length - 1; i >= 0; i--) {
const { start, end } = problems[i];
ccRaw = ccRaw.slice(0, start - cc.start) + ccRaw.slice(end - cc.start);
}
if (flags.unicodeSets) {
const prefix = prefixGraphemes.join("|");
return `[\\q{${prefix}}${ccRaw.slice(1, -1)}]`;
}
if (ccRaw.startsWith("[^")) {
ccRaw = `[\\${ccRaw.slice(1)}`;
}
const prefix = prefixGraphemes.sort((a, b) => b.length - a.length).join("|");
let fix = prefix;
let singleAlternative = problems.length === 1;
if (ccRaw !== "[]") {
fix += `|${ccRaw}`;
singleAlternative = false;
}
if (singleAlternative && cc.parent.type === "Alternative") {
return fix;
}
if (cc.parent.type === "Alternative" && cc.parent.elements.length === 1) {
return fix;
}
return `(?:${fix})`;
}
exports.default = (0, utils_1.createRule)("no-misleading-unicode-character", {
meta: {
docs: {
description: "disallow multi-code-point characters in character classes and quantifiers",
category: "Possible Errors",
recommended: true,
},
schema: [
{
type: "object",
properties: {
fixable: { type: "boolean" },
},
additionalProperties: false,
},
],
fixable: "code",
hasSuggestions: true,
messages: {
characterClass: "The character(s) {{ graphemes }} are all represented using multiple {{ unit }}.{{ uFlag }}",
quantifierMulti: "The character {{ grapheme }} is represented using multiple Unicode code points. The quantifier only applies to the last code point {{ last }} and not to the whole character.",
quantifierSurrogate: "The character {{ grapheme }} is represented using a surrogate pair. The quantifier only applies to the tailing surrogate {{ last }} and not to the whole character.",
fixCharacterClass: "Move the character(s) {{ graphemes }} outside the character class.",
fixQuantifier: "Wrap a group around {{ grapheme }}.",
},
type: "problem",
},
create(context) {
var _a, _b;
const fixable = (_b = (_a = context.options[0]) === null || _a === void 0 ? void 0 : _a.fixable) !== null && _b !== void 0 ? _b : false;
function makeFix(fix, messageId, data) {
if (fixable) {
return { fix };
}
return {
suggest: [{ messageId, data, fix }],
};
}
function createVisitor(regexpContext) {
const { node, patternSource, flags, getRegexpLocation, fixReplaceNode, } = regexpContext;
return {
onCharacterClassEnter(ccNode) {
const problems = getGraphemeProblems(ccNode, flags);
if (problems.length === 0) {
return;
}
const range = {
start: problems[0].start,
end: problems[problems.length - 1].end,
};
const fix = getGraphemeProblemsFix(problems, ccNode, flags);
const graphemes = problems
.map((p) => (0, mention_1.mention)(p.grapheme))
.join(", ");
const uFlag = problems.every((p) => p.problem === "Surrogate");
context.report(Object.assign({ node, loc: getRegexpLocation(range), messageId: "characterClass", data: {
graphemes,
unit: flags.unicode || flags.unicodeSets
? "code points"
: "char codes",
uFlag: uFlag ? " Use the `u` flag." : "",
} }, makeFix(fixReplaceNode(ccNode, () => fix), "fixCharacterClass", { graphemes })));
},
onQuantifierEnter(qNode) {
if (qNode.element.type !== "Character") {
return;
}
const grapheme = getGraphemeBeforeQuant(qNode);
const problem = getProblem(grapheme, flags);
if (problem === null) {
return;
}
context.report(Object.assign({ node, loc: getRegexpLocation(qNode), messageId: `quantifier${problem}`, data: {
grapheme: (0, mention_1.mention)(grapheme),
last: (0, mention_1.mentionChar)(qNode.element),
} }, makeFix((fixer) => {
const range = patternSource.getReplaceRange({
start: qNode.element.end - grapheme.length,
end: qNode.element.end,
});
if (!range) {
return null;
}
return range.replace(fixer, `(?:${grapheme})`);
}, "fixQuantifier", { grapheme: (0, mention_1.mention)(grapheme) })));
},
};
}
return (0, utils_1.defineRegexpVisitor)(context, {
createVisitor,
});
},
});
;