Repository: ota-meshi/eslint-plugin-regexp
Pull request #317 (2021-09-04 15:06:40)

Use `regexp-ast-analysis`'s caching
This is a branch of #315.

`regexp-ast-analysis` (RAA) finally implements a [caching system](https://rundevelopment.github.io/regexp-ast-analysis/docs/latest/interfaces/cache.html). Since the cache implements the `ReadonlyFlags` interface, it's really easy for us to use the cache. We just have to swap out the plain flags objects we currently use with a cache.

Right now, only RAA's `toCharSet` and `getFirst{Consumed,}Char{After,}` functions are cached. More functions may follow in the future.

**Other changes:**
Cache instances not only implement `ReadonlyFlags` (all fields optional) but even `Required<ReadonlyFlags>`. I took this chance to change the type of `flags` in `RegExpContext` to `Required<ReadonlyFlags>`. This means that we don't have to worry about flags being `undefined` anymore.

---

We added `RegExpContext.toCharSet` because RAA's `toCharSet` used to be uncached. After this PR, `RegExpContext.toCharSet` will just call RAA's `toCharSet` under the hood and do nothing more. Maybe we should remove it?


Patch:
diff --git a/lib/rules/no-useless-assertions.ts b/lib/rules/no-useless-assertions.ts
index 0418f7f..dca0308 100644
--- a/lib/rules/no-useless-assertions.ts
+++ b/lib/rules/no-useless-assertions.ts
@@ -15,6 +15,7 @@ import {
     getMatchingDirectionFromAssertionKind,
     hasSomeDescendant,
     isPotentiallyEmpty,
+    FirstConsumedChars,
 } from "regexp-ast-analysis"
 import { mention } from "../utils/mention"
 
@@ -211,14 +212,13 @@ export default createRule("no-useless-assertions", {
                     return
                 }
 
-                const firstOf = getFirstConsumedChar(
-                    assertion.alternatives,
-                    direction,
-                    flags,
+                const firstOf = FirstConsumedChars.toLook(
+                    getFirstConsumedChar(
+                        assertion.alternatives,
+                        direction,
+                        flags,
+                    ),
                 )
-                if (firstOf.empty) {
-                    return
-                }
 
                 // the idea here is that a negate lookaround accepts when non-negated version reject and vise versa.
                 const accept = assertion.negate ? "reject" : "accept"
diff --git a/lib/utils/index.ts b/lib/utils/index.ts
index 18deafd..5ca48fc 100644
--- a/lib/utils/index.ts
+++ b/lib/utils/index.ts
@@ -8,7 +8,7 @@ import type { Rule, AST, SourceCode } from "eslint"
 import { getStringIfConstant } from "./ast-utils"
 import type { ReadonlyFlags, ToCharSetElement } from "regexp-ast-analysis"
 // eslint-disable-next-line no-restricted-imports -- Implement RegExpContext#toCharSet
-import { toCharSet } from "regexp-ast-analysis"
+import { toCharSet, toCache } from "regexp-ast-analysis"
 import type { CharSet } from "refa"
 import { JS } from "refa"
 import type { UsageOfPattern } from "./get-usage-of-pattern"
@@ -89,7 +89,7 @@ type RegExpContextBase = {
     patternAst: Pattern
     patternSource: PatternSource
 
-    flags: ReadonlyFlags
+    flags: Required<ReadonlyFlags>
 }
 export type RegExpContextForLiteral = {
     node: ESTree.RegExpLiteral
@@ -109,7 +109,7 @@ type UnparsableRegExpContextBase = {
     node: ESTree.Expression
     regexpNode: ESTree.RegExpLiteral | ESTree.CallExpression
 
-    flags: ReadonlyFlags
+    flags: Required<ReadonlyFlags>
     flagsString: string | null
     ownsFlags: boolean
 
@@ -578,7 +578,7 @@ function buildRegExpContextBase({
     regexpNode,
     flagsNode,
     context,
-    flags,
+    flags: originalFlags,
     parsedPattern,
 }: {
     patternSource: PatternSource
@@ -589,8 +589,8 @@ function buildRegExpContextBase({
     parsedPattern: Pattern
 }): RegExpContextBase {
     const sourceCode = context.getSourceCode()
+    const flags = toCache(originalFlags)
 
-    const cacheCharSet = new WeakMap<ToCharSetElement, CharSet>()
     let cacheUsageOfPattern: UsageOfPattern | null = null
     return {
         toCharSet: (node, optionFlags) => {
@@ -599,13 +599,7 @@ function buildRegExpContextBase({
                 return toCharSet(node, optionFlags)
             }
 
-            let charSet = cacheCharSet.get(node)
-            if (charSet) {
-                return charSet
-            }
-            charSet = toCharSet(node, flags)
-            cacheCharSet.set(node, charSet)
-            return charSet
+            return toCharSet(node, flags)
         },
         getRegexpLocation: (range, offsets) => {
             if (offsets) {
@@ -652,7 +646,7 @@ function buildUnparsableRegExpContextBase({
     patternNode,
     regexpNode,
     context,
-    flags,
+    flags: originalFlags,
     flagsString,
     flagsNode,
     ownsFlags,
@@ -667,6 +661,7 @@ function buildUnparsableRegExpContextBase({
     ownsFlags: boolean
 }): UnparsableRegExpContextBase {
     const sourceCode = context.getSourceCode()
+    const flags = toCache(originalFlags)
 
     return {
         regexpNode,


Test patch:
diff --git a/tests/lib/rules/no-useless-assertions.ts b/tests/lib/rules/no-useless-assertions.ts
index d1a7ba4..a511603 100644
--- a/tests/lib/rules/no-useless-assertions.ts
+++ b/tests/lib/rules/no-useless-assertions.ts
@@ -356,5 +356,9 @@ tester.run("no-useless-assertions", rule as any, {
                 },
             ],
         },
+        {
+            code: String.raw`/(?=a|$)b/u`,
+            errors: ["The lookahead '(?=a|$)' will always reject."],
+        },
     ],
 })