routie dev init since i didn't adhere to any proper guidance up until now

This commit is contained in:
2026-04-29 22:27:29 -06:00
commit e1dabb71e2
15301 changed files with 3562618 additions and 0 deletions
@@ -0,0 +1,219 @@
/**
* Manages a customizable alphabet for sorting operations.
*
* Provides methods to generate, modify, and sort alphabets based on various
* criteria including Unicode ranges, case prioritization, and locale-specific
* ordering. Used by the 'custom' sort type to define character precedence.
*
* The class supports:
*
* - Generating alphabets from strings or Unicode ranges
* - Case prioritization and manipulation
* - Character positioning and removal
* - Multiple sorting strategies (natural, locale, char code).
*
* @example
*
* ```ts
* // Create a custom alphabet with specific character order
* const alphabet = Alphabet.generateFrom('aAbBcC')
* .prioritizeCase('uppercase')
* .getCharacters()
* // Returns: 'AaBbCc'
* ```
*/
export declare class Alphabet {
private characters
private constructor()
/**
* Generates an alphabet from the given characters.
*
* @param values - The characters to generate the alphabet from.
* @returns - The wrapped alphabet.
*/
static generateFrom(values: string[] | string): Alphabet
/**
* Generates an alphabet containing relevant characters from the Unicode
* standard. Contains the Unicode planes 0 and 1.
*
* @returns - The generated alphabet.
*/
static generateRecommendedAlphabet(): Alphabet
/**
* Generates an alphabet containing all characters from the Unicode standard
* except for irrelevant Unicode planes. Contains the Unicode planes 0, 1, 2
* and 3.
*
* @returns - The generated alphabet.
*/
static generateCompleteAlphabet(): Alphabet
private static getCharactersWithCase
/**
* Generates an alphabet containing relevant characters from the Unicode
* standard.
*
* @param maxCodePoint - The maximum code point to generate the alphabet to.
* @returns - The generated alphabet.
*/
private static generateAlphabetToRange
/**
* For each character with a lower and upper case, permutes the two cases so
* that the alphabet is ordered by the case priority entered.
*
* @example
*
* ```ts
* Alphabet.generateFrom('aAbBcdCD').prioritizeCase('uppercase') // Returns 'AaBbCDcd'.
* ```
*
* @param casePriority - The case to prioritize.
* @returns - The same alphabet instance with the cases prioritized.
*/
prioritizeCase(casePriority: 'lowercase' | 'uppercase'): this
/**
* Adds specific characters to the end of the alphabet.
*
* @example
*
* ```ts
* Alphabet.generateFrom('ab').pushCharacters('cd')
* // Returns 'abcd'
* ```
*
* @param values - The characters to push to the alphabet.
* @returns - The same alphabet instance without the specified characters.
*/
pushCharacters(values: string[] | string): this
/**
* Permutes characters with cases so that all characters with the entered
* case are put before the other characters.
*
* @param caseToComeFirst - The case to put before the other characters.
* @returns - The same alphabet instance with all characters with case
* before all the characters with the other case.
*/
placeAllWithCaseBeforeAllWithOtherCase(
caseToComeFirst: 'uppercase' | 'lowercase',
): this
/**
* Places a specific character right before another character in the
* alphabet.
*
* @example
*
* ```ts
* Alphabet.generateFrom('ab-cd/').placeCharacterBefore({
* characterBefore: '/',
* characterAfter: '-',
* })
* // Returns 'ab/-cd'
* ```
*
* @param params - The parameters for the operation.
* @param params.characterBefore - The character to come before
* characterAfter.
* @param params.characterAfter - The target character.
* @returns - The same alphabet instance with the specific character
* prioritized.
*/
placeCharacterBefore({
characterBefore,
characterAfter,
}: {
characterBefore: string
characterAfter: string
}): this
/**
* Places a specific character right after another character in the
* alphabet.
*
* @example
*
* ```ts
* Alphabet.generateFrom('ab-cd/').placeCharacterAfter({
* characterBefore: '/',
* characterAfter: '-',
* })
* // Returns 'abcd/-'
* ```
*
* @param params - The parameters for the operation.
* @param params.characterBefore - The target character.
* @param params.characterAfter - The character to come after
* characterBefore.
* @returns - The same alphabet instance with the specific character
* prioritized.
*/
placeCharacterAfter({
characterBefore,
characterAfter,
}: {
characterBefore: string
characterAfter: string
}): this
/**
* Removes specific characters from the alphabet by their range.
*
* @param range - The Unicode range to remove characters from.
* @param range.start - The starting Unicode codepoint.
* @param range.end - The ending Unicode codepoint.
* @returns - The same alphabet instance without the characters from the
* specified range.
*/
removeUnicodeRange({ start, end }: { start: number; end: number }): this
/**
* Sorts the alphabet by the sorting function provided.
*
* @param sortingFunction - The sorting function to use.
* @returns - The same alphabet instance sorted by the sorting function
* provided.
*/
sortBy(
sortingFunction: (characterA: string, characterB: string) => number,
): this
/**
* Sorts the alphabet by the natural order of the characters using
* `natural-orderby`.
*
* @param locale - The locale to use for sorting.
* @returns - The same alphabet instance sorted by the natural order of the
* characters.
*/
sortByNaturalSort(locale?: string): this
/**
* Removes specific characters from the alphabet.
*
* @example
*
* ```ts
* Alphabet.generateFrom('abcd').removeCharacters('dcc')
* // Returns 'ab'
* ```
*
* @param values - The characters to remove from the alphabet.
* @returns - The same alphabet instance without the specified characters.
*/
removeCharacters(values: string[] | string): this
/**
* Sorts the alphabet by the character code point.
*
* @returns - The same alphabet instance sorted by the character code point.
*/
sortByCharCodeAt(): this
/**
* Sorts the alphabet by the locale order of the characters.
*
* @param locales - The locales to use for sorting.
* @returns - The same alphabet instance sorted by the locale order of the
* characters.
*/
sortByLocaleCompare(locales?: Intl.LocalesArgument): this
/**
* Retrieves the characters from the alphabet.
*
* @returns The characters from the alphabet.
*/
getCharacters(): string
private placeCharacterBeforeOrAfter
private getCharactersWithCase
}
@@ -0,0 +1,397 @@
import { convertBooleanToSign } from './convert-boolean-to-sign.js'
import { compare } from 'natural-orderby'
/**
* Manages a customizable alphabet for sorting operations.
*
* Provides methods to generate, modify, and sort alphabets based on various
* criteria including Unicode ranges, case prioritization, and locale-specific
* ordering. Used by the 'custom' sort type to define character precedence.
*
* The class supports:
*
* - Generating alphabets from strings or Unicode ranges
* - Case prioritization and manipulation
* - Character positioning and removal
* - Multiple sorting strategies (natural, locale, char code).
*
* @example
*
* ```ts
* // Create a custom alphabet with specific character order
* const alphabet = Alphabet.generateFrom('aAbBcC')
* .prioritizeCase('uppercase')
* .getCharacters()
* // Returns: 'AaBbCc'
* ```
*/
var Alphabet = class Alphabet {
characters = []
constructor(characters) {
this.characters = characters
}
/**
* Generates an alphabet from the given characters.
*
* @param values - The characters to generate the alphabet from.
* @returns - The wrapped alphabet.
*/
static generateFrom(values) {
let arrayValues = typeof values === 'string' ? [...values] : values
if (arrayValues.length !== new Set(arrayValues).size) {
throw new Error('The alphabet must not contain repeated characters')
}
if (arrayValues.some(value => value.length !== 1)) {
throw new Error('The alphabet must contain single characters')
}
return new Alphabet(
arrayValues.map(value =>
Alphabet.getCharactersWithCase(value.codePointAt(0)),
),
)
}
/**
* Generates an alphabet containing relevant characters from the Unicode
* standard. Contains the Unicode planes 0 and 1.
*
* @returns - The generated alphabet.
*/
static generateRecommendedAlphabet() {
return Alphabet.generateAlphabetToRange(131072)
}
/**
* Generates an alphabet containing all characters from the Unicode standard
* except for irrelevant Unicode planes. Contains the Unicode planes 0, 1, 2
* and 3.
*
* @returns - The generated alphabet.
*/
static generateCompleteAlphabet() {
return Alphabet.generateAlphabetToRange(262144)
}
static getCharactersWithCase(codePoint) {
let character = String.fromCodePoint(codePoint)
let lowercaseCharacter = character.toLowerCase()
let uppercaseCharacter = character.toUpperCase()
return {
value: character,
codePoint,
...(lowercaseCharacter === character ? null : (
{ lowercaseCharacterCodePoint: lowercaseCharacter.codePointAt(0) }
)),
...(uppercaseCharacter === character ? null : (
{ uppercaseCharacterCodePoint: uppercaseCharacter.codePointAt(0) }
)),
}
}
/**
* Generates an alphabet containing relevant characters from the Unicode
* standard.
*
* @param maxCodePoint - The maximum code point to generate the alphabet to.
* @returns - The generated alphabet.
*/
static generateAlphabetToRange(maxCodePoint) {
return new Alphabet(
Array.from({ length: maxCodePoint }, (_, i) =>
Alphabet.getCharactersWithCase(i),
),
)
}
/**
* For each character with a lower and upper case, permutes the two cases so
* that the alphabet is ordered by the case priority entered.
*
* @example
*
* ```ts
* Alphabet.generateFrom('aAbBcdCD').prioritizeCase('uppercase') // Returns 'AaBbCDcd'.
* ```
*
* @param casePriority - The case to prioritize.
* @returns - The same alphabet instance with the cases prioritized.
*/
prioritizeCase(casePriority) {
let charactersWithCase = this.getCharactersWithCase()
let parsedIndexes = /* @__PURE__ */ new Set()
let indexByCodePoints = this.characters.reduce(
(indexByCodePoint, character, index) => {
indexByCodePoint[character.codePoint] = index
return indexByCodePoint
},
{},
)
for (let { character, index } of charactersWithCase) {
if (parsedIndexes.has(index)) {
continue
}
parsedIndexes.add(index)
let otherCharacterIndex =
indexByCodePoints[
character.uppercaseCharacterCodePoint ??
character.lowercaseCharacterCodePoint
]
if (otherCharacterIndex === void 0) {
continue
}
parsedIndexes.add(otherCharacterIndex)
if (!character.uppercaseCharacterCodePoint) {
if (
(casePriority === 'uppercase' && index < otherCharacterIndex) ||
(casePriority === 'lowercase' && index > otherCharacterIndex)
) {
continue
}
} else if (
(casePriority === 'uppercase' && index > otherCharacterIndex) ||
(casePriority === 'lowercase' && index < otherCharacterIndex)
) {
continue
}
this.characters[index] = this.characters[otherCharacterIndex]
this.characters[otherCharacterIndex] = character
}
return this
}
/**
* Adds specific characters to the end of the alphabet.
*
* @example
*
* ```ts
* Alphabet.generateFrom('ab').pushCharacters('cd')
* // Returns 'abcd'
* ```
*
* @param values - The characters to push to the alphabet.
* @returns - The same alphabet instance without the specified characters.
*/
pushCharacters(values) {
let arrayValues = typeof values === 'string' ? [...values] : values
let valuesSet = new Set(arrayValues)
let valuesAlreadyExisting = this.characters.filter(({ value }) =>
valuesSet.has(value),
)
if (valuesAlreadyExisting.length > 0) {
throw new Error(
`The alphabet already contains the characters ${valuesAlreadyExisting
.slice(0, 5)
.map(({ value }) => value)
.join(', ')}`,
)
}
if (arrayValues.some(value => value.length !== 1)) {
throw new Error('Only single characters may be pushed')
}
this.characters.push(
...[...valuesSet].map(value =>
Alphabet.getCharactersWithCase(value.codePointAt(0)),
),
)
return this
}
/**
* Permutes characters with cases so that all characters with the entered case
* are put before the other characters.
*
* @param caseToComeFirst - The case to put before the other characters.
* @returns - The same alphabet instance with all characters with case before
* all the characters with the other case.
*/
placeAllWithCaseBeforeAllWithOtherCase(caseToComeFirst) {
let otherCaseKey =
caseToComeFirst === 'uppercase' ?
'uppercaseCharacterCodePoint'
: 'lowercaseCharacterCodePoint'
let keep = []
let move = []
for (let character of this.characters) {
;(character[otherCaseKey] === void 0 ? keep : move).push(character)
}
this.characters = [...keep, ...move]
return this
}
/**
* Places a specific character right before another character in the alphabet.
*
* @example
*
* ```ts
* Alphabet.generateFrom('ab-cd/').placeCharacterBefore({
* characterBefore: '/',
* characterAfter: '-',
* })
* // Returns 'ab/-cd'
* ```
*
* @param params - The parameters for the operation.
* @param params.characterBefore - The character to come before
* characterAfter.
* @param params.characterAfter - The target character.
* @returns - The same alphabet instance with the specific character
* prioritized.
*/
placeCharacterBefore({ characterBefore, characterAfter }) {
return this.placeCharacterBeforeOrAfter({
characterBefore,
characterAfter,
type: 'before',
})
}
/**
* Places a specific character right after another character in the alphabet.
*
* @example
*
* ```ts
* Alphabet.generateFrom('ab-cd/').placeCharacterAfter({
* characterBefore: '/',
* characterAfter: '-',
* })
* // Returns 'abcd/-'
* ```
*
* @param params - The parameters for the operation.
* @param params.characterBefore - The target character.
* @param params.characterAfter - The character to come after characterBefore.
* @returns - The same alphabet instance with the specific character
* prioritized.
*/
placeCharacterAfter({ characterBefore, characterAfter }) {
return this.placeCharacterBeforeOrAfter({
characterBefore,
characterAfter,
type: 'after',
})
}
/**
* Removes specific characters from the alphabet by their range.
*
* @param range - The Unicode range to remove characters from.
* @param range.start - The starting Unicode codepoint.
* @param range.end - The ending Unicode codepoint.
* @returns - The same alphabet instance without the characters from the
* specified range.
*/
removeUnicodeRange({ start, end }) {
this.characters = this.characters.filter(
({ codePoint }) => codePoint < start || codePoint > end,
)
return this
}
/**
* Sorts the alphabet by the sorting function provided.
*
* @param sortingFunction - The sorting function to use.
* @returns - The same alphabet instance sorted by the sorting function
* provided.
*/
sortBy(sortingFunction) {
this.characters.sort((a, b) => sortingFunction(a.value, b.value))
return this
}
/**
* Sorts the alphabet by the natural order of the characters using
* `natural-orderby`.
*
* @param locale - The locale to use for sorting.
* @returns - The same alphabet instance sorted by the natural order of the
* characters.
*/
sortByNaturalSort(locale) {
let naturalCompare = compare({ locale })
return this.sortBy((a, b) => naturalCompare(a, b))
}
/**
* Removes specific characters from the alphabet.
*
* @example
*
* ```ts
* Alphabet.generateFrom('abcd').removeCharacters('dcc')
* // Returns 'ab'
* ```
*
* @param values - The characters to remove from the alphabet.
* @returns - The same alphabet instance without the specified characters.
*/
removeCharacters(values) {
this.characters = this.characters.filter(
({ value }) => !values.includes(value),
)
return this
}
/**
* Sorts the alphabet by the character code point.
*
* @returns - The same alphabet instance sorted by the character code point.
*/
sortByCharCodeAt() {
return this.sortBy((a, b) =>
convertBooleanToSign(a.charCodeAt(0) > b.charCodeAt(0)),
)
}
/**
* Sorts the alphabet by the locale order of the characters.
*
* @param locales - The locales to use for sorting.
* @returns - The same alphabet instance sorted by the locale order of the
* characters.
*/
sortByLocaleCompare(locales) {
return this.sortBy((a, b) => a.localeCompare(b, locales))
}
/**
* Retrieves the characters from the alphabet.
*
* @returns The characters from the alphabet.
*/
getCharacters() {
return this.characters.map(({ value }) => value).join('')
}
placeCharacterBeforeOrAfter({ characterBefore, characterAfter, type }) {
let indexOfCharacterAfter = this.characters.findIndex(
({ value }) => value === characterAfter,
)
let indexOfCharacterBefore = this.characters.findIndex(
({ value }) => value === characterBefore,
)
if (indexOfCharacterAfter === -1) {
throw new Error(`Character ${characterAfter} not found in alphabet`)
}
if (indexOfCharacterBefore === -1) {
throw new Error(`Character ${characterBefore} not found in alphabet`)
}
if (indexOfCharacterBefore <= indexOfCharacterAfter) {
return this
}
this.characters.splice(
type === 'before' ? indexOfCharacterAfter : indexOfCharacterBefore + 1,
0,
this.characters[
type === 'before' ? indexOfCharacterBefore : indexOfCharacterAfter
],
)
this.characters.splice(
type === 'before' ? indexOfCharacterBefore + 1 : indexOfCharacterAfter,
1,
)
return this
}
getCharactersWithCase() {
return this.characters
.map((character, index) => {
if (
!character.uppercaseCharacterCodePoint &&
!character.lowercaseCharacterCodePoint
) {
return null
}
return {
character,
index,
}
})
.filter(element => element !== null)
}
}
export { Alphabet }
@@ -0,0 +1,7 @@
/**
* Assert that the given member is of type `never`. This is useful for
* exhaustive checks in switch statements or conditional logic.
*
* @param _member - The member to check, which should be of type `never`.
*/
export declare function assertIsNever(_member: never): void
@@ -0,0 +1,8 @@
/**
* Assert that the given member is of type `never`. This is useful for
* exhaustive checks in switch statements or conditional logic.
*
* @param _member - The member to check, which should be of type `never`.
*/
function assertIsNever(_member) {}
export { assertIsNever }
@@ -0,0 +1,46 @@
import { RuleContext } from '@typescript-eslint/utils/ts-eslint'
import { AST_NODE_TYPES } from '@typescript-eslint/utils'
import { NodeOfType } from '../types/node-of-type.js'
type Sorter<
MessageId extends string,
Options extends BaseOptions[],
NodeTypes extends AST_NODE_TYPES,
> = (parameters: {
context: Readonly<RuleContext<MessageId, Options>>
matchedAstSelectors: ReadonlySet<string>
node: NodeOfType<NodeTypes>
}) => void
type AstListeners<NodeTypes extends AST_NODE_TYPES> = Record<
string,
(node: NodeOfType<NodeTypes>) => void
>
interface BaseOptions {
useConfigurationIf?: {
matchesAstSelector?: string
}
}
/**
* Builds the AST listeners for the rule based on the provided node types,
* context, and sorter function.
*
* @param params - The parameters object.
* @param params.nodeTypes - The AST node types to listen for.
* @param params.context - The rule context.
* @param params.sorter - The function that sorts the nodes based on the
* provided parameters.
* @returns An object containing the AST listeners for the specified node types.
*/
export declare function buildAstListeners<
MessageId extends string,
Options extends BaseOptions[],
NodeTypes extends AST_NODE_TYPES,
>({
nodeTypes,
context,
sorter,
}: {
sorter: Sorter<MessageId, Options, NoInfer<NodeTypes>>
context: Readonly<RuleContext<MessageId, Options>>
nodeTypes: NodeTypes[]
}): AstListeners<NodeTypes>
export {}
@@ -0,0 +1,67 @@
/**
* Builds the AST listeners for the rule based on the provided node types,
* context, and sorter function.
*
* @param params - The parameters object.
* @param params.nodeTypes - The AST node types to listen for.
* @param params.context - The rule context.
* @param params.sorter - The function that sorts the nodes based on the
* provided parameters.
* @returns An object containing the AST listeners for the specified node types.
*/
function buildAstListeners({ nodeTypes, context, sorter }) {
let emptyMatchedAstSelectors = /* @__PURE__ */ new Set()
let matchedAstSelectorsByNode = /* @__PURE__ */ new WeakMap()
let allAstSelectorMatchers = [
...new Set(
context.options
.map(option => option.useConfigurationIf?.matchesAstSelector)
.filter(matchesAstSelector => matchesAstSelector !== void 0),
),
].map(astSelector => [
astSelector,
buildMatchedAstSelectorsCollector({
matchedAstSelectorsByNode,
astSelector,
nodeTypes,
}),
])
return {
...Object.fromEntries(allAstSelectorMatchers),
...Object.fromEntries(nodeTypes.map(buildNodeTypeExitListener)),
}
function buildNodeTypeExitListener(nodeType) {
return [
`${nodeType}:exit`,
node =>
sorter({
matchedAstSelectors:
matchedAstSelectorsByNode.get(node) ?? emptyMatchedAstSelectors,
context,
node,
}),
]
}
}
function buildMatchedAstSelectorsCollector({
matchedAstSelectorsByNode,
astSelector,
nodeTypes,
}) {
return collectMatchedAstSelectors
function collectMatchedAstSelectors(node) {
if (!isNodeOfType(node)) {
return
}
let matchedAstSelectors = matchedAstSelectorsByNode.get(node)
if (!matchedAstSelectors) {
matchedAstSelectors = /* @__PURE__ */ new Set()
matchedAstSelectorsByNode.set(node, matchedAstSelectors)
}
matchedAstSelectors.add(astSelector)
}
function isNodeOfType(node) {
return nodeTypes.includes(node.type)
}
}
export { buildAstListeners }
@@ -0,0 +1,31 @@
import { OptionsByGroupIndexComputer } from './sort-nodes-by-groups.js'
import { CommonGroupsOptions } from '../types/common-groups-options.js'
import { CommonOptions } from '../types/common-options.js'
type Options = Pick<
CommonGroupsOptions<string, unknown, unknown>,
'customGroups' | 'groups'
> &
CommonOptions
/**
* Creates a function that retrieves overridden options for a specific group
* index.
*
* Returns a closure that captures the options and provides a convenient way to
* get overridden options for any group index. This is used in sorting
* algorithms that need to apply different sorting rules to different groups.
*
* @example
*
* ```ts
* const getOverriddenOptions = buildOptionsByGroupIndexComputer(options)
* const group1Options = getOverriddenOptions(0)
* const group2Options = getOverriddenOptions(1)
* ```
*
* @param options - Base sorting options with group configuration.
* @returns Function that takes a group index and returns overridden options.
*/
export declare function buildOptionsByGroupIndexComputer<T extends Options>(
options: T,
): OptionsByGroupIndexComputer<T>
export {}
@@ -0,0 +1,24 @@
import { computeOverriddenOptionsByGroupIndex } from './compute-overridden-options-by-group-index.js'
/**
* Creates a function that retrieves overridden options for a specific group
* index.
*
* Returns a closure that captures the options and provides a convenient way to
* get overridden options for any group index. This is used in sorting
* algorithms that need to apply different sorting rules to different groups.
*
* @example
*
* ```ts
* const getOverriddenOptions = buildOptionsByGroupIndexComputer(options)
* const group1Options = getOverriddenOptions(0)
* const group2Options = getOverriddenOptions(1)
* ```
*
* @param options - Base sorting options with group configuration.
* @returns Function that takes a group index and returns overridden options.
*/
function buildOptionsByGroupIndexComputer(options) {
return groupIndex => computeOverriddenOptionsByGroupIndex(options, groupIndex)
}
export { buildOptionsByGroupIndexComputer }
@@ -0,0 +1,10 @@
import { SortingNode } from '../types/sorting-node.js'
/**
* Builds a map from nodes to their corresponding sorting nodes.
*
* @param sortingNodes - An array of sorting nodes.
* @returns A map where each key is a node and the value is its sorting node.
*/
export declare function buildSortingNodeByNodeMap<
T extends Pick<SortingNode, 'node'>,
>(sortingNodes: T[]): Map<T['node'], T>
@@ -0,0 +1,14 @@
/**
* Builds a map from nodes to their corresponding sorting nodes.
*
* @param sortingNodes - An array of sorting nodes.
* @returns A map where each key is a node and the value is its sorting node.
*/
function buildSortingNodeByNodeMap(sortingNodes) {
let sortingNodeByNode = /* @__PURE__ */ new Map()
for (let sortingNode of sortingNodes) {
sortingNodeByNode.set(sortingNode.node, sortingNode)
}
return sortingNodeByNode
}
export { buildSortingNodeByNodeMap }
@@ -0,0 +1,13 @@
import { Comparator } from './default-comparator-by-options-computer.js'
import { CommonOptions } from '../../types/common-options.js'
import { SortingNode } from '../../types/sorting-node.js'
/**
* Creates a comparator function that sorts nodes by their line length.
*
* @param options - Options containing the sort order.
* @param options.order - The order direction ('asc' or 'desc').
* @returns A comparator function that compares two sorting nodes by their size.
*/
export declare function buildLineLengthComparator({
order,
}: Pick<CommonOptions, 'order'>): Comparator<SortingNode>
@@ -0,0 +1,14 @@
import { computeOrderedValue } from './compute-ordered-value.js'
/**
* Creates a comparator function that sorts nodes by their line length.
*
* @param options - Options containing the sort order.
* @param options.order - The order direction ('asc' or 'desc').
* @returns A comparator function that compares two sorting nodes by their size.
*/
function buildLineLengthComparator({ order }) {
return (a, b) => {
return computeOrderedValue(a.size - b.size, order)
}
}
export { buildLineLengthComparator }
@@ -0,0 +1,28 @@
import { CommonOptions } from '../../types/common-options.js'
/**
* Creates a function that formats strings for comparison.
*
* Applies transformations based on the provided options:
*
* - Case normalization (lowercase if ignoreCase is true)
* - Special character handling (keep, trim, or remove)
* - Whitespace removal (always applied).
*
* @param params - Parameters for string formatting.
* @param params.ignoreCase - Whether to convert strings to lowercase.
* @param params.specialCharacters - How to handle special characters:
*
* - 'keep': Keep all characters as-is
* - 'trim': Remove leading special characters
* - 'remove': Remove all special characters.
*
* @returns Function that formats a string for comparison.
* @throws {UnreachableCaseError} If an unknown special characters option is
* specified.
*/
export declare function buildStringFormatter({
specialCharacters,
ignoreCase,
}: Pick<CommonOptions, 'specialCharacters' | 'ignoreCase'>): (
value: string,
) => string
@@ -0,0 +1,51 @@
import { UnreachableCaseError } from '../unreachable-case-error.js'
/**
* Creates a function that formats strings for comparison.
*
* Applies transformations based on the provided options:
*
* - Case normalization (lowercase if ignoreCase is true)
* - Special character handling (keep, trim, or remove)
* - Whitespace removal (always applied).
*
* @param params - Parameters for string formatting.
* @param params.ignoreCase - Whether to convert strings to lowercase.
* @param params.specialCharacters - How to handle special characters:
*
* - 'keep': Keep all characters as-is
* - 'trim': Remove leading special characters
* - 'remove': Remove all special characters.
*
* @returns Function that formats a string for comparison.
* @throws {UnreachableCaseError} If an unknown special characters option is
* specified.
*/
function buildStringFormatter({ specialCharacters, ignoreCase }) {
return value => {
let valueToCompare = value
if (ignoreCase) {
valueToCompare = valueToCompare.toLowerCase()
}
switch (specialCharacters) {
case 'remove':
valueToCompare = valueToCompare.replaceAll(
/[^a-z\u{C0}-\u{24F}\u{1E00}-\u{1EFF}]+/giu,
'',
)
break
case 'trim':
valueToCompare = valueToCompare.replaceAll(
/^[^a-z\u{C0}-\u{24F}\u{1E00}-\u{1EFF}]+/giu,
'',
)
break
case 'keep':
break
/* v8 ignore next 2 -- @preserve Exhaustive guard. */
default:
throw new UnreachableCaseError(specialCharacters)
}
return valueToCompare.replaceAll(/\s/gu, '')
}
}
export { buildStringFormatter }
@@ -0,0 +1,10 @@
import { Comparator } from './default-comparator-by-options-computer.js'
import { AllCommonOptions } from '../../types/all-common-options.js'
import { SortingNode } from '../../types/sorting-node.js'
export declare function buildSubgroupOrderComparator({
groups,
order,
}: Pick<
AllCommonOptions<string, unknown, unknown>,
'groups' | 'order'
>): Comparator<SortingNode>
@@ -0,0 +1,52 @@
import { isGroupWithOverridesOption } from '../is-group-with-overrides-option.js'
import { isNewlinesBetweenOption } from '../is-newlines-between-option.js'
import { UnreachableCaseError } from '../unreachable-case-error.js'
import { computeOrderedValue } from './compute-ordered-value.js'
function buildSubgroupOrderComparator({ groups, order }) {
return (a, b) => {
let subgroupContainingA = computeSubgroupContainingNode(a, groups)
let subgroupContainingB = computeSubgroupContainingNode(b, groups)
if (
!subgroupContainingA ||
!subgroupContainingB ||
subgroupContainingA !== subgroupContainingB
) {
return 0
}
return computeOrderedValue(
subgroupContainingA.indexOf(a.group) -
subgroupContainingB.indexOf(b.group),
order,
)
}
}
function computeSubgroupContainingNode(sortingNode, groups) {
for (let group of groups) {
if (isNewlinesBetweenOption(group)) {
continue
}
if (typeof group === 'string' || Array.isArray(group)) {
if (doesStringSubgroupContainsNode(sortingNode, group)) {
return group
}
continue
}
/* v8 ignore else -- @preserve Exhaustive guard for unsupported group option. */
if (isGroupWithOverridesOption(group)) {
if (doesStringSubgroupContainsNode(sortingNode, group.group)) {
return group.group
}
continue
}
/* v8 ignore next -- @preserve Exhaustive guard for unsupported group option. */
throw new UnreachableCaseError(group)
}
return null
}
function doesStringSubgroupContainsNode(sortingNode, subgroup) {
if (typeof subgroup === 'string') {
return false
}
return subgroup.includes(sortingNode.group)
}
export { buildSubgroupOrderComparator }
@@ -0,0 +1,29 @@
import { CommonOptions } from '../../types/common-options.js'
/**
* Compares two strings alphabetically using locale-aware comparison.
*
* Applies string formatting based on options (case sensitivity, special
* characters handling) before performing the comparison.
*
* @param a - The first string to compare.
* @param b - The second string to compare.
* @param options - Comparison options.
* @param options.specialCharacters - How to handle special characters.
* @param options.ignoreCase - Whether to ignore case differences.
* @param options.locales - The locale(s) to use for comparison.
* @param options.order - The order direction ('asc' or 'desc').
* @returns A negative number if a < b, positive if a > b, or 0 if equal.
*/
export declare function compareAlphabetically(
a: string,
b: string,
{
specialCharacters,
ignoreCase,
locales,
order,
}: Pick<
CommonOptions,
'specialCharacters' | 'ignoreCase' | 'locales' | 'order'
>,
): number
@@ -0,0 +1,32 @@
import { computeOrderedValue } from './compute-ordered-value.js'
import { buildStringFormatter } from './build-string-formatter.js'
/**
* Compares two strings alphabetically using locale-aware comparison.
*
* Applies string formatting based on options (case sensitivity, special
* characters handling) before performing the comparison.
*
* @param a - The first string to compare.
* @param b - The second string to compare.
* @param options - Comparison options.
* @param options.specialCharacters - How to handle special characters.
* @param options.ignoreCase - Whether to ignore case differences.
* @param options.locales - The locale(s) to use for comparison.
* @param options.order - The order direction ('asc' or 'desc').
* @returns A negative number if a < b, positive if a > b, or 0 if equal.
*/
function compareAlphabetically(
a,
b,
{ specialCharacters, ignoreCase, locales, order },
) {
let formatString = buildStringFormatter({
specialCharacters,
ignoreCase,
})
return computeOrderedValue(
formatString(a).localeCompare(formatString(b), locales),
order,
)
}
export { compareAlphabetically }
@@ -0,0 +1,14 @@
import { CommonOptions } from '../../types/common-options.js'
export declare function compareByCustomSort(
a: string,
b: string,
{
specialCharacters,
ignoreCase,
alphabet,
order,
}: Pick<
CommonOptions,
'specialCharacters' | 'ignoreCase' | 'alphabet' | 'order'
>,
): number
@@ -0,0 +1,51 @@
import { computeOrderedValue } from './compute-ordered-value.js'
import { buildStringFormatter } from './build-string-formatter.js'
import { convertBooleanToSign } from '../convert-boolean-to-sign.js'
/**
* Cache for pre-computed character index maps to avoid recalculating for the
* same custom alphabets across multiple comparisons.
*/
var alphabetCache = /* @__PURE__ */ new Map()
function compareByCustomSort(
a,
b,
{ specialCharacters, ignoreCase, alphabet, order },
) {
let formatString = buildStringFormatter({
specialCharacters,
ignoreCase,
})
let indexByCharacters = alphabetCache.get(alphabet)
if (!indexByCharacters) {
indexByCharacters = /* @__PURE__ */ new Map()
for (let [index, character] of [...alphabet].entries()) {
indexByCharacters.set(character, index)
}
alphabetCache.set(alphabet, indexByCharacters)
}
let aValue = formatString(a)
let bValue = formatString(b)
let minLength = Math.min(aValue.length, bValue.length)
for (let i = 0; i < minLength; i++) {
let aCharacter = aValue[i]
let bCharacter = bValue[i]
let indexOfA = indexByCharacters.get(aCharacter)
let indexOfB = indexByCharacters.get(bCharacter)
indexOfA ??= Infinity
indexOfB ??= Infinity
if (indexOfA !== indexOfB) {
return computeOrderedValue(
convertBooleanToSign(indexOfA - indexOfB > 0),
order,
)
}
}
if (aValue.length === bValue.length) {
return 0
}
return computeOrderedValue(
convertBooleanToSign(aValue.length - bValue.length > 0),
order,
)
}
export { compareByCustomSort }
@@ -0,0 +1,30 @@
import { CommonOptions } from '../../types/common-options.js'
/**
* Compares two strings using natural sort order.
*
* Natural sorting handles embedded numbers intelligently, so "item2" comes
* before "item10". Applies string formatting based on options before performing
* the comparison.
*
* @param a - The first string to compare.
* @param b - The second string to compare.
* @param options - Comparison options.
* @param options.specialCharacters - How to handle special characters.
* @param options.ignoreCase - Whether to ignore case differences.
* @param options.locales - The locale(s) to use for comparison.
* @param options.order - The order direction ('asc' or 'desc').
* @returns A negative number if a < b, positive if a > b, or 0 if equal.
*/
export declare function compareNaturally(
a: string,
b: string,
{
specialCharacters,
ignoreCase,
locales,
order,
}: Pick<
CommonOptions,
'specialCharacters' | 'ignoreCase' | 'locales' | 'order'
>,
): number
@@ -0,0 +1,35 @@
import { computeOrderedValue } from './compute-ordered-value.js'
import { buildStringFormatter } from './build-string-formatter.js'
import { compare } from 'natural-orderby'
/**
* Compares two strings using natural sort order.
*
* Natural sorting handles embedded numbers intelligently, so "item2" comes
* before "item10". Applies string formatting based on options before performing
* the comparison.
*
* @param a - The first string to compare.
* @param b - The second string to compare.
* @param options - Comparison options.
* @param options.specialCharacters - How to handle special characters.
* @param options.ignoreCase - Whether to ignore case differences.
* @param options.locales - The locale(s) to use for comparison.
* @param options.order - The order direction ('asc' or 'desc').
* @returns A negative number if a < b, positive if a > b, or 0 if equal.
*/
function compareNaturally(
a,
b,
{ specialCharacters, ignoreCase, locales, order },
) {
let naturalCompare = compare({ locale: locales.toString() })
let formatString = buildStringFormatter({
specialCharacters,
ignoreCase,
})
return computeOrderedValue(
naturalCompare(formatString(a), formatString(b)),
order,
)
}
export { compareNaturally }
@@ -0,0 +1,25 @@
import {
ComparatorByOptionsComputer,
Comparator,
} from './default-comparator-by-options-computer.js'
import { CommonOptions } from '../../types/common-options.js'
import { SortingNode } from '../../types/sorting-node.js'
/**
* Computes the array of comparators to use for sorting based on options.
*
* Returns an array containing the main comparator and a fallback comparator. If
* the main comparator is the unsorted comparator, returns an empty array since
* no sorting should be performed.
*
* @param comparatorByOptionsComputer - Function that creates a comparator from
* options.
* @param options - The sorting options including fallback sort configuration.
* @returns An array of comparators, or empty array if sorting is disabled.
*/
export declare function computeComparators<
Options extends Pick<CommonOptions, 'fallbackSort'>,
T extends SortingNode,
>(
comparatorByOptionsComputer: ComparatorByOptionsComputer<Options, T>,
options: Options,
): Comparator<T>[]
@@ -0,0 +1,27 @@
import { unsortedComparator } from './unsorted-comparator.js'
/**
* Computes the array of comparators to use for sorting based on options.
*
* Returns an array containing the main comparator and a fallback comparator. If
* the main comparator is the unsorted comparator, returns an empty array since
* no sorting should be performed.
*
* @param comparatorByOptionsComputer - Function that creates a comparator from
* options.
* @param options - The sorting options including fallback sort configuration.
* @returns An array of comparators, or empty array if sorting is disabled.
*/
function computeComparators(comparatorByOptionsComputer, options) {
let mainComparator = comparatorByOptionsComputer(options)
if (mainComparator === unsortedComparator) {
return []
}
return [
mainComparator,
comparatorByOptionsComputer({
...options,
...options.fallbackSort,
}),
]
}
export { computeComparators }
@@ -0,0 +1,15 @@
import { CommonOptions } from '../../types/common-options.js'
/**
* Adjusts a comparison result value based on the specified sort order.
*
* For ascending order, returns the value unchanged. For descending order,
* negates the value to reverse the sort direction.
*
* @param value - The comparison result value to adjust.
* @param order - The order direction ('asc' or 'desc').
* @returns The adjusted comparison value.
*/
export declare function computeOrderedValue(
value: number,
order: CommonOptions['order'],
): number
@@ -0,0 +1,23 @@
import { UnreachableCaseError } from '../unreachable-case-error.js'
/**
* Adjusts a comparison result value based on the specified sort order.
*
* For ascending order, returns the value unchanged. For descending order,
* negates the value to reverse the sort direction.
*
* @param value - The comparison result value to adjust.
* @param order - The order direction ('asc' or 'desc').
* @returns The adjusted comparison value.
*/
function computeOrderedValue(value, order) {
switch (order) {
case 'desc':
return -value
case 'asc':
return value
/* v8 ignore next 2 -- @preserve Exhaustive guard. */
default:
throw new UnreachableCaseError(order)
}
}
export { computeOrderedValue }
@@ -0,0 +1,19 @@
import { CommonOptions, TypeOption } from '../../types/common-options.js'
import { GroupsOptions } from '../../types/common-groups-options.js'
import { SortingNode } from '../../types/sorting-node.js'
export type ComparatorByOptionsComputer<S, T extends SortingNode> = (
options: S,
) => Comparator<T>
export type Comparator<T extends SortingNode> = (a: T, b: T) => number
type Options = Pick<
CommonOptions<TypeOption>,
'specialCharacters' | 'ignoreCase' | 'alphabet' | 'locales' | 'order' | 'type'
> &
Pick<CommonOptions, 'fallbackSort'> & {
groups?: GroupsOptions
}
export declare let defaultComparatorByOptionsComputer: ComparatorByOptionsComputer<
Options,
SortingNode
>
export {}
@@ -0,0 +1,33 @@
import { UnreachableCaseError } from '../unreachable-case-error.js'
import { buildSubgroupOrderComparator } from './build-subgroup-order-comparator.js'
import { buildLineLengthComparator } from './build-line-length-comparator.js'
import { compareAlphabetically } from './compare-alphabetically.js'
import { compareByCustomSort } from './compare-by-custom-sort.js'
import { unsortedComparator } from './unsorted-comparator.js'
import { compareNaturally } from './compare-naturally.js'
var defaultComparatorByOptionsComputer = options => {
switch (options.type) {
case 'subgroup-order':
if (!options.groups) {
return unsortedComparator
}
return buildSubgroupOrderComparator({
...options,
groups: options.groups,
})
case 'alphabetical':
return (a, b) => compareAlphabetically(a.name, b.name, options)
case 'line-length':
return buildLineLengthComparator(options)
case 'unsorted':
return unsortedComparator
case 'natural':
return (a, b) => compareNaturally(a.name, b.name, options)
case 'custom':
return (a, b) => compareByCustomSort(a.name, b.name, options)
/* v8 ignore next 2 -- @preserve Exhaustive guard. */
default:
throw new UnreachableCaseError(options.type)
}
}
export { defaultComparatorByOptionsComputer }
@@ -0,0 +1,3 @@
import { Comparator } from './default-comparator-by-options-computer.js'
import { SortingNode } from '../../types/sorting-node.js'
export declare let unsortedComparator: Comparator<SortingNode>
@@ -0,0 +1,2 @@
var unsortedComparator = () => 0
export { unsortedComparator }
@@ -0,0 +1,36 @@
import { Settings } from './get-settings.js'
/**
* Merges configuration options with settings and defaults in priority order.
*
* Combines three levels of configuration with increasing priority:
*
* 1. Default values (lowest priority)
* 2. Global settings from ESLint configuration.
* 3. Rule-specific options (highest priority).
*
* This ensures that user-provided options always override settings, and
* settings always override defaults.
*
* @example
*
* ```ts
* const finalOptions = complete(
* { type: 'natural' }, // User options (highest priority)
* { order: 'asc' }, // Global settings
* { type: 'alphabetical', order: 'desc' }, // Defaults (lowest priority)
* )
* // Returns: { type: 'natural', order: 'asc' }
* ```
*
* @template T - Type of the configuration object.
* @param options - Rule-specific options provided by the user (highest
* priority).
* @param settings - Global settings from ESLint configuration.
* @param defaults - Default values for the configuration (lowest priority).
* @returns Merged configuration object with all three levels combined.
*/
export declare function complete<T extends Record<string, unknown>>(
options?: Partial<T>,
settings?: Settings,
defaults?: T,
): T
@@ -0,0 +1,38 @@
/**
* Merges configuration options with settings and defaults in priority order.
*
* Combines three levels of configuration with increasing priority:
*
* 1. Default values (lowest priority)
* 2. Global settings from ESLint configuration.
* 3. Rule-specific options (highest priority).
*
* This ensures that user-provided options always override settings, and
* settings always override defaults.
*
* @example
*
* ```ts
* const finalOptions = complete(
* { type: 'natural' }, // User options (highest priority)
* { order: 'asc' }, // Global settings
* { type: 'alphabetical', order: 'desc' }, // Defaults (lowest priority)
* )
* // Returns: { type: 'natural', order: 'asc' }
* ```
*
* @template T - Type of the configuration object.
* @param options - Rule-specific options provided by the user (highest
* priority).
* @param settings - Global settings from ESLint configuration.
* @param defaults - Default values for the configuration (lowest priority).
* @returns Merged configuration object with all three levels combined.
*/
function complete(options = {}, settings = {}, defaults = {}) {
return {
...defaults,
...settings,
...options,
}
}
export { complete }
@@ -0,0 +1,13 @@
import { TSESLint } from '@typescript-eslint/utils'
import { TSESTree } from '@typescript-eslint/types'
/**
* Recursively computes all scope references deeply for a given node.
*
* @param node - The AST node.
* @param sourceCode - The source code object.
* @returns The list of scope references.
*/
export declare function computeDeepScopeReferences(
node: TSESTree.Node,
sourceCode: TSESLint.SourceCode,
): TSESLint.Scope.Reference[]
@@ -0,0 +1,17 @@
/**
* Recursively computes all scope references deeply for a given node.
*
* @param node - The AST node.
* @param sourceCode - The source code object.
* @returns The list of scope references.
*/
function computeDeepScopeReferences(node, sourceCode) {
return computeScopeReference(sourceCode.getScope(node))
function computeScopeReference(scope) {
return [
...scope.references,
...scope.childScopes.flatMap(computeScopeReference),
]
}
}
export { computeDeepScopeReferences }
@@ -0,0 +1,42 @@
import { TSESTree } from '@typescript-eslint/types'
import { TSESLint } from '@typescript-eslint/utils'
import { SortingNodeWithDependencies } from './sort-nodes-by-dependencies.js'
export type ShouldIgnoreIdentifierComputer<T> = (parameters: {
identifier: TSESTree.JSXIdentifier | TSESTree.Identifier
referencingSortingNode: T
}) => boolean
export type AdditionalIdentifierDependenciesComputer<T> = (parameters: {
reference: TSESLint.Scope.Reference
referencingSortingNode: T
}) => T[]
export type ShouldIgnoreSortingNodeComputer<T> = (sortingNode: T) => boolean
/**
* Compute the list of dependencies for each sorting node.
*
* @param params - The parameters object.
* @param params.additionalIdentifierDependenciesComputer - A function to
* compute additional dependencies for an identifier.
* @param params.shouldIgnoreSortingNodeComputer - A function to determine if a
* sorting node should be ignored.
* @param params.shouldIgnoreIdentifierComputer - A function to determine if an
* identifier should be ignored.
* @param params.sortingNodes - The sorting nodes to compute dependencies for.
* @param params.sourceCode - The source code object.
* @returns A map of sorting nodes to their dependencies.
*/
export declare function computeDependenciesBySortingNode<
Node extends TSESTree.Node,
T extends Pick<SortingNodeWithDependencies<Node>, 'dependencyNames' | 'node'>,
>({
additionalIdentifierDependenciesComputer,
shouldIgnoreSortingNodeComputer,
shouldIgnoreIdentifierComputer,
sortingNodes,
sourceCode,
}: {
additionalIdentifierDependenciesComputer?: AdditionalIdentifierDependenciesComputer<T>
shouldIgnoreSortingNodeComputer?: ShouldIgnoreSortingNodeComputer<T>
shouldIgnoreIdentifierComputer?: ShouldIgnoreIdentifierComputer<T>
sourceCode: TSESLint.SourceCode
sortingNodes: T[]
}): Map<T, T[]>
@@ -0,0 +1,102 @@
import { computeDeepScopeReferences } from './compute-deep-scope-references.js'
import { rangeContainsRange } from './range-contains-range.js'
/**
* Compute the list of dependencies for each sorting node.
*
* @param params - The parameters object.
* @param params.additionalIdentifierDependenciesComputer - A function to
* compute additional dependencies for an identifier.
* @param params.shouldIgnoreSortingNodeComputer - A function to determine if a
* sorting node should be ignored.
* @param params.shouldIgnoreIdentifierComputer - A function to determine if an
* identifier should be ignored.
* @param params.sortingNodes - The sorting nodes to compute dependencies for.
* @param params.sourceCode - The source code object.
* @returns A map of sorting nodes to their dependencies.
*/
function computeDependenciesBySortingNode({
additionalIdentifierDependenciesComputer,
shouldIgnoreSortingNodeComputer,
shouldIgnoreIdentifierComputer,
sortingNodes,
sourceCode,
}) {
let returnValue = /* @__PURE__ */ new Map()
let references = sortingNodes.flatMap(sortingNode =>
computeDeepScopeReferences(sortingNode.node, sourceCode),
)
for (let reference of new Set(references)) {
let { identifier, resolved } = reference
if (!resolved) {
continue
}
let referencingSortingNode = findSortingNodeContainingIdentifier(
sortingNodes,
identifier,
)
if (!referencingSortingNode) {
continue
}
if (shouldIgnoreSortingNodeComputer?.(referencingSortingNode)) {
continue
}
let referencedNodes = returnValue.get(referencingSortingNode) ?? []
returnValue.set(referencingSortingNode, referencedNodes)
referencedNodes.push(
...computeMainIdentifierDependencies({
shouldIgnoreSortingNodeComputer,
shouldIgnoreIdentifierComputer,
referencingSortingNode,
sortingNodes,
identifier,
resolved,
}),
...(additionalIdentifierDependenciesComputer?.({
referencingSortingNode,
reference,
}) ?? []),
)
}
return returnValue
}
function computeMainIdentifierDependencies({
shouldIgnoreSortingNodeComputer,
shouldIgnoreIdentifierComputer,
referencingSortingNode,
sortingNodes,
identifier,
resolved,
}) {
if (
shouldIgnoreIdentifierComputer?.({
referencingSortingNode,
identifier,
})
) {
return []
}
let [firstIdentifier] = resolved.identifiers
if (!firstIdentifier) {
return []
}
let referencedSortingNode = findSortingNodeContainingIdentifier(
sortingNodes,
firstIdentifier,
)
if (!referencedSortingNode) {
return []
}
if (shouldIgnoreSortingNodeComputer?.(referencedSortingNode)) {
return []
}
if (referencedSortingNode === referencingSortingNode) {
return []
}
return [referencedSortingNode]
}
function findSortingNodeContainingIdentifier(sortingNodes, identifier) {
return sortingNodes.find(sortingNode =>
rangeContainsRange(sortingNode.node.range, identifier.range),
)
}
export { computeDependenciesBySortingNode }
@@ -0,0 +1,13 @@
import { TSESTree } from '@typescript-eslint/types'
import { TSESLint } from '@typescript-eslint/utils'
import { SortingNodeWithDependencies } from './sort-nodes-by-dependencies.js'
export declare function computeDependenciesOutsideFunctionsBySortingNode<
Node extends TSESTree.Node,
T extends Pick<SortingNodeWithDependencies<Node>, 'dependencyNames' | 'node'>,
>({
sortingNodes,
sourceCode,
}: {
sourceCode: TSESLint.SourceCode
sortingNodes: T[]
}): Map<T, T[]>
@@ -0,0 +1,29 @@
import { computeDependenciesBySortingNode } from './compute-dependencies-by-sorting-node.js'
import { computeParentNodesWithTypes } from './compute-parent-nodes-with-types.js'
import { AST_NODE_TYPES } from '@typescript-eslint/utils'
function computeDependenciesOutsideFunctionsBySortingNode({
sortingNodes,
sourceCode,
}) {
return computeDependenciesBySortingNode({
shouldIgnoreIdentifierComputer: buildShouldIgnoreIdentifierComputer(),
sortingNodes,
sourceCode,
})
function buildShouldIgnoreIdentifierComputer() {
return ({ referencingSortingNode, identifier }) => {
return (
computeParentNodesWithTypes({
allowedTypes: [
AST_NODE_TYPES.FunctionExpression,
AST_NODE_TYPES.ArrowFunctionExpression,
],
maxParent: referencingSortingNode.node,
consecutiveOnly: false,
node: identifier,
}).length > 0
)
}
}
}
export { computeDependenciesOutsideFunctionsBySortingNode }
@@ -0,0 +1,13 @@
import { GroupsOptions } from '../types/common-groups-options.js'
/**
* Computes the name of a group based on the provided group object.
*
* @param group - The group object.
* @returns A string if:
*
* - The group is a string.
* - The group is a commentAbove option with a string group.
*/
export declare function computeGroupName(
group: GroupsOptions[number],
): string | null
@@ -0,0 +1,33 @@
import { isGroupWithOverridesOption } from './is-group-with-overrides-option.js'
import { isNewlinesBetweenOption } from './is-newlines-between-option.js'
import { UnreachableCaseError } from './unreachable-case-error.js'
/**
* Computes the name of a group based on the provided group object.
*
* @param group - The group object.
* @returns A string if:
*
* - The group is a string.
* - The group is a commentAbove option with a string group.
*/
function computeGroupName(group) {
if (typeof group === 'string' || Array.isArray(group)) {
return computeStringGroupName(group)
}
if (isGroupWithOverridesOption(group)) {
return computeStringGroupName(group.group)
}
/* v8 ignore else -- @preserve Exhaustive guard for unsupported group option. */
if (isNewlinesBetweenOption(group)) {
return null
}
/* v8 ignore next -- @preserve Exhaustive guard for unsupported group option. */
throw new UnreachableCaseError(group)
}
function computeStringGroupName(group) {
if (typeof group === 'string') {
return group
}
return null
}
export { computeGroupName }
@@ -0,0 +1,72 @@
import {
CommonGroupsOptions,
AnyOfCustomGroup,
} from '../types/common-groups-options.js'
/**
* Parameters for computing the group of an element.
*
* @template CustomGroupMatchOptions - Type of custom group match options.
*/
interface ComputeGroupParameters<CustomGroupMatchOptions> {
/**
* Configuration options for grouping.
*/
options: Pick<
CommonGroupsOptions<string, unknown, CustomGroupMatchOptions>,
'customGroups' | 'groups'
>
/**
* Function to test if an element matches a custom group. Takes a custom
* group configuration and returns true if the element matches.
*/
customGroupMatcher: CustomGroupMatcher<CustomGroupMatchOptions>
/**
* List of predefined groups that the element belongs to. These are checked
* after custom groups as a fallback.
*/
predefinedGroups: string[]
}
type CustomGroupMatcher<MatchOptions> = (
customGroup: AnyOfCustomGroup<MatchOptions> | Partial<MatchOptions>,
) => boolean
/**
* Determines which group an element belongs to based on custom and predefined
* groups.
*
* The function checks groups in the following priority order:
*
* 1. Custom groups (if defined) - checked first, highest priority
* 2. Predefined groups - checked as fallback
* 3. Returns 'unknown' if no matching group is found.
*
* Only groups that exist in options.groups are considered valid.
*
* @example
*
* ```ts
* const group = computeGroup({
* options: {
* groups: ['react', 'external', 'internal'],
* customGroups: [{ groupName: 'react', anyOf: ['react', 'react-*'] }],
* },
* customGroupMatcher: customGroup => customGroup.anyOf.includes('react'),
* predefinedGroups: ['external'],
* name: 'react-dom',
* })
* // Returns: 'react'
* ```
*
* @template CustomGroupMatchOptions - Type of custom group match options.
* @param params - Parameters for group computation.
* @param params.options - Configuration with available groups and custom
* groups.
* @param params.customGroupMatcher - Matcher function for custom groups.
* @param params.predefinedGroups - Fallback predefined groups to check.
* @returns The matched group name or 'unknown' if no group matches.
*/
export declare function computeGroup<CustomGroupMatchOptions>({
customGroupMatcher,
predefinedGroups,
options,
}: ComputeGroupParameters<CustomGroupMatchOptions>): 'unknown' | string
export {}
@@ -0,0 +1,64 @@
import { computeGroupsNames } from './compute-groups-names.js'
/**
* Determines which group an element belongs to based on custom and predefined
* groups.
*
* The function checks groups in the following priority order:
*
* 1. Custom groups (if defined) - checked first, highest priority
* 2. Predefined groups - checked as fallback
* 3. Returns 'unknown' if no matching group is found.
*
* Only groups that exist in options.groups are considered valid.
*
* @example
*
* ```ts
* const group = computeGroup({
* options: {
* groups: ['react', 'external', 'internal'],
* customGroups: [{ groupName: 'react', anyOf: ['react', 'react-*'] }],
* },
* customGroupMatcher: customGroup => customGroup.anyOf.includes('react'),
* predefinedGroups: ['external'],
* name: 'react-dom',
* })
* // Returns: 'react'
* ```
*
* @template CustomGroupMatchOptions - Type of custom group match options.
* @param params - Parameters for group computation.
* @param params.options - Configuration with available groups and custom
* groups.
* @param params.customGroupMatcher - Matcher function for custom groups.
* @param params.predefinedGroups - Fallback predefined groups to check.
* @returns The matched group name or 'unknown' if no group matches.
*/
function computeGroup({ customGroupMatcher, predefinedGroups, options }) {
let groupsSet = new Set(computeGroupsNames(options.groups))
return (
computeFirstMatchingCustomGroupName(
groupsSet,
options.customGroups,
customGroupMatcher,
) ??
predefinedGroups.find(group => groupsSet.has(group)) ??
'unknown'
)
}
function computeFirstMatchingCustomGroupName(
groupsSet,
customGroups,
customGroupMatcher,
) {
for (let customGroup of customGroups) {
if (
customGroupMatcher(customGroup) &&
groupsSet.has(customGroup.groupName)
) {
return customGroup.groupName
}
}
return null
}
export { computeGroup }
@@ -0,0 +1,8 @@
import { GroupsOptions } from '../types/common-groups-options.js'
/**
* Computes the names of all groups based on the provided `GroupsOptions`.
*
* @param groups - An array of group options.
* @returns An array of computed group names as strings.
*/
export declare function computeGroupsNames(groups: GroupsOptions): string[]
@@ -0,0 +1,33 @@
import { isGroupWithOverridesOption } from './is-group-with-overrides-option.js'
import { isNewlinesBetweenOption } from './is-newlines-between-option.js'
import { UnreachableCaseError } from './unreachable-case-error.js'
/**
* Computes the names of all groups based on the provided `GroupsOptions`.
*
* @param groups - An array of group options.
* @returns An array of computed group names as strings.
*/
function computeGroupsNames(groups) {
return groups.flatMap(group => computeGroupNames(group))
}
function computeGroupNames(group) {
if (typeof group === 'string' || Array.isArray(group)) {
return computeStringGroupNames(group)
}
if (isGroupWithOverridesOption(group)) {
return computeStringGroupNames(group.group)
}
/* v8 ignore else -- @preserve Exhaustive guard for unsupported group option. */
if (isNewlinesBetweenOption(group)) {
return []
}
/* v8 ignore next -- @preserve Exhaustive guard for unsupported group option. */
throw new UnreachableCaseError(group)
}
function computeStringGroupNames(group) {
if (typeof group === 'string') {
return [group]
}
return group
}
export { computeGroupsNames }
@@ -0,0 +1,39 @@
import { SortingNodeWithDependencies } from './sort-nodes-by-dependencies.js'
/**
* Detects nodes that are part of circular dependency chains.
*
* Uses a depth-first search (DFS) algorithm with three-color marking to
* identify cycles in the dependency graph. When a cycle is detected, all nodes
* in that cycle are added to the result set.
*
* The algorithm tracks three states for each node:
*
* - Not visited: Node hasn't been processed yet
* - Visiting: Currently in the DFS path (gray in three-color marking)
* - Visited: Completely processed (black in three-color marking).
*
* A cycle is detected when we encounter a node that is already in the
* "visiting" state, meaning we've found a back edge in the graph.
*
* @example
*
* ```ts
* const nodes = [
* { name: 'A', dependencies: ['B'], dependencyNames: ['A'] },
* { name: 'B', dependencies: ['C'], dependencyNames: ['B'] },
* { name: 'C', dependencies: ['A'], dependencyNames: ['C'] },
* ]
* const circularNodes = computeNodesInCircularDependencies(nodes)
* // Returns: Set containing all three nodes (A, B, C)
* ```
*
* @template T - Type of sorting node with dependencies.
* @param elements - Array of nodes with dependency information.
* @returns Set of nodes that participate in circular dependencies.
*/
export declare function computeNodesInCircularDependencies<
T extends Pick<
SortingNodeWithDependencies,
'dependencyNames' | 'dependencies'
>,
>(elements: T[]): Set<T>
@@ -0,0 +1,85 @@
/**
* Detects nodes that are part of circular dependency chains.
*
* Uses a depth-first search (DFS) algorithm with three-color marking to
* identify cycles in the dependency graph. When a cycle is detected, all nodes
* in that cycle are added to the result set.
*
* The algorithm tracks three states for each node:
*
* - Not visited: Node hasn't been processed yet
* - Visiting: Currently in the DFS path (gray in three-color marking)
* - Visited: Completely processed (black in three-color marking).
*
* A cycle is detected when we encounter a node that is already in the
* "visiting" state, meaning we've found a back edge in the graph.
*
* @example
*
* ```ts
* const nodes = [
* { name: 'A', dependencies: ['B'], dependencyNames: ['A'] },
* { name: 'B', dependencies: ['C'], dependencyNames: ['B'] },
* { name: 'C', dependencies: ['A'], dependencyNames: ['C'] },
* ]
* const circularNodes = computeNodesInCircularDependencies(nodes)
* // Returns: Set containing all three nodes (A, B, C)
* ```
*
* @template T - Type of sorting node with dependencies.
* @param elements - Array of nodes with dependency information.
* @returns Set of nodes that participate in circular dependencies.
*/
function computeNodesInCircularDependencies(elements) {
let elementsInCycles = /* @__PURE__ */ new Set()
let visitingElements = /* @__PURE__ */ new Set()
let visitedElements = /* @__PURE__ */ new Set()
/**
* Performs depth-first search to detect cycles starting from the given
* element.
*
* Recursively traverses the dependency graph, maintaining a path of the
* current traversal. If a node in the current path is encountered again, a
* cycle is detected and all nodes in the cycle are marked.
*
* @param element - Current node being visited.
* @param path - Array of nodes in the current DFS path.
*/
function depthFirstSearch(element, path) {
if (visitedElements.has(element)) {
return
}
if (visitingElements.has(element)) {
let cycleStartIndex = path.indexOf(element)
/* v8 ignore else -- @preserve Visiting path already contains the element when this branch executes. */
if (cycleStartIndex !== -1) {
for (let cycleElements of path.slice(cycleStartIndex)) {
elementsInCycles.add(cycleElements)
}
}
return
}
visitingElements.add(element)
path.push(element)
for (let dependency of element.dependencies) {
let dependencyElement = elements
.filter(currentElement => currentElement !== element)
.find(currentElement =>
currentElement.dependencyNames.includes(dependency),
)
/* v8 ignore next -- @preserve Dependencies are pre-filtered; missing entries are defensive fallback. */
if (dependencyElement) {
depthFirstSearch(dependencyElement, [...path])
}
}
visitingElements.delete(element)
visitedElements.add(element)
}
for (let element of elements) {
if (!visitedElements.has(element)) {
depthFirstSearch(element, [])
}
}
return elementsInCycles
}
export { computeNodesInCircularDependencies }
@@ -0,0 +1,51 @@
import { CommonGroupsOptions } from '../types/common-groups-options.js'
import { CommonOptions } from '../types/common-options.js'
type Options = Pick<
CommonGroupsOptions<string, unknown, unknown>,
'customGroups' | 'groups'
> &
CommonOptions
/**
* Retrieves sorting options potentially overridden by a custom group or group
* with settings configuration.
*
* Checks if the group at the specified index is a custom group with its own
* sorting configuration. If so, returns the overridden options (type, order,
* fallbackSort). Otherwise, returns the original options.
*
* Custom groups can override:
*
* - Sort type (e.g., use 'natural' instead of global 'alphabetical')
* - Sort order (e.g., use 'desc' instead of global 'asc')
* - Fallback sort configuration.
*
* @example
*
* ```ts
* const options = {
* type: 'alphabetical',
* order: 'asc',
* fallbackSort: { type: 'natural' },
* groups: ['custom-group', 'other'],
* customGroups: [
* {
* groupName: 'custom-group',
* type: 'natural',
* order: 'desc',
* },
* ],
* }
* const overridden = computeOverriddenOptionsByGroupIndex(options, 0)
* // Returns: { type: 'natural', order: 'desc', fallbackSort: { type: 'natural' } }
* ```
*
* @param options - Combined group and sorting options.
* @param groupIndex - Index of the group to check for overrides.
* @returns Sorting options, potentially overridden by custom group
* configuration.
*/
export declare function computeOverriddenOptionsByGroupIndex<T extends Options>(
options: T,
groupIndex: number,
): T
export {}
@@ -0,0 +1,86 @@
import { isGroupWithOverridesOption } from './is-group-with-overrides-option.js'
import { computeGroupName } from './compute-group-name.js'
/**
* Retrieves sorting options potentially overridden by a custom group or group
* with settings configuration.
*
* Checks if the group at the specified index is a custom group with its own
* sorting configuration. If so, returns the overridden options (type, order,
* fallbackSort). Otherwise, returns the original options.
*
* Custom groups can override:
*
* - Sort type (e.g., use 'natural' instead of global 'alphabetical')
* - Sort order (e.g., use 'desc' instead of global 'asc')
* - Fallback sort configuration.
*
* @example
*
* ```ts
* const options = {
* type: 'alphabetical',
* order: 'asc',
* fallbackSort: { type: 'natural' },
* groups: ['custom-group', 'other'],
* customGroups: [
* {
* groupName: 'custom-group',
* type: 'natural',
* order: 'desc',
* },
* ],
* }
* const overridden = computeOverriddenOptionsByGroupIndex(options, 0)
* // Returns: { type: 'natural', order: 'desc', fallbackSort: { type: 'natural' } }
* ```
*
* @param options - Combined group and sorting options.
* @param groupIndex - Index of the group to check for overrides.
* @returns Sorting options, potentially overridden by custom group
* configuration.
*/
function computeOverriddenOptionsByGroupIndex(options, groupIndex) {
let { customGroups, groups } = options
let matchingGroup = groups[groupIndex]
let matchingGroupName = matchingGroup ? computeGroupName(matchingGroup) : null
let customGroup = customGroups.find(
currentGroup => matchingGroupName === currentGroup.groupName,
)
let returnValue = { ...options }
if (matchingGroup && isGroupWithOverridesOption(matchingGroup)) {
let {
newlinesInside,
commentAbove,
fallbackSort,
group,
...relevantGroupFields
} = matchingGroup
returnValue = {
...returnValue,
...relevantGroupFields,
fallbackSort: {
...returnValue.fallbackSort,
...fallbackSort,
},
}
}
if (customGroup) {
let {
elementNamePattern,
newlinesInside,
fallbackSort,
groupName,
...relevantCustomGroupFields
} = customGroup
returnValue = {
...returnValue,
...relevantCustomGroupFields,
fallbackSort: {
...returnValue.fallbackSort,
...fallbackSort,
},
}
}
return returnValue
}
export { computeOverriddenOptionsByGroupIndex }
@@ -0,0 +1,28 @@
import { AST_NODE_TYPES } from '@typescript-eslint/utils'
import { TSESTree } from '@typescript-eslint/types'
import { NodeOfType } from '../types/node-of-type.js'
/**
* Finds all parent nodes matching one of the specified AST node types.
*
* @param options - Options for the search.
* @param options.allowedTypes - Array of AST node types to match.
* @param options.consecutiveOnly - If true, stops searching after the first
* non-matching parent node is found.
* @param options.node - Starting node to search from.
* @param options.maxParent - Optional maximum exclusive parent node to stop the
* search at.
* @returns List of matching parent nodes.
*/
export declare function computeParentNodesWithTypes<
NodeType extends AST_NODE_TYPES,
>({
consecutiveOnly,
allowedTypes,
maxParent,
node,
}: {
maxParent: TSESTree.Node | null
consecutiveOnly: boolean
allowedTypes: NodeType[]
node: TSESTree.Node
}): NodeOfType<NodeType>[]
@@ -0,0 +1,35 @@
/**
* Finds all parent nodes matching one of the specified AST node types.
*
* @param options - Options for the search.
* @param options.allowedTypes - Array of AST node types to match.
* @param options.consecutiveOnly - If true, stops searching after the first
* non-matching parent node is found.
* @param options.node - Starting node to search from.
* @param options.maxParent - Optional maximum exclusive parent node to stop the
* search at.
* @returns List of matching parent nodes.
*/
function computeParentNodesWithTypes({
consecutiveOnly,
allowedTypes,
maxParent,
node,
}) {
let allowedTypesSet = new Set(allowedTypes)
let returnValue = []
let { parent } = node
while (parent) {
if (parent === maxParent) {
break
}
if (allowedTypesSet.has(parent.type)) {
returnValue.push(parent)
} else if (consecutiveOnly) {
break
}
;({ parent } = parent)
}
return returnValue
}
export { computeParentNodesWithTypes }
@@ -0,0 +1,18 @@
import { RegexOption } from '../../types/common-options.js'
/**
* Checks if all node names match the specified pattern.
*
* @param params - The parameters object.
* @param params.allNamesMatchPattern - The pattern to match against all node
* names.
* @param params.nodeNames - Array of node names to test against patterns.
* @returns True if all node names match the specified pattern, or if no pattern
* is specified; otherwise, false.
*/
export declare function passesAllNamesMatchPatternFilter({
allNamesMatchPattern,
nodeNames,
}: {
allNamesMatchPattern?: RegexOption
nodeNames: string[]
}): boolean
@@ -0,0 +1,18 @@
import { matches } from '../matches.js'
/**
* Checks if all node names match the specified pattern.
*
* @param params - The parameters object.
* @param params.allNamesMatchPattern - The pattern to match against all node
* names.
* @param params.nodeNames - Array of node names to test against patterns.
* @returns True if all node names match the specified pattern, or if no pattern
* is specified; otherwise, false.
*/
function passesAllNamesMatchPatternFilter({ allNamesMatchPattern, nodeNames }) {
if (!allNamesMatchPattern) {
return true
}
return nodeNames.every(nodeName => matches(nodeName, allNamesMatchPattern))
}
export { passesAllNamesMatchPatternFilter }
@@ -0,0 +1,17 @@
/**
* Checks if the given AST selector matches the expected AST selector.
*
* @param params - The parameters object.
* @param params.matchesAstSelector - The AST selector to match against, or
* undefined if no selector is specified.
* @param params.matchedAstSelectors - The matched AST selectors for a node.
* @returns True if the given AST selector matches the expected AST selector, or
* if no selector is specified; otherwise, false.
*/
export declare function passesAstSelectorFilter({
matchedAstSelectors,
matchesAstSelector,
}: {
matchedAstSelectors: ReadonlySet<string>
matchesAstSelector: undefined | string
}): boolean
@@ -0,0 +1,17 @@
/**
* Checks if the given AST selector matches the expected AST selector.
*
* @param params - The parameters object.
* @param params.matchesAstSelector - The AST selector to match against, or
* undefined if no selector is specified.
* @param params.matchedAstSelectors - The matched AST selectors for a node.
* @returns True if the given AST selector matches the expected AST selector, or
* if no selector is specified; otherwise, false.
*/
function passesAstSelectorFilter({ matchedAstSelectors, matchesAstSelector }) {
if (!matchesAstSelector) {
return true
}
return matchedAstSelectors.has(matchesAstSelector)
}
export { passesAstSelectorFilter }
@@ -0,0 +1,27 @@
/**
* Converts a boolean value to a sort direction multiplier.
*
* Used in sorting functions to convert boolean comparisons into numeric values
* suitable for array sort callbacks. This allows for concise expression of sort
* direction logic.
*
* @example
*
* ```ts
* // In ascending sort
* convertBooleanToSign(true) // Returns: 1
* convertBooleanToSign(false) // Returns: -1
* ```
*
* @example
*
* ```ts
* // Usage in sorting
* const sortMultiplier = convertBooleanToSign(order === 'asc')
* return sortMultiplier * (a - b)
* ```
*
* @param value - Boolean value to convert to a sign.
* @returns 1 if value is true, -1 if value is false.
*/
export declare function convertBooleanToSign(value: boolean): -1 | 1
@@ -0,0 +1,30 @@
/**
* Converts a boolean value to a sort direction multiplier.
*
* Used in sorting functions to convert boolean comparisons into numeric values
* suitable for array sort callbacks. This allows for concise expression of sort
* direction logic.
*
* @example
*
* ```ts
* // In ascending sort
* convertBooleanToSign(true) // Returns: 1
* convertBooleanToSign(false) // Returns: -1
* ```
*
* @example
*
* ```ts
* // Usage in sorting
* const sortMultiplier = convertBooleanToSign(order === 'asc')
* return sortMultiplier * (a - b)
* ```
*
* @param value - Boolean value to convert to a sign.
* @returns 1 if value is true, -1 if value is false.
*/
function convertBooleanToSign(value) {
return value ? 1 : -1
}
export { convertBooleanToSign }
@@ -0,0 +1,48 @@
import { ESLintUtils } from '@typescript-eslint/utils'
/**
* Factory function for creating ESLint rules with consistent structure and
* documentation.
*
* Wraps the ESLintUtils.RuleCreator to automatically generate documentation
* URLs for each rule based on its name. All rules created with this function
* will have their documentation hosted at perfectionist.dev.
*
* @see {@link https://typescript-eslint.io/packages/utils/} - TypeScript ESLint
* Utils documentation
* @see {@link https://perfectionist.dev/} - Perfectionist plugin documentation
*/
export declare let createEslintRule: <
Options extends readonly unknown[],
MessageIds extends string,
>({
meta,
name,
...rule
}: Readonly<
ESLintUtils.RuleWithMetaAndName<
Options,
MessageIds,
{
/**
* Indicates whether the rule is part of the recommended configuration.
*
* @default false
*/
recommended?: boolean
}
>
>) => ESLintUtils.RuleModule<
MessageIds,
Options,
{
/**
* Indicates whether the rule is part of the recommended configuration.
*
* @default false
*/
recommended?: boolean
},
ESLintUtils.RuleListener
> & {
name: string
}
@@ -0,0 +1,17 @@
import { ESLintUtils } from '@typescript-eslint/utils'
/**
* Factory function for creating ESLint rules with consistent structure and
* documentation.
*
* Wraps the ESLintUtils.RuleCreator to automatically generate documentation
* URLs for each rule based on its name. All rules created with this function
* will have their documentation hosted at perfectionist.dev.
*
* @see {@link https://typescript-eslint.io/packages/utils/} - TypeScript ESLint
* Utils documentation
* @see {@link https://perfectionist.dev/} - Perfectionist plugin documentation
*/
var createEslintRule = ESLintUtils.RuleCreator(
ruleName => `https://perfectionist.dev/rules/${ruleName}`,
)
export { createEslintRule }
@@ -0,0 +1,30 @@
import { TSESTree } from '@typescript-eslint/types'
import { SortingNode } from '../types/sorting-node.js'
/**
* Creates a Map for efficient lookup of node positions in the sorted array.
*
* Builds an index map that associates each sorting node with its position in
* the array. This is used to quickly determine the relative order of nodes
* without repeated array searches, improving performance when generating error
* messages for incorrectly sorted elements.
*
* @example
*
* ```ts
* const nodes = [
* { name: 'foo', node: fooNode },
* { name: 'bar', node: barNode },
* { name: 'baz', node: bazNode },
* ]
* const indexMap = createNodeIndexMap(nodes)
* indexMap.get(nodes[0]) // Returns: 0
* indexMap.get(nodes[2]) // Returns: 2
* ```
*
* @template Node - Type of the AST node.
* @param nodes - Array of sorting nodes in their sorted order.
* @returns Map where keys are sorting nodes and values are their indices.
*/
export declare function createNodeIndexMap<Node extends TSESTree.Node>(
nodes: SortingNode<Node>[],
): Map<SortingNode, number>
@@ -0,0 +1,33 @@
/**
* Creates a Map for efficient lookup of node positions in the sorted array.
*
* Builds an index map that associates each sorting node with its position in
* the array. This is used to quickly determine the relative order of nodes
* without repeated array searches, improving performance when generating error
* messages for incorrectly sorted elements.
*
* @example
*
* ```ts
* const nodes = [
* { name: 'foo', node: fooNode },
* { name: 'bar', node: barNode },
* { name: 'baz', node: bazNode },
* ]
* const indexMap = createNodeIndexMap(nodes)
* indexMap.get(nodes[0]) // Returns: 0
* indexMap.get(nodes[2]) // Returns: 2
* ```
*
* @template Node - Type of the AST node.
* @param nodes - Array of sorting nodes in their sorted order.
* @returns Map where keys are sorting nodes and values are their indices.
*/
function createNodeIndexMap(nodes) {
let nodeIndexMap = /* @__PURE__ */ new Map()
for (let [index, node] of nodes.entries()) {
nodeIndexMap.set(node, index)
}
return nodeIndexMap
}
export { createNodeIndexMap }
@@ -0,0 +1,129 @@
import { AnyOfCustomGroup } from '../types/common-groups-options.js'
import { RegexOption } from '../types/common-options.js'
/**
* Parameters for testing if an element matches a custom group.
*
* Contains all the properties of an element that can be used for matching
* against custom group criteria.
*/
interface DoesCustomGroupMatchParameters {
/**
* Optional value of the element. Used for matching against
* elementValuePattern in custom groups.
*/
elementValue?: string | null
/**
* Optional list of decorator names applied to the element. Used for
* matching against decoratorNamePattern in custom groups.
*/
decorators?: string[]
/**
* List of modifiers applied to the element (e.g., 'static', 'private',
* 'async'). Must include all modifiers specified in the custom group.
*/
modifiers: string[]
/**
* List of selectors that describe the element type. Used for matching
* against the selector field in custom groups.
*/
selectors: string[]
/**
* Name of the element. Used for matching against elementNamePattern in
* custom groups.
*/
elementName: string
}
/**
* Base structure for a single custom group configuration.
*
* Defines matching criteria that an element must satisfy to belong to this
* custom group. All specified criteria must match for the element to be
* considered part of the group.
*/
interface BaseCustomGroupMatchOptions {
/**
* Pattern to match against decorator names. Element must have at least one
* decorator matching this pattern.
*/
decoratorNamePattern?: RegexOption
/**
* Pattern to match against the element's value. Used for matching literal
* values, initializers, or expressions.
*/
elementValuePattern?: RegexOption
/**
* Regular expression pattern to match the element's name. Elements matching
* this pattern will be included in this custom group.
*/
elementNamePattern?: RegexOption
/**
* List of required modifiers. Element must have ALL specified modifiers to
* match.
*/
modifiers?: string[]
/**
* Required selector type. Element must have this exact selector to match.
*/
selector?: string
}
/**
* Checks whether an element matches the criteria of a custom group.
*
* Supports both single custom groups and "anyOf" groups (where matching any
* subgroup is sufficient). For single groups, all specified criteria must
* match. For "anyOf" groups, at least one subgroup must match.
*
* @example
*
* ```ts
* // Single custom group
* doesCustomGroupMatch({
* customGroup: {
* selector: 'property',
* modifiers: ['static'],
* elementNamePattern: 'on*',
* },
* elementName: 'onClick',
* selectors: ['property'],
* modifiers: ['static', 'readonly'],
* elementValue: null,
* decorators: [],
* })
* // Returns: true
* ```
*
* @example
*
* ```ts
* // AnyOf custom group
* doesCustomGroupMatch({
* customGroup: {
* anyOf: [
* { selector: 'method' },
* { selector: 'property', modifiers: ['static'] },
* ],
* },
* elementName: 'foo',
* selectors: ['method'],
* modifiers: [],
* elementValue: null,
* })
* // Returns: true (matches first subgroup)
* ```
*
* @template CustomGroupMatchOptions - Type of custom group match options.
* @param props - Combined parameters including the custom group and element
* properties.
* @returns True if the element matches the custom group criteria, false
* otherwise.
*/
export declare function doesCustomGroupMatch<
CustomGroupMatchOptions extends BaseCustomGroupMatchOptions,
>(
props: {
customGroup:
| AnyOfCustomGroup<CustomGroupMatchOptions>
| CustomGroupMatchOptions
} & DoesCustomGroupMatchParameters,
): boolean
export {}
@@ -0,0 +1,129 @@
import { matches } from './matches.js'
/**
* Checks whether an element matches the criteria of a custom group.
*
* Supports both single custom groups and "anyOf" groups (where matching any
* subgroup is sufficient). For single groups, all specified criteria must
* match. For "anyOf" groups, at least one subgroup must match.
*
* @example
*
* ```ts
* // Single custom group
* doesCustomGroupMatch({
* customGroup: {
* selector: 'property',
* modifiers: ['static'],
* elementNamePattern: 'on*',
* },
* elementName: 'onClick',
* selectors: ['property'],
* modifiers: ['static', 'readonly'],
* elementValue: null,
* decorators: [],
* })
* // Returns: true
* ```
*
* @example
*
* ```ts
* // AnyOf custom group
* doesCustomGroupMatch({
* customGroup: {
* anyOf: [
* { selector: 'method' },
* { selector: 'property', modifiers: ['static'] },
* ],
* },
* elementName: 'foo',
* selectors: ['method'],
* modifiers: [],
* elementValue: null,
* })
* // Returns: true (matches first subgroup)
* ```
*
* @template CustomGroupMatchOptions - Type of custom group match options.
* @param props - Combined parameters including the custom group and element
* properties.
* @returns True if the element matches the custom group criteria, false
* otherwise.
*/
function doesCustomGroupMatch(props) {
if ('anyOf' in props.customGroup) {
return props.customGroup.anyOf.some(subgroup =>
doesSingleCustomGroupMatch({
...props,
customGroup: subgroup,
}),
)
}
return doesSingleCustomGroupMatch({
...props,
customGroup: props.customGroup,
})
}
/**
* Checks whether an element matches a single custom group's criteria.
*
* Tests each criterion in sequence, returning false as soon as any criterion
* fails. All specified criteria must match for the function to return true. The
* checks are performed in the following order:
*
* 1. Selector match (exact)
* 2. Modifiers match (all required modifiers must be present)
* 3. Element name pattern match
* 4. Element value pattern match
* 5. Decorator name pattern match (at least one decorator must match).
*
* @param params - Parameters for matching.
* @param params.customGroup - Custom group configuration with matching
* criteria.
* @param params.elementName - Name of the element to test.
* @param params.elementValue - Optional value of the element.
* @param params.selectors - Element's selectors.
* @param params.modifiers - Element's modifiers.
* @param params.decorators - Element's decorators.
* @returns True if all specified criteria match, false otherwise.
*/
function doesSingleCustomGroupMatch({
elementValue,
customGroup,
elementName,
decorators,
selectors,
modifiers,
}) {
if (customGroup.selector && !selectors?.includes(customGroup.selector)) {
return false
}
if (customGroup.modifiers) {
for (let modifier of customGroup.modifiers) {
if (!modifiers?.includes(modifier)) {
return false
}
}
}
if ('elementNamePattern' in customGroup && customGroup.elementNamePattern) {
if (!matches(elementName, customGroup.elementNamePattern)) {
return false
}
}
if ('elementValuePattern' in customGroup && customGroup.elementValuePattern) {
if (!matches(elementValue ?? '', customGroup.elementValuePattern)) {
return false
}
}
if (
'decoratorNamePattern' in customGroup &&
customGroup.decoratorNamePattern
) {
let decoratorPattern = customGroup.decoratorNamePattern
if (!decorators?.some(decorator => matches(decorator, decoratorPattern))) {
return false
}
}
return true
}
export { doesCustomGroupMatch }
@@ -0,0 +1,14 @@
import { SortingNodeWithDependencies } from './sort-nodes-by-dependencies.js'
/**
* Checks whether the given sorting node has at least one of the given
* dependency names.
*
* @param sortingNode - The sorting node to check.
* @param dependencyNames - The dependency names to look for.
* @returns True if the sorting node has at least one of the dependency names,
* false otherwise.
*/
export declare function doesSortingNodeHaveOneOfDependencyNames(
sortingNode: Pick<SortingNodeWithDependencies, 'dependencyNames'>,
dependencyNames: string[],
): boolean
@@ -0,0 +1,16 @@
/**
* Checks whether the given sorting node has at least one of the given
* dependency names.
*
* @param sortingNode - The sorting node to check.
* @param dependencyNames - The dependency names to look for.
* @returns True if the sorting node has at least one of the dependency names,
* false otherwise.
*/
function doesSortingNodeHaveOneOfDependencyNames(sortingNode, dependencyNames) {
let sortingNodeDependencyNames = new Set(sortingNode.dependencyNames)
return dependencyNames.some(dependencyName =>
sortingNodeDependencyNames.has(dependencyName),
)
}
export { doesSortingNodeHaveOneOfDependencyNames }
@@ -0,0 +1,25 @@
interface GeneratePredefinedGroupsParameters {
cache: Map<string, string[]>
modifiers: string[]
selectors: string[]
}
/**
* Generates an ordered list of group names associated with the provided
* modifiers and selectors. The groups are generated by combining all possible
* combinations of modifiers with each selector at the end. Selectors are
* prioritized over the quantity of modifiers. For example, `protected abstract
* override get fields();` should prioritize the `'get-method'` group over the
* `'protected-abstract-override-method'` group.
*
* @param props - The properties including selectors, modifiers, and cache.
* @param props.selectors - The list of selectors.
* @param props.modifiers - The list of modifiers.
* @param props.cache - Cache to store computed groups.
* @returns An array of generated group names.
*/
export declare function generatePredefinedGroups({
selectors,
modifiers,
cache,
}: GeneratePredefinedGroupsParameters): string[]
export {}
@@ -0,0 +1,70 @@
import { getArrayCombinations } from './get-array-combinations.js'
/**
* Generates an ordered list of group names associated with the provided
* modifiers and selectors. The groups are generated by combining all possible
* combinations of modifiers with each selector at the end. Selectors are
* prioritized over the quantity of modifiers. For example, `protected abstract
* override get fields();` should prioritize the `'get-method'` group over the
* `'protected-abstract-override-method'` group.
*
* @param props - The properties including selectors, modifiers, and cache.
* @param props.selectors - The list of selectors.
* @param props.modifiers - The list of modifiers.
* @param props.cache - Cache to store computed groups.
* @returns An array of generated group names.
*/
function generatePredefinedGroups({ selectors, modifiers, cache }) {
let modifiersAndSelectorsKey = `${modifiers.join('&')}/${selectors.join('&')}`
let cachedValue = cache.get(modifiersAndSelectorsKey)
if (cachedValue) {
return cachedValue
}
let allModifiersCombinations = []
for (let i = modifiers.length; i > 0; i--) {
allModifiersCombinations.push(...getArrayCombinations(modifiers, i))
}
let allModifiersCombinationPermutations = allModifiersCombinations.flatMap(
result => getPermutations(result),
)
let returnValue = []
for (let selector of selectors) {
returnValue.push(
...allModifiersCombinationPermutations.map(
modifiersCombinationPermutation =>
[...modifiersCombinationPermutation, selector].join('-'),
),
selector,
)
}
cache.set(modifiersAndSelectorsKey, returnValue)
return returnValue
}
/**
* Generates all permutations of an array. This allows variations like
* `'abstract-override-protected-get-method'`,
* `'override-protected-abstract-get-method'`,
* `'protected-abstract-override-get-method'`, etc., to be entered by the user
* and always match the same group. Note that this can theoretically cause
* performance issues if too many modifiers are entered at once (e.g., 8
* modifiers result in 40,320 permutations, 9 in 362,880).
*
* @param elements - The array of elements to permute.
* @returns An array containing all permutations of the input elements.
*/
function getPermutations(elements) {
let result = []
function backtrack(first) {
if (first === elements.length) {
result.push([...elements])
return
}
for (let i = first; i < elements.length; i++) {
;[elements[first], elements[i]] = [elements[i], elements[first]]
backtrack(first + 1)
;[elements[first], elements[i]] = [elements[i], elements[first]]
}
}
backtrack(0)
return result
}
export { generatePredefinedGroups }
@@ -0,0 +1,11 @@
/**
* Generates all possible combinations of a specific size from an array.
*
* @param array - The array of strings to generate combinations from.
* @param number - The number of elements in each combination.
* @returns An array containing all possible combinations.
*/
export declare function getArrayCombinations(
array: string[],
number: number,
): string[][]
@@ -0,0 +1,24 @@
/**
* Generates all possible combinations of a specific size from an array.
*
* @param array - The array of strings to generate combinations from.
* @param number - The number of elements in each combination.
* @returns An array containing all possible combinations.
*/
function getArrayCombinations(array, number) {
let result = []
function backtrack(start, comb) {
if (comb.length === number) {
result.push([...comb])
return
}
for (let i = start; i < array.length; i++) {
comb.push(array[i])
backtrack(i + 1, comb)
comb.pop()
}
}
backtrack(0, [])
return result
}
export { getArrayCombinations }
@@ -0,0 +1,88 @@
import { TSESLint } from '@typescript-eslint/utils'
import { CommonGroupsOptions } from '../types/common-groups-options.js'
import { SortingNode } from '../types/sorting-node.js'
/**
* Parameters for checking if a required comment above a node exists.
*
* @template T - Type of the sorting node.
*/
interface GetCommentAboveMissingParameters<T extends SortingNode> {
/**
* Configuration options for grouping.
*/
options: Pick<
CommonGroupsOptions<string, unknown, unknown>,
'customGroups' | 'groups'
>
/**
* ESLint source code object for accessing comments.
*/
sourceCode: TSESLint.SourceCode
/**
* Index of the group on the left side (previous element). Null if there is
* no previous element.
*/
leftGroupIndex: number | null
/**
* Index of the group on the right side (current element).
*/
rightGroupIndex: number
/**
* The sorting node to check for required comments above.
*/
sortingNode: T
}
/**
* Determines if a comment should exist above a node when transitioning between
* groups.
*
* Checks if the group configuration requires a comment above nodes when they
* are the first element of their group following a different group. This is
* used to enforce comment separators between different sections of sorted
* code.
*
* The function returns null if:
*
* - The left and right elements are in the same or reversed group order
* - The group configuration doesn't require a comment above.
*
* @example
*
* ```ts
* const result = getCommentAboveThatShouldExist({
* options: {
* groups: [
* 'external',
* { commentAbove: 'Internal imports' },
* 'internal',
* ],
* },
* leftGroupIndex: 0, // 'external' group
* rightGroupIndex: 2, // 'internal' group
* sortingNode: internalImportNode,
* sourceCode,
* })
* // Returns: { comment: 'Internal imports', exists: false }
* ```
*
* @template T - Type of the sorting node.
* @param params - Parameters for checking comment requirements.
* @param params.options - Configuration with groups that may require comments.
* @param params.leftGroupIndex - Index of the previous element's group.
* @param params.rightGroupIndex - Index of the current element's group.
* @param params.sortingNode - Node to check for required comments.
* @param params.sourceCode - ESLint source code for accessing comments.
* @returns Object with required comment text and existence status, or null if
* no comment required.
*/
export declare function getCommentAboveThatShouldExist<T extends SortingNode>({
rightGroupIndex,
leftGroupIndex,
sortingNode,
sourceCode,
options,
}: GetCommentAboveMissingParameters<T>): {
comment: string
exists: boolean
} | null
export {}
@@ -0,0 +1,75 @@
import { isGroupWithOverridesOption } from './is-group-with-overrides-option.js'
import { getCommentsBefore } from './get-comments-before.js'
/**
* Determines if a comment should exist above a node when transitioning between
* groups.
*
* Checks if the group configuration requires a comment above nodes when they
* are the first element of their group following a different group. This is
* used to enforce comment separators between different sections of sorted
* code.
*
* The function returns null if:
*
* - The left and right elements are in the same or reversed group order
* - The group configuration doesn't require a comment above.
*
* @example
*
* ```ts
* const result = getCommentAboveThatShouldExist({
* options: {
* groups: [
* 'external',
* { commentAbove: 'Internal imports' },
* 'internal',
* ],
* },
* leftGroupIndex: 0, // 'external' group
* rightGroupIndex: 2, // 'internal' group
* sortingNode: internalImportNode,
* sourceCode,
* })
* // Returns: { comment: 'Internal imports', exists: false }
* ```
*
* @template T - Type of the sorting node.
* @param params - Parameters for checking comment requirements.
* @param params.options - Configuration with groups that may require comments.
* @param params.leftGroupIndex - Index of the previous element's group.
* @param params.rightGroupIndex - Index of the current element's group.
* @param params.sortingNode - Node to check for required comments.
* @param params.sourceCode - ESLint source code for accessing comments.
* @returns Object with required comment text and existence status, or null if
* no comment required.
*/
function getCommentAboveThatShouldExist({
rightGroupIndex,
leftGroupIndex,
sortingNode,
sourceCode,
options,
}) {
if (leftGroupIndex !== null && leftGroupIndex >= rightGroupIndex) {
return null
}
let rightGroup = options.groups[rightGroupIndex]
if (!rightGroup || !isGroupWithOverridesOption(rightGroup)) {
return null
}
let rightGroupCommentAbove = rightGroup.commentAbove
if (!rightGroupCommentAbove) {
return null
}
return {
comment: rightGroupCommentAbove,
exists: !!getCommentsBefore({
node: sortingNode.node,
sourceCode,
}).find(comment => commentMatches(comment.value, rightGroupCommentAbove)),
}
}
function commentMatches(comment, expected) {
return comment.toLowerCase().includes(expected.toLowerCase().trim())
}
export { getCommentAboveThatShouldExist }
@@ -0,0 +1,24 @@
import { TSESLint } from '@typescript-eslint/utils'
import { TSESTree } from '@typescript-eslint/types'
interface GetCommentsBeforeParameters {
tokenValueToIgnoreBefore?: string
sourceCode: TSESLint.SourceCode
node: TSESTree.Node
}
/**
* Returns a list of comments before a given node, excluding ones that are right
* after code. Includes comment blocks, ignore shebang comments.
*
* @param params - Parameters object.
* @param params.node - The node to get comments before.
* @param params.sourceCode - The source code object.
* @param [params.tokenValueToIgnoreBefore] - Allows the following token to
* directly precede the node.
* @returns An array of comments before the given node.
*/
export declare function getCommentsBefore({
tokenValueToIgnoreBefore,
sourceCode,
node,
}: GetCommentsBeforeParameters): TSESTree.Comment[]
export {}
@@ -0,0 +1,37 @@
/**
* Returns a list of comments before a given node, excluding ones that are right
* after code. Includes comment blocks, ignore shebang comments.
*
* @param params - Parameters object.
* @param params.node - The node to get comments before.
* @param params.sourceCode - The source code object.
* @param [params.tokenValueToIgnoreBefore] - Allows the following token to
* directly precede the node.
* @returns An array of comments before the given node.
*/
function getCommentsBefore({ tokenValueToIgnoreBefore, sourceCode, node }) {
let commentsBefore = getRelevantCommentsBeforeNodeOrToken(sourceCode, node)
let tokenBeforeNode = sourceCode.getTokenBefore(node)
if (
commentsBefore.length > 0 ||
!tokenValueToIgnoreBefore ||
tokenBeforeNode?.value !== tokenValueToIgnoreBefore
) {
return commentsBefore
}
return getRelevantCommentsBeforeNodeOrToken(sourceCode, tokenBeforeNode)
}
function getRelevantCommentsBeforeNodeOrToken(source, node) {
return source
.getCommentsBefore(node)
.filter(comment => !isShebangComment(comment))
.filter(comment => {
return (
source.getTokenBefore(comment)?.loc.end.line !== comment.loc.end.line
)
})
}
function isShebangComment(comment) {
return comment.type === 'Shebang' || comment.type === 'Hashbang'
}
export { getCommentsBefore }
@@ -0,0 +1,45 @@
import { TSESTree } from '@typescript-eslint/types'
import { TSESLint } from '@typescript-eslint/utils'
/**
* Extracts the name of a decorator from its AST node.
*
* Processes the decorator text to extract just the name portion, removing the
* '@' prefix if present and any parameters or arguments that follow the name.
* This is useful for sorting and matching decorators by their base name.
*
* @example
*
* ```ts
* // Simple decorator
* getDecoratorName({ sourceCode, decorator: @Component });
* // Returns: 'Component'
* ```
*
* @example
*
* ```ts
* // Decorator with parameters
* getDecoratorName({ sourceCode, decorator: @Injectable({ providedIn: 'root' }) });
* // Returns: 'Injectable'
* ```
*
* @example
*
* ```ts
* // Namespaced decorator
* getDecoratorName({ sourceCode, decorator: @angular.Component() });
* // Returns: 'angular.Component'
* ```
*
* @param params - Parameters object.
* @param params.sourceCode - ESLint source code object for text extraction.
* @param params.decorator - Decorator AST node to extract name from.
* @returns The decorator name without '@' prefix and parameters.
*/
export declare function getDecoratorName({
sourceCode,
decorator,
}: {
sourceCode: TSESLint.SourceCode
decorator: TSESTree.Decorator
}): string
@@ -0,0 +1,44 @@
/**
* Extracts the name of a decorator from its AST node.
*
* Processes the decorator text to extract just the name portion, removing the
* '@' prefix if present and any parameters or arguments that follow the name.
* This is useful for sorting and matching decorators by their base name.
*
* @example
*
* ```ts
* // Simple decorator
* getDecoratorName({ sourceCode, decorator: @Component });
* // Returns: 'Component'
* ```
*
* @example
*
* ```ts
* // Decorator with parameters
* getDecoratorName({ sourceCode, decorator: @Injectable({ providedIn: 'root' }) });
* // Returns: 'Injectable'
* ```
*
* @example
*
* ```ts
* // Namespaced decorator
* getDecoratorName({ sourceCode, decorator: @angular.Component() });
* // Returns: 'angular.Component'
* ```
*
* @param params - Parameters object.
* @param params.sourceCode - ESLint source code object for text extraction.
* @param params.decorator - Decorator AST node to extract name from.
* @returns The decorator name without '@' prefix and parameters.
*/
function getDecoratorName({ sourceCode, decorator }) {
let fullName = sourceCode.getText(decorator)
if (fullName.startsWith('@')) {
fullName = fullName.slice(1)
}
return fullName.split('(')[0]
}
export { getDecoratorName }
@@ -0,0 +1,28 @@
import { TSESTree } from '@typescript-eslint/types'
/**
* Retrieves enum members from a TypeScript enum declaration node.
*
* Handles AST shape changes in TS-ESTree `@typescript-eslint/types`:
*
* - Newer parser versions wrap enum members in `body.members` and deprecate
* `members` on the enum node.
* - Older parser versions expose members directly on the enum node as `members`.
* The fallback keeps backward compatibility with older parser releases.
*
* @example
*
* ```ts
* enum Color {
* Red = 'RED',
* Green = 'GREEN',
* Blue = 'BLUE',
* }
* // Returns array of three TSEnumMember nodes
* ```
*
* @param value - TypeScript enum declaration AST node.
* @returns Array of enum member nodes.
*/
export declare function getEnumMembers(
value: TSESTree.TSEnumDeclaration,
): TSESTree.TSEnumMember[]
@@ -0,0 +1,28 @@
/**
* Retrieves enum members from a TypeScript enum declaration node.
*
* Handles AST shape changes in TS-ESTree `@typescript-eslint/types`:
*
* - Newer parser versions wrap enum members in `body.members` and deprecate
* `members` on the enum node.
* - Older parser versions expose members directly on the enum node as `members`.
* The fallback keeps backward compatibility with older parser releases.
*
* @example
*
* ```ts
* enum Color {
* Red = 'RED',
* Green = 'GREEN',
* Blue = 'BLUE',
* }
* // Returns array of three TSEnumMember nodes
* ```
*
* @param value - TypeScript enum declaration AST node.
* @returns Array of enum member nodes.
*/
function getEnumMembers(value) {
return value.body?.members ?? value.members
}
export { getEnumMembers }
@@ -0,0 +1,47 @@
import { TSESLint } from '@typescript-eslint/utils'
/**
* Determines which lines have the specified ESLint rule disabled via comments.
*
* Parses ESLint disable comments in the source code to identify lines where a
* specific rule should not be enforced. Handles all ESLint disable directives:
*
* - `eslint-disable-next-line` - Disables the rule for the next line
* - `eslint-disable-line` - Disables the rule for the current line
* - `eslint-disable` - Disables the rule from this point forward
* - `eslint-enable` - Re-enables the rule after a previous disable.
*
* The function correctly handles:
*
* - Rule-specific disables (e.g., `eslint-disable-next-line rule-name`)
* - Global disables (e.g., `eslint-disable-next-line` without specific rules)
* - Nested disable/enable pairs.
*
* @example
*
* ```ts
* // Source code with disable comments:
* // eslint-disable-next-line perfectionist/sort-imports
* import { z } from 'zod'
* import { a } from 'a'
*
* // eslint-disable perfectionist/sort-imports
* import { y } from 'y'
* import { b } from 'b'
* // eslint-enable perfectionist/sort-imports
*
* getEslintDisabledLines({
* sourceCode,
* ruleName: 'perfectionist/sort-imports',
* })
* // Returns: [2, 5, 6] (lines where the rule is disabled)
* ```
*
* @param props - Configuration object.
* @param props.sourceCode - ESLint source code object containing comments.
* @param props.ruleName - Name of the rule to check for disable directives.
* @returns Array of line numbers (1-indexed) where the rule is disabled.
*/
export declare function getEslintDisabledLines(props: {
sourceCode: TSESLint.SourceCode
ruleName: string
}): number[]
@@ -0,0 +1,115 @@
import { UnreachableCaseError } from './unreachable-case-error.js'
import { getEslintDisabledRules } from './get-eslint-disabled-rules.js'
/**
* Determines which lines have the specified ESLint rule disabled via comments.
*
* Parses ESLint disable comments in the source code to identify lines where a
* specific rule should not be enforced. Handles all ESLint disable directives:
*
* - `eslint-disable-next-line` - Disables the rule for the next line
* - `eslint-disable-line` - Disables the rule for the current line
* - `eslint-disable` - Disables the rule from this point forward
* - `eslint-enable` - Re-enables the rule after a previous disable.
*
* The function correctly handles:
*
* - Rule-specific disables (e.g., `eslint-disable-next-line rule-name`)
* - Global disables (e.g., `eslint-disable-next-line` without specific rules)
* - Nested disable/enable pairs.
*
* @example
*
* ```ts
* // Source code with disable comments:
* // eslint-disable-next-line perfectionist/sort-imports
* import { z } from 'zod'
* import { a } from 'a'
*
* // eslint-disable perfectionist/sort-imports
* import { y } from 'y'
* import { b } from 'b'
* // eslint-enable perfectionist/sort-imports
*
* getEslintDisabledLines({
* sourceCode,
* ruleName: 'perfectionist/sort-imports',
* })
* // Returns: [2, 5, 6] (lines where the rule is disabled)
* ```
*
* @param props - Configuration object.
* @param props.sourceCode - ESLint source code object containing comments.
* @param props.ruleName - Name of the rule to check for disable directives.
* @returns Array of line numbers (1-indexed) where the rule is disabled.
*/
function getEslintDisabledLines(props) {
let { sourceCode, ruleName } = props
let returnValue = []
let lineRulePermanentlyDisabled = null
for (let comment of sourceCode.getAllComments()) {
let eslintDisabledRules = getEslintDisabledRules(comment.value)
if (!eslintDisabledRules) {
continue
}
/* v8 ignore next 3 -- @preserve Hard to test this false branch. */
if (
!(
eslintDisabledRules.rules === 'all' ||
eslintDisabledRules.rules.includes(ruleName)
)
) {
continue
}
switch (eslintDisabledRules.eslintDisableDirective) {
case 'eslint-disable-next-line':
returnValue.push(comment.loc.end.line + 1)
continue
case 'eslint-disable-line':
returnValue.push(comment.loc.start.line)
continue
case 'eslint-disable':
lineRulePermanentlyDisabled ??= comment.loc.start.line
break
case 'eslint-enable':
/* v8 ignore next -- @preserve Hard to cover in test without raising another ESLint error. */
if (!lineRulePermanentlyDisabled) {
continue
}
returnValue.push(
...createArrayFromTo(
lineRulePermanentlyDisabled + 1,
comment.loc.start.line,
),
)
lineRulePermanentlyDisabled = null
break
/* v8 ignore next 2 -- @preserve Exhaustive guard. */
default:
throw new UnreachableCaseError(
eslintDisabledRules.eslintDisableDirective,
)
}
}
return returnValue
}
/**
* Creates an array of consecutive integers from start to end (inclusive).
*
* Helper function to generate an array of line numbers for ranges disabled by
* eslint-disable/eslint-enable comment pairs.
*
* @example
*
* ```ts
* createArrayFromTo(5, 8)
* // Returns: [5, 6, 7, 8]
* ```
*
* @param i - Starting number (inclusive).
* @param index - Ending number (inclusive).
* @returns Array of consecutive integers from i to index.
*/
function createArrayFromTo(i, index) {
return Array.from({ length: index - i + 1 }, (_, item) => i + item)
}
export { getEslintDisabledLines }
@@ -0,0 +1,57 @@
/**
* Array of all ESLint disable directive types. Used to identify and parse
* ESLint disable comments in source code.
*/
declare let eslintDisableDirectives: readonly [
'eslint-disable',
'eslint-enable',
'eslint-disable-line',
'eslint-disable-next-line',
]
/**
* Type representing one of the ESLint disable directive types. Can be
* 'eslint-disable', 'eslint-enable', 'eslint-disable-line', or
* 'eslint-disable-next-line'.
*/
type EslintDisableDirective = (typeof eslintDisableDirectives)[number]
/**
* Parses an ESLint disable comment to extract the directive type and affected
* rules.
*
* Analyzes comment text to determine if it contains an ESLint disable directive
* and which rules are affected. Returns null if the comment is not a valid
* ESLint disable directive.
*
* @example
*
* ```ts
* getEslintDisabledRules('eslint-disable')
* // Returns: { eslintDisableDirective: 'eslint-disable', rules: 'all' }
* ```
*
* @example
*
* ```ts
* getEslintDisabledRules('eslint-disable-next-line no-console, no-alert')
* // Returns: {
* // eslintDisableDirective: 'eslint-disable-next-line',
* // rules: ['no-console', 'no-alert']
* // }
* ```
*
* @example
*
* ```ts
* getEslintDisabledRules('regular comment')
* // Returns: null
* ```
*
* @param comment - Comment text to parse (without comment delimiters).
* @returns Object containing directive type and affected rules, or null if not
* a disable comment.
*/
export declare function getEslintDisabledRules(comment: string): {
eslintDisableDirective: EslintDisableDirective
rules: string[] | 'all'
} | null
export {}
@@ -0,0 +1,116 @@
/**
* Array of all ESLint disable directive types. Used to identify and parse
* ESLint disable comments in source code.
*/
var eslintDisableDirectives = [
'eslint-disable',
'eslint-enable',
'eslint-disable-line',
'eslint-disable-next-line',
]
/**
* Parses an ESLint disable comment to extract the directive type and affected
* rules.
*
* Analyzes comment text to determine if it contains an ESLint disable directive
* and which rules are affected. Returns null if the comment is not a valid
* ESLint disable directive.
*
* @example
*
* ```ts
* getEslintDisabledRules('eslint-disable')
* // Returns: { eslintDisableDirective: 'eslint-disable', rules: 'all' }
* ```
*
* @example
*
* ```ts
* getEslintDisabledRules('eslint-disable-next-line no-console, no-alert')
* // Returns: {
* // eslintDisableDirective: 'eslint-disable-next-line',
* // rules: ['no-console', 'no-alert']
* // }
* ```
*
* @example
*
* ```ts
* getEslintDisabledRules('regular comment')
* // Returns: null
* ```
*
* @param comment - Comment text to parse (without comment delimiters).
* @returns Object containing directive type and affected rules, or null if not
* a disable comment.
*/
function getEslintDisabledRules(comment) {
for (let eslintDisableDirective of eslintDisableDirectives) {
let disabledRules = getEslintDisabledRulesByType(
comment,
eslintDisableDirective,
)
if (disabledRules) {
return {
eslintDisableDirective,
rules: disabledRules,
}
}
}
return null
}
/**
* Extracts disabled rules from a comment for a specific ESLint directive type.
*
* Attempts to parse the comment as the specified ESLint disable directive.
* Returns the list of disabled rules if the comment matches the directive,
* 'all' if no specific rules are mentioned (global disable), or null if the
* comment doesn't match the directive pattern.
*
* @example
*
* ```ts
* getEslintDisabledRulesByType('eslint-disable', 'eslint-disable')
* // Returns: 'all'
* ```
*
* @example
*
* ```ts
* getEslintDisabledRulesByType(
* 'eslint-disable-line rule1, rule2',
* 'eslint-disable-line',
* )
* // Returns: ['rule1', 'rule2']
* ```
*
* @example
*
* ```ts
* getEslintDisabledRulesByType(
* 'eslint-disable-line rule1',
* 'eslint-disable-next-line',
* )
* // Returns: null (wrong directive type)
* ```
*
* @param comment - Comment text to parse.
* @param eslintDisableDirective - Specific directive type to match against.
* @returns Array of rule names, 'all' for global disable, or null if no match.
*/
function getEslintDisabledRulesByType(comment, eslintDisableDirective) {
let trimmedCommentValue = comment.trim()
if (eslintDisableDirective === trimmedCommentValue) {
return 'all'
}
let regexp = new RegExp(String.raw`^${eslintDisableDirective} ((?:.|\s)*)$`)
let disableRulesMatchValue = trimmedCommentValue.match(regexp)?.[1]
if (!disableRulesMatchValue) {
return null
}
return disableRulesMatchValue
.split(',')
.map(rule => rule.trim())
.filter(rule => !!rule)
}
export { getEslintDisabledRules }
@@ -0,0 +1,41 @@
import { GroupsOptions } from '../types/common-groups-options.js'
import { SortingNode } from '../types/sorting-node.js'
/**
* Type representing a single group or an array of group names. Used in group
* configuration where elements can belong to multiple subgroups.
*/
type Group = GroupsOptions[number]
/**
* Determines the index of the group that a node belongs to.
*
* Searches through the groups array to find which group contains the node.
* Supports simple groups (string), subgroups (array of strings) and objects
* containing a `group` property. For subgroups, the node matches if its group
* is any element in the array.
*
* The function returns the index of the matching group. If no group matches, it
* returns the length of the groups array, which conventionally represents the
* "unknown" group and ensures such nodes are sorted last.
*
* @example
*
* ```ts
* const groups = ['imports', ['types', 'interfaces'], 'functions']
* const node1 = { group: 'imports', name: 'lodash' }
* const node2 = { group: 'types', name: 'User' }
* const node3 = { group: 'unknown-group', name: 'misc' }
*
* getGroupIndex(groups, node1) // Returns: 0
* getGroupIndex(groups, node2) // Returns: 1 (matches subgroup)
* getGroupIndex(groups, node3) // Returns: 3 (groups.length, unknown group)
* ```
*
* @param groups - Array of group configurations (strings or arrays of strings).
* @param node - Sorting node with a group property to match.
* @returns Index of the matching group, or groups.length if no match found.
*/
export declare function getGroupIndex(
groups: Group[],
node: SortingNode,
): number
export {}
@@ -0,0 +1,62 @@
import { isGroupWithOverridesOption } from './is-group-with-overrides-option.js'
import { isNewlinesBetweenOption } from './is-newlines-between-option.js'
import { UnreachableCaseError } from './unreachable-case-error.js'
/**
* Determines the index of the group that a node belongs to.
*
* Searches through the groups array to find which group contains the node.
* Supports simple groups (string), subgroups (array of strings) and objects
* containing a `group` property. For subgroups, the node matches if its group
* is any element in the array.
*
* The function returns the index of the matching group. If no group matches, it
* returns the length of the groups array, which conventionally represents the
* "unknown" group and ensures such nodes are sorted last.
*
* @example
*
* ```ts
* const groups = ['imports', ['types', 'interfaces'], 'functions']
* const node1 = { group: 'imports', name: 'lodash' }
* const node2 = { group: 'types', name: 'User' }
* const node3 = { group: 'unknown-group', name: 'misc' }
*
* getGroupIndex(groups, node1) // Returns: 0
* getGroupIndex(groups, node2) // Returns: 1 (matches subgroup)
* getGroupIndex(groups, node3) // Returns: 3 (groups.length, unknown group)
* ```
*
* @param groups - Array of group configurations (strings or arrays of strings).
* @param node - Sorting node with a group property to match.
* @returns Index of the matching group, or groups.length if no match found.
*/
function getGroupIndex(groups, node) {
for (let max = groups.length, i = 0; i < max; i++) {
let currentGroup = groups[i]
if (doesGroupMatch(currentGroup, node.group)) {
return i
}
}
return groups.length
}
function doesGroupMatch(group, groupName) {
if (typeof group === 'string' || Array.isArray(group)) {
return doesStringGroupMatch(group, groupName)
}
if (isGroupWithOverridesOption(group)) {
return doesStringGroupMatch(group.group, groupName)
}
/* v8 ignore else -- @preserve Exhaustive guard: other directives are filtered out earlier. */
if (isNewlinesBetweenOption(group)) {
return false
}
/* v8 ignore next -- @preserve Exhaustive guard: other directives are filtered out earlier. */
throw new UnreachableCaseError(group)
}
function doesStringGroupMatch(group, groupName) {
if (typeof group === 'string') {
return group === groupName
}
return group.includes(groupName)
}
export { getGroupIndex }
@@ -0,0 +1,44 @@
import { TSESLint } from '@typescript-eslint/utils'
import { SortingNode } from '../types/sorting-node.js'
/**
* Counts the number of empty lines between two AST nodes.
*
* Extracts the lines between the end of the left node and the start of the
* right node, then counts only the completely empty lines (containing only
* whitespace). This is used to determine if nodes are separated by newlines and
* to enforce newline formatting rules.
*
* @example
*
* ```ts
* // Source code:
* // const a = 1;
* //
* // const b = 2;
*
* getLinesBetween(sourceCode, nodeA, nodeB)
* // Returns: 1 (one empty line between nodes)
* ```
*
* @example
*
* ```ts
* // Source code:
* // const a = 1;
* // // comment
* // const b = 2;
*
* getLinesBetween(sourceCode, nodeA, nodeB)
* // Returns: 0 (no empty lines, comment line is not empty)
* ```
*
* @param source - ESLint source code object containing the lines array.
* @param left - Node or object containing the left/first node.
* @param right - Node or object containing the right/second node.
* @returns Number of empty lines between the two nodes.
*/
export declare function getLinesBetween(
source: TSESLint.SourceCode,
left: Pick<SortingNode, 'node'>,
right: Pick<SortingNode, 'node'>,
): number
@@ -0,0 +1,43 @@
/**
* Counts the number of empty lines between two AST nodes.
*
* Extracts the lines between the end of the left node and the start of the
* right node, then counts only the completely empty lines (containing only
* whitespace). This is used to determine if nodes are separated by newlines and
* to enforce newline formatting rules.
*
* @example
*
* ```ts
* // Source code:
* // const a = 1;
* //
* // const b = 2;
*
* getLinesBetween(sourceCode, nodeA, nodeB)
* // Returns: 1 (one empty line between nodes)
* ```
*
* @example
*
* ```ts
* // Source code:
* // const a = 1;
* // // comment
* // const b = 2;
*
* getLinesBetween(sourceCode, nodeA, nodeB)
* // Returns: 0 (no empty lines, comment line is not empty)
* ```
*
* @param source - ESLint source code object containing the lines array.
* @param left - Node or object containing the left/first node.
* @param right - Node or object containing the right/second node.
* @returns Number of empty lines between the two nodes.
*/
function getLinesBetween(source, left, right) {
return source.lines
.slice(left.node.loc.end.line, right.node.loc.start.line - 1)
.filter(line => line.trim().length === 0).length
}
export { getLinesBetween }
@@ -0,0 +1,126 @@
import { TSESLint } from '@typescript-eslint/utils'
import {
NewlinesBetweenOption,
CommonGroupsOptions,
} from '../types/common-groups-options.js'
import { SortingNode } from '../types/sorting-node.js'
/**
* Function type for customizing newlines between specific nodes.
*
* Allows overriding the computed newlines requirement based on the specific
* nodes being compared. Can return 'ignore' to skip newline checking or a
* number to specify exact newlines required.
*
* @template T - Type of the sorting node.
* @param props - Properties for computing newlines.
* @param props.computedNewlinesBetween - Default computed newlines requirement.
* @param props.left - Left/first node.
* @param props.right - Right/second node.
* @returns Number of required newlines or 'ignore' to skip checking.
*/
export type NewlinesBetweenValueGetter<T extends SortingNode> = (props: {
computedNewlinesBetween: NewlinesBetweenOption
right: T
left: T
}) => NewlinesBetweenOption
/**
* Parameters for checking newlines between nodes and generating errors.
*
* @template MessageIds - Type of error message identifiers.
* @template T - Type of the sorting node.
*/
interface GetNewlinesBetweenErrorsParameters<
MessageIds extends string,
T extends SortingNode,
> {
/**
* Optional function to customize newlines between specific nodes.
*/
newlinesBetweenValueGetter?: NewlinesBetweenValueGetter<T>
/**
* Configuration options for newlines and groups.
*/
options: CommonGroupsOptions<string, unknown, unknown>
/**
* ESLint source code object for accessing lines.
*/
sourceCode: TSESLint.SourceCode
/**
* Error message ID for missing required newlines.
*/
missedSpacingError: MessageIds
/**
* Error message ID for extra unwanted newlines.
*/
extraSpacingError: MessageIds
/**
* Group index of the right/second node.
*/
rightGroupIndex: number
/**
* Group index of the left/first node.
*/
leftGroupIndex: number
/**
* Right/second node in the comparison.
*/
right: T
/**
* Left/first node in the comparison.
*/
left: T
}
/**
* Checks if the newlines between two nodes match the required configuration.
*
* Validates the number of empty lines between two nodes against the expected
* newlines based on their group indices and configuration. Generates
* appropriate error messages when the actual newlines don't match the
* requirement.
*
* The function returns no errors if:
*
* - The left node's group index is greater than the right's (wrong order)
* - The nodes are in different partitions
* - The newlines configuration is set to 'ignore'
* - The actual newlines match the expected newlines.
*
* @example
*
* ```ts
* // Configuration requires 1 newline between different groups
* const errors = getNewlinesBetweenErrors({
* options: { newlinesBetween: 1, groups: ['imports', 'types'] },
* leftGroupIndex: 0, // imports group
* rightGroupIndex: 1, // types group
* left: importNode,
* right: typeNode,
* sourceCode,
* missedSpacingError: 'missedNewline',
* extraSpacingError: 'extraNewline',
* })
* // If no newline between nodes: Returns ['missedNewline']
* // If 2+ newlines between nodes: Returns ['extraNewline']
* // If exactly 1 newline: Returns []
* ```
*
* @template MessageIds - Type of error message identifiers.
* @template T - Type of the sorting node.
* @param params - Parameters for newline checking.
* @returns Array of error message IDs (empty if no errors).
*/
export declare function getNewlinesBetweenErrors<
MessageIds extends string,
T extends SortingNode,
>({
newlinesBetweenValueGetter,
missedSpacingError,
extraSpacingError,
rightGroupIndex,
leftGroupIndex,
sourceCode,
options,
right,
left,
}: GetNewlinesBetweenErrorsParameters<MessageIds, T>): MessageIds[]
export {}
@@ -0,0 +1,82 @@
import { getNewlinesBetweenOption } from './get-newlines-between-option.js'
import { getLinesBetween } from './get-lines-between.js'
/**
* Checks if the newlines between two nodes match the required configuration.
*
* Validates the number of empty lines between two nodes against the expected
* newlines based on their group indices and configuration. Generates
* appropriate error messages when the actual newlines don't match the
* requirement.
*
* The function returns no errors if:
*
* - The left node's group index is greater than the right's (wrong order)
* - The nodes are in different partitions
* - The newlines configuration is set to 'ignore'
* - The actual newlines match the expected newlines.
*
* @example
*
* ```ts
* // Configuration requires 1 newline between different groups
* const errors = getNewlinesBetweenErrors({
* options: { newlinesBetween: 1, groups: ['imports', 'types'] },
* leftGroupIndex: 0, // imports group
* rightGroupIndex: 1, // types group
* left: importNode,
* right: typeNode,
* sourceCode,
* missedSpacingError: 'missedNewline',
* extraSpacingError: 'extraNewline',
* })
* // If no newline between nodes: Returns ['missedNewline']
* // If 2+ newlines between nodes: Returns ['extraNewline']
* // If exactly 1 newline: Returns []
* ```
*
* @template MessageIds - Type of error message identifiers.
* @template T - Type of the sorting node.
* @param params - Parameters for newline checking.
* @returns Array of error message IDs (empty if no errors).
*/
function getNewlinesBetweenErrors({
newlinesBetweenValueGetter,
missedSpacingError,
extraSpacingError,
rightGroupIndex,
leftGroupIndex,
sourceCode,
options,
right,
left,
}) {
if (
leftGroupIndex > rightGroupIndex ||
left.partitionId !== right.partitionId
) {
return []
}
let newlinesBetween = getNewlinesBetweenOption({
nextNodeGroupIndex: rightGroupIndex,
nodeGroupIndex: leftGroupIndex,
options,
})
newlinesBetween =
newlinesBetweenValueGetter?.({
computedNewlinesBetween: newlinesBetween,
right,
left,
}) ?? newlinesBetween
if (newlinesBetween === 'ignore') {
return []
}
let numberOfEmptyLinesBetween = getLinesBetween(sourceCode, left, right)
if (numberOfEmptyLinesBetween < newlinesBetween) {
return [missedSpacingError]
}
if (numberOfEmptyLinesBetween > newlinesBetween) {
return [extraSpacingError]
}
return []
}
export { getNewlinesBetweenErrors }
@@ -0,0 +1,43 @@
import {
NewlinesBetweenOption,
CommonGroupsOptions,
} from '../types/common-groups-options.js'
/**
* Parameters for determining newlines requirement between nodes.
*
* Contains group indices and configuration options needed to calculate the
* required number of newlines between two nodes.
*/
export interface GetNewlinesBetweenOptionParameters {
/**
* Configuration options for newlines and groups.
*/
options: CommonGroupsOptions<string, unknown, unknown>
/**
* Group index of the next/second node.
*/
nextNodeGroupIndex: number
/**
* Group index of the current/first node.
*/
nodeGroupIndex: number
}
/**
* Get the `newlinesBetween` option to use between two consecutive nodes. The
* result is based on the global `newlinesBetween` option and the custom groups,
* which can override the global option.
*
* - If the two nodes are in the same custom group, the `newlinesInside` option of
* the group is used.
*
* @param props - The function arguments.
* @param props.nextNodeGroupIndex - The next node index to sort.
* @param props.nodeGroupIndex - The current node index to sort.
* @param props.options - Newlines between related options.
* @returns - The `newlinesBetween` option to use.
*/
export declare function getNewlinesBetweenOption({
nextNodeGroupIndex,
nodeGroupIndex,
options,
}: GetNewlinesBetweenOptionParameters): NewlinesBetweenOption
@@ -0,0 +1,145 @@
import { isGroupWithOverridesOption } from './is-group-with-overrides-option.js'
import { isNewlinesBetweenOption } from './is-newlines-between-option.js'
import { computeGroupName } from './compute-group-name.js'
/**
* Get the `newlinesBetween` option to use between two consecutive nodes. The
* result is based on the global `newlinesBetween` option and the custom groups,
* which can override the global option.
*
* - If the two nodes are in the same custom group, the `newlinesInside` option of
* the group is used.
*
* @param props - The function arguments.
* @param props.nextNodeGroupIndex - The next node index to sort.
* @param props.nodeGroupIndex - The current node index to sort.
* @param props.options - Newlines between related options.
* @returns - The `newlinesBetween` option to use.
*/
function getNewlinesBetweenOption({
nextNodeGroupIndex,
nodeGroupIndex,
options,
}) {
if (nodeGroupIndex === nextNodeGroupIndex) {
return computeNewlinesInsideOption({
groupIndex: nodeGroupIndex,
options,
})
}
if (nextNodeGroupIndex >= nodeGroupIndex + 2) {
return computeNewlinesBetweenOptionForDifferentGroups({
nextNodeGroupIndex,
nodeGroupIndex,
options,
})
}
return options.newlinesBetween
}
function computeNewlinesBetweenOptionForDifferentGroups({
nextNodeGroupIndex,
nodeGroupIndex,
options,
}) {
if (nextNodeGroupIndex === nodeGroupIndex + 2) {
let groupBetween = options.groups[nodeGroupIndex + 1]
if (isNewlinesBetweenOption(groupBetween)) {
return groupBetween.newlinesBetween
}
return options.newlinesBetween
}
let groupsWithAllNewlinesBetween = buildGroupsWithAllNewlinesBetween(
options.groups.slice(nodeGroupIndex, nextNodeGroupIndex + 1),
options.newlinesBetween,
)
let newlinesBetweenOptions = new Set(
groupsWithAllNewlinesBetween
.filter(isNewlinesBetweenOption)
.map(group => group.newlinesBetween),
)
let numberNewlinesBetween = [...newlinesBetweenOptions].filter(
option => typeof option === 'number',
)
let maxNewlinesBetween =
numberNewlinesBetween.length > 0 ? Math.max(...numberNewlinesBetween) : null
if (maxNewlinesBetween !== null && maxNewlinesBetween >= 1) {
return maxNewlinesBetween
}
if (newlinesBetweenOptions.has('ignore')) {
return 'ignore'
}
return 0
}
function computeNewlinesInsideOption({ groupIndex, options }) {
let globalNewlinesInsideOption = computeGlobalNewlinesInsideOption()
let group = options.groups[groupIndex]
if (!group) {
return globalNewlinesInsideOption
}
let groupName = computeGroupName(group)
let nodeCustomGroup = options.customGroups.find(
customGroup => customGroup.groupName === groupName,
)
let groupOverrideNewlinesInside =
isGroupWithOverridesOption(group) ? group.newlinesInside : null
return (
nodeCustomGroup?.newlinesInside ??
groupOverrideNewlinesInside ??
globalNewlinesInsideOption
)
function computeGlobalNewlinesInsideOption() {
switch (options.newlinesInside) {
case 'newlinesBetween':
return options.newlinesBetween === 'ignore' ? 'ignore' : 0
case 'ignore':
return 'ignore'
default:
return options.newlinesInside
}
}
}
/**
* Inserts newlines settings between groups that don't already have them.
*
* Fills in missing newlines settings between adjacent groups using the global
* newlines option. This ensures every transition between groups has an explicit
* newlines setting for consistent calculation.
*
* @example
*
* ```ts
* buildGroupsWithAllNewlinesBetween(
* ['imports', 'types', { newlinesBetween: 2 }, 'functions'],
* 1,
* )
* // Returns: [
* // 'imports',
* // { newlinesBetween: 1 }, // Added
* // 'types',
* // { newlinesBetween: 2 }, // Already existed
* // 'functions'
* // ]
* ```
*
* @param groups - Array of groups with optional inline newlines settings.
* @param globalNewlinesBetweenOption - Default newlines to use for missing
* settings.
* @returns Groups array with newlines settings filled in between all groups.
*/
function buildGroupsWithAllNewlinesBetween(
groups,
globalNewlinesBetweenOption,
) {
let returnValue = []
for (let i = 0; i < groups.length; i++) {
let group = groups[i]
if (!isNewlinesBetweenOption(group)) {
let previousGroup = groups[i - 1]
if (previousGroup && !isNewlinesBetweenOption(previousGroup)) {
returnValue.push({ newlinesBetween: globalNewlinesBetweenOption })
}
}
returnValue.push(group)
}
return returnValue
}
export { getNewlinesBetweenOption }
@@ -0,0 +1,33 @@
import { TSESTree } from '@typescript-eslint/types'
/**
* Type representing an AST node that may have decorators.
*
* Extends the base Node type with an optional decorators array. Used for
* TypeScript/JavaScript decorators on classes, methods, and properties.
*/
type NodeWithDecorator = {
decorators: TSESTree.Decorator[]
} & TSESTree.Node
/**
* Safely retrieves decorators from an AST node.
*
* Provides a safe way to access the decorators property which may not exist on
* all nodes or in all parser versions. Returns an empty array when decorators
* are undefined, ensuring consistent behavior across different AST structures.
*
* @example
*
* ```ts
* // Node without decorators
* const plainMethod = { type: 'MethodDefinition', ... };
* getNodeDecorators(plainMethod);
* // Returns: []
* ```
*
* @param node - AST node that may contain decorators.
* @returns Array of decorator nodes, empty array if none exist.
*/
export declare function getNodeDecorators(
node: NodeWithDecorator,
): TSESTree.Decorator[]
export {}
@@ -0,0 +1,23 @@
/**
* Safely retrieves decorators from an AST node.
*
* Provides a safe way to access the decorators property which may not exist on
* all nodes or in all parser versions. Returns an empty array when decorators
* are undefined, ensuring consistent behavior across different AST structures.
*
* @example
*
* ```ts
* // Node without decorators
* const plainMethod = { type: 'MethodDefinition', ... };
* getNodeDecorators(plainMethod);
* // Returns: []
* ```
*
* @param node - AST node that may contain decorators.
* @returns Array of decorator nodes, empty array if none exist.
*/
function getNodeDecorators(node) {
return node.decorators ?? []
}
export { getNodeDecorators }
@@ -0,0 +1,84 @@
import { TSESTree } from '@typescript-eslint/types'
import { TSESLint } from '@typescript-eslint/utils'
import { CommonPartitionOptions } from '../types/common-partition-options.js'
/**
* Parameters for determining the complete range of a node.
*
* Configures how to calculate the node's range including associated comments
* and parentheses.
*/
interface GetNodeRangeParameters {
/**
* Optional configuration for comment handling.
*/
options?: Pick<CommonPartitionOptions, 'partitionByComment'>
/**
* Whether to exclude the highest-level block comment from the range. Useful
* for preserving file-level documentation comments in their original
* position.
*/
ignoreHighestBlockComment?: boolean
/**
* ESLint source code object for accessing comments and tokens.
*/
sourceCode: TSESLint.SourceCode
/**
* AST node to get the range for.
*/
node: TSESTree.Node
}
/**
* Determines the complete range of a node including its associated comments.
*
* Calculates the full range that should be considered when moving or analyzing
* a node. This includes:
*
* - The node itself
* - Parentheses surrounding the node (if any)
* - Preceding comments that "belong" to the node.
*
* The function intelligently determines which comments should be included by:
*
* - Including comments directly above the node (no empty lines between)
* - Stopping at partition comments (used to separate sections)
* - Stopping at ESLint disable/enable comments
* - Optionally excluding the highest block comment (e.g., file headers).
*
* @example
*
* ```ts
* // Source code:
* // This comment belongs to the function
* // So does this one
* function foo() {}
*
* const range = getNodeRange({ node: functionNode, sourceCode })
* // Returns range including both comments
* ```
*
* @example
*
* ```ts
* // Source code:
* /* File header comment *\/
* // Function comment
* function bar() { }
*
* const range = getNodeRange({
* node: functionNode,
* sourceCode,
* ignoreHighestBlockComment: true
* });
* // Returns range including line comment but not block comment
* ```
*
* @param params - Parameters for range calculation.
* @returns Tuple of [start, end] positions including relevant comments.
*/
export declare function getNodeRange({
ignoreHighestBlockComment,
sourceCode,
options,
node,
}: GetNodeRangeParameters): TSESTree.Range
export {}
@@ -0,0 +1,118 @@
import { getEslintDisabledRules } from './get-eslint-disabled-rules.js'
import { isPartitionComment } from './is-partition-comment.js'
import { getCommentsBefore } from './get-comments-before.js'
import { ASTUtils } from '@typescript-eslint/utils'
/**
* Determines the complete range of a node including its associated comments.
*
* Calculates the full range that should be considered when moving or analyzing
* a node. This includes:
*
* - The node itself
* - Parentheses surrounding the node (if any)
* - Preceding comments that "belong" to the node.
*
* The function intelligently determines which comments should be included by:
*
* - Including comments directly above the node (no empty lines between)
* - Stopping at partition comments (used to separate sections)
* - Stopping at ESLint disable/enable comments
* - Optionally excluding the highest block comment (e.g., file headers).
*
* @example
*
* ```ts
* // Source code:
* // This comment belongs to the function
* // So does this one
* function foo() {}
*
* const range = getNodeRange({ node: functionNode, sourceCode })
* // Returns range including both comments
* ```
*
* @example
*
* ```ts
* // Source code:
* /* File header comment *\/
* // Function comment
* function bar() { }
*
* const range = getNodeRange({
* node: functionNode,
* sourceCode,
* ignoreHighestBlockComment: true
* });
* // Returns range including line comment but not block comment
* ```
*
* @param params - Parameters for range calculation.
* @returns Tuple of [start, end] positions including relevant comments.
*/
function getNodeRange({
ignoreHighestBlockComment,
sourceCode,
options,
node,
}) {
let start = node.range.at(0)
let end = node.range.at(1)
if (ASTUtils.isParenthesized(node, sourceCode)) {
let bodyOpeningParen = sourceCode.getTokenBefore(
node,
ASTUtils.isOpeningParenToken,
)
let bodyClosingParen = sourceCode.getTokenAfter(
node,
ASTUtils.isClosingParenToken,
)
start = bodyOpeningParen.range.at(0)
end = bodyClosingParen.range.at(1)
}
let comments = getCommentsBefore({
sourceCode,
node,
})
let highestBlockComment = comments.find(comment => comment.type === 'Block')
/**
* Iterate on all comments starting from the bottom until we reach the last of
* the comments, a newline between comments, a partition comment, or a
* eslint-disable comment.
*/
let relevantTopComment
for (let i = comments.length - 1; i >= 0; i--) {
let comment = comments[i]
let eslintDisabledRules = getEslintDisabledRules(comment.value)
if (
isPartitionComment({
partitionByComment: options?.partitionByComment ?? false,
comment,
}) ||
eslintDisabledRules?.eslintDisableDirective === 'eslint-disable' ||
eslintDisabledRules?.eslintDisableDirective === 'eslint-enable'
) {
break
}
/**
* Check for newlines between comments or between the first comment and the
* node.
*/
let previousCommentOrNodeStartLine =
i === comments.length - 1 ?
node.loc.start.line
: comments[i + 1].loc.start.line
if (comment.loc.end.line !== previousCommentOrNodeStartLine - 1) {
break
}
if (ignoreHighestBlockComment && comment === highestBlockComment) {
break
}
relevantTopComment = comment
}
if (relevantTopComment) {
start = relevantTopComment.range.at(0)
}
return [start, end]
}
export { getNodeRange }
@@ -0,0 +1,58 @@
import { GroupsOptions } from '../types/common-groups-options.js'
/**
* Parameters for cleaning and normalizing groups configuration.
*
* Contains the groups array that needs to be cleaned up.
*/
interface GetOptionsWithCleanGroupsParameters<CustomTypeOption extends string> {
/**
* Groups configuration that may contain empty arrays or single-element
* arrays.
*/
groups: GroupsOptions<CustomTypeOption>
}
/**
* Cleans and normalizes the groups configuration in options.
*
* Performs the following optimizations on the groups array:
*
* - Removes empty arrays (they serve no purpose in grouping)
* - Converts single-element arrays to plain strings (simplifies structure)
* - Preserves multi-element arrays as-is (maintains subgroups).
*
* This normalization ensures consistent group handling and eliminates
* unnecessary complexity in the configuration.
*
* @example
*
* ```ts
* getOptionsWithCleanGroups({
* groups: [
* 'imports',
* ['types'], // Single element - will become 'types'
* ['hooks', 'utils'], // Multiple elements - preserved as array
* [], // Empty - will be removed
* 'components',
* ],
* })
* // Returns: {
* // groups: [
* // 'imports',
* // 'types',
* // ['hooks', 'utils'],
* // 'components'
* // ]
* // }
* ```
*
* @template CustomTypeOption - Custom type option string for GroupsOptions.
* @template Options - Type of options extending
* GetOptionsWithCleanGroupsParameters.
* @param options - Options object containing groups to clean.
* @returns Options with cleaned and normalized groups array.
*/
export declare function getOptionsWithCleanGroups<
CustomTypeOption extends string,
Options extends GetOptionsWithCleanGroupsParameters<CustomTypeOption>,
>(options: Options): Options
export {}
@@ -0,0 +1,78 @@
/**
* Cleans and normalizes the groups configuration in options.
*
* Performs the following optimizations on the groups array:
*
* - Removes empty arrays (they serve no purpose in grouping)
* - Converts single-element arrays to plain strings (simplifies structure)
* - Preserves multi-element arrays as-is (maintains subgroups).
*
* This normalization ensures consistent group handling and eliminates
* unnecessary complexity in the configuration.
*
* @example
*
* ```ts
* getOptionsWithCleanGroups({
* groups: [
* 'imports',
* ['types'], // Single element - will become 'types'
* ['hooks', 'utils'], // Multiple elements - preserved as array
* [], // Empty - will be removed
* 'components',
* ],
* })
* // Returns: {
* // groups: [
* // 'imports',
* // 'types',
* // ['hooks', 'utils'],
* // 'components'
* // ]
* // }
* ```
*
* @template CustomTypeOption - Custom type option string for GroupsOptions.
* @template Options - Type of options extending
* GetOptionsWithCleanGroupsParameters.
* @param options - Options object containing groups to clean.
* @returns Options with cleaned and normalized groups array.
*/
function getOptionsWithCleanGroups(options) {
return {
...options,
groups: options.groups
.filter(group => !Array.isArray(group) || group.length > 0)
.map(group =>
Array.isArray(group) ? getCleanedNestedGroups(group) : group,
),
}
}
/**
* Simplifies a nested group array by converting single-element arrays to
* strings.
*
* Helper function that normalizes array groups:
*
* - Single-element arrays are unwrapped to plain strings
* - Multi-element arrays are preserved as-is
* - Empty single-element arrays remain as arrays.
*
* @example
*
* ```ts
* getCleanedNestedGroups(['types']) // Returns: 'types'
* getCleanedNestedGroups(['a', 'b']) // Returns: ['a', 'b']
* getCleanedNestedGroups(['']) // Returns: ['']
* ```
*
* @param nestedGroup - Array of group names to potentially simplify.
* @returns Simplified string if single non-empty element, otherwise the
* original array.
*/
function getCleanedNestedGroups(nestedGroup) {
return nestedGroup.length === 1 && nestedGroup[0] ?
nestedGroup[0]
: nestedGroup
}
export { getOptionsWithCleanGroups }
@@ -0,0 +1,58 @@
import { TSESLint } from '@typescript-eslint/utils'
import { CommonPartitionOptions } from '../types/common-partition-options.js'
import { CommonGroupsOptions } from '../types/common-groups-options.js'
import { CommonOptions } from '../types/common-options.js'
/**
* Global settings for the Perfectionist plugin.
*
* These settings can be configured in ESLint configuration under the
* 'perfectionist' key and apply to all Perfectionist rules unless overridden by
* rule-specific options.
*/
export type Settings = Partial<
Pick<
CommonGroupsOptions<string, unknown, unknown>,
'newlinesBetween' | 'newlinesInside'
> &
CommonPartitionOptions &
CommonOptions
>
/**
* Extracts and validates Perfectionist settings from ESLint configuration.
*
* Retrieves global Perfectionist settings that apply to all rules. Validates
* that only allowed settings are provided and throws an error if invalid
* options are detected. This ensures configuration errors are caught early with
* clear error messages.
*
* The settings are accessed from the 'perfectionist' key in ESLint's shared
* configuration settings.
*
* @example
*
* ```ts
* // Valid usage:
* const settings = getSettings(context.settings)
* // Returns: { type: 'natural', order: 'asc', ignoreCase: true }
* ```
*
* @example
*
* ```ts
* // Invalid setting throws error:
* getSettings({
* perfectionist: {
* type: 'natural',
* invalidOption: true, // This will throw
* },
* })
* // Throws: Error: Invalid Perfectionist setting(s): invalidOption
* ```
*
* @param settings - ESLint shared configuration settings object.
* @returns Validated Perfectionist settings or empty object if none configured.
* @throws {Error} If invalid settings are provided.
*/
export declare function getSettings(
settings: TSESLint.SharedConfigurationSettings,
): Settings
@@ -0,0 +1,72 @@
/**
* Extracts and validates Perfectionist settings from ESLint configuration.
*
* Retrieves global Perfectionist settings that apply to all rules. Validates
* that only allowed settings are provided and throws an error if invalid
* options are detected. This ensures configuration errors are caught early with
* clear error messages.
*
* The settings are accessed from the 'perfectionist' key in ESLint's shared
* configuration settings.
*
* @example
*
* ```ts
* // Valid usage:
* const settings = getSettings(context.settings)
* // Returns: { type: 'natural', order: 'asc', ignoreCase: true }
* ```
*
* @example
*
* ```ts
* // Invalid setting throws error:
* getSettings({
* perfectionist: {
* type: 'natural',
* invalidOption: true, // This will throw
* },
* })
* // Throws: Error: Invalid Perfectionist setting(s): invalidOption
* ```
*
* @param settings - ESLint shared configuration settings object.
* @returns Validated Perfectionist settings or empty object if none configured.
* @throws {Error} If invalid settings are provided.
*/
function getSettings(settings) {
if (!settings['perfectionist']) {
return {}
}
/**
* Identifies settings keys that are not in the allowed list.
*
* @param object - Settings object to validate.
* @returns Array of invalid setting names.
*/
function getInvalidOptions(object) {
let allowedOptions = new Set([
'partitionByComment',
'partitionByNewLine',
'specialCharacters',
'newlinesBetween',
'newlinesInside',
'fallbackSort',
'ignoreCase',
'alphabet',
'locales',
'order',
'type',
])
return Object.keys(object).filter(key => !allowedOptions.has(key))
}
let perfectionistSettings = settings['perfectionist']
let invalidOptions = getInvalidOptions(perfectionistSettings)
if (invalidOptions.length > 0) {
throw new Error(
`Invalid Perfectionist setting(s): ${invalidOptions.join(', ')}`,
)
}
return settings['perfectionist']
}
export { getSettings }
@@ -0,0 +1,40 @@
import {
GroupWithOverridesOption,
GroupsOptions,
} from '../types/common-groups-options.js'
/**
* Type guard to check if a group option is a group with overrides
* configuration.
*
* Determines whether a group element is a special configuration object that
* contains group settings rather than being a regular group name or newlines
* option.
*
* @example
*
* ```ts
* const groups = [
* 'imports',
* { group: 'foo', commentAbove: '// Components' },
* 'components',
* { newlinesBetween: 1 },
* 'utils',
* ]
*
* isGroupWithOverridesOption(groups[0]) // false (string)
* isGroupWithOverridesOption(groups[1]) // true
* isGroupWithOverridesOption(groups[3]) // false (newlines option)
* ```
*
* @param groupOption - A single element from the groups configuration array.
* @returns True if the element is a group with overrides configuration object.
*/
export declare function isGroupWithOverridesOption<
CustomTypeOption extends string,
AdditionalSortOptions,
>(
groupOption: GroupsOptions<CustomTypeOption, AdditionalSortOptions>[number],
): groupOption is GroupWithOverridesOption<
CustomTypeOption,
AdditionalSortOptions
>
@@ -0,0 +1,31 @@
/**
* Type guard to check if a group option is a group with overrides
* configuration.
*
* Determines whether a group element is a special configuration object that
* contains group settings rather than being a regular group name or newlines
* option.
*
* @example
*
* ```ts
* const groups = [
* 'imports',
* { group: 'foo', commentAbove: '// Components' },
* 'components',
* { newlinesBetween: 1 },
* 'utils',
* ]
*
* isGroupWithOverridesOption(groups[0]) // false (string)
* isGroupWithOverridesOption(groups[1]) // true
* isGroupWithOverridesOption(groups[3]) // false (newlines option)
* ```
*
* @param groupOption - A single element from the groups configuration array.
* @returns True if the element is a group with overrides configuration object.
*/
function isGroupWithOverridesOption(groupOption) {
return typeof groupOption === 'object' && 'group' in groupOption
}
export { isGroupWithOverridesOption }
@@ -0,0 +1,40 @@
import {
GroupNewlinesBetweenOption,
GroupsOptions,
} from '../types/common-groups-options.js'
/**
* Type guard to check if a group option is a newlines-between configuration.
*
* Determines whether a group element contains a `newlinesBetween` property,
* which indicates it's a special configuration object that controls spacing
* between groups rather than being a regular group name or comment option.
*
* Newlines-between options are placed between group names in the configuration
* to specify how many newlines should separate those groups in the sorted
* output.
*
* @example
*
* ```ts
* const groups = [
* 'imports',
* { newlinesBetween: 1 }, // Add 1 newline between imports and types
* 'types',
* { newlinesBetween: 2 }, // Add 2 newlines between types and components
* 'components',
* { commentAbove: '// Utils' }, // Not a newlines option
* 'utils',
* ]
*
* isNewlinesBetweenOption(groups[0]) // false (string)
* isNewlinesBetweenOption(groups[1]) // true (has newlinesBetween)
* isNewlinesBetweenOption(groups[3]) // true (has newlinesBetween)
* isNewlinesBetweenOption(groups[5]) // false (comment option)
* ```
*
* @param groupOption - A single element from the groups configuration array.
* @returns True if the element is a newlines-between configuration object.
*/
export declare function isNewlinesBetweenOption(
groupOption: GroupsOptions[number],
): groupOption is GroupNewlinesBetweenOption
@@ -0,0 +1,37 @@
/**
* Type guard to check if a group option is a newlines-between configuration.
*
* Determines whether a group element contains a `newlinesBetween` property,
* which indicates it's a special configuration object that controls spacing
* between groups rather than being a regular group name or comment option.
*
* Newlines-between options are placed between group names in the configuration
* to specify how many newlines should separate those groups in the sorted
* output.
*
* @example
*
* ```ts
* const groups = [
* 'imports',
* { newlinesBetween: 1 }, // Add 1 newline between imports and types
* 'types',
* { newlinesBetween: 2 }, // Add 2 newlines between types and components
* 'components',
* { commentAbove: '// Utils' }, // Not a newlines option
* 'utils',
* ]
*
* isNewlinesBetweenOption(groups[0]) // false (string)
* isNewlinesBetweenOption(groups[1]) // true (has newlinesBetween)
* isNewlinesBetweenOption(groups[3]) // true (has newlinesBetween)
* isNewlinesBetweenOption(groups[5]) // false (comment option)
* ```
*
* @param groupOption - A single element from the groups configuration array.
* @returns True if the element is a newlines-between configuration object.
*/
function isNewlinesBetweenOption(groupOption) {
return typeof groupOption === 'object' && 'newlinesBetween' in groupOption
}
export { isNewlinesBetweenOption }

Some files were not shown because too many files have changed in this diff Show More