diff --git a/.changeset/cyan-waves-notice.md b/.changeset/cyan-waves-notice.md new file mode 100644 index 000000000..35e7d5f07 --- /dev/null +++ b/.changeset/cyan-waves-notice.md @@ -0,0 +1,5 @@ +--- +"eslint-plugin-regexp": minor +--- + +Update refa, regexp-ast-analysis, and scslre diff --git a/lib/rules/confusing-quantifier.ts b/lib/rules/confusing-quantifier.ts index e769725e5..ce108e7ac 100644 --- a/lib/rules/confusing-quantifier.ts +++ b/lib/rules/confusing-quantifier.ts @@ -29,11 +29,15 @@ export default createRule("confusing-quantifier", { */ function createVisitor({ node, + flags, getRegexpLocation, }: RegExpContext): RegExpVisitor.Handlers { return { onQuantifierEnter(qNode) { - if (qNode.min > 0 && isPotentiallyEmpty(qNode.element)) { + if ( + qNode.min > 0 && + isPotentiallyEmpty(qNode.element, flags) + ) { const proposal = quantToString({ ...qNode, min: 0 }) context.report({ node, diff --git a/lib/rules/negation.ts b/lib/rules/negation.ts index e779c127e..7c3f7bbc6 100644 --- a/lib/rules/negation.ts +++ b/lib/rules/negation.ts @@ -53,13 +53,15 @@ export default createRule("negation", { // All other character sets are either case-invariant // (/./, /\s/, /\d/) or inconsistent (/\w/). + // FIXME: TS Error + // @ts-expect-error -- FIXME const ccSet = toCharSet(ccNode, flags) const negatedElementSet = toCharSet( + // FIXME: TS Error + // @ts-expect-error -- FIXME { ...element, - // FIXME: TS Error - // @ts-expect-error -- FIXME negate: !element.negate, }, flags, diff --git a/lib/rules/no-contradiction-with-assertion.ts b/lib/rules/no-contradiction-with-assertion.ts index 55f25fc65..a65e28236 100644 --- a/lib/rules/no-contradiction-with-assertion.ts +++ b/lib/rules/no-contradiction-with-assertion.ts @@ -43,7 +43,7 @@ function isTrivialAssertion( } if (assertion.kind === "lookahead" || assertion.kind === "lookbehind") { - if (isPotentiallyEmpty(assertion.alternatives)) { + if (isPotentiallyEmpty(assertion.alternatives, flags)) { // The assertion is guaranteed to trivially accept/reject. return true } @@ -80,6 +80,7 @@ function isTrivialAssertion( function* getNextElements( start: Element, dir: MatchingDirection, + flags: ReadonlyFlags, ): Iterable { let element = start @@ -110,7 +111,7 @@ function* getNextElements( for (let i = index + inc; i >= 0 && i < elements.length; i += inc) { const e = elements[i] yield e - if (!isZeroLength(e)) { + if (!isZeroLength(e, flags)) { return } } @@ -142,6 +143,7 @@ function tryFindContradictionIn( element: Element, dir: MatchingDirection, condition: (e: Element | Alternative) => boolean, + flags: ReadonlyFlags, ): boolean { if (condition(element)) { return true @@ -151,7 +153,7 @@ function tryFindContradictionIn( // Go into the alternatives of groups let some = false element.alternatives.forEach((a) => { - if (tryFindContradictionInAlternative(a, dir, condition)) { + if (tryFindContradictionInAlternative(a, dir, condition, flags)) { some = true } }) @@ -160,7 +162,7 @@ function tryFindContradictionIn( if (element.type === "Quantifier" && element.max === 1) { // Go into the element of quantifiers if their maximum is 1 - return tryFindContradictionIn(element.element, dir, condition) + return tryFindContradictionIn(element.element, dir, condition, flags) } if ( @@ -173,7 +175,7 @@ function tryFindContradictionIn( // Since we don't consume characters, we want to keep going even if we // find a contradiction inside the lookaround. element.alternatives.forEach((a) => - tryFindContradictionInAlternative(a, dir, condition), + tryFindContradictionInAlternative(a, dir, condition, flags), ) } @@ -188,6 +190,7 @@ function tryFindContradictionInAlternative( alternative: Alternative, dir: MatchingDirection, condition: (e: Element | Alternative) => boolean, + flags: ReadonlyFlags, ): boolean { if (condition(alternative)) { return true @@ -199,10 +202,10 @@ function tryFindContradictionInAlternative( const inc = dir === "ltr" ? 1 : -1 for (let i = first; i >= 0 && i < elements.length; i += inc) { const e = elements[i] - if (tryFindContradictionIn(e, dir, condition)) { + if (tryFindContradictionIn(e, dir, condition, flags)) { return true } - if (!isZeroLength(e)) { + if (!isZeroLength(e, flags)) { break } } @@ -271,8 +274,10 @@ export default createRule("no-contradiction-with-assertion", { getFirstConsumedChar(assertion, dir, flags), ) - for (const element of getNextElements(assertion, dir)) { - if (tryFindContradictionIn(element, dir, contradicts)) { + for (const element of getNextElements(assertion, dir, flags)) { + if ( + tryFindContradictionIn(element, dir, contradicts, flags) + ) { break } } diff --git a/lib/rules/no-dupe-characters-character-class.ts b/lib/rules/no-dupe-characters-character-class.ts index fccca5013..67a908e41 100644 --- a/lib/rules/no-dupe-characters-character-class.ts +++ b/lib/rules/no-dupe-characters-character-class.ts @@ -15,6 +15,7 @@ import { defineRegexpVisitor, toCharSetSource, fixRemoveCharacterClassElement, + assertValidFlags, } from "../utils" import type { CharRange, CharSet } from "refa" import { JS } from "refa" @@ -276,6 +277,8 @@ export default createRule("no-dupe-characters-character-class", { // report characters that are already matched by some range or set for (const char of characters) { for (const other of rangesAndSets) { + // FIXME: TS Error + // @ts-expect-error -- FIXME if (toCharSet(other, flags).has(char.value)) { reportSubset(regexpContext, char, other) subsets.add(char) @@ -292,7 +295,11 @@ export default createRule("no-dupe-characters-character-class", { } if ( + // FIXME: TS Error + // @ts-expect-error -- FIXME toCharSet(element, flags).isSubsetOf( + // FIXME: TS Error + // @ts-expect-error -- FIXME toCharSet(other, flags), ) ) { @@ -317,9 +324,13 @@ export default createRule("no-dupe-characters-character-class", { const totalOthers = characterTotal.union( ...rangesAndSets .filter((e) => !subsets.has(e) && e !== element) + // FIXME: TS Error + // @ts-expect-error -- FIXME .map((e) => toCharSet(e, flags)), ) + // FIXME: TS Error + // @ts-expect-error -- FIXME const elementCharSet = toCharSet(element, flags) if (elementCharSet.isSubsetOf(totalOthers)) { const superSetElements = ccNode.elements @@ -359,6 +370,8 @@ export default createRule("no-dupe-characters-character-class", { const intersection = toCharSet( range, flags, + // FIXME: TS Error + // @ts-expect-error -- FIXME ).intersect(toCharSet(other, flags)) if (intersection.isEmpty) { continue @@ -379,6 +392,7 @@ export default createRule("no-dupe-characters-character-class", { // (see GH #189). // to prevent this, we will create a new CharSet // using `createCharSet` + assertValidFlags(flags) const interest = JS.createCharSet( interestingRanges, flags, diff --git a/lib/rules/no-dupe-disjunctions.ts b/lib/rules/no-dupe-disjunctions.ts index 020fc9340..97fb76cf7 100644 --- a/lib/rules/no-dupe-disjunctions.ts +++ b/lib/rules/no-dupe-disjunctions.ts @@ -14,6 +14,7 @@ import { defineRegexpVisitor, fixRemoveCharacterClassElement, fixRemoveAlternative, + assertValidFlags, } from "../utils" import { getParser, isCoveredNode, isEqualNodes } from "../utils/regexp-ast" import type { Expression, FiniteAutomaton, NoParent, ReadonlyNFA } from "refa" @@ -434,6 +435,7 @@ function getPartialSubsetRelation( */ function faToSource(fa: FiniteAutomaton, flags: ReadonlyFlags): string { try { + assertValidFlags(flags) return JS.toLiteral(fa.toRegex(), { flags }).source } catch (_error) { return "" diff --git a/lib/rules/no-empty-capturing-group.ts b/lib/rules/no-empty-capturing-group.ts index 80f1ccee8..8e1d8cc7a 100644 --- a/lib/rules/no-empty-capturing-group.ts +++ b/lib/rules/no-empty-capturing-group.ts @@ -22,11 +22,12 @@ export default createRule("no-empty-capturing-group", { */ function createVisitor({ node, + flags, getRegexpLocation, }: RegExpContext): RegExpVisitor.Handlers { return { onCapturingGroupEnter(cgNode) { - if (isZeroLength(cgNode)) { + if (isZeroLength(cgNode, flags)) { context.report({ node, loc: getRegexpLocation(cgNode), diff --git a/lib/rules/no-empty-lookarounds-assertion.ts b/lib/rules/no-empty-lookarounds-assertion.ts index 705fa321d..91305b780 100644 --- a/lib/rules/no-empty-lookarounds-assertion.ts +++ b/lib/rules/no-empty-lookarounds-assertion.ts @@ -24,6 +24,7 @@ export default createRule("no-empty-lookarounds-assertion", { */ function createVisitor({ node, + flags, getRegexpLocation, }: RegExpContext): RegExpVisitor.Handlers { return { @@ -35,7 +36,7 @@ export default createRule("no-empty-lookarounds-assertion", { return } - if (isPotentiallyEmpty(aNode.alternatives)) { + if (isPotentiallyEmpty(aNode.alternatives, flags)) { context.report({ node, loc: getRegexpLocation(aNode), diff --git a/lib/rules/no-misleading-capturing-group.ts b/lib/rules/no-misleading-capturing-group.ts index 8be3768cd..ab8c1d770 100644 --- a/lib/rules/no-misleading-capturing-group.ts +++ b/lib/rules/no-misleading-capturing-group.ts @@ -1,3 +1,5 @@ +/* eslint-disable eslint-comments/disable-enable-pair -- x */ +/* eslint-disable complexity -- x */ import type { RegExpVisitor } from "@eslint-community/regexpp/visitor" import type { Alternative, @@ -48,10 +50,11 @@ function* iterReverse(array: readonly T[]): Iterable { function* getStartQuantifiers( root: Element | Alternative | Alternative[], direction: MatchingDirection, + flags: ReadonlyFlags, ): Iterable { if (Array.isArray(root)) { for (const a of root) { - yield* getStartQuantifiers(a, direction) + yield* getStartQuantifiers(a, direction, flags) } return } @@ -60,6 +63,7 @@ function* getStartQuantifiers( case "Character": case "CharacterClass": case "CharacterSet": + case "ExpressionCharacterClass": case "Backreference": // we can't go into terminals break @@ -71,8 +75,8 @@ function* getStartQuantifiers( const elements = direction === "ltr" ? root.elements : iterReverse(root.elements) for (const e of elements) { - if (isEmpty(e)) continue - yield* getStartQuantifiers(e, direction) + if (isEmpty(e, flags)) continue + yield* getStartQuantifiers(e, direction, flags) break } break @@ -82,17 +86,15 @@ function* getStartQuantifiers( // of this rule break case "Group": - yield* getStartQuantifiers(root.alternatives, direction) + yield* getStartQuantifiers(root.alternatives, direction, flags) break case "Quantifier": yield root if (root.max === 1) { - yield* getStartQuantifiers(root.element, direction) + yield* getStartQuantifiers(root.element, direction, flags) } break default: - // FIXME: TS Error - // @ts-expect-error -- FIXME yield assertNever(root) } } @@ -168,6 +170,9 @@ function uncachedGetSingleRepeatedChar( case "Character": case "CharacterClass": case "CharacterSet": + case "ExpressionCharacterClass": + // FIXME: TS Error + // @ts-expect-error -- FIXME return toCharSet(element, flags) case "CapturingGroup": @@ -181,8 +186,6 @@ function uncachedGetSingleRepeatedChar( return getSingleRepeatedChar(element.element, flags, cache) default: - // FIXME: TS Error - // @ts-expect-error -- FIXME return assertNever(element) } } @@ -226,6 +229,7 @@ function getTradingQuantifiersAfter( case "Character": case "CharacterClass": case "CharacterSet": + case "ExpressionCharacterClass": return state.intersect( getSingleRepeatedChar(element, flags), ) @@ -236,8 +240,6 @@ function getTradingQuantifiersAfter( return state default: - // FIXME: TS Error - // @ts-expect-error -- FIXME return assertNever(element) } }, @@ -329,6 +331,7 @@ export default createRule("no-misleading-capturing-group", { const startQuantifiers = getStartQuantifiers( capturingGroup.alternatives, direction, + flags, ) for (const quantifier of startQuantifiers) { @@ -409,6 +412,7 @@ export default createRule("no-misleading-capturing-group", { const endQuantifiers = getStartQuantifiers( capturingGroup.alternatives, invertMatchingDirection(direction), + flags, ) for (const quantifier of endQuantifiers) { @@ -439,7 +443,10 @@ export default createRule("no-misleading-capturing-group", { } if ( trader.quant.min >= 1 && - !isPotentiallyZeroLength(trader.quant.element) + !isPotentiallyZeroLength( + trader.quant.element, + flags, + ) ) context.report({ node, diff --git a/lib/rules/no-optional-assertion.ts b/lib/rules/no-optional-assertion.ts index b77938684..54ad3ec40 100644 --- a/lib/rules/no-optional-assertion.ts +++ b/lib/rules/no-optional-assertion.ts @@ -8,6 +8,7 @@ import type { } from "@eslint-community/regexpp/ast" import type { RegExpContext } from "../utils" import { createRule, defineRegexpVisitor } from "../utils" +import type { ReadonlyFlags } from "regexp-ast-analysis" import { isZeroLength } from "regexp-ast-analysis" type ZeroQuantifier = Quantifier & { min: 0 } @@ -26,7 +27,11 @@ function isZeroQuantifier(node: Quantifier): node is ZeroQuantifier { * consume characters. For more information and examples on optional assertions, see the documentation page of this * rule. */ -function isOptional(assertion: Assertion, quantifier: ZeroQuantifier): boolean { +function isOptional( + assertion: Assertion, + quantifier: ZeroQuantifier, + flags: ReadonlyFlags, +): boolean { let element: Assertion | Quantifier | Group | CapturingGroup = assertion while (element.parent !== quantifier) { const parent: Quantifier | Alternative = element.parent @@ -37,7 +42,7 @@ function isOptional(assertion: Assertion, quantifier: ZeroQuantifier): boolean { continue // we will ignore this element. } - if (!isZeroLength(e)) { + if (!isZeroLength(e, flags)) { // Some element around our target element can possibly consume characters. // This means, we found a path from or to the assertion which can consume characters. return false @@ -52,7 +57,7 @@ function isOptional(assertion: Assertion, quantifier: ZeroQuantifier): boolean { element = parent.parent } else { // parent.type === "Quantifier" - if (parent.max > 1 && !isZeroLength(parent)) { + if (parent.max > 1 && !isZeroLength(parent, flags)) { // If an ascendant quantifier of the element has maximum of 2 or more, we have to check whether // the quantifier itself has zero length. // E.g. in /(?:a|(\b|-){2})?/ the \b is not optional @@ -87,6 +92,7 @@ export default createRule("no-optional-assertion", { */ function createVisitor({ node, + flags, getRegexpLocation, }: RegExpContext): RegExpVisitor.Handlers { // The closest quantifier with a minimum of 0 is stored at index = 0. @@ -105,7 +111,7 @@ export default createRule("no-optional-assertion", { onAssertionEnter(assertion) { const q = zeroQuantifierStack[0] - if (q && isOptional(assertion, q)) { + if (q && isOptional(assertion, q, flags)) { context.report({ node, loc: getRegexpLocation(assertion), diff --git a/lib/rules/no-potentially-useless-backreference.ts b/lib/rules/no-potentially-useless-backreference.ts index 25c7225c6..2758bda10 100644 --- a/lib/rules/no-potentially-useless-backreference.ts +++ b/lib/rules/no-potentially-useless-backreference.ts @@ -28,11 +28,12 @@ export default createRule("no-potentially-useless-backreference", { */ function createVisitor({ node, + flags, getRegexpLocation, }: RegExpContext): RegExpVisitor.Handlers { return { onBackreferenceEnter(backreference) { - if (isEmptyBackreference(backreference)) { + if (isEmptyBackreference(backreference, flags)) { // handled by regexp/no-useless-backreference return } diff --git a/lib/rules/no-super-linear-move.ts b/lib/rules/no-super-linear-move.ts index 5c71f65ec..b0567d36f 100644 --- a/lib/rules/no-super-linear-move.ts +++ b/lib/rules/no-super-linear-move.ts @@ -8,7 +8,7 @@ import type { RegExpContext } from "../utils" import { createRule, defineRegexpVisitor } from "../utils" import { UsageOfPattern } from "../utils/get-usage-of-pattern" import { analyse } from "scslre" -import type { Descendant } from "regexp-ast-analysis" +import type { Descendant, ReadonlyFlags } from "regexp-ast-analysis" import { isPotentiallyEmpty, getMatchingDirection, @@ -52,13 +52,14 @@ function dedupeReports(reports: Iterable): Report[] { */ function* findReachableQuantifiers( node: Descendant | Alternative, + flags: ReadonlyFlags, ): Iterable { switch (node.type) { case "CapturingGroup": case "Group": case "Pattern": { for (const a of node.alternatives) { - yield* findReachableQuantifiers(a) + yield* findReachableQuantifiers(a, flags) } break } @@ -66,7 +67,7 @@ function* findReachableQuantifiers( case "Assertion": { if (node.kind === "lookahead" || node.kind === "lookbehind") { for (const a of node.alternatives) { - yield* findReachableQuantifiers(a) + yield* findReachableQuantifiers(a, flags) } } break @@ -84,9 +85,9 @@ function* findReachableQuantifiers( dir === "ltr" ? i : node.elements.length - 1 - i const element = node.elements[elementIndex] - yield* findReachableQuantifiers(element) + yield* findReachableQuantifiers(element, flags) - if (!isPotentiallyEmpty(element)) { + if (!isPotentiallyEmpty(element, flags)) { break } } @@ -206,7 +207,7 @@ export default createRule("no-super-linear-move", { getJSRegexppAst(regexpContext, true), ) - for (const q of findReachableQuantifiers(patternAst)) { + for (const q of findReachableQuantifiers(patternAst, flags)) { if (q.max !== Infinity) { // we are only interested in star quantifiers continue diff --git a/lib/rules/no-useless-assertions.ts b/lib/rules/no-useless-assertions.ts index ecffb3310..b78778483 100644 --- a/lib/rules/no-useless-assertions.ts +++ b/lib/rules/no-useless-assertions.ts @@ -85,7 +85,7 @@ function createReorderingGetFirstCharAfter( const start = elements.indexOf(afterThis) for (let i = start + inc; i >= 0 && i < elements.length; i += inc) { const other = elements[i] - if (!isZeroLength(other)) { + if (!isZeroLength(other, flags)) { break } if (hasForbidden(other)) { @@ -318,7 +318,7 @@ export default createRule("no-useless-assertions", { assertion: LookaroundAssertion, getFirstCharAfterFn: GetFirstCharAfter, ): void { - if (isPotentiallyEmpty(assertion.alternatives)) { + if (isPotentiallyEmpty(assertion.alternatives, flags)) { // we don't handle trivial accept/reject based on emptiness return } @@ -372,7 +372,7 @@ export default createRule("no-useless-assertions", { (d) => d !== assertion && d.type === "Assertion", ) ) { - const range = getLengthRange(assertion.alternatives) + const range = getLengthRange(assertion.alternatives, flags) // we only check the first character, so it's only correct if the assertion requires only one // character if (range.max === 1) { diff --git a/lib/rules/no-useless-backreference.ts b/lib/rules/no-useless-backreference.ts index 8a2aab546..0c7fe0321 100644 --- a/lib/rules/no-useless-backreference.ts +++ b/lib/rules/no-useless-backreference.ts @@ -7,6 +7,7 @@ import type { } from "@eslint-community/regexpp/ast" import type { RegExpContext } from "../utils" import { createRule, defineRegexpVisitor } from "../utils" +import type { ReadonlyFlags } from "regexp-ast-analysis" import { getClosestAncestor, getMatchingDirection, @@ -38,7 +39,10 @@ function hasNegatedLookaroundInBetween( * Returns the message id specifying the reason why the backreference is * useless. */ -function getUselessMessageId(backRef: Backreference): string | null { +function getUselessMessageId( + backRef: Backreference, + flags: ReadonlyFlags, +): string | null { const group = backRef.resolved const closestAncestor = getClosestAncestor(backRef, group) @@ -69,7 +73,7 @@ function getUselessMessageId(backRef: Backreference): string | null { return "backward" } - if (isZeroLength(group)) { + if (isZeroLength(group, flags)) { // if the referenced group does not consume characters, then any // backreference will trivially be replaced with the empty string return "empty" @@ -108,11 +112,12 @@ export default createRule("no-useless-backreference", { */ function createVisitor({ node, + flags, getRegexpLocation, }: RegExpContext): RegExpVisitor.Handlers { return { onBackreferenceEnter(backRef) { - const messageId = getUselessMessageId(backRef) + const messageId = getUselessMessageId(backRef, flags) if (messageId) { context.report({ diff --git a/lib/rules/no-useless-quantifier.ts b/lib/rules/no-useless-quantifier.ts index 5e1e96367..b24f4f692 100644 --- a/lib/rules/no-useless-quantifier.ts +++ b/lib/rules/no-useless-quantifier.ts @@ -35,7 +35,8 @@ export default createRule("no-useless-quantifier", { function createVisitor( regexpContext: RegExpContext, ): RegExpVisitor.Handlers { - const { node, getRegexpLocation, fixReplaceNode } = regexpContext + const { node, flags, getRegexpLocation, fixReplaceNode } = + regexpContext /** * Returns a fix that replaces the given quantifier with its @@ -83,7 +84,7 @@ export default createRule("no-useless-quantifier", { // the quantified element already accepts the empty string // e.g. (||)* - if (isEmpty(qNode.element)) { + if (isEmpty(qNode.element, flags)) { context.report({ node, loc: getRegexpLocation(qNode), @@ -99,7 +100,7 @@ export default createRule("no-useless-quantifier", { qNode.min === 0 && qNode.max === 1 && qNode.greedy && - isPotentiallyEmpty(qNode.element) + isPotentiallyEmpty(qNode.element, flags) ) { context.report({ node, @@ -112,7 +113,7 @@ export default createRule("no-useless-quantifier", { // the quantified is zero length // e.g. (\b){5} - if (qNode.min >= 1 && isZeroLength(qNode.element)) { + if (qNode.min >= 1 && isZeroLength(qNode.element, flags)) { context.report({ node, loc: getRegexpLocation(qNode), diff --git a/lib/rules/optimal-quantifier-concatenation.ts b/lib/rules/optimal-quantifier-concatenation.ts index 116bd3206..6ad9a21b6 100644 --- a/lib/rules/optimal-quantifier-concatenation.ts +++ b/lib/rules/optimal-quantifier-concatenation.ts @@ -17,8 +17,13 @@ import type { AST } from "eslint" import type { RegExpContext, Quant } from "../utils" import { createRule, defineRegexpVisitor, quantToString } from "../utils" import type { Ancestor, ReadonlyFlags } from "regexp-ast-analysis" -import { Chars, hasSomeDescendant, toCharSet } from "regexp-ast-analysis" -import { getParser, getPossiblyConsumedChar } from "../utils/regexp-ast" +import { + Chars, + hasSomeDescendant, + toCharSet, + getConsumedChars, +} from "regexp-ast-analysis" +import { getParser } from "../utils/regexp-ast" import type { CharSet } from "refa" import { joinEnglishList, mention } from "../utils/mention" import { canSimplifyQuantifier } from "../utils/regexp-ast/simplify-quantifier" @@ -73,6 +78,8 @@ function getSingleConsumedChar( case "CharacterSet": case "CharacterClass": return { + // FIXME: TS Error + // @ts-expect-error -- FIXME char: toCharSet(element, flags), complete: true, } @@ -181,10 +188,10 @@ function getQuantifiersReplacement( const rSingle = getSingleConsumedChar(right.element, flags) const lPossibleChar = lSingle.complete ? lSingle.char - : getPossiblyConsumedChar(left.element, flags).char + : getConsumedChars(left.element, flags).chars const rPossibleChar = rSingle.complete ? rSingle.char - : getPossiblyConsumedChar(right.element, flags).char + : getConsumedChars(right.element, flags).chars const greedy = left.greedy let lQuant: Readonly, rQuant: Readonly @@ -342,8 +349,8 @@ function getNestedReplacement( return null } - const nestedPossible = getPossiblyConsumedChar(nested.element, flags) - if (single.char.isSupersetOf(nestedPossible.char)) { + const nestedPossible = getConsumedChars(nested.element, flags) + if (single.char.isSupersetOf(nestedPossible.chars)) { const { min } = nested if (min === 0) { return { @@ -513,12 +520,8 @@ function getLoc( function getCapturingGroupStack(element: Element): string { let result = "" for ( - // FIXME: TS Error - // @ts-expect-error -- FIXME let p: Ancestor = element.parent; p.type !== "Pattern"; - // FIXME: TS Error - // @ts-expect-error -- FIXME p = p.parent ) { if (p.type === "CapturingGroup") { diff --git a/lib/rules/prefer-character-class.ts b/lib/rules/prefer-character-class.ts index 26356a39c..16bf94a8a 100644 --- a/lib/rules/prefer-character-class.ts +++ b/lib/rules/prefer-character-class.ts @@ -155,6 +155,8 @@ function categorizeRawAlts( isCharacter: true, alternative, element, + // FIXME: TS Error + // @ts-expect-error -- FIXME char: toCharSet(element, flags), } } @@ -219,6 +221,8 @@ function parseRawAlts( return { isCharacter: true, elements, + // FIXME: TS Error + // @ts-expect-error -- FIXME char: toCharSet(a.element, flags), raw: a.alternative.raw, } @@ -352,8 +356,12 @@ function totalIsAll( for (const a of alternatives) { if (a.isCharacter) { if (total === undefined) { + // FIXME: TS Error + // @ts-expect-error -- FIXME total = toCharSet(a.element, flags) } else { + // FIXME: TS Error + // @ts-expect-error -- FIXME total = total.union(toCharSet(a.element, flags)) } } diff --git a/lib/rules/prefer-d.ts b/lib/rules/prefer-d.ts index cdf22777d..5e151c683 100644 --- a/lib/rules/prefer-d.ts +++ b/lib/rules/prefer-d.ts @@ -71,6 +71,8 @@ export default createRule("prefer-d", { }: RegExpContext): RegExpVisitor.Handlers { return { onCharacterClassEnter(ccNode) { + // FIXME: TS Error + // @ts-expect-error -- FIXME const charSet = toCharSet(ccNode, flags) let predefined: string | undefined = undefined diff --git a/lib/rules/prefer-lookaround.ts b/lib/rules/prefer-lookaround.ts index 3892e3faf..957df82a1 100644 --- a/lib/rules/prefer-lookaround.ts +++ b/lib/rules/prefer-lookaround.ts @@ -23,14 +23,13 @@ import type { import type { Expression, Literal } from "estree" import type { Rule } from "eslint" import { mention } from "../utils/mention" -import { - getFirstConsumedCharPlusAfter, - getPossiblyConsumedChar, -} from "../utils/regexp-ast" +import { getFirstConsumedCharPlusAfter } from "../utils/regexp-ast" +import type { ReadonlyFlags } from "regexp-ast-analysis" import { getLengthRange, isZeroLength, FirstConsumedChars, + getConsumedChars, } from "regexp-ast-analysis" import type { CharSet } from "refa" @@ -122,15 +121,15 @@ function getSideEffectsWhenReplacingCapturingGroup( const result = new Set() if (start) { - const { char } = getPossiblyConsumedChar(start, flags) - if (!hasDisjoint(char, elements.slice(1))) { + const { chars } = getConsumedChars(start, flags) + if (!hasDisjoint(chars, elements.slice(1))) { result.add(SideEffect.startRef) } else { const last = elements[elements.length - 1] const lastChar = FirstConsumedChars.toLook( getFirstConsumedCharPlusAfter(last, "rtl", flags), ) - if (!lastChar.char.isDisjointWith(char)) { + if (!lastChar.char.isDisjointWith(chars)) { result.add(SideEffect.startRef) } } @@ -139,12 +138,12 @@ function getSideEffectsWhenReplacingCapturingGroup( if (end && flags.global) { const first = elements[0] if (first) { - const { char } = getPossiblyConsumedChar(end, flags) + const { chars } = getConsumedChars(end, flags) const firstChar = FirstConsumedChars.toLook( getFirstConsumedCharPlusAfter(first, "ltr", flags), ) - if (!firstChar.char.isDisjointWith(char)) { + if (!firstChar.char.isDisjointWith(chars)) { result.add(SideEffect.endRef) } } @@ -156,11 +155,11 @@ function getSideEffectsWhenReplacingCapturingGroup( function hasDisjoint(target: CharSet, targetElements: Element[]) { for (const element of targetElements) { if (isConstantLength(element)) { - const elementChars = getPossiblyConsumedChar(element, flags) - if (elementChars.char.isEmpty) { + const elementChars = getConsumedChars(element, flags) + if (elementChars.chars.isEmpty) { continue } - if (elementChars.char.isDisjointWith(target)) { + if (elementChars.chars.isDisjointWith(target)) { return true } } else { @@ -175,7 +174,7 @@ function getSideEffectsWhenReplacingCapturingGroup( /** Checks whether the given element is constant length. */ function isConstantLength(target: Element): boolean { - const range = getLengthRange(target) + const range = getLengthRange(target, flags) return range.min === range.max } } @@ -183,8 +182,9 @@ function getSideEffectsWhenReplacingCapturingGroup( /** Checks whether the given element is a capturing group of length 1 or greater. */ function isCapturingGroupAndNotZeroLength( element: Element, + flags: ReadonlyFlags, ): element is CapturingGroup { - return element.type === "CapturingGroup" && !isZeroLength(element) + return element.type === "CapturingGroup" && !isZeroLength(element, flags) } type ParsedStartPattern = { @@ -227,7 +227,10 @@ type ParsedElements = { /** * Parse the elements of the pattern. */ -function parsePatternElements(node: Pattern): ParsedElements | null { +function parsePatternElements( + node: Pattern, + flags: ReadonlyFlags, +): ParsedElements | null { if (node.alternatives.length > 1) { return null } @@ -236,11 +239,11 @@ function parsePatternElements(node: Pattern): ParsedElements | null { let start: ParsedStartPattern | null = null for (const element of elements) { - if (isZeroLength(element)) { + if (isZeroLength(element, flags)) { leadingElements.push(element) continue } - if (isCapturingGroupAndNotZeroLength(element)) { + if (isCapturingGroupAndNotZeroLength(element, flags)) { const capturingGroup = element start = { leadingElements, @@ -261,12 +264,12 @@ function parsePatternElements(node: Pattern): ParsedElements | null { let end: ParsedEndPattern | null = null const trailingElements: Element[] = [] for (const element of [...elements].reverse()) { - if (isZeroLength(element)) { + if (isZeroLength(element, flags)) { trailingElements.unshift(element) continue } - if (isCapturingGroupAndNotZeroLength(element)) { + if (isCapturingGroupAndNotZeroLength(element, flags)) { const capturingGroup = element end = { capturingGroup, @@ -411,8 +414,8 @@ export default createRule("prefer-lookaround", { function createVisitor( regexpContext: RegExpContext, ): RegExpVisitor.Handlers { - const { regexpNode, patternAst } = regexpContext - const parsedElements = parsePatternElements(patternAst) + const { regexpNode, flags, patternAst } = regexpContext + const parsedElements = parsePatternElements(patternAst, flags) if (!parsedElements) { return {} } diff --git a/lib/rules/prefer-predefined-assertion.ts b/lib/rules/prefer-predefined-assertion.ts index 0c33e8157..f172d977b 100644 --- a/lib/rules/prefer-predefined-assertion.ts +++ b/lib/rules/prefer-predefined-assertion.ts @@ -203,6 +203,8 @@ export default createRule("prefer-predefined-assertion", { } } + // FIXME: TS Error + // @ts-expect-error -- FIXME const charSet = toCharSet(chars, flags) if (charSet.isAll) { replaceEdgeAssertion(aNode, false) diff --git a/lib/rules/prefer-w.ts b/lib/rules/prefer-w.ts index 51d9d5e5b..4f11b9e73 100644 --- a/lib/rules/prefer-w.ts +++ b/lib/rules/prefer-w.ts @@ -92,6 +92,8 @@ export default createRule("prefer-w", { }: RegExpContext): RegExpVisitor.Handlers { return { onCharacterClassEnter(ccNode: CharacterClass) { + // FIXME: TS Error + // @ts-expect-error -- FIXME const charSet = toCharSet(ccNode, flags) let predefined: string | undefined = undefined diff --git a/lib/rules/require-unicode-regexp.ts b/lib/rules/require-unicode-regexp.ts index 8f2e5831e..52a5ebd18 100644 --- a/lib/rules/require-unicode-regexp.ts +++ b/lib/rules/require-unicode-regexp.ts @@ -146,6 +146,8 @@ function isCompatibleCharLike( flags: ReadonlyFlags, uFlags: ReadonlyFlags, ): boolean { + // FIXME: TS Error + // @ts-expect-error -- FIXME const cs = toCharSet(char, flags) if (!cs.isDisjointWith(SURROGATES)) { // If the character (class/set) contains high or low @@ -154,6 +156,8 @@ function isCompatibleCharLike( return false } + // FIXME: TS Error + // @ts-expect-error -- FIXME const uCs = toCharSet(char, uFlags) // Compare the ranges. @@ -199,12 +203,16 @@ function isCompatibleQuantifier( return undefined } + // FIXME: TS Error + // @ts-expect-error -- FIXME const cs = toCharSet(q.element, flags) if (!cs.isSupersetOf(SURROGATES)) { // failed condition 1 return false } + // FIXME: TS Error + // @ts-expect-error -- FIXME const uCs = toCharSet(q.element, uFlags) if (!uCs.isSupersetOf(SURROGATES) || !uCs.isSupersetOf(ASTRAL)) { // failed condition 2 diff --git a/lib/rules/sort-alternatives.ts b/lib/rules/sort-alternatives.ts index 7078bd6ab..a67cd742e 100644 --- a/lib/rules/sort-alternatives.ts +++ b/lib/rules/sort-alternatives.ts @@ -19,6 +19,7 @@ import { CP_APOSTROPHE, createRule, defineRegexpVisitor, + assertValidFlags, } from "../utils" import type { GetLongestPrefixOptions, @@ -30,10 +31,11 @@ import { canReorder, getLongestPrefix, toCharSet, + getConsumedChars, } from "regexp-ast-analysis" import type { CharSet, Word, ReadonlyWord } from "refa" import { NFA, JS, transform } from "refa" -import { getParser, getPossiblyConsumedChar } from "../utils/regexp-ast" +import { getParser } from "../utils/regexp-ast" interface AllowedChars { allowed: CharSet @@ -43,6 +45,7 @@ const cache = new Map>() /** */ function getAllowedChars(flags: ReadonlyFlags) { + assertValidFlags(flags) const cacheKey = (flags.ignoreCase ? "i" : "") + (flags.unicode ? "u" : "") let result = cache.get(cacheKey) if (result === undefined) { @@ -168,6 +171,8 @@ function getLexicographicallySmallestFromAlternative( // fast path to avoid converting simple alternatives into NFAs const smallest: Word = [] for (const e of elements) { + // FIXME: TS Error + // @ts-expect-error -- FIXME const cs = toCharSet(e, flags) if (cs.isEmpty) return undefined smallest.push(cs.ranges[0].min) @@ -545,11 +550,11 @@ export default createRule("sort-alternatives", { const possibleCharsCache = new Map() const parser = getParser(regexpContext) - /** A cached version of getPossiblyConsumedChar */ + /** A cached version of getConsumedChars */ function getPossibleChars(a: Alternative): CharSet { let chars = possibleCharsCache.get(a) if (chars === undefined) { - chars = getPossiblyConsumedChar(a, flags).char + chars = getConsumedChars(a, flags).chars possibleCharsCache.set(a, chars) } return chars diff --git a/lib/utils/index.ts b/lib/utils/index.ts index f6681445f..a7b8d3144 100644 --- a/lib/utils/index.ts +++ b/lib/utils/index.ts @@ -208,6 +208,18 @@ export function parseFlags(flags: string): ReadonlyFlags { return cached } +/** + * Asserts that the given flags are valid (no `u` and `v` flag together). + * @param flags + */ +export function assertValidFlags( + flags: ReadonlyFlags, +): asserts flags is JS.Flags { + if (!JS.isFlags(flags)) { + throw new Error(`Invalid flags: ${JSON.stringify(flags)}`) + } +} + /** * Define the rule. * @param ruleName ruleName @@ -1071,6 +1083,7 @@ export function toCharSetSource( charSetOrChar: CharSet | number, flags: ReadonlyFlags, ): string { + assertValidFlags(flags) let charSet if (typeof charSetOrChar === "number") { charSet = JS.createCharSet([charSetOrChar], flags) diff --git a/lib/utils/partial-parser.ts b/lib/utils/partial-parser.ts index 7278d693b..ed1428a3a 100644 --- a/lib/utils/partial-parser.ts +++ b/lib/utils/partial-parser.ts @@ -206,7 +206,7 @@ export class PartialParser { } return { type: "CharacterClass", - characters: JS.createCharSet([range], this.parser.ast.flags), + characters: JS.createCharSet([range], this.parser.flags), } } // FIXME: TS Error diff --git a/lib/utils/regexp-ast/case-variation.ts b/lib/utils/regexp-ast/case-variation.ts index 5af72aa30..8a771607a 100644 --- a/lib/utils/regexp-ast/case-variation.ts +++ b/lib/utils/regexp-ast/case-variation.ts @@ -4,6 +4,7 @@ import { hasSomeDescendant, toCharSet, isEmptyBackreference, + toUnicodeSet, } from "regexp-ast-analysis" import type { Alternative, @@ -93,7 +94,9 @@ export function isCaseVariant( return unicode case "property": // just check for equality - return !toCharSet(e, iSet).equals(toCharSet(e, iUnset)) + return !toUnicodeSet(e, iSet).equals( + toUnicodeSet(e, iUnset), + ) default: // all other character sets are case-invariant return false @@ -123,7 +126,7 @@ export function isCaseVariant( } return ( - !isEmptyBackreference(d) && + !isEmptyBackreference(d, flags) && isCaseVariant(d.resolved, flags) ) @@ -139,7 +142,9 @@ export function isCaseVariant( return d.elements.some(ccElementIsCaseVariant) } // just check for equality - return !toCharSet(d, iSet).equals(toCharSet(d, iUnset)) + return !toUnicodeSet(d, iSet).equals( + toUnicodeSet(d, iUnset), + ) default: return false diff --git a/lib/utils/regexp-ast/index.ts b/lib/utils/regexp-ast/index.ts index 6c595903e..597d1ee3c 100644 --- a/lib/utils/regexp-ast/index.ts +++ b/lib/utils/regexp-ast/index.ts @@ -1,9 +1,4 @@ -import type { - RegExpLiteral, - Pattern, - Element, - Alternative, -} from "@eslint-community/regexpp/ast" +import type { RegExpLiteral, Pattern } from "@eslint-community/regexpp/ast" import type { Rule } from "eslint" import type { Expression } from "estree" import { @@ -13,14 +8,6 @@ import { } from "@eslint-community/regexpp" import { getStaticValue } from "../ast-utils" import { JS } from "refa" -import type { CharRange, CharSet } from "refa" -import type { ReadonlyFlags } from "regexp-ast-analysis" -import { - Chars, - hasSomeDescendant, - isEmptyBackreference, - toCharSet, -} from "regexp-ast-analysis" import type { RegExpContext } from ".." export { getFirstConsumedCharPlusAfter } from "./common" export type { ShortCircuit } from "./common" @@ -85,67 +72,6 @@ export function extractCaptures(patternNode: RegExpLiteral | Pattern): { return { count, names } } -export interface PossiblyConsumedChar { - char: CharSet - /** - * Whether `char` is exact. - * - * If `false`, then `char` is only guaranteed to be a superset of the - * actually possible characters. - */ - exact: boolean -} - -/** - * Returns the union of all characters that can possibly be consumed by the - * given element. - */ -export function getPossiblyConsumedChar( - element: Element | Pattern | Alternative, - flags: ReadonlyFlags, -): PossiblyConsumedChar { - const ranges: CharRange[] = [] - let exact = true - - // we misuse hasSomeDescendant to iterate all relevant elements - hasSomeDescendant( - element, - (d) => { - if ( - d.type === "Character" || - d.type === "CharacterClass" || - d.type === "CharacterSet" - ) { - const c = toCharSet(d, flags) - ranges.push(...c.ranges) - exact = exact && !c.isEmpty - } else if (d.type === "Backreference" && !isEmptyBackreference(d)) { - const c = getPossiblyConsumedChar(d.resolved, flags) - ranges.push(...c.char.ranges) - exact = exact && c.exact && c.char.size < 2 - } - - // always continue to the next element - return false - }, - // don't go into assertions - (d) => { - if (d.type === "CharacterClass") { - return false - } - if (d.type === "Assertion") { - exact = false - return false - } - return true - }, - ) - - const char = Chars.empty(flags).union(ranges) - - return { char, exact } -} - /** * Create a `JS.RegexppAst` object as required by refa's `JS.Parser.fromAst` * method and `ParsedLiteral` interface of the scslre library. diff --git a/lib/utils/regexp-ast/is-covered.ts b/lib/utils/regexp-ast/is-covered.ts index 00eae38db..b97617e7b 100644 --- a/lib/utils/regexp-ast/is-covered.ts +++ b/lib/utils/regexp-ast/is-covered.ts @@ -435,6 +435,8 @@ function normalizeNodeWithoutCache( node.type === "Character" || node.type === "CharacterClassRange" ) { + // FIXME: TS Error + // @ts-expect-error -- FIXME return NormalizedCharacter.fromElement(node, options) } if (node.type === "Alternative") { diff --git a/lib/utils/regexp-ast/is-equals.ts b/lib/utils/regexp-ast/is-equals.ts index 31d29636d..9f4710959 100644 --- a/lib/utils/regexp-ast/is-equals.ts +++ b/lib/utils/regexp-ast/is-equals.ts @@ -1,5 +1,9 @@ -import type { ToCharSetElement, ReadonlyFlags } from "regexp-ast-analysis" -import { toCharSet } from "regexp-ast-analysis" +import type { + ToCharSetElement, + ReadonlyFlags, + ToUnicodeSetElement, +} from "regexp-ast-analysis" +import { toUnicodeSet } from "regexp-ast-analysis" import type { Alternative, Assertion, @@ -24,12 +28,12 @@ import type { ShortCircuit } from "./common" * Returns whether the two given character element as equal in the characters * that they accept. * - * This is equivalent to `toCharSet(a).equals(toCharSet(b))` but implemented + * This is equivalent to `toUnicodeSet(a).equals(toUnicodeSet(b))` but implemented * more efficiently. */ function isEqualChar( - a: ToCharSetElement, - b: ToCharSetElement, + a: ToUnicodeSetElement, + b: ToUnicodeSetElement, flags: ReadonlyFlags, ): boolean { if (a.type === "Character") { @@ -56,7 +60,7 @@ function isEqualChar( return true } - return toCharSet(a, flags).equals(toCharSet(b, flags)) + return toUnicodeSet(a, flags).equals(toUnicodeSet(b, flags)) } const EQUALS_CHECKER = { diff --git a/lib/utils/regexp-ast/simplify-quantifier.ts b/lib/utils/regexp-ast/simplify-quantifier.ts index 3559f3d72..2853347f5 100644 --- a/lib/utils/regexp-ast/simplify-quantifier.ts +++ b/lib/utils/regexp-ast/simplify-quantifier.ts @@ -1,3 +1,5 @@ +/* eslint-disable eslint-comments/disable-enable-pair -- x */ +/* eslint-disable complexity -- x */ import type { JS } from "refa" import { DFA, NFA } from "refa" import type { MatchingDirection, ReadonlyFlags } from "regexp-ast-analysis" @@ -7,6 +9,7 @@ import { hasSomeDescendant, isZeroLength, isPotentiallyZeroLength, + getConsumedChars, } from "regexp-ast-analysis" import type { Alternative, @@ -15,7 +18,6 @@ import type { QuantifiableElement, Quantifier, } from "@eslint-community/regexpp/ast" -import { getPossiblyConsumedChar } from "." /** * Wraps the given function to be cached by a `WeakMap`. @@ -38,11 +40,9 @@ function weakCachedFn( const containsAssertions = weakCachedFn((node: Node) => { return hasSomeDescendant(node, (n) => n.type === "Assertion") }) -/** A cached (and curried) version of {@link getPossiblyConsumedChar}. */ +/** A cached (and curried) version of {@link getConsumedChars}. */ const cachedGetPossiblyConsumedChar = weakCachedFn((flags: ReadonlyFlags) => { - return weakCachedFn((element: Element) => - getPossiblyConsumedChar(element, flags), - ) + return weakCachedFn((element: Element) => getConsumedChars(element, flags)) }) export type CanSimplify = { @@ -65,7 +65,7 @@ export function canSimplifyQuantifier( if (quantifier.min === quantifier.max) { return CANNOT_SIMPLIFY } - if (isZeroLength(quantifier)) { + if (isZeroLength(quantifier, flags)) { return CANNOT_SIMPLIFY } if (containsAssertions(quantifier)) { @@ -75,7 +75,7 @@ export function canSimplifyQuantifier( // find the full set of quantifiers that precede this one const direction = getMatchingDirection(quantifier) - const preceding = getPrecedingQuantifiers(quantifier, direction) + const preceding = getPrecedingQuantifiers(quantifier, direction, flags) if (!preceding) { // there is something that is not a quantifier return CANNOT_SIMPLIFY @@ -105,6 +105,7 @@ function canAbsorb( initialPreceding, quantifier, direction, + flags, ) if (!preceding) { return CANNOT_SIMPLIFY @@ -132,7 +133,7 @@ function canAbsorb( return formal.every((q) => { // try splitting the quantifier - const parts = splitQuantifierIntoTails(q, direction) + const parts = splitQuantifierIntoTails(q, direction, flags) if (!parts) return false const result = canAbsorb(parts, options) if (result.canSimplify) dependencies.push(...result.dependencies) @@ -178,7 +179,7 @@ function canAbsorbElementFast( return false } - if (!isNonFinite(quantifier)) { + if (!isNonFinite(quantifier, flags)) { // to absorb `E*`, the `Q` needs to be non-finite language return false } @@ -186,12 +187,12 @@ function canAbsorbElementFast( const qChar = cachedGetPossiblyConsumedChar(flags)(quantifier.element) const eChar = cachedGetPossiblyConsumedChar(flags)(element) - if (qChar.char.isDisjointWith(eChar.char)) { + if (qChar.chars.isDisjointWith(eChar.chars)) { // Since `Q` and `E` are disjoint, there is no way for `Q` to absorb `E*` return false } - if (eChar.exact && !eChar.char.without(qChar.char).isEmpty) { + if (eChar.exact && !eChar.chars.without(qChar.chars).isEmpty) { // At least one char in `E` cannot be absorbed by `Q` return false } @@ -212,7 +213,7 @@ function canAbsorbElementFast( return false } - if (qChar.exact && qChar.char.isSupersetOf(eChar.char)) { + if (qChar.exact && qChar.chars.isSupersetOf(eChar.chars)) { return true } } @@ -221,13 +222,13 @@ function canAbsorbElementFast( } /** Returns whether the given node accepts a non-finite language. */ -function isNonFinite(node: Node): boolean { +function isNonFinite(node: Node, flags: ReadonlyFlags): boolean { return hasSomeDescendant( node, (n) => n.type === "Quantifier" && n.max === Infinity && - !isZeroLength(n.element), + !isZeroLength(n.element, flags), // don't decent into assertions (n) => n.type !== "Assertion", ) @@ -293,11 +294,12 @@ function canAbsorbElementFormal( function splitQuantifierIntoTails( quantifier: Quantifier, direction: MatchingDirection, + flags: ReadonlyFlags, ): Quantifier[] | undefined { - if (isPotentiallyZeroLength(quantifier)) { + if (isPotentiallyZeroLength(quantifier, flags)) { return undefined } - return getTailQuantifiers(quantifier.element, direction) + return getTailQuantifiers(quantifier.element, direction, flags) } /** @@ -311,16 +313,22 @@ function removeTargetQuantifier( quantifiers: readonly Quantifier[], target: Element, direction: MatchingDirection, + flags: ReadonlyFlags, ): Quantifier[] | undefined { const result: Quantifier[] = [] for (const q of quantifiers) { if (hasSomeDescendant(q, target)) { - const inner = splitQuantifierIntoTails(q, direction) + const inner = splitQuantifierIntoTails(q, direction, flags) if (inner === undefined) { return undefined } - const mapped = removeTargetQuantifier(inner, target, direction) + const mapped = removeTargetQuantifier( + inner, + target, + direction, + flags, + ) if (mapped === undefined) { return undefined } @@ -371,6 +379,7 @@ function unionQuantifiers(sets: Iterable): QuantifierSet { function getTailQuantifiers( element: Element | Alternative, direction: MatchingDirection, + flags: ReadonlyFlags, ): [Quantifier, ...Quantifier[]] | undefined { switch (element.type) { case "Assertion": @@ -378,6 +387,7 @@ function getTailQuantifiers( case "Character": case "CharacterClass": case "CharacterSet": + case "ExpressionCharacterClass": return undefined case "Quantifier": @@ -387,7 +397,7 @@ function getTailQuantifiers( case "CapturingGroup": return unionQuantifiers( element.alternatives.map((a) => - getTailQuantifiers(a, direction), + getTailQuantifiers(a, direction, flags), ), ) @@ -398,7 +408,7 @@ function getTailQuantifiers( : element.elements for (const e of elements) { // skip empty elements - if (isEmpty(e)) continue + if (isEmpty(e, flags)) continue if (e.type === "Quantifier") { return [e] @@ -414,12 +424,10 @@ function getTailQuantifiers( // TODO: Assertions aren't supported for now. return undefined } - return getPrecedingQuantifiers(parent, direction) + return getPrecedingQuantifiers(parent, direction, flags) } default: - // FIXME: TS Error - // @ts-expect-error -- FIXME return assertNever(element) } } @@ -430,6 +438,7 @@ function getTailQuantifiers( function getPrecedingQuantifiers( element: Element, direction: MatchingDirection, + flags: ReadonlyFlags, ): [Quantifier, ...Quantifier[]] | undefined { const parent = element.parent if (parent.type === "Quantifier") { @@ -439,13 +448,13 @@ function getPrecedingQuantifiers( } if (parent.max === 1) { // the quantifier is essentially equivalent to a simple group - return getPrecedingQuantifiers(parent, direction) + return getPrecedingQuantifiers(parent, direction, flags) } // Both the elements preceding the quantifier as well as the quantifier itself have to be considered return unionQuantifiers([ - getPrecedingQuantifiers(parent, direction), - getTailQuantifiers(parent.element, direction), + getPrecedingQuantifiers(parent, direction, flags), + getTailQuantifiers(parent.element, direction, flags), ]) } if (parent.type !== "Alternative") { @@ -463,13 +472,13 @@ function getPrecedingQuantifiers( const preceding = parent.elements[precedingIndex] // skip empty elements - if (isEmpty(preceding)) continue + if (isEmpty(preceding, flags)) continue - return getTailQuantifiers(preceding, direction) + return getTailQuantifiers(preceding, direction, flags) } if (parent.parent.type === "Pattern") { return undefined } - return getPrecedingQuantifiers(parent.parent, direction) + return getPrecedingQuantifiers(parent.parent, direction, flags) } diff --git a/package-lock.json b/package-lock.json index d3fdbc71f..7bf425756 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,9 +14,9 @@ "comment-parser": "^1.4.0", "grapheme-splitter": "^1.0.4", "jsdoctypeparser": "^9.0.0", - "refa": "^0.11.0", - "regexp-ast-analysis": "^0.6.0", - "scslre": "^0.2.0" + "refa": "^0.12.0", + "regexp-ast-analysis": "^0.7.0", + "scslre": "^0.3.0" }, "devDependencies": { "@changesets/cli": "^2.26.2", @@ -1861,9 +1861,9 @@ } }, "node_modules/@eslint-community/regexpp": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.6.2.tgz", - "integrity": "sha512-pPTNuaAG3QMH+buKyBIGJs3g/S5y0caxw0ygM3YyE6yJFySwiGGSzA+mM3KJ8QQvzeLh3blwgSonkFjgQdxzMw==", + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.8.0.tgz", + "integrity": "sha512-JylOEEzDiOryeUnFbQz+oViCXS0KsvR1mvHkoMiu5+UiBvy+RYX7tzlIIIEstF/gVa2tj9AQXk3dgnxv6KxhFg==", "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } @@ -5352,6 +5352,42 @@ "eslint": ">=6.0.0" } }, + "node_modules/eslint-plugin-regexp/node_modules/refa": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/refa/-/refa-0.11.0.tgz", + "integrity": "sha512-486O8/pQXwj9jV0mVvUnTsxq0uknpBnNJ0eCUhkZqJRQ8KutrT1PhzmumdCeM1hSBF2eMlFPmwECRER4IbKXlQ==", + "dev": true, + "dependencies": { + "@eslint-community/regexpp": "^4.5.0" + }, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/eslint-plugin-regexp/node_modules/regexp-ast-analysis": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/regexp-ast-analysis/-/regexp-ast-analysis-0.6.0.tgz", + "integrity": "sha512-OLxjyjPkVH+rQlBLb1I/P/VTmamSjGkvN5PTV5BXP432k3uVz727J7H29GA5IFiY0m7e1xBN7049Wn59FY3DEQ==", + "dev": true, + "dependencies": { + "@eslint-community/regexpp": "^4.5.0", + "refa": "^0.11.0" + }, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/eslint-plugin-regexp/node_modules/scslre": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/scslre/-/scslre-0.2.0.tgz", + "integrity": "sha512-4hc49fUMmX3jM0XdFUAPBrs1xwEcdHa0KyjEsjFs+Zfc66mpFpq5YmRgDtl+Ffo6AtJIilfei+yKw8fUn3N88w==", + "dev": true, + "dependencies": { + "@eslint-community/regexpp": "^4.5.0", + "refa": "^0.11.0", + "regexp-ast-analysis": "^0.6.0" + } + }, "node_modules/eslint-plugin-vue": { "version": "9.7.0", "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.7.0.tgz", @@ -9334,11 +9370,11 @@ } }, "node_modules/refa": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/refa/-/refa-0.11.0.tgz", - "integrity": "sha512-486O8/pQXwj9jV0mVvUnTsxq0uknpBnNJ0eCUhkZqJRQ8KutrT1PhzmumdCeM1hSBF2eMlFPmwECRER4IbKXlQ==", + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/refa/-/refa-0.12.0.tgz", + "integrity": "sha512-3yCTEBzW93NF2uNsmXCuwmrnu0VcNldYSeFi8t43maVi/J8/M3QOf61XhfcyZiI+SPCQbAucVRhv1Iv7WU0B9Q==", "dependencies": { - "@eslint-community/regexpp": "^4.5.0" + "@eslint-community/regexpp": "^4.8.0" }, "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" @@ -9351,12 +9387,12 @@ "dev": true }, "node_modules/regexp-ast-analysis": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/regexp-ast-analysis/-/regexp-ast-analysis-0.6.0.tgz", - "integrity": "sha512-OLxjyjPkVH+rQlBLb1I/P/VTmamSjGkvN5PTV5BXP432k3uVz727J7H29GA5IFiY0m7e1xBN7049Wn59FY3DEQ==", + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/regexp-ast-analysis/-/regexp-ast-analysis-0.7.0.tgz", + "integrity": "sha512-Y7L2aE4KOKkatMh6YzzwSeTcUWDUHyo6G7nZReIXtB50KzDZT+HCPnBmgeHKSaCwbiEviRBeI8JS7m4KF2pzjw==", "dependencies": { - "@eslint-community/regexpp": "^4.5.0", - "refa": "^0.11.0" + "@eslint-community/regexpp": "^4.8.0", + "refa": "^0.12.0" }, "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" @@ -9573,13 +9609,16 @@ "peer": true }, "node_modules/scslre": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/scslre/-/scslre-0.2.0.tgz", - "integrity": "sha512-4hc49fUMmX3jM0XdFUAPBrs1xwEcdHa0KyjEsjFs+Zfc66mpFpq5YmRgDtl+Ffo6AtJIilfei+yKw8fUn3N88w==", + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/scslre/-/scslre-0.3.0.tgz", + "integrity": "sha512-3A6sD0WYP7+QrjbfNA2FN3FsOaGGFoekCVgTyypy53gPxhbkCIjtO6YWgdrfM+n/8sI8JeXZOIxsHjMTNxQ4nQ==", "dependencies": { - "@eslint-community/regexpp": "^4.5.0", - "refa": "^0.11.0", - "regexp-ast-analysis": "^0.6.0" + "@eslint-community/regexpp": "^4.8.0", + "refa": "^0.12.0", + "regexp-ast-analysis": "^0.7.0" + }, + "engines": { + "node": "^14.0.0 || >=16.0.0" } }, "node_modules/semver": { @@ -12737,9 +12776,9 @@ } }, "@eslint-community/regexpp": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.6.2.tgz", - "integrity": "sha512-pPTNuaAG3QMH+buKyBIGJs3g/S5y0caxw0ygM3YyE6yJFySwiGGSzA+mM3KJ8QQvzeLh3blwgSonkFjgQdxzMw==" + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.8.0.tgz", + "integrity": "sha512-JylOEEzDiOryeUnFbQz+oViCXS0KsvR1mvHkoMiu5+UiBvy+RYX7tzlIIIEstF/gVa2tj9AQXk3dgnxv6KxhFg==" }, "@eslint/eslintrc": { "version": "2.1.2", @@ -15216,6 +15255,38 @@ "refa": "^0.11.0", "regexp-ast-analysis": "^0.6.0", "scslre": "^0.2.0" + }, + "dependencies": { + "refa": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/refa/-/refa-0.11.0.tgz", + "integrity": "sha512-486O8/pQXwj9jV0mVvUnTsxq0uknpBnNJ0eCUhkZqJRQ8KutrT1PhzmumdCeM1hSBF2eMlFPmwECRER4IbKXlQ==", + "dev": true, + "requires": { + "@eslint-community/regexpp": "^4.5.0" + } + }, + "regexp-ast-analysis": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/regexp-ast-analysis/-/regexp-ast-analysis-0.6.0.tgz", + "integrity": "sha512-OLxjyjPkVH+rQlBLb1I/P/VTmamSjGkvN5PTV5BXP432k3uVz727J7H29GA5IFiY0m7e1xBN7049Wn59FY3DEQ==", + "dev": true, + "requires": { + "@eslint-community/regexpp": "^4.5.0", + "refa": "^0.11.0" + } + }, + "scslre": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/scslre/-/scslre-0.2.0.tgz", + "integrity": "sha512-4hc49fUMmX3jM0XdFUAPBrs1xwEcdHa0KyjEsjFs+Zfc66mpFpq5YmRgDtl+Ffo6AtJIilfei+yKw8fUn3N88w==", + "dev": true, + "requires": { + "@eslint-community/regexpp": "^4.5.0", + "refa": "^0.11.0", + "regexp-ast-analysis": "^0.6.0" + } + } } }, "eslint-plugin-vue": { @@ -18128,11 +18199,11 @@ } }, "refa": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/refa/-/refa-0.11.0.tgz", - "integrity": "sha512-486O8/pQXwj9jV0mVvUnTsxq0uknpBnNJ0eCUhkZqJRQ8KutrT1PhzmumdCeM1hSBF2eMlFPmwECRER4IbKXlQ==", + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/refa/-/refa-0.12.0.tgz", + "integrity": "sha512-3yCTEBzW93NF2uNsmXCuwmrnu0VcNldYSeFi8t43maVi/J8/M3QOf61XhfcyZiI+SPCQbAucVRhv1Iv7WU0B9Q==", "requires": { - "@eslint-community/regexpp": "^4.5.0" + "@eslint-community/regexpp": "^4.8.0" } }, "regenerator-runtime": { @@ -18142,12 +18213,12 @@ "dev": true }, "regexp-ast-analysis": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/regexp-ast-analysis/-/regexp-ast-analysis-0.6.0.tgz", - "integrity": "sha512-OLxjyjPkVH+rQlBLb1I/P/VTmamSjGkvN5PTV5BXP432k3uVz727J7H29GA5IFiY0m7e1xBN7049Wn59FY3DEQ==", + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/regexp-ast-analysis/-/regexp-ast-analysis-0.7.0.tgz", + "integrity": "sha512-Y7L2aE4KOKkatMh6YzzwSeTcUWDUHyo6G7nZReIXtB50KzDZT+HCPnBmgeHKSaCwbiEviRBeI8JS7m4KF2pzjw==", "requires": { - "@eslint-community/regexpp": "^4.5.0", - "refa": "^0.11.0" + "@eslint-community/regexpp": "^4.8.0", + "refa": "^0.12.0" } }, "regexp.prototype.flags": { @@ -18286,13 +18357,13 @@ "peer": true }, "scslre": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/scslre/-/scslre-0.2.0.tgz", - "integrity": "sha512-4hc49fUMmX3jM0XdFUAPBrs1xwEcdHa0KyjEsjFs+Zfc66mpFpq5YmRgDtl+Ffo6AtJIilfei+yKw8fUn3N88w==", + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/scslre/-/scslre-0.3.0.tgz", + "integrity": "sha512-3A6sD0WYP7+QrjbfNA2FN3FsOaGGFoekCVgTyypy53gPxhbkCIjtO6YWgdrfM+n/8sI8JeXZOIxsHjMTNxQ4nQ==", "requires": { - "@eslint-community/regexpp": "^4.5.0", - "refa": "^0.11.0", - "regexp-ast-analysis": "^0.6.0" + "@eslint-community/regexpp": "^4.8.0", + "refa": "^0.12.0", + "regexp-ast-analysis": "^0.7.0" } }, "semver": { diff --git a/package.json b/package.json index 00304a8b6..883c38966 100644 --- a/package.json +++ b/package.json @@ -107,9 +107,9 @@ "comment-parser": "^1.4.0", "grapheme-splitter": "^1.0.4", "jsdoctypeparser": "^9.0.0", - "refa": "^0.11.0", - "regexp-ast-analysis": "^0.6.0", - "scslre": "^0.2.0" + "refa": "^0.12.0", + "regexp-ast-analysis": "^0.7.0", + "scslre": "^0.3.0" }, "publishConfig": { "access": "public"