Skip to content

Commit

Permalink
Rewrite sorting rule and utilities
Browse files Browse the repository at this point in the history
  • Loading branch information
stormwarning committed Aug 29, 2024
1 parent c0ed6dc commit dc003c0
Show file tree
Hide file tree
Showing 13 changed files with 938 additions and 1 deletion.
449 changes: 449 additions & 0 deletions src/rules/new-order.ts

Large diffs are not rendered by default.

127 changes: 127 additions & 0 deletions src/utils/compare.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import type { SortingNode } from './types'

interface BaseCompareOptions {
/**
* Custom function to get the value of the node. By default, returns the node's name.
*/
nodeValueGetter?: (node: SortingNode) => string
order: 'desc' | 'asc'
}

interface AlphabeticalCompareOptions extends BaseCompareOptions {
type: 'alphabetical'
ignoreCase?: boolean
}

interface LineLengthCompareOptions extends BaseCompareOptions {
maxLineLength?: number
type: 'line-length'
}

interface NaturalCompareOptions extends BaseCompareOptions {
ignoreCase?: boolean
type: 'natural'
}

export type CompareOptions =
| AlphabeticalCompareOptions
// | LineLengthCompareOptions
| NaturalCompareOptions

export function compare(a: SortingNode, b: SortingNode, options: CompareOptions): number {
/** Don't sort unsassigned imports. */
if (a.group === 'unassigned' || b.group === 'unassigned') return 0

if (b.dependencies?.includes(a.name)) {
return -1
}

if (a.dependencies?.includes(b.name)) {
return 1
}

let orderCoefficient = options.order === 'asc' ? 1 : -1
let sortingFunction: (a: SortingNode, b: SortingNode) => number

let formatString =
options.type === 'line-length' || !options.ignoreCase

Check failure on line 47 in src/utils/compare.ts

View workflow job for this annotation

GitHub Actions / build

This comparison appears to be unintentional because the types '"alphabetical" | "natural"' and '"line-length"' have no overlap.
? (string: string) => string
: (string: string) => string.toLowerCase()

let nodeValueGetter = options.nodeValueGetter ?? ((node: SortingNode) => node.name)

if (options.type === 'alphabetical') {
sortingFunction = (aNode, bNode) =>
formatString(nodeValueGetter(aNode)).localeCompare(formatString(nodeValueGetter(bNode)))
} else if (options.type === 'natural') {
let prepareNumeric = (string: string) => {
let formattedNumberPattern = /^[+-]?[\d ,_]+(\.[\d ,_]+)?$/
if (formattedNumberPattern.test(string)) {
return string.replaceAll(/[ ,_]/g, '')
}

return string
}

sortingFunction = (aNode, bNode) => {
let aImport = stripProtocol(nodeValueGetter(aNode))
let bImport = stripProtocol(nodeValueGetter(bNode))

if (aImport.startsWith('.') && bImport.startsWith('.')) {
return compareDotSegments(aImport, bImport)
}

return compareString(aImport, bImport)
}
//
// naturalCompare(
// prepareNumeric(formatString(nodeValueGetter(aNode))),
// prepareNumeric(formatString(nodeValueGetter(bNode))),
// )
} else {
sortingFunction = (aNode, bNode) => {
let aSize = aNode.size

Check failure on line 83 in src/utils/compare.ts

View workflow job for this annotation

GitHub Actions / build

Property 'size' does not exist on type 'SortingNode<Node>'.
let bSize = bNode.size

Check failure on line 84 in src/utils/compare.ts

View workflow job for this annotation

GitHub Actions / build

Property 'size' does not exist on type 'SortingNode<Node>'.

let { maxLineLength } = options

if (maxLineLength) {
let isTooLong = (size: number, node: SortingNode) =>
size > maxLineLength && node.hasMultipleImportDeclarations

if (isTooLong(aSize, aNode)) {
aSize = nodeValueGetter(aNode).length + 10
}

if (isTooLong(bSize, bNode)) {
bSize = nodeValueGetter(bNode).length + 10
}
}

return aSize - bSize
}
}

return orderCoefficient * sortingFunction(a, b)
}

function stripProtocol(name: string) {
return name.replace(/^(node|bun):/, '')
}

function compareString(first: string, second: string) {
return first.localeCompare(second, 'en', { numeric: true })
}

function compareDotSegments(first: string, second: string) {
let regex = /\.+(?=\/)/g

let firstCount = (first.match(regex) ?? []).join('').length
let secondCount = (second.match(regex) ?? []).join('').length

if (secondCount < firstCount) return -1
if (firstCount < secondCount) return 1

// If segment length is the same, compare the path alphabetically.
return compareString(first, second)
}
44 changes: 44 additions & 0 deletions src/utils/get-comment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { AST_TOKEN_TYPES, type TSESLint, type TSESTree } from '@typescript-eslint/utils'

export function getCommentBefore(
node: TSESTree.Node,
source: TSESLint.SourceCode,
): TSESTree.Comment | undefined {
let [tokenBefore, tokenOrCommentBefore] = source.getTokensBefore(node, {
filter: ({ value, type }) =>
!(type === AST_TOKEN_TYPES.Punctuator && [',', ';'].includes(value)),
includeComments: true,
count: 2,
}) as Array<TSESTree.Token | undefined>

if (
(tokenOrCommentBefore?.type === AST_TOKEN_TYPES.Block ||
tokenOrCommentBefore?.type === AST_TOKEN_TYPES.Line) &&
node.loc.start.line - tokenOrCommentBefore.loc.end.line <= 1 &&
tokenBefore?.loc.end.line !== tokenOrCommentBefore.loc.start.line
) {
return tokenOrCommentBefore
}

return undefined
}

export function getCommentAfter(
node: TSESTree.Node,
source: TSESLint.SourceCode,
): TSESTree.Comment | undefined {
let token = source.getTokenAfter(node, {
filter: ({ value, type }) =>
!(type === AST_TOKEN_TYPES.Punctuator && [',', ';'].includes(value)),
includeComments: true,
})

if (
(token?.type === AST_TOKEN_TYPES.Block || token?.type === AST_TOKEN_TYPES.Line) &&
node.loc.end.line === token.loc.end.line
) {
return token
}

return undefined
}
18 changes: 18 additions & 0 deletions src/utils/get-group-number.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { Group, SortingNode } from './types.js'

export function getGroupNumber(groups: Group[], node: SortingNode): number {
for (let max = groups.length, index = 0; index < max; index++) {
let currentGroup = groups[index]

if (
node.group === currentGroup ||
(Array.isArray(currentGroup) &&
typeof node.group === 'string' &&
currentGroup.includes(node.group))
) {
return index
}
}

return groups.length
}
13 changes: 13 additions & 0 deletions src/utils/get-lines-between.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { TSESLint } from '@typescript-eslint/utils'

import type { SortingNode } from './types.js'

export function getLinesBetween(
source: TSESLint.SourceCode,
left: SortingNode,
right: SortingNode,
) {
let linesBetween = source.lines.slice(left.node.loc.end.line, right.node.loc.start.line - 1)

return linesBetween.filter((line) => line.trim().length === 0).length
}
48 changes: 48 additions & 0 deletions src/utils/get-node-range.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { ASTUtils, type TSESLint, type TSESTree } from '@typescript-eslint/utils'

import { getCommentBefore } from './get-comment.js'
import { isPartitionComment } from './is-partition-comment.js'

export function getNodeRange(
node: TSESTree.Node,
sourceCode: TSESLint.SourceCode,
additionalOptions?: {
partitionComment?: string[] | boolean | string
},
): TSESTree.Range {
let start = node.range.at(0)!
let end = node.range.at(1)!

let raw = sourceCode.text.slice(start, end)

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 comment = getCommentBefore(node, sourceCode)

if (raw.endsWith(';') || raw.endsWith(',')) {
let tokensAfter = sourceCode.getTokensAfter(node, {
includeComments: true,
count: 2,
})

if (node.loc.start.line === tokensAfter.at(1)?.loc.start.line) {
end -= 1
}
}

if (
comment &&
!isPartitionComment(additionalOptions?.partitionComment ?? false, comment.value)
) {
start = comment.range.at(0)!
}

return [start, end]
}
18 changes: 18 additions & 0 deletions src/utils/is-partition-comment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// eslint-disable-next-line import/no-extraneous-dependencies
import { minimatch } from 'minimatch'

export const isPartitionComment = (
partitionComment: string[] | boolean | string,
comment: string,
) =>
(Array.isArray(partitionComment) &&
partitionComment.some((pattern) =>
minimatch(comment.trim(), pattern, {
nocomment: true,
}),
)) ??
(typeof partitionComment === 'string' &&
minimatch(comment.trim(), partitionComment, {
nocomment: true,
})) ??
partitionComment === true
12 changes: 12 additions & 0 deletions src/utils/pairwise.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export function pairwise<T>(nodes: T[], callback: (left: T, right: T, iteration: number) => void) {
if (nodes.length > 1) {
for (let index = 1; index < nodes.length; index++) {
let left = nodes.at(index - 1)
let right = nodes.at(index)

if (left && right) {
callback(left, right, index - 1)
}
}
}
}
6 changes: 6 additions & 0 deletions src/utils/sort-nodes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { compare, type CompareOptions } from './compare.js'
import type { SortingNode } from './types'

export function sortNodes<T extends SortingNode>(nodes: T[], options: CompareOptions): T[] {
return [...nodes].sort((a, b) => compare(a, b, options))
}
28 changes: 28 additions & 0 deletions src/utils/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import type { TSESTree } from '@typescript-eslint/utils'

export interface SortingNode<Node extends TSESTree.Node = TSESTree.Node> {
name: string
node: Node
dependencies?: string[]
group?: string
hasMultipleImportDeclarations?: boolean
}

export type Group =
| 'unassigned'
| 'builtin'
| 'framework'
| 'external'
| 'internal'
| 'local'
| 'style'
| 'object'
| 'unknown'

export interface Options {
groups: Group[]
ignoreCase: boolean
newlinesBetween: 'ignore' | 'always' | 'never'
order: 'asc' | 'desc'
type: 'alphabetical' | 'natural'
}
18 changes: 18 additions & 0 deletions src/utils/use-groups.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { Group } from './types'

export function useGroups(groups: string[]) {
let group: undefined | string
// For lookup performance
let groupsSet = new Set(groups.flat())

let defineGroup = (value: Group, override = false) => {
if ((!group || override) && groupsSet.has(value)) {
group = value
}
}

return {
getGroup: () => group ?? 'unknown',
defineGroup,
}
}
Loading

0 comments on commit dc003c0

Please sign in to comment.