Skip to content

Commit

Permalink
feat(sort-maps): adds groups, customGroups and newlinesBetween
Browse files Browse the repository at this point in the history
  • Loading branch information
hugop95 committed Jan 17, 2025
1 parent a2b8d79 commit a49ca89
Show file tree
Hide file tree
Showing 5 changed files with 830 additions and 26 deletions.
86 changes: 86 additions & 0 deletions docs/content/rules/sort-maps.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,86 @@ new Map([

Each group of map members (separated by empty lines) is treated independently, and the order within each group is preserved.

### newlinesBetween

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

Specifies how new lines should be handled between map members.

- `ignore` — Do not report errors related to new lines between map members.
- `always` — Enforce one new line between each group, and forbid new lines inside a group.
- `never` — No new lines are allowed in maps.

This option is only applicable when `partitionByNewLine` is `false`.

### groups

<sub>
type: `Array<string | string[]>`
</sub>
<sub>default: `[]`</sub>

Allows you to specify a list of groups for sorting. Groups help organize elements into categories.

Each element will be assigned a single group specified in the `groups` option (or the `unknown` group if no match is found).
The order of items in the `groups` option determines how groups are ordered.

Within a given group, members will be sorted according to the `type`, `order`, `ignoreCase`, etc. options.

Individual groups can be combined together by placing them in an array. The order of groups in that array does not matter.
All members of the groups in the array will be sorted together as if they were part of a single group.

### customGroups

<sub>
type: `Array<CustomGroupDefinition | CustomGroupAnyOfDefinition>`
</sub>
<sub>default: `{}`</sub>

You can define your own groups and use regexp patterns to match specific object type members.

A custom group definition may follow one of the two following interfaces:

```ts
interface CustomGroupDefinition {
groupName: string
type?: 'alphabetical' | 'natural' | 'line-length' | 'unsorted'
order?: 'asc' | 'desc'
elementNamePattern?: string
}

```
An array element will match a `CustomGroupDefinition` group if it matches all the filters of the custom group's definition.

or:

```ts
interface CustomGroupAnyOfDefinition {
groupName: string
type?: 'alphabetical' | 'natural' | 'line-length' | 'unsorted'
order?: 'asc' | 'desc'
anyOf: Array<{
elementNamePattern?: string
}>
}
```

An array element will match a `CustomGroupAnyOfDefinition` group if it matches all the filters of at least one of the `anyOf` items.

#### Attributes

- `groupName`: The group's name, which needs to be put in the `groups` option.
- `elementNamePattern`: If entered, will check that the name of the element matches the pattern entered.
- `type`: Overrides the sort type for that custom group. `unsorted` will not sort the group.
- `order`: Overrides the sort order for that custom group

#### Match importance

The `customGroups` list is ordered:
The first custom group definition that matches an element will be used.

Custom groups have a higher priority than any predefined group.

## Usage

<CodeTabs
Expand All @@ -201,6 +281,9 @@ Each group of map members (separated by empty lines) is treated independently, a
specialCharacters: 'keep',
partitionByNewLine: false,
partitionByComment: false,
newlinesBetween: false,
groups: [],
customGroups: [],
},
],
},
Expand All @@ -227,6 +310,9 @@ Each group of map members (separated by empty lines) is treated independently, a
specialCharacters: 'keep',
partitionByNewLine: false,
partitionByComment: false,
newlinesBetween: false,
groups: [],
customGroups: [],
},
],
},
Expand Down
139 changes: 113 additions & 26 deletions rules/sort-maps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,44 +4,61 @@ import type { SortingNode } from '../types/sorting-node'
import type { Options } from './sort-maps/types'

import {
buildCustomGroupsArrayJsonSchema,
partitionByCommentJsonSchema,
partitionByNewLineJsonSchema,
specialCharactersJsonSchema,
newlinesBetweenJsonSchema,
ignoreCaseJsonSchema,
buildTypeJsonSchema,
alphabetJsonSchema,
localesJsonSchema,
groupsJsonSchema,
orderJsonSchema,
} from '../utils/common-json-schemas'
import { validateGeneratedGroupsConfiguration } from '../utils/validate-generated-groups-configuration'
import { validateCustomSortConfiguration } from '../utils/validate-custom-sort-configuration'
import { getCustomGroupsCompareOptions } from '../utils/get-custom-groups-compare-options'
import { getEslintDisabledLines } from '../utils/get-eslint-disabled-lines'
import { doesCustomGroupMatch } from './sort-maps/does-custom-group-match'
import { isNodeEslintDisabled } from '../utils/is-node-eslint-disabled'
import { hasPartitionComment } from '../utils/has-partition-comment'
import { createNodeIndexMap } from '../utils/create-node-index-map'
import { sortNodesByGroups } from '../utils/sort-nodes-by-groups'
import { getCommentsBefore } from '../utils/get-comments-before'
import { getNewlinesErrors } from '../utils/get-newlines-errors'
import { singleCustomGroupJsonSchema } from './sort-maps/types'
import { createEslintRule } from '../utils/create-eslint-rule'
import { getLinesBetween } from '../utils/get-lines-between'
import { getGroupNumber } from '../utils/get-group-number'
import { getSourceCode } from '../utils/get-source-code'
import { toSingleLine } from '../utils/to-single-line'
import { rangeToDiff } from '../utils/range-to-diff'
import { getSettings } from '../utils/get-settings'
import { isSortable } from '../utils/is-sortable'
import { makeFixes } from '../utils/make-fixes'
import { sortNodes } from '../utils/sort-nodes'
import { useGroups } from '../utils/use-groups'
import { complete } from '../utils/complete'
import { pairwise } from '../utils/pairwise'

type MESSAGE_ID = 'unexpectedMapElementsOrder'
type MESSAGE_ID =
| 'missedSpacingBetweenMapElementsMembers'
| 'extraSpacingBetweenMapElementsMembers'
| 'unexpectedMapElementsGroupOrder'
| 'unexpectedMapElementsOrder'

let defaultOptions: Required<Options[0]> = {
specialCharacters: 'keep',
partitionByComment: false,
partitionByNewLine: false,
newlinesBetween: 'ignore',
type: 'alphabetical',
ignoreCase: true,
customGroups: [],
locales: 'en-US',
alphabet: '',
order: 'asc',
groups: [],
}

export default createEslintRule<Options, MESSAGE_ID>({
Expand All @@ -63,6 +80,12 @@ export default createEslintRule<Options, MESSAGE_ID>({
let settings = getSettings(context.settings)
let options = complete(context.options.at(0), settings, defaultOptions)
validateCustomSortConfiguration(options)
validateGeneratedGroupsConfiguration({
customGroups: options.customGroups,
groups: options.groups,
selectors: [],
modifiers: [],
})

let sourceCode = getSourceCode(context)
let eslintDisabledLines = getEslintDisabledLines({
Expand Down Expand Up @@ -104,12 +127,33 @@ export default createEslintRule<Options, MESSAGE_ID>({
}

let lastSortingNode = formattedMembers.at(-1)?.at(-1)

let { defineGroup, getGroup } = useGroups(options)
for (let customGroup of options.customGroups) {
if (
doesCustomGroupMatch({
elementName: name,
customGroup,
})
) {
defineGroup(customGroup.groupName, true)
/**
* If the custom group is not referenced in the `groups` option, it
* will be ignored
*/
if (getGroup() === customGroup.groupName) {
break
}
}
}

let sortingNode: SortingNode = {
isEslintDisabled: isNodeEslintDisabled(
element,
eslintDisabledLines,
),
size: rangeToDiff(element, sourceCode),
group: getGroup(),
node: element,
name,
}
Expand All @@ -136,7 +180,11 @@ export default createEslintRule<Options, MESSAGE_ID>({
let sortNodesExcludingEslintDisabled = (
ignoreEslintDisabledNodes: boolean,
): SortingNode[] =>
sortNodes(nodes, options, { ignoreEslintDisabledNodes })
sortNodesByGroups(nodes, options, {
getGroupCompareOptions: groupNumber =>
getCustomGroupsCompareOptions(options, groupNumber),
ignoreEslintDisabledNodes,
})
let sortedNodes = sortNodesExcludingEslintDisabled(false)
let sortedNodesExcludingEslintDisabled =
sortNodesExcludingEslintDisabled(true)
Expand All @@ -147,31 +195,59 @@ export default createEslintRule<Options, MESSAGE_ID>({
let leftIndex = nodeIndexMap.get(left)!
let rightIndex = nodeIndexMap.get(right)!

let leftNumber = getGroupNumber(options.groups, left)
let rightNumber = getGroupNumber(options.groups, right)

let indexOfRightExcludingEslintDisabled =
sortedNodesExcludingEslintDisabled.indexOf(right)

let messageIds: MESSAGE_ID[] = []

if (
leftIndex < rightIndex &&
leftIndex < indexOfRightExcludingEslintDisabled
leftIndex > rightIndex ||
leftIndex >= indexOfRightExcludingEslintDisabled
) {
return
messageIds.push(
leftNumber === rightNumber
? 'unexpectedMapElementsOrder'
: 'unexpectedMapElementsGroupOrder',
)
}

context.report({
fix: fixer =>
makeFixes({
sortedNodes: sortedNodesExcludingEslintDisabled,
sourceCode,
options,
fixer,
nodes,
}),
data: {
right: toSingleLine(right.name),
left: toSingleLine(left.name),
},
messageId: 'unexpectedMapElementsOrder',
node: right.node,
})
messageIds = [
...messageIds,
...getNewlinesErrors({
missedSpacingError: 'missedSpacingBetweenMapElementsMembers',
extraSpacingError: 'extraSpacingBetweenMapElementsMembers',
rightNum: rightNumber,
leftNum: leftNumber,
sourceCode,
options,
right,
left,
}),
]

for (let messageId of messageIds) {
context.report({
fix: fixer =>
makeFixes({
sortedNodes: sortedNodesExcludingEslintDisabled,
sourceCode,
options,
fixer,
nodes,
}),
data: {
right: toSingleLine(right.name),
left: toSingleLine(left.name),
rightGroup: right.group,
leftGroup: left.group,
},
node: right.node,
messageId,
})
}
})
}
}
Expand All @@ -186,27 +262,38 @@ export default createEslintRule<Options, MESSAGE_ID>({
description:
'Allows you to use comments to separate the maps members into logical groups.',
},
customGroups: buildCustomGroupsArrayJsonSchema({
singleCustomGroupJsonSchema,
}),
partitionByNewLine: partitionByNewLineJsonSchema,
specialCharacters: specialCharactersJsonSchema,
newlinesBetween: newlinesBetweenJsonSchema,
ignoreCase: ignoreCaseJsonSchema,
alphabet: alphabetJsonSchema,
type: buildTypeJsonSchema(),
locales: localesJsonSchema,
groups: groupsJsonSchema,
order: orderJsonSchema,
},
additionalProperties: false,
type: 'object',
},
],
messages: {
unexpectedMapElementsGroupOrder:
'Expected "{{right}}" ({{rightGroup}}) to come before "{{left}}" ({{leftGroup}}).',
missedSpacingBetweenMapElementsMembers:
'Missed spacing between "{{left}}" and "{{right}}" members.',
extraSpacingBetweenMapElementsMembers:
'Extra spacing between "{{left}}" and "{{right}}" members.',
unexpectedMapElementsOrder:
'Expected "{{right}}" to come before "{{left}}".',
},
docs: {
url: 'https://perfectionist.dev/rules/sort-maps',
description: 'Enforce sorted Map elements.',
recommended: true,
},
messages: {
unexpectedMapElementsOrder:
'Expected "{{right}}" to come before "{{left}}".',
},
type: 'suggestion',
fixable: 'code',
},
Expand Down
34 changes: 34 additions & 0 deletions rules/sort-maps/does-custom-group-match.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import type { SingleCustomGroup, AnyOfCustomGroup } from './types'

import { matches } from '../../utils/matches'

interface DoesCustomGroupMatchProps {
customGroup: SingleCustomGroup | AnyOfCustomGroup
elementName: string
}

export let doesCustomGroupMatch = (
props: DoesCustomGroupMatchProps,
): boolean => {
if ('anyOf' in props.customGroup) {
// At least one subgroup must match
return props.customGroup.anyOf.some(subgroup =>
doesCustomGroupMatch({ ...props, customGroup: subgroup }),
)
}

if (
'elementNamePattern' in props.customGroup &&
props.customGroup.elementNamePattern
) {
let matchesElementNamePattern: boolean = matches(
props.elementName,
props.customGroup.elementNamePattern,
)
if (!matchesElementNamePattern) {
return false
}
}

return true
}
Loading

0 comments on commit a49ca89

Please sign in to comment.