Skip to content

Commit 7f1a6c4

Browse files
committed
refactor(sort-regexp): extract helpers into modules
1 parent 061e510 commit 7f1a6c4

19 files changed

+528
-360
lines changed

rules/sort-regexp.ts

Lines changed: 8 additions & 360 deletions
Large diffs are not rendered by default.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/**
2+
* Converts the provided code point into its string representation.
3+
*
4+
* @param value - Code point to convert.
5+
* @returns String representation of the code point.
6+
*/
7+
export function codePointToString(value: number): string {
8+
return String.fromCodePoint(value)
9+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import type { CharacterClass } from '@eslint-community/regexpp/ast'
2+
import type { TSESTree } from '@typescript-eslint/types'
3+
4+
import type { SortingNode } from '../../types/sorting-node'
5+
6+
import { getCharacterClassElementSortKey } from './get-character-class-element-sort-key'
7+
8+
/**
9+
* Creates a sorting node for a character class element.
10+
*
11+
* @param parameters - Character class element metadata.
12+
* @returns Sorting node describing the element.
13+
*/
14+
export function createCharacterClassSortingNode({
15+
literalNode,
16+
element,
17+
}: {
18+
element: CharacterClass['elements'][number]
19+
literalNode: TSESTree.Literal
20+
}): SortingNode<TSESTree.Literal> {
21+
let key = getCharacterClassElementSortKey(element)
22+
23+
return {
24+
group: 'character-class',
25+
isEslintDisabled: false,
26+
size: key.raw.length,
27+
name: key.normalized,
28+
node: literalNode,
29+
partitionId: 0,
30+
}
31+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import type { TSESTree } from '@typescript-eslint/types'
2+
3+
import type { SortingNode } from '../../types/sorting-node'
4+
5+
import { isNodeEslintDisabled } from '../../utils/is-node-eslint-disabled'
6+
7+
/**
8+
* Builds sorting nodes for every flag attached to a regular expression literal.
9+
*
10+
* @param parameters - Literal context alongside enabled flags.
11+
* @returns Sorting nodes representing each flag.
12+
*/
13+
export function createFlagSortingNodes({
14+
eslintDisabledLines,
15+
literalNode,
16+
flags,
17+
}: {
18+
eslintDisabledLines: number[]
19+
literalNode: TSESTree.Literal
20+
flags: string
21+
}): SortingNode<TSESTree.Literal>[] {
22+
let isDisabled = isNodeEslintDisabled(literalNode, eslintDisabledLines)
23+
24+
return [...flags].map(flag => ({
25+
isEslintDisabled: isDisabled,
26+
node: literalNode,
27+
partitionId: 0,
28+
group: 'flags',
29+
name: flag,
30+
size: 1,
31+
}))
32+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import type { Alternative } from '@eslint-community/regexpp/ast'
2+
import type { TSESTree } from '@typescript-eslint/types'
3+
import type { TSESLint } from '@typescript-eslint/utils'
4+
5+
import { AST_NODE_TYPES } from '@typescript-eslint/types'
6+
7+
/**
8+
* Produces a pseudo literal node representing a regex alternative.
9+
*
10+
* @param parameters - Source literal context and alternative.
11+
* @returns Literal node mirroring the alternative segment.
12+
*/
13+
export function createPseudoLiteralNode({
14+
literalNode,
15+
alternative,
16+
sourceCode,
17+
}: {
18+
sourceCode: TSESLint.SourceCode
19+
literalNode: TSESTree.Literal
20+
alternative: Alternative
21+
}): TSESTree.Literal {
22+
let [literalStart] = literalNode.range
23+
let offsetStart = literalStart + alternative.start
24+
let offsetEnd = literalStart + alternative.end
25+
let range: TSESTree.Range = [offsetStart, offsetEnd]
26+
let loc = {
27+
start: sourceCode.getLocFromIndex(range[0]),
28+
end: sourceCode.getLocFromIndex(range[1]),
29+
}
30+
31+
return {
32+
type: AST_NODE_TYPES.Literal,
33+
value: alternative.raw,
34+
raw: alternative.raw,
35+
parent: literalNode,
36+
range,
37+
loc,
38+
} as TSESTree.Literal
39+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import type { Alternative } from '@eslint-community/regexpp/ast'
2+
import type { TSESLint } from '@typescript-eslint/utils'
3+
import type { TSESTree } from '@typescript-eslint/types'
4+
5+
import type { SortingNode } from '../../types/sorting-node'
6+
import type { Options } from './types'
7+
8+
import { doesCustomGroupMatch } from '../../utils/does-custom-group-match'
9+
import { isNodeEslintDisabled } from '../../utils/is-node-eslint-disabled'
10+
import { createPseudoLiteralNode } from './create-pseudo-literal-node'
11+
import { getAlternativeAlias } from './get-alternative-alias'
12+
import { getSortingNodeName } from './get-sorting-node-name'
13+
import { computeGroup } from '../../utils/compute-group'
14+
import { getSelector } from './get-selector'
15+
16+
interface CreateSortingNodeParameters {
17+
sourceCode: TSESLint.SourceCode
18+
literalNode: TSESTree.Literal
19+
eslintDisabledLines: number[]
20+
alternative: Alternative
21+
options: ResolvedOptions
22+
}
23+
24+
type ResolvedOptions = Required<Options[0]>
25+
26+
/**
27+
* Builds a sortable node representation for a regex alternative.
28+
*
29+
* @param parameters - Alternative context with rule settings.
30+
* @returns Sorting node ready for ordering logic.
31+
*/
32+
export function createSortingNode({
33+
eslintDisabledLines,
34+
literalNode,
35+
alternative,
36+
sourceCode,
37+
options,
38+
}: CreateSortingNodeParameters): SortingNode<TSESTree.Literal> {
39+
let alternativeAlias = getAlternativeAlias(alternative)
40+
let selector = getSelector({ alternativeAlias })
41+
let name = getSortingNodeName({
42+
alternativeAlias,
43+
alternative,
44+
options,
45+
})
46+
47+
let group = computeGroup({
48+
customGroupMatcher: customGroup =>
49+
doesCustomGroupMatch({
50+
elementValue: alternative.raw,
51+
selectors: [selector],
52+
elementName: name,
53+
modifiers: [],
54+
customGroup,
55+
}),
56+
predefinedGroups: [selector],
57+
options,
58+
})
59+
60+
let pseudoNode = createPseudoLiteralNode({
61+
literalNode,
62+
alternative,
63+
sourceCode,
64+
})
65+
66+
return {
67+
isEslintDisabled: isNodeEslintDisabled(literalNode, eslintDisabledLines),
68+
size: pseudoNode.range[1] - pseudoNode.range[0],
69+
node: pseudoNode,
70+
partitionId: 0,
71+
group,
72+
name,
73+
}
74+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/**
2+
* Detects whether one alternative shadows (is a prefix of) another.
3+
*
4+
* @param first - First alternative text.
5+
* @param second - Second alternative text.
6+
* @returns True when either alternative makes the other unreachable.
7+
*/
8+
export function doesAlternativeShadowOther(
9+
first: string,
10+
second: string,
11+
): boolean {
12+
if (first.length === 0 || second.length === 0) {
13+
return true
14+
}
15+
16+
if (first.length === second.length) {
17+
return first === second
18+
}
19+
20+
if (first.length < second.length) {
21+
return second.startsWith(first)
22+
}
23+
24+
return first.startsWith(second)
25+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import type { Alternative } from '@eslint-community/regexpp/ast'
2+
3+
/**
4+
* Extracts an alias name for a given alternative, if present.
5+
*
6+
* @param alternative - Alternative to inspect.
7+
* @returns Alias name or null when absent.
8+
*/
9+
export function getAlternativeAlias(alternative: Alternative): string | null {
10+
let [element] = alternative.elements
11+
if (element && element.type === 'CapturingGroup' && element.name) {
12+
return element.name
13+
}
14+
15+
if (alternative.parent.type === 'CapturingGroup' && alternative.parent.name) {
16+
return alternative.parent.name
17+
}
18+
19+
return null
20+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import type { CharacterClass } from '@eslint-community/regexpp/ast'
2+
3+
import { isLowercaseCharacter } from './is-lowercase-character'
4+
import { isUppercaseCharacter } from './is-uppercase-character'
5+
import { isDigitCharacter } from './is-digit-character'
6+
7+
/**
8+
* Maps a character class element to a sortable category bucket.
9+
*
10+
* @param element - Character class element to categories.
11+
* @returns Numeric category representing the element group.
12+
*/
13+
export function getCharacterClassElementCategory(
14+
element: CharacterClass['elements'][number],
15+
): number {
16+
let category = 4
17+
18+
switch (element.type) {
19+
case 'CharacterClassRange': {
20+
if (
21+
isDigitCharacter(element.min.value) &&
22+
isDigitCharacter(element.max.value)
23+
) {
24+
category = 0
25+
} else if (
26+
isUppercaseCharacter(element.min.value) &&
27+
isUppercaseCharacter(element.max.value)
28+
) {
29+
category = 1
30+
} else if (
31+
isLowercaseCharacter(element.min.value) &&
32+
isLowercaseCharacter(element.max.value)
33+
) {
34+
category = 2
35+
} else {
36+
category = 3
37+
}
38+
39+
break
40+
}
41+
case 'CharacterSet': {
42+
switch (element.kind) {
43+
case 'digit': {
44+
category = 0
45+
46+
break
47+
}
48+
case 'space': {
49+
category = 3
50+
51+
break
52+
}
53+
case 'word': {
54+
category = 2
55+
56+
break
57+
}
58+
// No default
59+
}
60+
61+
break
62+
}
63+
case 'Character': {
64+
if (isDigitCharacter(element.value)) {
65+
category = 0
66+
} else if (isUppercaseCharacter(element.value)) {
67+
category = 1
68+
} else if (isLowercaseCharacter(element.value)) {
69+
category = 2
70+
} else {
71+
category = 3
72+
}
73+
74+
break
75+
}
76+
/* No default. */
77+
}
78+
79+
return category
80+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import type { CharacterClass } from '@eslint-community/regexpp/ast'
2+
3+
/**
4+
* Returns the raw representation of a character class element.
5+
*
6+
* @param element - Character class element to read.
7+
* @returns Raw text of the element.
8+
*/
9+
export function getCharacterClassElementRaw(
10+
element: CharacterClass['elements'][number],
11+
): string {
12+
return element.raw
13+
}

0 commit comments

Comments
 (0)