Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(sort-interfaces): add optionalityOrder option #117

Merged
merged 1 commit into from
Mar 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions docs/rules/sort-interfaces.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ type Group = 'multiline' | CustomGroup

interface Options {
type?: 'alphabetical' | 'natural' | 'line-length'
optionalityOrder?: 'ignore' | 'optional-first' | 'required-first'
order?: 'asc' | 'desc'
'ignore-case'?: boolean
groups?: (Group | Group[])[]
Expand All @@ -117,6 +118,13 @@ interface Options {
- `natural` - sort in natural order.
- `line-length` - sort by code line length.

### optionalityOrder

<sub>(default: `'ignore'`)</sub>

- `optional-first` - put all optional members first.
- `required-first` - put all required members first.

### order

<sub>(default: `'asc'`)</sub>
Expand Down
139 changes: 108 additions & 31 deletions rules/sort-interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ import { minimatch } from 'minimatch'

import type { SortingNode } from '../typings'

import { OptionalityOrder, SortOrder, SortType } from '../typings'
import { createEslintRule } from '../utils/create-eslint-rule'
import { isMemberOptional } from '../utils/is-member-optional'
import { getLinesBetween } from '../utils/get-lines-between'
import { getGroupNumber } from '../utils/get-group-number'
import { toSingleLine } from '../utils/to-single-line'
import { rangeToDiff } from '../utils/range-to-diff'
import { isPositive } from '../utils/is-positive'
import { SortOrder, SortType } from '../typings'
import { useGroups } from '../utils/use-groups'
import { sortNodes } from '../utils/sort-nodes'
import { makeFixes } from '../utils/make-fixes'
Expand All @@ -23,6 +24,7 @@ type Group<T extends string[]> = 'multiline' | 'unknown' | T[number]
type Options<T extends string[]> = [
Partial<{
'custom-groups': { [key: string]: string[] | string }
optionalityOrder: OptionalityOrder
groups: (Group<T>[] | Group<T>)[]
'partition-by-new-line': boolean
'ignore-pattern': string[]
Expand All @@ -49,6 +51,15 @@ export default createEslintRule<Options<string[]>, MESSAGE_ID>({
'custom-groups': {
type: 'object',
},
optionalityOrder: {
enum: [
OptionalityOrder.ignore,
OptionalityOrder['optional-first'],
OptionalityOrder['required-first'],
],
default: OptionalityOrder.ignore,
type: 'string',
},
type: {
enum: [
SortType.alphabetical,
Expand Down Expand Up @@ -100,6 +111,7 @@ export default createEslintRule<Options<string[]>, MESSAGE_ID>({
TSInterfaceDeclaration: node => {
if (node.body.body.length > 1) {
let options = complete(context.options.at(0), {
optionalityOrder: OptionalityOrder.ignore,
'partition-by-new-line': false,
type: SortType.alphabetical,
'ignore-case': false,
Expand Down Expand Up @@ -194,16 +206,81 @@ export default createEslintRule<Options<string[]>, MESSAGE_ID>({
[[]],
)

for (let nodes of formattedMembers) {
pairwise(nodes, (left, right) => {
let leftNum = getGroupNumber(options.groups, left)
let rightNum = getGroupNumber(options.groups, right)
let toSorted = (nodes: SortingNode[]) => {
let grouped: {
[key: string]: SortingNode[]
} = {}

if (
leftNum > rightNum ||
(leftNum === rightNum &&
isPositive(compare(left, right, options)))
) {
for (let currentNode of nodes) {
let groupNum = getGroupNumber(options.groups, currentNode)

if (!(groupNum in grouped)) {
grouped[groupNum] = [currentNode]
} else {
grouped[groupNum] = sortNodes(
[...grouped[groupNum], currentNode],
options,
)
}
}

let sortedNodes: SortingNode[] = []

for (let group of Object.keys(grouped).sort(
(a, b) => Number(a) - Number(b),
)) {
sortedNodes.push(...sortNodes(grouped[group], options))
}

return sortedNodes
}

let checkGroupSort = (left: SortingNode, right: SortingNode) => {
let leftNum = getGroupNumber(options.groups, left)
let rightNum = getGroupNumber(options.groups, right)

return (
leftNum > rightNum ||
(leftNum === rightNum &&
isPositive(compare(left, right, options)))
)
}

let checkOrder = (
members: SortingNode[],
left: SortingNode,
right: SortingNode,
iteration: number,
) => {
if (options.optionalityOrder === OptionalityOrder.ignore) {
return checkGroupSort(left, right)
}

let switchIndex = members.findIndex(
(_, i) =>
i &&
isMemberOptional(members[i - 1].node) !==
isMemberOptional(members[i].node),
)

if (iteration < switchIndex && iteration + 1 !== switchIndex) {
return checkGroupSort(left, right)
}

if (isMemberOptional(left.node) !== isMemberOptional(right.node)) {
return (
isMemberOptional(left.node) !==
(options.optionalityOrder ===
OptionalityOrder['optional-first'])
)
}

return checkGroupSort(left, right)
}

for (let nodes of formattedMembers) {
pairwise(nodes, (left, right, iteration) => {
if (checkOrder(nodes, left, right, iteration)) {
context.report({
messageId: 'unexpectedInterfacePropertiesOrder',
data: {
Expand All @@ -212,29 +289,29 @@ export default createEslintRule<Options<string[]>, MESSAGE_ID>({
},
node: right.node,
fix: fixer => {
let grouped: {
[key: string]: SortingNode[]
} = {}

for (let currentNode of nodes) {
let groupNum = getGroupNumber(options.groups, currentNode)

if (!(groupNum in grouped)) {
grouped[groupNum] = [currentNode]
} else {
grouped[groupNum] = sortNodes(
[...grouped[groupNum], currentNode],
options,
)
}
}
let sortedNodes

let sortedNodes: SortingNode[] = []
if (options.optionalityOrder !== OptionalityOrder.ignore) {
let optionalNodes = nodes.filter(member =>
isMemberOptional(member.node),
)
let requiredNodes = nodes.filter(
member => !isMemberOptional(member.node),
)

for (let group of Object.keys(grouped).sort(
(a, b) => Number(a) - Number(b),
)) {
sortedNodes.push(...sortNodes(grouped[group], options))
sortedNodes =
options.optionalityOrder ===
OptionalityOrder['optional-first']
? [
...toSorted(optionalNodes),
...toSorted(requiredNodes),
]
: [
...toSorted(requiredNodes),
...toSorted(optionalNodes),
]
} else {
sortedNodes = toSorted(nodes)
}

return makeFixes(
Expand Down
Loading
Loading