564 lines
21 KiB
JavaScript
564 lines
21 KiB
JavaScript
"use strict";
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
const utils_1 = require("../utils");
|
|
const ast_utils_1 = require("../utils/ast-utils");
|
|
const regexp_ast_1 = require("../utils/regexp-ast");
|
|
const type_tracker_1 = require("../utils/type-tracker");
|
|
class RegExpReference {
|
|
get defineNode() {
|
|
return this.regExpContext.regexpNode;
|
|
}
|
|
constructor(regExpContext) {
|
|
this.readNodes = new Map();
|
|
this.state = {
|
|
usedNodes: new Map(),
|
|
track: true,
|
|
};
|
|
this.regExpContext = regExpContext;
|
|
}
|
|
addReadNode(node) {
|
|
this.readNodes.set(node, {});
|
|
}
|
|
setDefineId(codePathId, loopNode) {
|
|
this.defineId = { codePathId, loopNode };
|
|
}
|
|
markAsUsedInSearch(node) {
|
|
const exprState = this.readNodes.get(node);
|
|
if (exprState) {
|
|
exprState.marked = true;
|
|
}
|
|
this.addUsedNode("search", node);
|
|
}
|
|
markAsUsedInSplit(node) {
|
|
const exprState = this.readNodes.get(node);
|
|
if (exprState) {
|
|
exprState.marked = true;
|
|
}
|
|
this.addUsedNode("split", node);
|
|
}
|
|
markAsUsedInExec(node, codePathId, loopNode) {
|
|
const exprState = this.readNodes.get(node);
|
|
if (exprState) {
|
|
exprState.marked = true;
|
|
exprState.usedInExec = { id: { codePathId, loopNode } };
|
|
}
|
|
this.addUsedNode("exec", node);
|
|
}
|
|
markAsUsedInTest(node, codePathId, loopNode) {
|
|
const exprState = this.readNodes.get(node);
|
|
if (exprState) {
|
|
exprState.marked = true;
|
|
exprState.usedInTest = { id: { codePathId, loopNode } };
|
|
}
|
|
this.addUsedNode("test", node);
|
|
}
|
|
isUsed(kinds) {
|
|
for (const kind of kinds) {
|
|
if (this.state.usedNodes.has(kind)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
isCannotTrack() {
|
|
return !this.state.track;
|
|
}
|
|
markAsUsed(kind, exprNode) {
|
|
this.addUsedNode(kind, exprNode);
|
|
}
|
|
markAsCannotTrack() {
|
|
this.state.track = false;
|
|
}
|
|
getUsedNodes() {
|
|
return this.state.usedNodes;
|
|
}
|
|
addUsedNode(kind, exprNode) {
|
|
const list = this.state.usedNodes.get(kind);
|
|
if (list) {
|
|
list.push(exprNode);
|
|
}
|
|
else {
|
|
this.state.usedNodes.set(kind, [exprNode]);
|
|
}
|
|
}
|
|
}
|
|
function fixRemoveFlag({ flagsString, fixReplaceFlags }, flag) {
|
|
if (flagsString) {
|
|
return fixReplaceFlags(flagsString.replace(flag, ""));
|
|
}
|
|
return null;
|
|
}
|
|
function createUselessIgnoreCaseFlagVisitor(context) {
|
|
return (0, utils_1.defineRegexpVisitor)(context, {
|
|
createVisitor(regExpContext) {
|
|
const { flags, regexpNode, ownsFlags, getFlagLocation } = regExpContext;
|
|
if (!flags.ignoreCase || !ownsFlags) {
|
|
return {};
|
|
}
|
|
return {
|
|
onPatternLeave(pattern) {
|
|
if (!(0, regexp_ast_1.isCaseVariant)(pattern, flags, false)) {
|
|
context.report({
|
|
node: regexpNode,
|
|
loc: getFlagLocation("i"),
|
|
messageId: "uselessIgnoreCaseFlag",
|
|
fix: fixRemoveFlag(regExpContext, "i"),
|
|
});
|
|
}
|
|
},
|
|
};
|
|
},
|
|
});
|
|
}
|
|
function createUselessMultilineFlagVisitor(context) {
|
|
return (0, utils_1.defineRegexpVisitor)(context, {
|
|
createVisitor(regExpContext) {
|
|
const { flags, regexpNode, ownsFlags, getFlagLocation } = regExpContext;
|
|
if (!flags.multiline || !ownsFlags) {
|
|
return {};
|
|
}
|
|
let unnecessary = true;
|
|
return {
|
|
onAssertionEnter(node) {
|
|
if (node.kind === "start" || node.kind === "end") {
|
|
unnecessary = false;
|
|
}
|
|
},
|
|
onPatternLeave() {
|
|
if (unnecessary) {
|
|
context.report({
|
|
node: regexpNode,
|
|
loc: getFlagLocation("m"),
|
|
messageId: "uselessMultilineFlag",
|
|
fix: fixRemoveFlag(regExpContext, "m"),
|
|
});
|
|
}
|
|
},
|
|
};
|
|
},
|
|
});
|
|
}
|
|
function createUselessDotAllFlagVisitor(context) {
|
|
return (0, utils_1.defineRegexpVisitor)(context, {
|
|
createVisitor(regExpContext) {
|
|
const { flags, regexpNode, ownsFlags, getFlagLocation } = regExpContext;
|
|
if (!flags.dotAll || !ownsFlags) {
|
|
return {};
|
|
}
|
|
let unnecessary = true;
|
|
return {
|
|
onCharacterSetEnter(node) {
|
|
if (node.kind === "any") {
|
|
unnecessary = false;
|
|
}
|
|
},
|
|
onPatternLeave() {
|
|
if (unnecessary) {
|
|
context.report({
|
|
node: regexpNode,
|
|
loc: getFlagLocation("s"),
|
|
messageId: "uselessDotAllFlag",
|
|
fix: fixRemoveFlag(regExpContext, "s"),
|
|
});
|
|
}
|
|
},
|
|
};
|
|
},
|
|
});
|
|
}
|
|
function createUselessGlobalFlagVisitor(context, strictTypes) {
|
|
function reportUselessGlobalFlag(regExpReference, data) {
|
|
const { getFlagLocation } = regExpReference.regExpContext;
|
|
const node = regExpReference.defineNode;
|
|
context.report({
|
|
node,
|
|
loc: getFlagLocation("g"),
|
|
messageId: data.kind === 0
|
|
? "uselessGlobalFlagForSplit"
|
|
: data.kind === 1
|
|
? "uselessGlobalFlagForSearch"
|
|
: data.kind === 3
|
|
? "uselessGlobalFlagForTest"
|
|
: data.kind === 2
|
|
? "uselessGlobalFlagForExec"
|
|
: "uselessGlobalFlag",
|
|
fix: data.fixable
|
|
? fixRemoveFlag(regExpReference.regExpContext, "g")
|
|
: null,
|
|
});
|
|
}
|
|
function getReportData(regExpReference) {
|
|
let countOfUsedInExecOrTest = 0;
|
|
for (const readData of regExpReference.readNodes.values()) {
|
|
if (!readData.marked) {
|
|
return null;
|
|
}
|
|
const usedInExecOrTest = readData.usedInExec || readData.usedInTest;
|
|
if (usedInExecOrTest) {
|
|
if (!regExpReference.defineId) {
|
|
return null;
|
|
}
|
|
if (regExpReference.defineId.codePathId ===
|
|
usedInExecOrTest.id.codePathId &&
|
|
regExpReference.defineId.loopNode ===
|
|
usedInExecOrTest.id.loopNode) {
|
|
countOfUsedInExecOrTest++;
|
|
if (countOfUsedInExecOrTest > 1) {
|
|
return null;
|
|
}
|
|
continue;
|
|
}
|
|
else {
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
return buildReportData(regExpReference);
|
|
}
|
|
function buildReportData(regExpReference) {
|
|
const usedNodes = regExpReference.getUsedNodes();
|
|
if (usedNodes.size === 1) {
|
|
const [[method, nodes]] = usedNodes;
|
|
const fixable = nodes.length === 1 && nodes.includes(regExpReference.defineNode);
|
|
if (method === "split") {
|
|
return {
|
|
kind: 0,
|
|
fixable,
|
|
};
|
|
}
|
|
if (method === "search") {
|
|
return { kind: 1, fixable };
|
|
}
|
|
if (method === "exec" && nodes.length === 1)
|
|
return { kind: 2, fixable };
|
|
if (method === "test" && nodes.length === 1)
|
|
return { kind: 3, fixable };
|
|
}
|
|
return { kind: 4 };
|
|
}
|
|
return createRegExpReferenceExtractVisitor(context, {
|
|
flag: "global",
|
|
exit(regExpReferenceList) {
|
|
for (const regExpReference of regExpReferenceList) {
|
|
const report = getReportData(regExpReference);
|
|
if (report != null) {
|
|
reportUselessGlobalFlag(regExpReference, report);
|
|
}
|
|
}
|
|
},
|
|
isUsedShortCircuit(regExpReference) {
|
|
return regExpReference.isUsed([
|
|
"match",
|
|
"matchAll",
|
|
"replace",
|
|
"replaceAll",
|
|
]);
|
|
},
|
|
strictTypes,
|
|
});
|
|
}
|
|
function createUselessStickyFlagVisitor(context, strictTypes) {
|
|
function reportUselessStickyFlag(regExpReference, data) {
|
|
const { getFlagLocation } = regExpReference.regExpContext;
|
|
const node = regExpReference.defineNode;
|
|
context.report({
|
|
node,
|
|
loc: getFlagLocation("y"),
|
|
messageId: "uselessStickyFlag",
|
|
fix: data.fixable
|
|
? fixRemoveFlag(regExpReference.regExpContext, "y")
|
|
: null,
|
|
});
|
|
}
|
|
function getReportData(regExpReference) {
|
|
for (const readData of regExpReference.readNodes.values()) {
|
|
if (!readData.marked) {
|
|
return null;
|
|
}
|
|
}
|
|
return buildReportData(regExpReference);
|
|
}
|
|
function buildReportData(regExpReference) {
|
|
const usedNodes = regExpReference.getUsedNodes();
|
|
if (usedNodes.size === 1) {
|
|
const [[method, nodes]] = usedNodes;
|
|
const fixable = nodes.length === 1 && nodes.includes(regExpReference.defineNode);
|
|
if (method === "split") {
|
|
return {
|
|
fixable,
|
|
};
|
|
}
|
|
}
|
|
return {};
|
|
}
|
|
return createRegExpReferenceExtractVisitor(context, {
|
|
flag: "sticky",
|
|
exit(regExpReferenceList) {
|
|
for (const regExpReference of regExpReferenceList) {
|
|
const report = getReportData(regExpReference);
|
|
if (report != null) {
|
|
reportUselessStickyFlag(regExpReference, report);
|
|
}
|
|
}
|
|
},
|
|
isUsedShortCircuit(regExpReference) {
|
|
return regExpReference.isUsed([
|
|
"search",
|
|
"exec",
|
|
"test",
|
|
"match",
|
|
"matchAll",
|
|
"replace",
|
|
"replaceAll",
|
|
]);
|
|
},
|
|
strictTypes,
|
|
});
|
|
}
|
|
function createRegExpReferenceExtractVisitor(context, { flag, exit, isUsedShortCircuit, strictTypes, }) {
|
|
const typeTracer = (0, type_tracker_1.createTypeTracker)(context);
|
|
let stack = null;
|
|
const regExpReferenceMap = new Map();
|
|
const regExpReferenceList = [];
|
|
function verifyForSearchOrSplit(node, kind) {
|
|
const regExpReference = regExpReferenceMap.get(node.arguments[0]);
|
|
if (regExpReference == null || isUsedShortCircuit(regExpReference)) {
|
|
return;
|
|
}
|
|
if (strictTypes
|
|
? !typeTracer.isString(node.callee.object)
|
|
: !typeTracer.maybeString(node.callee.object)) {
|
|
regExpReference.markAsCannotTrack();
|
|
return;
|
|
}
|
|
if (kind === "search") {
|
|
regExpReference.markAsUsedInSearch(node.arguments[0]);
|
|
}
|
|
else {
|
|
regExpReference.markAsUsedInSplit(node.arguments[0]);
|
|
}
|
|
}
|
|
function verifyForExecOrTest(node, kind) {
|
|
const regExpReference = regExpReferenceMap.get(node.callee.object);
|
|
if (regExpReference == null || isUsedShortCircuit(regExpReference)) {
|
|
return;
|
|
}
|
|
if (kind === "exec") {
|
|
regExpReference.markAsUsedInExec(node.callee.object, stack.codePathId, stack.loopStack[0]);
|
|
}
|
|
else {
|
|
regExpReference.markAsUsedInTest(node.callee.object, stack.codePathId, stack.loopStack[0]);
|
|
}
|
|
}
|
|
return (0, utils_1.compositingVisitors)((0, utils_1.defineRegexpVisitor)(context, {
|
|
createVisitor(regExpContext) {
|
|
const { flags, regexpNode } = regExpContext;
|
|
if (flags[flag]) {
|
|
const regExpReference = new RegExpReference(regExpContext);
|
|
regExpReferenceList.push(regExpReference);
|
|
regExpReferenceMap.set(regexpNode, regExpReference);
|
|
for (const ref of (0, ast_utils_1.extractExpressionReferences)(regexpNode, context)) {
|
|
if (ref.type === "argument" || ref.type === "member") {
|
|
regExpReferenceMap.set(ref.node, regExpReference);
|
|
regExpReference.addReadNode(ref.node);
|
|
}
|
|
else {
|
|
regExpReference.markAsCannotTrack();
|
|
}
|
|
}
|
|
}
|
|
return {};
|
|
},
|
|
}), {
|
|
"Program:exit"() {
|
|
exit(regExpReferenceList.filter((regExpReference) => {
|
|
if (!regExpReference.readNodes.size) {
|
|
return false;
|
|
}
|
|
if (regExpReference.isCannotTrack()) {
|
|
return false;
|
|
}
|
|
if (isUsedShortCircuit(regExpReference)) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}));
|
|
},
|
|
onCodePathStart(codePath) {
|
|
stack = {
|
|
codePathId: codePath.id,
|
|
upper: stack,
|
|
loopStack: [],
|
|
};
|
|
},
|
|
onCodePathEnd() {
|
|
var _a;
|
|
stack = (_a = stack === null || stack === void 0 ? void 0 : stack.upper) !== null && _a !== void 0 ? _a : null;
|
|
},
|
|
["WhileStatement, DoWhileStatement, ForStatement, ForInStatement, ForOfStatement, " +
|
|
":matches(WhileStatement, DoWhileStatement, ForStatement, ForInStatement, ForOfStatement) > :statement"](node) {
|
|
stack === null || stack === void 0 ? void 0 : stack.loopStack.unshift(node);
|
|
},
|
|
["WhileStatement, DoWhileStatement, ForStatement, ForInStatement, ForOfStatement, " +
|
|
":matches(WhileStatement, DoWhileStatement, ForStatement, ForInStatement, ForOfStatement) > :statement" +
|
|
":exit"]() {
|
|
stack === null || stack === void 0 ? void 0 : stack.loopStack.shift();
|
|
},
|
|
"Literal, NewExpression, CallExpression:exit"(node) {
|
|
if (!stack) {
|
|
return;
|
|
}
|
|
const regExpReference = regExpReferenceMap.get(node);
|
|
if (!regExpReference || regExpReference.defineNode !== node) {
|
|
return;
|
|
}
|
|
regExpReference.setDefineId(stack.codePathId, stack.loopStack[0]);
|
|
},
|
|
"CallExpression:exit"(node) {
|
|
if (!stack) {
|
|
return;
|
|
}
|
|
if (!(0, ast_utils_1.isKnownMethodCall)(node, {
|
|
search: 1,
|
|
split: 1,
|
|
test: 1,
|
|
exec: 1,
|
|
match: 1,
|
|
matchAll: 1,
|
|
replace: 2,
|
|
replaceAll: 2,
|
|
})) {
|
|
return;
|
|
}
|
|
if (node.callee.property.name === "search" ||
|
|
node.callee.property.name === "split") {
|
|
verifyForSearchOrSplit(node, node.callee.property.name);
|
|
}
|
|
else if (node.callee.property.name === "test" ||
|
|
node.callee.property.name === "exec") {
|
|
verifyForExecOrTest(node, node.callee.property.name);
|
|
}
|
|
else if (node.callee.property.name === "match" ||
|
|
node.callee.property.name === "matchAll" ||
|
|
node.callee.property.name === "replace" ||
|
|
node.callee.property.name === "replaceAll") {
|
|
const regExpReference = regExpReferenceMap.get(node.arguments[0]);
|
|
regExpReference === null || regExpReference === void 0 ? void 0 : regExpReference.markAsUsed(node.callee.property.name, node.arguments[0]);
|
|
}
|
|
},
|
|
});
|
|
}
|
|
function createOwnedRegExpFlagsVisitor(context) {
|
|
const sourceCode = context.sourceCode;
|
|
function removeFlags(node) {
|
|
const newFlags = node.regex.flags.replace(/[^u]+/gu, "");
|
|
if (newFlags === node.regex.flags) {
|
|
return;
|
|
}
|
|
context.report({
|
|
node,
|
|
loc: (0, ast_utils_1.getFlagsLocation)(sourceCode, node, node),
|
|
messageId: "uselessFlagsOwned",
|
|
fix(fixer) {
|
|
const range = (0, ast_utils_1.getFlagsRange)(node);
|
|
return fixer.replaceTextRange(range, newFlags);
|
|
},
|
|
});
|
|
}
|
|
return (0, utils_1.defineRegexpVisitor)(context, {
|
|
createSourceVisitor(regExpContext) {
|
|
var _a;
|
|
const { patternSource, regexpNode } = regExpContext;
|
|
if (patternSource.isStringValue()) {
|
|
patternSource.getOwnedRegExpLiterals().forEach(removeFlags);
|
|
}
|
|
else {
|
|
if (regexpNode.arguments.length >= 2) {
|
|
const ownedNode = (_a = patternSource.regexpValue) === null || _a === void 0 ? void 0 : _a.ownedNode;
|
|
if (ownedNode) {
|
|
removeFlags(ownedNode);
|
|
}
|
|
}
|
|
}
|
|
return {};
|
|
},
|
|
});
|
|
}
|
|
function parseOption(userOption) {
|
|
var _a;
|
|
const ignore = new Set();
|
|
let strictTypes = true;
|
|
if (userOption) {
|
|
for (const i of (_a = userOption.ignore) !== null && _a !== void 0 ? _a : []) {
|
|
ignore.add(i);
|
|
}
|
|
if (userOption.strictTypes != null) {
|
|
strictTypes = userOption.strictTypes;
|
|
}
|
|
}
|
|
return {
|
|
ignore,
|
|
strictTypes,
|
|
};
|
|
}
|
|
exports.default = (0, utils_1.createRule)("no-useless-flag", {
|
|
meta: {
|
|
docs: {
|
|
description: "disallow unnecessary regex flags",
|
|
category: "Best Practices",
|
|
recommended: true,
|
|
default: "warn",
|
|
},
|
|
fixable: "code",
|
|
schema: [
|
|
{
|
|
type: "object",
|
|
properties: {
|
|
ignore: {
|
|
type: "array",
|
|
items: {
|
|
enum: ["i", "m", "s", "g", "y"],
|
|
},
|
|
uniqueItems: true,
|
|
},
|
|
strictTypes: { type: "boolean" },
|
|
},
|
|
additionalProperties: false,
|
|
},
|
|
],
|
|
messages: {
|
|
uselessIgnoreCaseFlag: "The 'i' flag is unnecessary because the pattern only contains case-invariant characters.",
|
|
uselessMultilineFlag: "The 'm' flag is unnecessary because the pattern does not contain start (^) or end ($) assertions.",
|
|
uselessDotAllFlag: "The 's' flag is unnecessary because the pattern does not contain dots (.).",
|
|
uselessGlobalFlag: "The 'g' flag is unnecessary because the regex does not use global search.",
|
|
uselessGlobalFlagForTest: "The 'g' flag is unnecessary because the regex is used only once in 'RegExp.prototype.test'.",
|
|
uselessGlobalFlagForExec: "The 'g' flag is unnecessary because the regex is used only once in 'RegExp.prototype.exec'.",
|
|
uselessGlobalFlagForSplit: "The 'g' flag is unnecessary because 'String.prototype.split' ignores the 'g' flag.",
|
|
uselessGlobalFlagForSearch: "The 'g' flag is unnecessary because 'String.prototype.search' ignores the 'g' flag.",
|
|
uselessStickyFlag: "The 'y' flag is unnecessary because 'String.prototype.split' ignores the 'y' flag.",
|
|
uselessFlagsOwned: "The flags of this RegExp literal are useless because only the source of the regex is used.",
|
|
},
|
|
type: "suggestion",
|
|
},
|
|
create(context) {
|
|
const { ignore, strictTypes } = parseOption(context.options[0]);
|
|
let visitor = {};
|
|
if (!ignore.has("i")) {
|
|
visitor = (0, utils_1.compositingVisitors)(visitor, createUselessIgnoreCaseFlagVisitor(context));
|
|
}
|
|
if (!ignore.has("m")) {
|
|
visitor = (0, utils_1.compositingVisitors)(visitor, createUselessMultilineFlagVisitor(context));
|
|
}
|
|
if (!ignore.has("s")) {
|
|
visitor = (0, utils_1.compositingVisitors)(visitor, createUselessDotAllFlagVisitor(context));
|
|
}
|
|
if (!ignore.has("g")) {
|
|
visitor = (0, utils_1.compositingVisitors)(visitor, createUselessGlobalFlagVisitor(context, strictTypes));
|
|
}
|
|
if (!ignore.has("y")) {
|
|
visitor = (0, utils_1.compositingVisitors)(visitor, createUselessStickyFlagVisitor(context, strictTypes));
|
|
}
|
|
visitor = (0, utils_1.compositingVisitors)(visitor, createOwnedRegExpFlagsVisitor(context));
|
|
return visitor;
|
|
},
|
|
});
|