From 8b588c91a60c1415e764387c050c3e64de771c04 Mon Sep 17 00:00:00 2001 From: Nikolay Latyshev Date: Sun, 19 Jan 2025 20:02:26 +1100 Subject: [PATCH] feat: added support for regexp literal --- src/constants.ts | 1 + src/helpers.ts | 35 +++++++++++++++++++++++++------ src/index.ts | 6 +++--- src/providers/caniuse-provider.ts | 9 +++++++- src/rules/compat.ts | 29 ++++++++++++++++++------- src/types.ts | 14 +++++++++++-- test/e2e.spec.ts | 18 +++++++++++++++- 7 files changed, 91 insertions(+), 21 deletions(-) diff --git a/src/constants.ts b/src/constants.ts index ff13313c..6ed7a19d 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -54,4 +54,5 @@ export enum AstNodeTypes { MemberExpression = "MemberExpression", CallExpression = "CallExpression", NewExpression = "NewExpression", + Literal = "Literal", } diff --git a/src/helpers.ts b/src/helpers.ts index a5bebb7f..69aa42f9 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -1,16 +1,16 @@ /* eslint no-nested-ternary: off */ import browserslist from "browserslist"; +import { AstNodeTypes, TargetNameMappings } from "./constants"; import { AstMetadataApiWithTargetsResolver, + BrowserListConfig, + BrowsersListOpts, + Context, ESLintNode, + HandleFailingRule, SourceCode, - BrowserListConfig, Target, - HandleFailingRule, - Context, - BrowsersListOpts, } from "./types"; -import { TargetNameMappings } from "./constants"; /* 3) Figures out which browsers user is targeting @@ -109,8 +109,31 @@ export function lintExpressionStatement( ); } +function checkRegexpLiteral(node: ESLintNode): boolean { + return ( + node.type === AstNodeTypes.Literal && + (!!node.regex || node.parent?.callee?.name === "RegExp") + ); +} + +export function lintLiteral( + context: Context, + handleFailingRule: HandleFailingRule, + rules: AstMetadataApiWithTargetsResolver[], + sourceCode: SourceCode, + node: ESLintNode +) { + const isRegexpLiteral = checkRegexpLiteral(node); + const failingRule = rules.find((rule) => + rule.syntaxes?.some( + (syntax) => (isRegexpLiteral ? node.raw.includes(syntax) : false) // non-regexp literals are not supported yet + ) + ); + if (failingRule) handleFailingRule(failingRule, node); +} + function isStringLiteral(node: ESLintNode): boolean { - return node.type === "Literal" && typeof node.value === "string"; + return node.type === AstNodeTypes.Literal && typeof node.value === "string"; } function protoChainFromMemberExpression(node: ESLintNode): string[] { diff --git a/src/index.ts b/src/index.ts index db100212..bbe1b6b2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,9 +6,9 @@ //------------------------------------------------------------------------------ // Requirements //------------------------------------------------------------------------------ -import recommended from "./config/recommended"; -import pkg from "../package.json"; import type { Linter } from "eslint"; +import pkg from "../package.json"; +import recommended from "./config/recommended"; //------------------------------------------------------------------------------ // Plugin Definition @@ -32,7 +32,7 @@ const plugin = { const configs = { "flat/recommended": { - name: 'compat/flat/recommended', + name: "compat/flat/recommended", plugins: { compat: plugin }, ...recommended.flat, } as Linter.FlatConfig, diff --git a/src/providers/caniuse-provider.ts b/src/providers/caniuse-provider.ts index bc4360a9..07dcd829 100644 --- a/src/providers/caniuse-provider.ts +++ b/src/providers/caniuse-provider.ts @@ -1,5 +1,5 @@ import * as lite from "caniuse-lite"; -import { STANDARD_TARGET_NAME_MAPPING, AstNodeTypes } from "../constants"; +import { AstNodeTypes, STANDARD_TARGET_NAME_MAPPING } from "../constants"; import { AstMetadataApiWithTargetsResolver, Target } from "../types"; /** @@ -241,6 +241,13 @@ const CanIUseProvider: Array = [ astNodeType: AstNodeTypes.NewExpression, object: "Float64Array", }, + { + caniuseId: "js-regexp-lookbehind", + astNodeType: AstNodeTypes.Literal, + name: "Lookbehind", + object: "RegExp", + syntaxes: ["?<=", "? ({ ...rule, getUnsupportedTargets, diff --git a/src/rules/compat.ts b/src/rules/compat.ts index f4a0fda8..07a9bc79 100644 --- a/src/rules/compat.ts +++ b/src/rules/compat.ts @@ -5,27 +5,28 @@ * Tells eslint to lint certain nodes (lintCallExpression, lintMemberExpression, lintNewExpression) * Gets protochain for the ESLint nodes the plugin is interested in */ -import fs from "fs"; +import { Rule } from "eslint"; import findUp from "find-up"; +import fs from "fs"; import memoize from "lodash.memoize"; -import { Rule } from "eslint"; import { + determineTargetsFromConfig, lintCallExpression, + lintExpressionStatement, + lintLiteral, lintMemberExpression, lintNewExpression, - lintExpressionStatement, parseBrowsersListVersion, - determineTargetsFromConfig, } from "../helpers"; // will be deprecated and introduced to this file +import { nodes } from "../providers"; import { - ESLintNode, AstMetadataApiWithTargetsResolver, BrowserListConfig, - HandleFailingRule, - Context, BrowsersListOpts, + Context, + ESLintNode, + HandleFailingRule, } from "../types"; -import { nodes } from "../providers"; type ESLint = { [astNodeTypeName: string]: (node: ESLintNode) => void; @@ -45,6 +46,9 @@ function getName(node: ESLintNode): string { case "CallExpression": { return node.callee!.name; } + case "Literal": { + return node.type; + } default: throw new Error("not found"); } @@ -110,6 +114,7 @@ type RulesFilteredByTargets = { NewExpression: AstMetadataApiWithTargetsResolver[]; MemberExpression: AstMetadataApiWithTargetsResolver[]; ExpressionStatement: AstMetadataApiWithTargetsResolver[]; + Literal: AstMetadataApiWithTargetsResolver[]; }; /** @@ -124,6 +129,7 @@ const getRulesForTargets = memoize( NewExpression: [] as AstMetadataApiWithTargetsResolver[], MemberExpression: [] as AstMetadataApiWithTargetsResolver[], ExpressionStatement: [] as AstMetadataApiWithTargetsResolver[], + Literal: [] as AstMetadataApiWithTargetsResolver[], }; const targets = JSON.parse(targetsJSON); @@ -248,6 +254,13 @@ export default { ], sourceCode ), + Literal: lintLiteral.bind( + null, + context, + handleFailingRule, + targetedRules.Literal, + sourceCode + ), // Keep track of all the defined variables. Do not report errors for nodes that are not defined Identifier(node: ESLintNode) { if (node.parent) { diff --git a/src/types.ts b/src/types.ts index ad1f341e..08a16fe8 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,7 +1,7 @@ import { APIKind } from "ast-metadata-inferer/lib/types"; +import type { Options as DefaultBrowsersListOpts } from "browserslist"; import { Rule } from "eslint"; import { TargetNameMappings } from "./constants"; -import type { Options as DefaultBrowsersListOpts } from "browserslist"; export type BrowserListConfig = | string @@ -18,8 +18,13 @@ type AstMetadataApi = { type?: string; name?: string; object: string; - astNodeType: "MemberExpression" | "CallExpression" | "NewExpression"; + astNodeType: + | "MemberExpression" + | "CallExpression" + | "NewExpression" + | "Literal"; property?: string; + syntaxes?: string[]; protoChainId: string; protoChain: Array; }; @@ -49,6 +54,11 @@ export type ESLintNode = { name: string; type?: string; }; + regex?: { + flags: string; + pattern: string; + }; + raw: string; }; export type SourceCode = import("eslint").SourceCode; diff --git a/test/e2e.spec.ts b/test/e2e.spec.ts index 7e779f4e..daba0971 100644 --- a/test/e2e.spec.ts +++ b/test/e2e.spec.ts @@ -1,6 +1,6 @@ import { RuleTester } from "eslint"; -import rule from "../src/rules/compat"; import { parser } from "typescript-eslint"; +import rule from "../src/rules/compat"; const ruleTester = new RuleTester({ languageOptions: { @@ -691,5 +691,21 @@ ruleTester.run("compat", rule, { }, ], }, + { + code: "/(?<=y)x/, new RegExp('(?= 16.3", "iOS >= 16.3"], + }, + errors: [ + { + message: + "Lookbehind is not supported in Safari 16.3, iOS Safari 16.3", + }, + { + message: + "Lookbehind is not supported in Safari 16.3, iOS Safari 16.3", + }, + ], + }, ], });