Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ We use [browserslist](https://github.com/browserslist/browserslist) to resolve b

## 4. Customizing rules

By default, the plugin will report on all lookahead and lookbehind regexp as well as their negative counterparts. To enable only individual rules like erroring only on lookbehind expressions, you can pass a list of rules that you wish to enable as options in your eslint. **Note that once a single rule is passed as a configuration option, all of the other rules are disabled by default and you are in full control.**
By default, the plugin will report on all lookahead and lookbehind regexp as well as their negative counterparts(if they are not supported with above browserslist target settings). To enable only individual rules like erroring only on lookbehind expressions, you can pass a list of rules that you wish to enable as options in your eslint. **Note that once a single rule is passed as a configuration option, all of the other rules are disabled by default and you are in full control.**

```js
rules: {
Expand All @@ -53,14 +53,16 @@ rules: {
}
```

As an example, passing both no-lookbehind and no-negative-lookbehind as options will cause the plugin to error on all lookbehind and negative lookbehind expressions, but it will not cause it to report errors on lookahead or negative lookahead expressions.
## 5. Disable Browserslist Support

By default, the plugin will use yours project's browserslist settings to find availability of lookahead/lookbehind and their negative counterparts. However, if you want to disable this feature to report all usages(still controlled by above rules settings) as errors, you can pass an additional object options.

```js
rules: {
'no-lookahead-lookbehind-regexp/no-lookahead-lookbehind-regexp': [
'error',
'no-lookbehind',
'no-negative-lookbehind',
'no-lookahead',
{ browserslist: false },
],
}
```
Expand Down
4 changes: 2 additions & 2 deletions benchmark/benchmark.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const config = {

const eslint = new ESLint(config);
const benchmark = new Benchmark(
"ESLint self benchmark w/o browserlist",
"ESLint self benchmark w/o browserslist",
(deferred: { resolve: Function }) => {
eslint
.lintFiles("src/**/*.ts")
Expand Down Expand Up @@ -63,7 +63,7 @@ eslint
});

const browserlistBenchmark = new Benchmark(
"ESLint self benchmark with browserlist",
"ESLint self benchmark with browserslist",
(deferred: { resolve: Function; reject: Function }) => {
eslint
.lintFiles("src/**/*.ts")
Expand Down
3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "eslint-plugin-no-lookahead-lookbehind-regexp",
"version": "0.1.0",
"version": "0.2.0",
"main": "lib/index.js",
"types": "lib/index.d.ts",
"repository": "git@github.com:JonasBa/eslint-plugin-no-lookahead-lookbehind-regexp.git",
Expand All @@ -12,7 +12,6 @@
},
"scripts": {
"benchmark": "ts-node test/benchmark.ts",
"prepare": "yarn test && yarn build",
"build": "rm -rf ./lib && yarn tsc",
"tsc": "tsc",
"test": "jest",
Expand Down
32 changes: 17 additions & 15 deletions src/helpers/analyzeRegExpForLookaheadAndLookbehind.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ describe("analyzeRegExpForLookaheadAndLookbehind", () => {
it("does not return false positives for an escaped sequence", () => {
for (const group of groups) {
expect(
analyzeRegExpForLookaheadAndLookbehind(`\\(${group}`, {
rules: getExpressionsToCheckFromConfiguration([]),
}).length
analyzeRegExpForLookaheadAndLookbehind(
`\\(${group}`,
getExpressionsToCheckFromConfiguration([]).rules
).length
).toBe(0);
}
});
Expand All @@ -24,9 +25,10 @@ describe("analyzeRegExpForLookaheadAndLookbehind", () => {
["negative lookbehind", 0, "(?<!)"],
])(`Single match %s - at %i`, (type, position, expression) => {
expect(
analyzeRegExpForLookaheadAndLookbehind(expression, {
rules: getExpressionsToCheckFromConfiguration([]),
})[0]
analyzeRegExpForLookaheadAndLookbehind(
expression,
getExpressionsToCheckFromConfiguration([]).rules
)[0]
).toEqual({
type: type.replace("negative ", ""),
position: position,
Expand All @@ -41,9 +43,10 @@ describe("analyzeRegExpForLookaheadAndLookbehind", () => {
["negative lookbehind", 0, 8, "(?<!t).*(?<!t)"],
])(`Multiple match %s - at %i and %i`, (type, first, second, expression) => {
expect(
analyzeRegExpForLookaheadAndLookbehind(expression, {
rules: getExpressionsToCheckFromConfiguration([]),
})
analyzeRegExpForLookaheadAndLookbehind(
expression,
getExpressionsToCheckFromConfiguration([]).rules
)
).toEqual([
{
type: type.replace("negative ", ""),
Expand Down Expand Up @@ -76,15 +79,14 @@ describe("analyzeRegExpForLookaheadAndLookbehind", () => {
for (const expression in expressions) {
if (rule === expression) {
expect(
analyzeRegExpForLookaheadAndLookbehind(expressions[expression], {
rules: { [expression]: 0 },
})
analyzeRegExpForLookaheadAndLookbehind(expressions[expression], { [expression]: 0 })
).toEqual([]);
} else {
expect(
analyzeRegExpForLookaheadAndLookbehind(expressions[expression], {
rules: getExpressionsToCheckFromConfiguration([]),
})
analyzeRegExpForLookaheadAndLookbehind(
expressions[expression],
getExpressionsToCheckFromConfiguration([]).rules
)
).toEqual([
{
position: 0,
Expand Down
11 changes: 6 additions & 5 deletions src/helpers/analyzeRegExpForLookaheadAndLookbehind.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export type CheckableExpression =

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

type UnsupportedExpression = {
Expand All @@ -16,7 +17,7 @@ type UnsupportedExpression = {

function analyzeRegExpForLookaheadAndLookbehind(
input: string,
options: AnalyzeOptions
rules: AnalyzeOptions["rules"]
): 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
Expand Down Expand Up @@ -47,7 +48,7 @@ function analyzeRegExpForLookaheadAndLookbehind(

// Lookahead
if (peek() === "=") {
if (options.rules["no-lookahead"]) {
if (rules["no-lookahead"]) {
matchedExpressions.push({
type: "lookahead",
position: start,
Expand All @@ -58,7 +59,7 @@ function analyzeRegExpForLookaheadAndLookbehind(
}
// Negative lookahead
if (peek() === "!") {
if (options.rules["no-negative-lookahead"]) {
if (rules["no-negative-lookahead"]) {
matchedExpressions.push({
type: "lookahead",
negative: 1,
Expand All @@ -72,7 +73,7 @@ function analyzeRegExpForLookaheadAndLookbehind(
// Lookbehind
if (peek() === "<") {
if (input.charAt(current + 2) === "=") {
if (options.rules["no-lookbehind"]) {
if (rules["no-lookbehind"]) {
matchedExpressions.push({
type: "lookbehind",
position: start,
Expand All @@ -84,7 +85,7 @@ function analyzeRegExpForLookaheadAndLookbehind(
}
// Negative Lookbehind
if (input.charAt(current + 2) === "!") {
if (options.rules["no-negative-lookbehind"]) {
if (rules["no-negative-lookbehind"]) {
matchedExpressions.push({
type: "lookbehind",
negative: 1,
Expand Down
27 changes: 14 additions & 13 deletions src/helpers/caniuse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ export function collectBrowserTargets(
configPath: string,
config?: { production: string[]; development: string[] } | Array<string> | string
): { targets: BrowserTarget[]; hasConfig: boolean } {
const browserslistConfig = browserslist.findConfig(configPath);
const hasConfig = (browserslistConfig && browserslistConfig.defaults.length > 0) || false;
const targets = new Set<string>();

function addTarget(target: string): void {
Expand All @@ -35,35 +37,35 @@ export function collectBrowserTargets(
}).forEach(addTarget);
}

// If user had eslint config and also has browserlist config, then merge the two
if (targets.size > 0 && browserslist.findConfig(configPath)) {
// If user had eslint config and also has browserslist config, then merge the two
if (targets.size > 0 && hasConfig) {
browserslist(undefined, { path: configPath }).forEach(addTarget);
return { targets: Array.from(targets).map(transformTarget), hasConfig: true };
return { targets: Array.from(targets).map(transformTarget), hasConfig };
}

// If they only use an eslint config, then return what we have
if (targets.size > 0 && !browserslist.findConfig(configPath)) {
return { targets: Array.from(targets).map(transformTarget), hasConfig: true };
if (targets.size > 0 && !hasConfig) {
return { targets: Array.from(targets).map(transformTarget), hasConfig };
}

// ** Warning
// If they dont use a browserlist config, then return an empty targets array and disable the use of the regexp lookahead and lookbehind entirely.
if (!browserslist.findConfig(configPath)) {
return { targets: [], hasConfig: false };
// If they don't use a browserslist config, then return an empty targets array and disable the use of the regexp lookahead and lookbehind entirely.
if (!hasConfig) {
return { targets: [], hasConfig };
}

browserslist(undefined, { path: configPath }).forEach(addTarget);
// If we couldnt find anything, return empty targets and indicate that no config was found
return { targets: Array.from(targets).map(transformTarget), hasConfig: false };
return { targets: Array.from(targets).map(transformTarget), hasConfig };
}

// Returns a list of browser targets that do not support a feature.
// In case feature stats are not found in the database, we will assume that the feature is supported,
// this can result in false positives when querying for versions that may not have been released yet (typo or user mistake)
// Since the equivalent can happen in case of specifying some super old version, the proper way to possibly handle
// this would be to throw an error, but since I dont know how often that happens or if it may cause false positives later on
// if caniuse db changes... I'm leaning towards throwing an error here, but it's not the plugin's responsability to validate browserlist config - opinions are welcome.
// TODO: check if browserlist throws an error lower in the stack if config is invalid, this would likely be the best solution
// if caniuse db changes... I'm leaning towards throwing an error here, but it's not the plugin's responsability to validate browserslist config - opinions are welcome.
// TODO: check if browserslist throws an error lower in the stack if config is invalid, this would likely be the best solution
export function collectUnsupportedTargets(id: string, targets: BrowserTarget[]): BrowserTarget[] {
const data = lite.feature(lite.features[id]);

Expand Down Expand Up @@ -114,8 +116,7 @@ export function resolveCaniUseBrowserTarget(target: string): string {

export function formatLinterMessage(
violators: ReturnType<typeof analyzeRegExpForLookaheadAndLookbehind>,
targets: ReturnType<typeof collectUnsupportedTargets>
): string {
targets: ReturnType<typeof collectUnsupportedTargets>): string {
// If browser has no targets and we still want to report an error, it means that the feature is banned from use.
if (!targets.length) {
if (violators.length === 1) {
Expand Down
6 changes: 3 additions & 3 deletions src/helpers/createReport.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import * as ESTree from "estree";
import { Rule } from "eslint";

import { analyzeRegExpForLookaheadAndLookbehind } from "./../helpers/analyzeRegExpForLookAheadAndLookbehind";
import { collectUnsupportedTargets, formatLinterMessage } from "./../helpers/caniuse";
import { analyzeRegExpForLookaheadAndLookbehind } from "../helpers/analyzeRegExpForLookaheadAndLookbehind";
import { collectUnsupportedTargets, formatLinterMessage } from "../helpers/caniuse";

export function createContextReport(
node: ESTree.Literal & Rule.NodeParentExtension,
context: Rule.RuleContext,
violators: ReturnType<typeof analyzeRegExpForLookaheadAndLookbehind>,
targets: ReturnType<typeof collectUnsupportedTargets>
targets: ReturnType<typeof collectUnsupportedTargets>,
): void {
context.report({
node: node,
Expand Down
2 changes: 1 addition & 1 deletion src/rules/noLookAheadLookBehindRegExp.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { RuleTester } from "eslint";
import { noLookaheadLookbehindRegexp } from "./noLookaheadLookbehindRegex";

// Rule tester for when no browserlist is passed, so lookahead and lookbehind should not be allowed
// Rule tester for when no browserslist is passed, so lookahead and lookbehind should not be allowed
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for also fixing my typos :)

const tester = new RuleTester({
parser: require.resolve("@typescript-eslint/parser"),
parserOptions: {
Expand Down
67 changes: 46 additions & 21 deletions src/rules/noLookaheadLookbehindRegex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import {
analyzeRegExpForLookaheadAndLookbehind,
AnalyzeOptions,
CheckableExpression,
} from "./../helpers/analyzeRegExpForLookAheadAndLookbehind";
import { collectBrowserTargets, collectUnsupportedTargets } from "./../helpers/caniuse";
import { isStringLiteralRegExp, isRegExpLiteral } from "./../helpers/ast";
} 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"] = {
Expand All @@ -17,21 +17,35 @@ export const DEFAULT_OPTIONS: AnalyzeOptions["rules"] = {
"no-negative-lookbehind": 1,
};

export const DEFAULT_CONF: AnalyzeOptions["conf"] = {
browserslist: true,
};

function isPlainObject(obj: any) {
return Object.prototype.toString.call(obj) === "[object Object]";
}

export const getExpressionsToCheckFromConfiguration = (
options: Rule.RuleContext["options"]
): AnalyzeOptions["rules"] => {
if (!options.length) return DEFAULT_OPTIONS;
): { rules: AnalyzeOptions["rules"]; conf: AnalyzeOptions["conf"] } => {
if (!options.length) return { rules: DEFAULT_OPTIONS, conf: DEFAULT_CONF };
let rules: CheckableExpression[] = options;
let conf: AnalyzeOptions["conf"] = {};
if (isPlainObject(options[options.length - 1])) {
rules = options.slice(0, -1);
conf = options[options.length - 1];
}

const validOptions: CheckableExpression[] = options.filter((option: unknown) => {
const validOptions: CheckableExpression[] = rules.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 { rules: DEFAULT_OPTIONS, conf };
}

return validOptions.reduce<AnalyzeOptions["rules"]>(
const expressions = validOptions.reduce<AnalyzeOptions["rules"]>(
(acc: AnalyzeOptions["rules"], opt) => {
acc[opt as keyof typeof DEFAULT_OPTIONS] = 1;
return acc;
Expand All @@ -43,12 +57,17 @@ export const getExpressionsToCheckFromConfiguration = (
"no-negative-lookbehind": 0,
}
);
return {
rules: expressions,
conf,
};
};

const noLookaheadLookbehindRegexp: Rule.RuleModule = {
meta: {
docs: {
description: "disallow the use of lookahead and lookbehind regexes if unsupported by browser",
description:
"disallow the use of lookahead and lookbehind regular expressions if unsupported by browser",
category: "Compatibility",
recommended: true,
},
Expand All @@ -57,27 +76,33 @@ const noLookaheadLookbehindRegexp: Rule.RuleModule = {
create(context: Rule.RuleContext) {
const browsers = context.settings.browsers || context.settings.targets;
const { targets, hasConfig } = collectBrowserTargets(context.getFilename(), browsers);
// Lookahead assertions are part of JavaScript's original regular expression support and are thus supported in all browsers.
const unsupportedTargets = collectUnsupportedTargets("js-regexp-lookbehind", targets);
const rules = getExpressionsToCheckFromConfiguration(context.options);
const {
rules,
conf: { browserslist },
} = getExpressionsToCheckFromConfiguration(context.options);

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

return {
Literal(node: ESTree.Literal & Rule.NodeParentExtension): void {
let input: string = "";
if (isStringLiteralRegExp(node) && typeof node.raw === "string") {
const unsupportedGroups = analyzeRegExpForLookaheadAndLookbehind(
node.raw,
{ rules } // For string literals, we need to pass the raw value which includes escape characters.
);
if (unsupportedGroups.length > 0) {
// For string literals, we need to pass the raw value which includes escape characters.
input = node.raw;
} else if (isRegExpLiteral(node)) {
input = node.regex.pattern;
}
if (input) {
const unsupportedGroups = analyzeRegExpForLookaheadAndLookbehind(input, rules);
if (unsupportedGroups.length === 0) return;
if (!browserslist) {
createContextReport(node, context, unsupportedGroups, unsupportedTargets);
return;
}
} else if (isRegExpLiteral(node)) {
const unsupportedGroups = analyzeRegExpForLookaheadAndLookbehind(node.regex.pattern, {
rules,
});
if (unsupportedGroups.length > 0) {
if (unsupportedGroups.some((group) => group.type === "lookbehind")) {
createContextReport(node, context, unsupportedGroups, unsupportedTargets);
}
}
Expand Down