Skip to content

Commit

Permalink
Update order rule types and utils
Browse files Browse the repository at this point in the history
  • Loading branch information
stormwarning committed Dec 13, 2024
1 parent 4b55cfc commit f6bc787
Show file tree
Hide file tree
Showing 7 changed files with 285 additions and 113 deletions.
223 changes: 153 additions & 70 deletions src/rules/order.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,18 @@ import { AST_NODE_TYPES, ESLintUtils, type TSESTree, type TSESLint } from '@type

import { compare } from '../utils/compare.js'
import { computeGroup, isSideEffectImport } from '../utils/compute-group.js'
import { getCommentBefore } from '../utils/get-comment.js'
import { getCommentsBefore } from '../utils/get-comment.js'
import { getEslintDisabledLines } from '../utils/get-eslint-disabled-lines.js'
import { getGroupNumber } from '../utils/get-group-number.js'
import { getLinesBetween } from '../utils/get-lines-between.js'
import { getNewlineErrors } from '../utils/get-newline-errors.js'
import { getNodeRange } from '../utils/get-node-range.js'
import { isNodeEslintDisabled } from '../utils/is-node-eslint-disabled.js'
import { makeFixes } from '../utils/make-fixes.js'
import { makeNewlineFixes } from '../utils/make-newline-fixes.js'
import { pairwise } from '../utils/pairwise.js'
import { rangeToDiff } from '../utils/range-to-diff.js'
import { sortNodesByGroups } from '../utils/sort-nodes-by-groups.js'
import { sortNodes } from '../utils/sort-nodes.js'
import type { ImportDeclarationNode, Options, SortingNode } from '../utils/types.js'

Expand All @@ -22,13 +29,15 @@ export const IMPORT_GROUPS = [
'unknown',
] as const

type MessageId = 'out-of-order' | 'needs-newline' | 'extra-newline'

// eslint-disable-next-line new-cap
const createRule = ESLintUtils.RuleCreator(
(name) =>
`https://github.com/stormwarning/eslint-plugin-import-sorting/blob/main/docs/rules/${name}.md`,
)

export default createRule({
export default createRule<unknown[], MessageId>({
name: 'order',
meta: {
type: 'suggestion',
Expand All @@ -53,6 +62,10 @@ export default createRule({
order: 'asc',
type: 'natural',
}
let eslintDisabledLines = getEslintDisabledLines({
ruleName: context.id,
sourceCode,
})
let nodes: SortingNode[] = []

function registerNode(node: ImportDeclarationNode) {
Expand All @@ -63,19 +76,20 @@ export default createRule({
} else if (node.type === AST_NODE_TYPES.TSImportEqualsDeclaration) {
name =
node.moduleReference.type === AST_NODE_TYPES.TSExternalModuleReference
? // @ts-expect-error -- `value` is not in the type definition.
`${node.moduleReference.expression.value}`
: sourceCode.text.slice(...node.moduleReference.range)
? node.moduleReference.expression.value
: sourceCode.getText(node.moduleReference)
} else {
let decl = node.declarations[0].init as TSESTree.CallExpression
let declValue = (decl.arguments[0] as TSESTree.Literal).value
name = declValue!.toString()
let { value } = decl.arguments[0] as TSESTree.Literal
name = value!.toString()
}

nodes.push({
group: computeGroup(node, settings, sourceCode),
node,
isEslintDisabled: isNodeEslintDisabled(node, eslintDisabledLines),
name,
node,
size: rangeToDiff(node, sourceCode),
})
}

Expand All @@ -95,92 +109,161 @@ export default createRule({
},
// eslint-disable-next-line @typescript-eslint/naming-convention
'Program:exit'() {
let hasContentBetweenNodes = (left: SortingNode, right: SortingNode): boolean =>
sourceCode.getTokensBetween(
left.node,
getCommentBefore(right.node, sourceCode) ?? right.node,
{
includeComments: true,
},
).length > 0
function hasContentBetweenNodes(left: SortingNode, right: SortingNode): boolean {
return (
sourceCode.getTokensBetween(left.node, right.node, {
includeComments: false,
}).length > 0
)
}
//
// let hasContentBetweenNodes = (left: SortingNode, right: SortingNode): boolean =>
// sourceCode.getTokensBetween(
// left.node,
// getCommentBefore(right.node, sourceCode) ?? right.node,
// {
// includeComments: true,
// },
// ).length > 0

let splittedNodes: SortingNode[][] = [[]]
let formattedNodes: SortingNode[][] = [[]]

for (let node of nodes) {
let lastNode = splittedNodes.at(-1)?.at(-1)
let lastNode = formattedNodes.at(-1)?.at(-1)

if (lastNode && hasContentBetweenNodes(lastNode, node)) {
splittedNodes.push([node])
/**
* Including `node` in this empty array allows groups
* of imports separated by other statements to be
* sorted, but may break other aspects.
*/
formattedNodes.push([node])
} else {
splittedNodes.at(-1)!.push(node)
formattedNodes.at(-1)!.push(node)
}
}

for (let nodeList of splittedNodes) {
for (let nodeList of formattedNodes) {
let sortedNodes = sortNodesByGroups(nodeList, options, {})
pairwise(nodeList, (left, right) => {
let leftNumber = getGroupNumber(IMPORT_GROUPS, left)
let rightNumber = getGroupNumber(IMPORT_GROUPS, right)
let indexOfLeft = sortedNodes.indexOf(left)
let indexOfRight = sortedNodes.indexOf(right)

let numberOfEmptyLinesBetween = getLinesBetween(sourceCode, left, right)
let messages: MessageId[] = []

if (
!(
isSideEffectImport(left.node, sourceCode) &&
isSideEffectImport(right.node, sourceCode)
) &&
!hasContentBetweenNodes(left, right) &&
(leftNumber > rightNumber ||
(leftNumber === rightNumber && compare(left, right, options) > 0))
) {
context.report({
messageId: 'out-of-order',
data: {
left: left.name,
right: right.name,
},
node: right.node,
fix: (fixer) => fix(fixer, nodeList, sourceCode, options),
})
if (indexOfLeft > indexOfRight) {
messages.push(
leftNumber === rightNumber ? 'out-of-order' : 'out-of-order',
)
}

if (options.newlinesBetween === 'never' && numberOfEmptyLinesBetween > 0) {
messages = [
...messages,
...getNewlineErrors({
missingLineError: 'needs-newline',
extraLineError: 'extra-newline',
left,
leftNumber,
right,
rightNumber,
sourceCode,
options,
}),
]

for (let message of messages) {
context.report({
messageId: 'extra-newline',
fix: (fixer) => [
...makeFixes({
fixer,
nodes: nodeList,
sortedNodes,
sourceCode,
//
// options,
}),
...makeNewlineFixes({
fixer,
nodes: nodeList,
sortedNodes,
sourceCode,
options,
}),
],
data: {
left: left.name,
rightGroup: right.group,
leftGroup: left.group,
right: right.name,
left: left.name,
},
node: right.node,
fix: (fixer) => fix(fixer, nodeList, sourceCode, options),
messageId: message,
})
}

if (options.newlinesBetween === 'always') {
if (leftNumber < rightNumber && numberOfEmptyLinesBetween === 0) {
context.report({
messageId: 'needs-newline',
data: {
left: left.name,
right: right.name,
},
node: right.node,
fix: (fixer) => fix(fixer, nodeList, sourceCode, options),
})
} else if (
numberOfEmptyLinesBetween > 1 ||
(leftNumber === rightNumber && numberOfEmptyLinesBetween > 0)
) {
context.report({
messageId: 'extra-newline',
data: {
left: left.name,
right: right.name,
},
node: right.node,
fix: (fixer) => fix(fixer, nodeList, sourceCode, options),
})
}
}
//
// let numberOfEmptyLinesBetween = getLinesBetween(sourceCode, left, right)
//
// if (
// !(
// isSideEffectImport(left.node, sourceCode) &&
// isSideEffectImport(right.node, sourceCode)
// ) &&
// !hasContentBetweenNodes(left, right) &&
// (leftNumber > rightNumber ||
// (leftNumber === rightNumber && compare(left, right, options) > 0))
// ) {
// context.report({
// messageId: 'out-of-order',
// data: {
// left: left.name,
// right: right.name,
// },
// node: right.node,
// fix: (fixer) => fix(fixer, nodeList, sourceCode, options),
// })
// }
//
// if (options.newlinesBetween === 'never' && numberOfEmptyLinesBetween > 0) {
// context.report({
// messageId: 'extra-newline',
// data: {
// left: left.name,
// right: right.name,
// },
// node: right.node,
// fix: (fixer) => fix(fixer, nodeList, sourceCode, options),
// })
// }
//
// if (options.newlinesBetween === 'always') {
// if (leftNumber < rightNumber && numberOfEmptyLinesBetween === 0) {
// context.report({
// messageId: 'needs-newline',
// data: {
// left: left.name,
// right: right.name,
// },
// node: right.node,
// fix: (fixer) => fix(fixer, nodeList, sourceCode, options),
// })
// } else if (
// numberOfEmptyLinesBetween > 1 ||
// (leftNumber === rightNumber && numberOfEmptyLinesBetween > 0)
// ) {
// context.report({
// messageId: 'extra-newline',
// data: {
// left: left.name,
// right: right.name,
// },
// node: right.node,
// fix: (fixer) => fix(fixer, nodeList, sourceCode, options),
// })
// }
// }
})
}
},
Expand Down
16 changes: 8 additions & 8 deletions src/utils/compare.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,32 @@
import type { SortingNode } from './types.js'

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

interface NaturalCompareOptions extends BaseCompareOptions {
interface NaturalCompareOptions<T extends SortingNode> extends BaseCompareOptions<T> {
ignoreCase?: boolean
type: 'natural'
}

export type CompareOptions = NaturalCompareOptions
export type CompareOptions<T extends SortingNode> = NaturalCompareOptions<T>

export function compare(a: SortingNode, b: SortingNode, options: CompareOptions): number {
/** Don't sort unsassigned imports. */
export function compare<T extends SortingNode>(a: T, b: T, options: CompareOptions<T>): number {
/** Don't sort unassigned 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 sortingFunction: (a: T, b: T) => number

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

sortingFunction = (aNode, bNode) => {
let aImport = stripProtocol(nodeValueGetter(aNode))
Expand Down
Loading

0 comments on commit f6bc787

Please sign in to comment.