Skip to content

Commit

Permalink
feat(eslint): allow individual rule disabling
Browse files Browse the repository at this point in the history
  • Loading branch information
JonasBa committed Apr 9, 2022
1 parent 36b4562 commit 000d568
Show file tree
Hide file tree
Showing 5 changed files with 154 additions and 41 deletions.
5 changes: 2 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
# eslint-plugin-no-lookahead-lookbehind-regexp

eslint-plugin-no-lookahead-lookbehind-regexp
==============================
<img src="https://github.com/JonasBa/eslint-plugin-no-lookahead-lookbehind-regexp/blob/main/example.gif?raw=true" width="70%"/>

Lint the use of lookahead and lookbehind regexp expression. The expression is problematic, as compiling it in an unsupported browser will throw an error and possibly crash your browser. The plugin handles both [literal and constructor notation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/RegExp#literal_notation_and_constructor).
Lint the use of lookahead and lookbehind regexp expression. The expression is problematic, as compiling it in an unsupported browser will throw an error and possibly crash your browser. The plugin handles both Regexp [literal and constructor notations](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/RegExp#literal_notation_and_constructor).

### 1. Install

Expand Down
62 changes: 58 additions & 4 deletions src/helpers/analyzeRegExpForLookaheadAndLookbehind.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
import { analyzeRegExpForLookaheadAndLookbehind } from "./analyzeRegExpForLookaheadAndLookbehind";
import { getExpressionsToCheckFromConfiguration } from "../rules/noLookaheadLookbehindRegex";
import {
analyzeRegExpForLookaheadAndLookbehind,
CheckableExpression,
} from "./analyzeRegExpForLookaheadAndLookbehind";

const groups = ["?=", "?<=", "?!", "?<!"];

describe("analyzeRegExpForLookaheadAndLookbehind", () => {
it("does not return false positives for an escaped sequence", () => {
for (const group of groups) {
expect(analyzeRegExpForLookaheadAndLookbehind(`\\(${group}`).length).toBe(0);
expect(
analyzeRegExpForLookaheadAndLookbehind(`\\(${group}`, {
rules: getExpressionsToCheckFromConfiguration([]),
}).length
).toBe(0);
}
});

Expand All @@ -15,7 +23,11 @@ describe("analyzeRegExpForLookaheadAndLookbehind", () => {
["lookbehind", 0, "(?<=)"],
["negative lookbehind", 0, "(?<!)"],
])(`Single match %s - at %i`, (type, position, expression) => {
expect(analyzeRegExpForLookaheadAndLookbehind(expression)[0]).toEqual({
expect(
analyzeRegExpForLookaheadAndLookbehind(expression, {
rules: getExpressionsToCheckFromConfiguration([]),
})[0]
).toEqual({
type: type.replace("negative ", ""),
position: position,
...(type.includes("negative") ? { negative: 1 } : {}),
Expand All @@ -28,7 +40,11 @@ describe("analyzeRegExpForLookaheadAndLookbehind", () => {
["lookbehind", 0, 8, "(?<=t).*(?<=t)"],
["negative lookbehind", 0, 8, "(?<!t).*(?<!t)"],
])(`Multiple match %s - at %i and %i`, (type, first, second, expression) => {
expect(analyzeRegExpForLookaheadAndLookbehind(expression)).toEqual([
expect(
analyzeRegExpForLookaheadAndLookbehind(expression, {
rules: getExpressionsToCheckFromConfiguration([]),
})
).toEqual([
{
type: type.replace("negative ", ""),
position: first,
Expand All @@ -41,4 +57,42 @@ describe("analyzeRegExpForLookaheadAndLookbehind", () => {
},
]);
});

it.each([
["no-lookahead"],
["no-negative-lookahead"],
["no-lookbehind"],
["no-negative-lookbehind"],
])("Does not warn if %s rule is disabled", (rule) => {
const expressions: Record<CheckableExpression | string, string> = {
"no-lookahead": "(?=test)",
"no-lookbehind": "(?<=test)",
"no-negative-lookahead": "(?!test)",
"no-negative-lookbehind": "(?<!test)",
};

if (!expressions[rule as CheckableExpression]) throw new Error(`No test for ${rule}`);

for (const expression in expressions) {
if (rule === expression) {
expect(
analyzeRegExpForLookaheadAndLookbehind(expressions[expression], {
rules: { [expression]: 0 },
})
).toEqual([]);
} else {
expect(
analyzeRegExpForLookaheadAndLookbehind(expressions[expression], {
rules: getExpressionsToCheckFromConfiguration([]),
})
).toEqual([
{
position: 0,
type: expression.replace("no-", "").replace("negative-", ""),
...(expression.startsWith("no-negative") ? { negative: 1 } : {}),
},
]);
}
}
});
});
74 changes: 47 additions & 27 deletions src/helpers/analyzeRegExpForLookaheadAndLookbehind.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,26 @@
export type CheckableExpression =
| "lookahead"
| "lookbehind"
| "negative-lookahead"
| "negative-lookbehind";

export type AnalyzeOptions = {
rules: Partial<{ [key in `no-${CheckableExpression}`]: 0 | 1 }>;
};

type UnsupportedExpression = {
type: "lookahead" | "lookbehind";
negative?: 1;
position: number;
};

function analyzeRegExpForLookaheadAndLookbehind(input: string): UnsupportedExpression[] {
function analyzeRegExpForLookaheadAndLookbehind(
input: string,
options: AnalyzeOptions
): UnsupportedExpression[] {
// Lookahead and lookbehind require min 5 characters to be useful, however
// an expression like /(?=)/ which uses only 4, although not useful, can still crash an application
if (input.length < 4) return [];

let current = 0;

const peek = (): string => input.charAt(current + 1);
Expand All @@ -35,45 +47,53 @@ function analyzeRegExpForLookaheadAndLookbehind(input: string): UnsupportedExpre

// Lookahead
if (peek() === "=") {
matchedExpressions.push({
type: "lookahead",
position: start,
});
if (options.rules["no-lookahead"]) {
matchedExpressions.push({
type: "lookahead",
position: start,
});
}
advance();
break;
}
// Negative lookahead
if (peek() === "!") {
matchedExpressions.push({
type: "lookahead",
negative: 1,
position: start,
});
if (options.rules["no-negative-lookahead"]) {
matchedExpressions.push({
type: "lookahead",
negative: 1,
position: start,
});
}
advance();
break;
}

// Lookbehind
if (peek() === "<") {
// Lookbehind
if (input.charAt(current + 2) === "=") {
matchedExpressions.push({
type: "lookbehind",
position: start,
});
advance();
advance();
break;
if (options.rules["no-lookbehind"]) {
matchedExpressions.push({
type: "lookbehind",
position: start,
});
advance();
advance();
break;
}
}
// Negative Lookbehind
if (input.charAt(current + 2) === "!") {
matchedExpressions.push({
type: "lookbehind",
negative: 1,
position: start,
});
advance();
advance();
break;
if (options.rules["no-negative-lookbehind"]) {
matchedExpressions.push({
type: "lookbehind",
negative: 1,
position: start,
});
advance();
advance();
break;
}
}
}
} else {
Expand Down
52 changes: 46 additions & 6 deletions src/rules/noLookaheadLookbehindRegex.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,50 @@
import { Rule } from "eslint";
import * as ESTree from "estree";

import { analyzeRegExpForLookaheadAndLookbehind } from "./../helpers/analyzeRegExpForLookAheadAndLookbehind";
import {
analyzeRegExpForLookaheadAndLookbehind,
AnalyzeOptions,
CheckableExpression,
} from "./../helpers/analyzeRegExpForLookAheadAndLookbehind";
import { collectBrowserTargets, collectUnsupportedTargets } from "./../helpers/caniuse";

import { isStringLiteralRegExp, isRegExpLiteral } from "./../helpers/ast";
import { createContextReport } from "../helpers/createReport";

export const DEFAULT_OPTIONS: AnalyzeOptions["rules"] = {
"no-lookahead": 1,
"no-lookbehind": 1,
"no-negative-lookahead": 1,
"no-negative-lookbehind": 1,
};

export const getExpressionsToCheckFromConfiguration = (
options: Rule.RuleContext["options"]
): AnalyzeOptions["rules"] => {
if (!options.length) return DEFAULT_OPTIONS;

const validOptions: CheckableExpression[] = options.filter((option: unknown) => {
if (typeof option !== "string") return false;
return DEFAULT_OPTIONS[option as keyof typeof DEFAULT_OPTIONS];
});

if (!validOptions.length) {
return DEFAULT_OPTIONS;
}

return validOptions.reduce<AnalyzeOptions["rules"]>(
(acc: AnalyzeOptions["rules"], opt) => {
acc[opt as keyof typeof DEFAULT_OPTIONS] = 1;
return acc;
},
{
"no-lookahead": 0,
"no-lookbehind": 0,
"no-negative-lookahead": 0,
"no-negative-lookbehind": 0,
}
);
};

const noLookaheadLookbehindRegexp: Rule.RuleModule = {
meta: {
docs: {
Expand All @@ -20,23 +58,25 @@ const noLookaheadLookbehindRegexp: Rule.RuleModule = {
const browsers = context.settings.browsers || context.settings.targets;
const { targets, hasConfig } = collectBrowserTargets(context.getFilename(), browsers);
const unsupportedTargets = collectUnsupportedTargets("js-regexp-lookbehind", targets);
const rules = getExpressionsToCheckFromConfiguration(context.options);

// If there are no unsupported targets resolved from the browserlist config, then we can skip this rule
if (!unsupportedTargets.length && hasConfig) return {};

console.log(context);

return {
Literal(node: ESTree.Literal & Rule.NodeParentExtension): void {
if (isStringLiteralRegExp(node) && typeof node.raw === "string") {
const unsupportedGroups = analyzeRegExpForLookaheadAndLookbehind(
node.raw // For string literals, we need to pass the raw value which includes escape characters.
node.raw,
{ rules } // For string literals, we need to pass the raw value which includes escape characters.
);
if (unsupportedGroups.length > 0) {
createContextReport(node, context, unsupportedGroups, unsupportedTargets);
}
} else if (isRegExpLiteral(node)) {
const unsupportedGroups = analyzeRegExpForLookaheadAndLookbehind(node.regex.pattern);
const unsupportedGroups = analyzeRegExpForLookaheadAndLookbehind(node.regex.pattern, {
rules,
});
if (unsupportedGroups.length > 0) {
createContextReport(node, context, unsupportedGroups, unsupportedTargets);
}
Expand Down
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,5 +69,5 @@
"skipLibCheck": true /* Skip type checking of declaration files. */,
"forceConsistentCasingInFileNames": false /* Disallow inconsistently-cased references to the same file. */
},
"include": ["src/index.ts", "src/**/*.ts", "benchmark/**/*.ts"]
"include": ["src/index.ts", "src/**/*.ts"]
}

0 comments on commit 000d568

Please sign in to comment.