Skip to content

Commit 6710c23

Browse files
authored
Merge pull request #4 from latel/main
Fix browserslist support, added options to disable browserlist feature
2 parents 355f729 + a7315a1 commit 6710c23

9 files changed

+96
-66
lines changed

README.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ We use [browserslist](https://github.com/browserslist/browserslist) to resolve b
3939

4040
## 4. Customizing rules
4141

42-
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.**
42+
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.**
4343

4444
```js
4545
rules: {
@@ -53,14 +53,16 @@ rules: {
5353
}
5454
```
5555

56-
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.
56+
## 5. Disable Browserslist Support
57+
58+
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.
5759

5860
```js
5961
rules: {
6062
'no-lookahead-lookbehind-regexp/no-lookahead-lookbehind-regexp': [
6163
'error',
62-
'no-lookbehind',
63-
'no-negative-lookbehind',
64+
'no-lookahead',
65+
{ browserslist: false },
6466
],
6567
}
6668
```

benchmark/benchmark.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ const config = {
2121

2222
const eslint = new ESLint(config);
2323
const benchmark = new Benchmark(
24-
"ESLint self benchmark w/o browserlist",
24+
"ESLint self benchmark w/o browserslist",
2525
(deferred: { resolve: Function }) => {
2626
eslint
2727
.lintFiles("src/**/*.ts")
@@ -63,7 +63,7 @@ eslint
6363
});
6464

6565
const browserlistBenchmark = new Benchmark(
66-
"ESLint self benchmark with browserlist",
66+
"ESLint self benchmark with browserslist",
6767
(deferred: { resolve: Function; reject: Function }) => {
6868
eslint
6969
.lintFiles("src/**/*.ts")

package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "eslint-plugin-no-lookahead-lookbehind-regexp",
3-
"version": "0.1.0",
3+
"version": "0.2.0",
44
"main": "lib/index.js",
55
"types": "lib/index.d.ts",
66
"repository": "git@github.com:JonasBa/eslint-plugin-no-lookahead-lookbehind-regexp.git",
@@ -12,7 +12,6 @@
1212
},
1313
"scripts": {
1414
"benchmark": "ts-node test/benchmark.ts",
15-
"prepare": "yarn test && yarn build",
1615
"build": "rm -rf ./lib && yarn tsc",
1716
"tsc": "tsc",
1817
"test": "jest",

src/helpers/analyzeRegExpForLookaheadAndLookbehind.test.ts

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@ describe("analyzeRegExpForLookaheadAndLookbehind", () => {
1010
it("does not return false positives for an escaped sequence", () => {
1111
for (const group of groups) {
1212
expect(
13-
analyzeRegExpForLookaheadAndLookbehind(`\\(${group}`, {
14-
rules: getExpressionsToCheckFromConfiguration([]),
15-
}).length
13+
analyzeRegExpForLookaheadAndLookbehind(
14+
`\\(${group}`,
15+
getExpressionsToCheckFromConfiguration([]).rules
16+
).length
1617
).toBe(0);
1718
}
1819
});
@@ -24,9 +25,10 @@ describe("analyzeRegExpForLookaheadAndLookbehind", () => {
2425
["negative lookbehind", 0, "(?<!)"],
2526
])(`Single match %s - at %i`, (type, position, expression) => {
2627
expect(
27-
analyzeRegExpForLookaheadAndLookbehind(expression, {
28-
rules: getExpressionsToCheckFromConfiguration([]),
29-
})[0]
28+
analyzeRegExpForLookaheadAndLookbehind(
29+
expression,
30+
getExpressionsToCheckFromConfiguration([]).rules
31+
)[0]
3032
).toEqual({
3133
type: type.replace("negative ", ""),
3234
position: position,
@@ -41,9 +43,10 @@ describe("analyzeRegExpForLookaheadAndLookbehind", () => {
4143
["negative lookbehind", 0, 8, "(?<!t).*(?<!t)"],
4244
])(`Multiple match %s - at %i and %i`, (type, first, second, expression) => {
4345
expect(
44-
analyzeRegExpForLookaheadAndLookbehind(expression, {
45-
rules: getExpressionsToCheckFromConfiguration([]),
46-
})
46+
analyzeRegExpForLookaheadAndLookbehind(
47+
expression,
48+
getExpressionsToCheckFromConfiguration([]).rules
49+
)
4750
).toEqual([
4851
{
4952
type: type.replace("negative ", ""),
@@ -76,15 +79,14 @@ describe("analyzeRegExpForLookaheadAndLookbehind", () => {
7679
for (const expression in expressions) {
7780
if (rule === expression) {
7881
expect(
79-
analyzeRegExpForLookaheadAndLookbehind(expressions[expression], {
80-
rules: { [expression]: 0 },
81-
})
82+
analyzeRegExpForLookaheadAndLookbehind(expressions[expression], { [expression]: 0 })
8283
).toEqual([]);
8384
} else {
8485
expect(
85-
analyzeRegExpForLookaheadAndLookbehind(expressions[expression], {
86-
rules: getExpressionsToCheckFromConfiguration([]),
87-
})
86+
analyzeRegExpForLookaheadAndLookbehind(
87+
expressions[expression],
88+
getExpressionsToCheckFromConfiguration([]).rules
89+
)
8890
).toEqual([
8991
{
9092
position: 0,

src/helpers/analyzeRegExpForLookaheadAndLookbehind.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export type CheckableExpression =
66

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

1112
type UnsupportedExpression = {
@@ -16,7 +17,7 @@ type UnsupportedExpression = {
1617

1718
function analyzeRegExpForLookaheadAndLookbehind(
1819
input: string,
19-
options: AnalyzeOptions
20+
rules: AnalyzeOptions["rules"]
2021
): UnsupportedExpression[] {
2122
// Lookahead and lookbehind require min 5 characters to be useful, however
2223
// an expression like /(?=)/ which uses only 4, although not useful, can still crash an application
@@ -47,7 +48,7 @@ function analyzeRegExpForLookaheadAndLookbehind(
4748

4849
// Lookahead
4950
if (peek() === "=") {
50-
if (options.rules["no-lookahead"]) {
51+
if (rules["no-lookahead"]) {
5152
matchedExpressions.push({
5253
type: "lookahead",
5354
position: start,
@@ -58,7 +59,7 @@ function analyzeRegExpForLookaheadAndLookbehind(
5859
}
5960
// Negative lookahead
6061
if (peek() === "!") {
61-
if (options.rules["no-negative-lookahead"]) {
62+
if (rules["no-negative-lookahead"]) {
6263
matchedExpressions.push({
6364
type: "lookahead",
6465
negative: 1,
@@ -72,7 +73,7 @@ function analyzeRegExpForLookaheadAndLookbehind(
7273
// Lookbehind
7374
if (peek() === "<") {
7475
if (input.charAt(current + 2) === "=") {
75-
if (options.rules["no-lookbehind"]) {
76+
if (rules["no-lookbehind"]) {
7677
matchedExpressions.push({
7778
type: "lookbehind",
7879
position: start,
@@ -84,7 +85,7 @@ function analyzeRegExpForLookaheadAndLookbehind(
8485
}
8586
// Negative Lookbehind
8687
if (input.charAt(current + 2) === "!") {
87-
if (options.rules["no-negative-lookbehind"]) {
88+
if (rules["no-negative-lookbehind"]) {
8889
matchedExpressions.push({
8990
type: "lookbehind",
9091
negative: 1,

src/helpers/caniuse.ts

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ export function collectBrowserTargets(
1414
configPath: string,
1515
config?: { production: string[]; development: string[] } | Array<string> | string
1616
): { targets: BrowserTarget[]; hasConfig: boolean } {
17+
const browserslistConfig = browserslist.findConfig(configPath);
18+
const hasConfig = (browserslistConfig && browserslistConfig.defaults.length > 0) || false;
1719
const targets = new Set<string>();
1820

1921
function addTarget(target: string): void {
@@ -35,35 +37,35 @@ export function collectBrowserTargets(
3537
}).forEach(addTarget);
3638
}
3739

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

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

4951
// ** Warning
50-
// If they dont use a browserlist config, then return an empty targets array and disable the use of the regexp lookahead and lookbehind entirely.
51-
if (!browserslist.findConfig(configPath)) {
52-
return { targets: [], hasConfig: false };
52+
// 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.
53+
if (!hasConfig) {
54+
return { targets: [], hasConfig };
5355
}
5456

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

6062
// Returns a list of browser targets that do not support a feature.
6163
// In case feature stats are not found in the database, we will assume that the feature is supported,
6264
// this can result in false positives when querying for versions that may not have been released yet (typo or user mistake)
6365
// Since the equivalent can happen in case of specifying some super old version, the proper way to possibly handle
6466
// 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
65-
// 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.
66-
// TODO: check if browserlist throws an error lower in the stack if config is invalid, this would likely be the best solution
67+
// 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.
68+
// TODO: check if browserslist throws an error lower in the stack if config is invalid, this would likely be the best solution
6769
export function collectUnsupportedTargets(id: string, targets: BrowserTarget[]): BrowserTarget[] {
6870
const data = lite.feature(lite.features[id]);
6971

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

115117
export function formatLinterMessage(
116118
violators: ReturnType<typeof analyzeRegExpForLookaheadAndLookbehind>,
117-
targets: ReturnType<typeof collectUnsupportedTargets>
118-
): string {
119+
targets: ReturnType<typeof collectUnsupportedTargets>): string {
119120
// If browser has no targets and we still want to report an error, it means that the feature is banned from use.
120121
if (!targets.length) {
121122
if (violators.length === 1) {

src/helpers/createReport.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import * as ESTree from "estree";
22
import { Rule } from "eslint";
33

4-
import { analyzeRegExpForLookaheadAndLookbehind } from "./../helpers/analyzeRegExpForLookAheadAndLookbehind";
5-
import { collectUnsupportedTargets, formatLinterMessage } from "./../helpers/caniuse";
4+
import { analyzeRegExpForLookaheadAndLookbehind } from "../helpers/analyzeRegExpForLookaheadAndLookbehind";
5+
import { collectUnsupportedTargets, formatLinterMessage } from "../helpers/caniuse";
66

77
export function createContextReport(
88
node: ESTree.Literal & Rule.NodeParentExtension,
99
context: Rule.RuleContext,
1010
violators: ReturnType<typeof analyzeRegExpForLookaheadAndLookbehind>,
11-
targets: ReturnType<typeof collectUnsupportedTargets>
11+
targets: ReturnType<typeof collectUnsupportedTargets>,
1212
): void {
1313
context.report({
1414
node: node,

src/rules/noLookAheadLookBehindRegExp.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { RuleTester } from "eslint";
22
import { noLookaheadLookbehindRegexp } from "./noLookaheadLookbehindRegex";
33

4-
// Rule tester for when no browserlist is passed, so lookahead and lookbehind should not be allowed
4+
// Rule tester for when no browserslist is passed, so lookahead and lookbehind should not be allowed
55
const tester = new RuleTester({
66
parser: require.resolve("@typescript-eslint/parser"),
77
parserOptions: {

src/rules/noLookaheadLookbehindRegex.ts

Lines changed: 46 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@ import {
55
analyzeRegExpForLookaheadAndLookbehind,
66
AnalyzeOptions,
77
CheckableExpression,
8-
} from "./../helpers/analyzeRegExpForLookAheadAndLookbehind";
9-
import { collectBrowserTargets, collectUnsupportedTargets } from "./../helpers/caniuse";
10-
import { isStringLiteralRegExp, isRegExpLiteral } from "./../helpers/ast";
8+
} from "../helpers/analyzeRegExpForLookaheadAndLookbehind";
9+
import { collectBrowserTargets, collectUnsupportedTargets } from "../helpers/caniuse";
10+
import { isStringLiteralRegExp, isRegExpLiteral } from "../helpers/ast";
1111
import { createContextReport } from "../helpers/createReport";
1212

1313
export const DEFAULT_OPTIONS: AnalyzeOptions["rules"] = {
@@ -17,21 +17,35 @@ export const DEFAULT_OPTIONS: AnalyzeOptions["rules"] = {
1717
"no-negative-lookbehind": 1,
1818
};
1919

20+
export const DEFAULT_CONF: AnalyzeOptions["conf"] = {
21+
browserslist: true,
22+
};
23+
24+
function isPlainObject(obj: any) {
25+
return Object.prototype.toString.call(obj) === "[object Object]";
26+
}
27+
2028
export const getExpressionsToCheckFromConfiguration = (
2129
options: Rule.RuleContext["options"]
22-
): AnalyzeOptions["rules"] => {
23-
if (!options.length) return DEFAULT_OPTIONS;
30+
): { rules: AnalyzeOptions["rules"]; conf: AnalyzeOptions["conf"] } => {
31+
if (!options.length) return { rules: DEFAULT_OPTIONS, conf: DEFAULT_CONF };
32+
let rules: CheckableExpression[] = options;
33+
let conf: AnalyzeOptions["conf"] = {};
34+
if (isPlainObject(options[options.length - 1])) {
35+
rules = options.slice(0, -1);
36+
conf = options[options.length - 1];
37+
}
2438

25-
const validOptions: CheckableExpression[] = options.filter((option: unknown) => {
39+
const validOptions: CheckableExpression[] = rules.filter((option: unknown) => {
2640
if (typeof option !== "string") return false;
2741
return DEFAULT_OPTIONS[option as keyof typeof DEFAULT_OPTIONS];
2842
});
2943

3044
if (!validOptions.length) {
31-
return DEFAULT_OPTIONS;
45+
return { rules: DEFAULT_OPTIONS, conf };
3246
}
3347

34-
return validOptions.reduce<AnalyzeOptions["rules"]>(
48+
const expressions = validOptions.reduce<AnalyzeOptions["rules"]>(
3549
(acc: AnalyzeOptions["rules"], opt) => {
3650
acc[opt as keyof typeof DEFAULT_OPTIONS] = 1;
3751
return acc;
@@ -43,12 +57,17 @@ export const getExpressionsToCheckFromConfiguration = (
4357
"no-negative-lookbehind": 0,
4458
}
4559
);
60+
return {
61+
rules: expressions,
62+
conf,
63+
};
4664
};
4765

4866
const noLookaheadLookbehindRegexp: Rule.RuleModule = {
4967
meta: {
5068
docs: {
51-
description: "disallow the use of lookahead and lookbehind regexes if unsupported by browser",
69+
description:
70+
"disallow the use of lookahead and lookbehind regular expressions if unsupported by browser",
5271
category: "Compatibility",
5372
recommended: true,
5473
},
@@ -57,27 +76,33 @@ const noLookaheadLookbehindRegexp: Rule.RuleModule = {
5776
create(context: Rule.RuleContext) {
5877
const browsers = context.settings.browsers || context.settings.targets;
5978
const { targets, hasConfig } = collectBrowserTargets(context.getFilename(), browsers);
79+
// Lookahead assertions are part of JavaScript's original regular expression support and are thus supported in all browsers.
6080
const unsupportedTargets = collectUnsupportedTargets("js-regexp-lookbehind", targets);
61-
const rules = getExpressionsToCheckFromConfiguration(context.options);
81+
const {
82+
rules,
83+
conf: { browserslist },
84+
} = getExpressionsToCheckFromConfiguration(context.options);
6285

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

6689
return {
6790
Literal(node: ESTree.Literal & Rule.NodeParentExtension): void {
91+
let input: string = "";
6892
if (isStringLiteralRegExp(node) && typeof node.raw === "string") {
69-
const unsupportedGroups = analyzeRegExpForLookaheadAndLookbehind(
70-
node.raw,
71-
{ rules } // For string literals, we need to pass the raw value which includes escape characters.
72-
);
73-
if (unsupportedGroups.length > 0) {
93+
// For string literals, we need to pass the raw value which includes escape characters.
94+
input = node.raw;
95+
} else if (isRegExpLiteral(node)) {
96+
input = node.regex.pattern;
97+
}
98+
if (input) {
99+
const unsupportedGroups = analyzeRegExpForLookaheadAndLookbehind(input, rules);
100+
if (unsupportedGroups.length === 0) return;
101+
if (!browserslist) {
74102
createContextReport(node, context, unsupportedGroups, unsupportedTargets);
103+
return;
75104
}
76-
} else if (isRegExpLiteral(node)) {
77-
const unsupportedGroups = analyzeRegExpForLookaheadAndLookbehind(node.regex.pattern, {
78-
rules,
79-
});
80-
if (unsupportedGroups.length > 0) {
105+
if (unsupportedGroups.some((group) => group.type === "lookbehind")) {
81106
createContextReport(node, context, unsupportedGroups, unsupportedTargets);
82107
}
83108
}

0 commit comments

Comments
 (0)