Skip to content

Commit

Permalink
Add canReorder util method (#256)
Browse files Browse the repository at this point in the history
* Add `canReorder` util method

* Use refa's CharBase

* Fixed buggy regex
  • Loading branch information
RunDevelopment authored Jul 27, 2021
1 parent 65e38dd commit 93d762b
Show file tree
Hide file tree
Showing 7 changed files with 2,683 additions and 80 deletions.
117 changes: 61 additions & 56 deletions lib/rules/sort-alternatives.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,5 @@
import type { RegExpVisitor } from "regexpp/visitor"
import type {
Alternative,
CapturingGroup,
Element,
Group,
LookaroundAssertion,
Pattern,
} from "regexpp/ast"
import type { Alternative, Element, Pattern } from "regexpp/ast"
import type { RegExpContext } from "../utils"
import {
CP_MINUS,
Expand All @@ -17,16 +10,15 @@ import {
} from "../utils"
import type { ReadonlyFlags } from "regexp-ast-analysis"
import {
getLengthRange,
Chars,
getFirstCharAfter,
getFirstConsumedChar,
hasSomeDescendant,
isEmptyBackreference,
} from "regexp-ast-analysis"
import type { CharRange, CharSet } from "refa"
import { JS } from "refa"
import type { SourceLocation, Position } from "estree"
import { canReorder } from "../utils/reorder-alternatives"

interface AllowedChars {
allowed: CharSet
Expand Down Expand Up @@ -90,40 +82,6 @@ function getConsumedChars(
return Chars.empty(context.flags).union(ranges)
}

type Parent = Group | CapturingGroup | Pattern | LookaroundAssertion

/**
* Assuming that the given group only consumes the given characters, this will
* return whether the alternatives of the group can be reordered freely without
* affecting the behavior of the regex.
*
* This also assumes that the alternatives of the given group do not contain
* capturing group in such a way that their order matters.
*/
function canReorder(
parent: Parent,
consumedChars: CharSet,
context: RegExpContext,
): boolean {
const lengthRange = getLengthRange(parent.alternatives)
if (lengthRange && lengthRange.min === lengthRange.max) {
return true
}

if (parent.type === "Pattern" || parent.type === "Assertion") {
return false
}

return (
getFirstCharAfter(parent, "rtl", context.flags).char.isDisjointWith(
consumedChars,
) &&
getFirstCharAfter(parent, "ltr", context.flags).char.isDisjointWith(
consumedChars,
)
)
}

/**
* Returns whether the given element contains only literal characters and
* groups/other elements containing literal characters.
Expand Down Expand Up @@ -195,6 +153,15 @@ function sortAlternatives(
})
}

/**
* Returns whether the given string is a valid integer.
* @param str
* @returns
*/
function isIntegerString(str: string): boolean {
return /^(?:0|[1-9]\d*)$/.test(str)
}

/**
* This tries to sort the given alternatives by assuming that all alternatives
* are a number.
Expand All @@ -204,7 +171,7 @@ function trySortNumberAlternatives(alternatives: Alternative[]): void {
{
let start = 0
for (let i = 0; i < alternatives.length; i++) {
if (!/^(?:0|[1-9]\d*)$/.test(alternatives[i].raw)) {
if (!isIntegerString(alternatives[i].raw)) {
if (start < i) {
numberRanges.push([start, i])
}
Expand Down Expand Up @@ -274,6 +241,36 @@ function getReorderingBounds<T>(
return [first, last]
}

interface Run<T> {
index: number
elements: T[]
}

/**
* Returns an array of runs of elements that fulfill the given condition.
*/
function getRuns<T>(iter: Iterable<T>, condFn: (item: T) => boolean): Run<T>[] {
const runs: Run<T>[] = []

let elements: T[] = []
let index = 0

for (const item of iter) {
if (condFn(item)) {
elements.push(item)
} else {
if (elements.length > 0) {
runs.push({ index, elements })
elements = []
}
}

index++
}

return runs
}

export default createRule("sort-alternatives", {
meta: {
docs: {
Expand Down Expand Up @@ -345,7 +342,7 @@ export default createRule("sort-alternatives", {
}

/** The handler for parents */
function onParent(parent: Parent): void {
function onParent(parent: Alternative["parent"]): void {
if (parent.alternatives.length < 2) {
return
}
Expand Down Expand Up @@ -381,20 +378,28 @@ export default createRule("sort-alternatives", {

const alternatives = [...parent.alternatives]

if (canReorder(parent, consumedChars, regexpContext)) {
if (canReorder(alternatives, regexpContext)) {
// alternatives can be reordered freely
sortAlternatives(alternatives, regexpContext)
trySortNumberAlternatives(alternatives)
} else if (
!consumedChars.isDisjointWith(Chars.digit(flags)) &&
canReorder(
parent,
consumedChars.intersect(Chars.digit(flags)),
regexpContext,
)
) {
} else if (!consumedChars.isDisjointWith(Chars.digit(flags))) {
// let's try to at least sort numbers
trySortNumberAlternatives(alternatives)
const runs = getRuns(alternatives, (a) =>
isIntegerString(a.raw),
)
for (const { index, elements } of runs) {
if (
elements.length > 1 &&
canReorder(elements, regexpContext)
) {
trySortNumberAlternatives(elements)
alternatives.splice(
index,
elements.length,
...elements,
)
}
}
}

enforceSorted(alternatives)
Expand Down
Loading

0 comments on commit 93d762b

Please sign in to comment.