Skip to content

Commit

Permalink
feat: improve ability to expand custom groups
Browse files Browse the repository at this point in the history
  • Loading branch information
hugop95 committed Sep 1, 2024
1 parent 73b1b54 commit 5088619
Show file tree
Hide file tree
Showing 5 changed files with 1,119 additions and 161 deletions.
110 changes: 93 additions & 17 deletions docs/content/rules/sort-classes.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -450,9 +450,60 @@ abstract class Example extends BaseExample {

### customGroups

<sub>default: `{}`</sub>
<sub>default: `[]`</sub>

You can define your own groups for class members using custom glob patterns for matching.
You can define your own groups to match very specific 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'
selector?: string
modifiers?: string[]
elementNamePattern?: string
decoratorNamePattern?: string
}
```
A class member will match a `CustomGroupDefinition` group if it matches all the filters of the custom group's definition.

or:

```ts
interface CustomGroupBlockDefinition {
groupName: string
type?: 'alphabetical' | 'natural' | 'line-length' | 'unsorted'
order?: 'asc' | 'desc'
anyOf: Array<{
selector?: string
modifiers?: string[]
elementNamePattern?: string
decoratorNamePattern?: string
}>
}
```

A class member will match a `CustomGroupBlockDefinition` 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.
- `selector`: Filter on the `selector` of the element.
- `modifiers`: Filter on the `modifiers` of the element. (All the modifiers of the element must be present in that list)
- `elementNamePattern`: If entered, will check that the name of the element matches the pattern entered.
- `decoratorNamePattern`: If entered, will check that at least one `decorator` 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. If you want a predefined group to take precedence over a custom group,
you must write a custom group definition that does the same as what the predefined group does (using `selector` and `modifiers` filters), and put it first in the list.

Example:

Expand All @@ -461,23 +512,48 @@ Example:
groups: [
'static-block',
'index-signature',
'static-property',
['protected-property', 'protected-accessor-property'],
['private-property', 'private-accessor-property'],
['property', 'accessor-property'],
+ 'input-properties', // [!code ++]
+ 'output-properties', // [!code ++]
'constructor',
'static-method',
'protected-method',
'private-method',
'static-private-method',
'method',
+ 'unsorted-methods-and-other-properties', // [!code ++]
['get-method', 'set-method'],
'unknown',
+ 'value', // [!code ++]
],
+ customGroups: { // [!code ++]
+ value: 'value', // [!code ++]
+ } // [!code ++]
+ customGroups: [ // [!code ++]
+ [ // [!code ++]
+ { // [!code ++]
+ // `constructor()` members must not match // [!code ++]
+ // `unsorted-methods-and-other-properties` // [!code ++]
+ // so make them match this first // [!code ++]
+ groupName: 'constructor', // [!code ++]
+ selector: 'constructor', // [!code ++]
+ }, // [!code ++]
+ { // [!code ++]
+ groupName: 'input-properties', // [!code ++]
+ selector: 'property', // [!code ++]
+ modifiers: ['decorated'], // [!code ++]
+ decoratorNamePattern: 'Input', // [!code ++]
+ }, // [!code ++]
+ { // [!code ++]
+ groupName: 'output-properties', // [!code ++]
+ selector: 'property', // [!code ++]
+ modifiers: ['decorated'], // [!code ++]
+ decoratorNamePattern: 'Output', // [!code ++]
+ }, // [!code ++]
+ { // [!code ++]
+ groupName: 'unsorted-methods-and-other-properties', // [!code ++]
+ type: 'unsorted', // [!code ++]
+ anyOf: [ // [!code ++]
+ { // [!code ++]
+ selector: 'method', // [!code ++]
+ }, // [!code ++]
+ { // [!code ++]
+ selector: 'property', // [!code ++]
+ }, // [!code ++]
+ ] // [!code ++]
+ }, // [!code ++]
+ ] // [!code ++]
+ ] // [!code ++]
}
```

Expand Down Expand Up @@ -518,7 +594,7 @@ Example:
['get-method', 'set-method'],
'unknown',
],
customGroups: {},
customGroups: [],
},
],
},
Expand Down Expand Up @@ -558,7 +634,7 @@ Example:
['get-method', 'set-method'],
'unknown',
],
customGroups: {},
customGroups: [],
},
],
},
Expand Down
105 changes: 104 additions & 1 deletion rules/sort-classes-utils.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,23 @@
import type { TSESTree } from '@typescript-eslint/utils'

import type { Modifier, Selector } from './sort-classes'
import { minimatch } from 'minimatch'

import type {
SortClassesOptions,
SingleCustomGroup,
CustomGroupBlock,
Modifier,
Selector,
} from './sort-classes.types'
import type { CompareOptions } from '../utils/compare'

interface CustomGroupMatchesProps {
customGroup: SingleCustomGroup | CustomGroupBlock
selectors: Selector[]
modifiers: Modifier[]
decorators: string[]
elementName: string
}
/**
* Cache computed groups by modifiers and selectors for performance
*/
Expand Down Expand Up @@ -138,3 +154,90 @@ export const getOverloadSignatureGroups = (
...staticOverloadSignaturesByName.values(),
].filter(group => group.length > 1)
}

/**
* Returns whether a custom group matches the given properties
*/
export const customGroupMatches = (props: CustomGroupMatchesProps): boolean => {
if ('anyOf' in props.customGroup) {
// At least one subgroup must match
return props.customGroup.anyOf.some(subgroup =>
customGroupMatches({ ...props, customGroup: subgroup }),
)
}
if (
props.customGroup.selector &&
!props.selectors.includes(props.customGroup.selector)
) {
return false
}

if (props.customGroup.modifiers) {
for (let modifier of props.customGroup.modifiers) {
if (!props.modifiers.includes(modifier)) {
return false
}
}
}

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

if (
'decoratorNamePattern' in props.customGroup &&
props.customGroup.decoratorNamePattern
) {
let decoratorPattern = props.customGroup.decoratorNamePattern
let matchesDecoratorNamePattern: boolean = props.decorators.some(
decorator =>
minimatch(decorator, decoratorPattern, {
nocomment: true,
}),
)
if (!matchesDecoratorNamePattern) {
return false
}
}

return true
}

/**
* Returns the compare options used to sort a given group.
* If the group is a custom group, its options will be favored over the default options.
* Returns null if the group should not be sorted
*/
export const getCompareOptions = (
options: Required<SortClassesOptions[0]>,
groupNumber: number,
): CompareOptions | null => {
let group = options.groups[groupNumber]
let customGroup =
typeof group === 'string' && Array.isArray(options.customGroups)
? options.customGroups.find(g => group === g.groupName)
: null
if (customGroup?.type === 'unsorted') {
return null
}
return {
type: customGroup?.type ?? options.type,
order:
customGroup && 'order' in customGroup && customGroup.order
? customGroup.order
: options.order,
ignoreCase: options.ignoreCase,
}
}
Loading

0 comments on commit 5088619

Please sign in to comment.