Skip to content

Commit ee71626

Browse files
authored
feat: added support for regexp literal (#644)
1 parent 8682159 commit ee71626

7 files changed

Lines changed: 91 additions & 21 deletions

File tree

src/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,4 +54,5 @@ export enum AstNodeTypes {
5454
MemberExpression = "MemberExpression",
5555
CallExpression = "CallExpression",
5656
NewExpression = "NewExpression",
57+
Literal = "Literal",
5758
}

src/helpers.ts

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
/* eslint no-nested-ternary: off */
22
import browserslist from "browserslist";
3+
import { AstNodeTypes, TargetNameMappings } from "./constants";
34
import {
45
AstMetadataApiWithTargetsResolver,
6+
BrowserListConfig,
7+
BrowsersListOpts,
8+
Context,
59
ESLintNode,
10+
HandleFailingRule,
611
SourceCode,
7-
BrowserListConfig,
812
Target,
9-
HandleFailingRule,
10-
Context,
11-
BrowsersListOpts,
1213
} from "./types";
13-
import { TargetNameMappings } from "./constants";
1414

1515
/*
1616
3) Figures out which browsers user is targeting
@@ -109,8 +109,31 @@ export function lintExpressionStatement(
109109
);
110110
}
111111

112+
function checkRegexpLiteral(node: ESLintNode): boolean {
113+
return (
114+
node.type === AstNodeTypes.Literal &&
115+
(!!node.regex || node.parent?.callee?.name === "RegExp")
116+
);
117+
}
118+
119+
export function lintLiteral(
120+
context: Context,
121+
handleFailingRule: HandleFailingRule,
122+
rules: AstMetadataApiWithTargetsResolver[],
123+
sourceCode: SourceCode,
124+
node: ESLintNode
125+
) {
126+
const isRegexpLiteral = checkRegexpLiteral(node);
127+
const failingRule = rules.find((rule) =>
128+
rule.syntaxes?.some(
129+
(syntax) => (isRegexpLiteral ? node.raw.includes(syntax) : false) // non-regexp literals are not supported yet
130+
)
131+
);
132+
if (failingRule) handleFailingRule(failingRule, node);
133+
}
134+
112135
function isStringLiteral(node: ESLintNode): boolean {
113-
return node.type === "Literal" && typeof node.value === "string";
136+
return node.type === AstNodeTypes.Literal && typeof node.value === "string";
114137
}
115138

116139
function protoChainFromMemberExpression(node: ESLintNode): string[] {

src/index.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@
66
//------------------------------------------------------------------------------
77
// Requirements
88
//------------------------------------------------------------------------------
9-
import recommended from "./config/recommended";
10-
import pkg from "../package.json";
119
import type { Linter } from "eslint";
10+
import pkg from "../package.json";
11+
import recommended from "./config/recommended";
1212

1313
//------------------------------------------------------------------------------
1414
// Plugin Definition
@@ -32,7 +32,7 @@ const plugin = {
3232

3333
const configs = {
3434
"flat/recommended": {
35-
name: 'compat/flat/recommended',
35+
name: "compat/flat/recommended",
3636
plugins: { compat: plugin },
3737
...recommended.flat,
3838
} as Linter.FlatConfig,

src/providers/caniuse-provider.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as lite from "caniuse-lite";
2-
import { STANDARD_TARGET_NAME_MAPPING, AstNodeTypes } from "../constants";
2+
import { AstNodeTypes, STANDARD_TARGET_NAME_MAPPING } from "../constants";
33
import { AstMetadataApiWithTargetsResolver, Target } from "../types";
44

55
/**
@@ -241,6 +241,13 @@ const CanIUseProvider: Array<AstMetadataApiWithTargetsResolver> = [
241241
astNodeType: AstNodeTypes.NewExpression,
242242
object: "Float64Array",
243243
},
244+
{
245+
caniuseId: "js-regexp-lookbehind",
246+
astNodeType: AstNodeTypes.Literal,
247+
name: "Lookbehind",
248+
object: "RegExp",
249+
syntaxes: ["?<=", "?<!"],
250+
},
244251
].map((rule) => ({
245252
...rule,
246253
getUnsupportedTargets,

src/rules/compat.ts

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,27 +5,28 @@
55
* Tells eslint to lint certain nodes (lintCallExpression, lintMemberExpression, lintNewExpression)
66
* Gets protochain for the ESLint nodes the plugin is interested in
77
*/
8-
import fs from "fs";
8+
import { Rule } from "eslint";
99
import findUp from "find-up";
10+
import fs from "fs";
1011
import memoize from "lodash.memoize";
11-
import { Rule } from "eslint";
1212
import {
13+
determineTargetsFromConfig,
1314
lintCallExpression,
15+
lintExpressionStatement,
16+
lintLiteral,
1417
lintMemberExpression,
1518
lintNewExpression,
16-
lintExpressionStatement,
1719
parseBrowsersListVersion,
18-
determineTargetsFromConfig,
1920
} from "../helpers"; // will be deprecated and introduced to this file
21+
import { nodes } from "../providers";
2022
import {
21-
ESLintNode,
2223
AstMetadataApiWithTargetsResolver,
2324
BrowserListConfig,
24-
HandleFailingRule,
25-
Context,
2625
BrowsersListOpts,
26+
Context,
27+
ESLintNode,
28+
HandleFailingRule,
2729
} from "../types";
28-
import { nodes } from "../providers";
2930

3031
type ESLint = {
3132
[astNodeTypeName: string]: (node: ESLintNode) => void;
@@ -45,6 +46,9 @@ function getName(node: ESLintNode): string {
4546
case "CallExpression": {
4647
return node.callee!.name;
4748
}
49+
case "Literal": {
50+
return node.type;
51+
}
4852
default:
4953
throw new Error("not found");
5054
}
@@ -110,6 +114,7 @@ type RulesFilteredByTargets = {
110114
NewExpression: AstMetadataApiWithTargetsResolver[];
111115
MemberExpression: AstMetadataApiWithTargetsResolver[];
112116
ExpressionStatement: AstMetadataApiWithTargetsResolver[];
117+
Literal: AstMetadataApiWithTargetsResolver[];
113118
};
114119

115120
/**
@@ -124,6 +129,7 @@ const getRulesForTargets = memoize(
124129
NewExpression: [] as AstMetadataApiWithTargetsResolver[],
125130
MemberExpression: [] as AstMetadataApiWithTargetsResolver[],
126131
ExpressionStatement: [] as AstMetadataApiWithTargetsResolver[],
132+
Literal: [] as AstMetadataApiWithTargetsResolver[],
127133
};
128134
const targets = JSON.parse(targetsJSON);
129135

@@ -248,6 +254,13 @@ export default {
248254
],
249255
sourceCode
250256
),
257+
Literal: lintLiteral.bind(
258+
null,
259+
context,
260+
handleFailingRule,
261+
targetedRules.Literal,
262+
sourceCode
263+
),
251264
// Keep track of all the defined variables. Do not report errors for nodes that are not defined
252265
Identifier(node: ESLintNode) {
253266
if (node.parent) {

src/types.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { APIKind } from "ast-metadata-inferer/lib/types";
2+
import type { Options as DefaultBrowsersListOpts } from "browserslist";
23
import { Rule } from "eslint";
34
import { TargetNameMappings } from "./constants";
4-
import type { Options as DefaultBrowsersListOpts } from "browserslist";
55

66
export type BrowserListConfig =
77
| string
@@ -18,8 +18,13 @@ type AstMetadataApi = {
1818
type?: string;
1919
name?: string;
2020
object: string;
21-
astNodeType: "MemberExpression" | "CallExpression" | "NewExpression";
21+
astNodeType:
22+
| "MemberExpression"
23+
| "CallExpression"
24+
| "NewExpression"
25+
| "Literal";
2226
property?: string;
27+
syntaxes?: string[];
2328
protoChainId: string;
2429
protoChain: Array<string>;
2530
};
@@ -49,6 +54,11 @@ export type ESLintNode = {
4954
name: string;
5055
type?: string;
5156
};
57+
regex?: {
58+
flags: string;
59+
pattern: string;
60+
};
61+
raw: string;
5262
};
5363

5464
export type SourceCode = import("eslint").SourceCode;

test/e2e.spec.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { RuleTester } from "eslint";
2-
import rule from "../src/rules/compat";
32
import { parser } from "typescript-eslint";
3+
import rule from "../src/rules/compat";
44

55
const ruleTester = new RuleTester({
66
languageOptions: {
@@ -691,5 +691,21 @@ ruleTester.run("compat", rule, {
691691
},
692692
],
693693
},
694+
{
695+
code: "/(?<=y)x/, new RegExp('(?<!y)x'), 'x', true, false, null, 1, 0n",
696+
settings: {
697+
browsers: ["Safari >= 16.3", "iOS >= 16.3"],
698+
},
699+
errors: [
700+
{
701+
message:
702+
"Lookbehind is not supported in Safari 16.3, iOS Safari 16.3",
703+
},
704+
{
705+
message:
706+
"Lookbehind is not supported in Safari 16.3, iOS Safari 16.3",
707+
},
708+
],
709+
},
694710
],
695711
});

0 commit comments

Comments
 (0)