Skip to content

Commit

Permalink
feat(sort-objects): handle context-based configurations
Browse files Browse the repository at this point in the history
  • Loading branch information
hugop95 authored Dec 7, 2024
1 parent ac1e7c4 commit a3ee3ff
Show file tree
Hide file tree
Showing 6 changed files with 247 additions and 29 deletions.
42 changes: 42 additions & 0 deletions docs/content/rules/sort-objects.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,42 @@ Allows you to choose whether to sort standard object declarations.
Allows you to choose whether to sort destructured objects.
The `groups` attribute allows you to specify whether to use groups to sort destructured objects.

### useConfigurationIf

<sub>
type: `{ allNamesMatchPattern?: string }`
</sub>
<sub>default: `{}`</sub>

Allows you to specify filters to match a particular options configuration for a given object.

The first matching options configuration will be used. If no configuration matches, the default options configuration will be used.

- `allNamesMatchPattern` — A regexp pattern that all object keys must match.

Example configuration:
```ts
{
'perfectionist/sort-objects': [
'error',
{
groups: ['r', 'g', 'b'], // Sort colors by RGB
customGroups: {
r: 'r',
g: 'g',
b: 'b',
},
useConfigurationIf: {
allNamesMatchPattern: '^r|g|b$',
},
},
{
type: 'alphabetical' // Fallback configuration
}
],
}
```

### groups

<sub>
Expand Down Expand Up @@ -391,8 +427,11 @@ const user = {
partitionByComment: false,
partitionByNewLine: false,
newlinesBetween: 'ignore',
objectDeclarations: true,
destructuredObjects: true,
styledComponents: true,
ignorePattern: [],
useConfigurationIf: {},
groups: [],
customGroups: {},
},
Expand Down Expand Up @@ -422,8 +461,11 @@ const user = {
partitionByComment: false,
partitionByNewLine: false,
newlinesBetween: 'ignore',
objectDeclarations: true,
destructuredObjects: true,
styledComponents: true,
ignorePattern: [],
useConfigurationIf: {},
groups: [],
customGroups: {},
},
Expand Down
90 changes: 61 additions & 29 deletions rules/sort-objects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { SortingNodeWithDependencies } from '../utils/sort-nodes-by-depende
import {
partitionByCommentJsonSchema,
partitionByNewLineJsonSchema,
useConfigurationIfJsonSchema,
specialCharactersJsonSchema,
newlinesBetweenJsonSchema,
customGroupsJsonSchema,
Expand All @@ -20,6 +21,7 @@ import {
} from '../utils/sort-nodes-by-dependencies'
import { validateNewlinesAndPartitionConfiguration } from '../utils/validate-newlines-and-partition-configuration'
import { validateGroupsConfiguration } from '../utils/validate-groups-configuration'
import { getMatchingContextOptions } from '../utils/get-matching-context-options'
import { getEslintDisabledLines } from '../utils/get-eslint-disabled-lines'
import { isNodeEslintDisabled } from '../utils/is-node-eslint-disabled'
import { hasPartitionComment } from '../utils/is-partition-comment'
Expand All @@ -42,28 +44,29 @@ import { complete } from '../utils/complete'
import { pairwise } from '../utils/pairwise'
import { matches } from '../utils/matches'

type Options = [
Partial<{
destructuredObjects: { groups: boolean } | boolean
type: 'alphabetical' | 'line-length' | 'natural'
customGroups: Record<string, string[] | string>
partitionByComment: string[] | boolean | string
newlinesBetween: 'ignore' | 'always' | 'never'
specialCharacters: 'remove' | 'trim' | 'keep'
locales: NonNullable<Intl.LocalesArgument>
groups: (Group[] | Group)[]
partitionByNewLine: boolean
objectDeclarations: boolean
styledComponents: boolean
/**
* @deprecated for {@link `destructuredObjects`} and {@link `objectDeclarations`}
*/
destructureOnly: boolean
ignorePattern: string[]
order: 'desc' | 'asc'
ignoreCase: boolean
}>,
]
type Options = Partial<{
useConfigurationIf: {
allNamesMatchPattern?: string
}
destructuredObjects: { groups: boolean } | boolean
type: 'alphabetical' | 'line-length' | 'natural'
customGroups: Record<string, string[] | string>
partitionByComment: string[] | boolean | string
newlinesBetween: 'ignore' | 'always' | 'never'
specialCharacters: 'remove' | 'trim' | 'keep'
locales: NonNullable<Intl.LocalesArgument>
groups: (Group[] | Group)[]
partitionByNewLine: boolean
objectDeclarations: boolean
styledComponents: boolean
/**
* @deprecated for {@link `destructuredObjects`} and {@link `objectDeclarations`}
*/
destructureOnly: boolean
ignorePattern: string[]
order: 'desc' | 'asc'
ignoreCase: boolean
}>[]

type MESSAGE_ID =
| 'missedSpacingBetweenObjectMembers'
Expand All @@ -83,6 +86,7 @@ let defaultOptions: Required<Options[0]> = {
objectDeclarations: true,
styledComponents: true,
destructureOnly: false,
useConfigurationIf: {},
type: 'alphabetical',
ignorePattern: [],
ignoreCase: true,
Expand All @@ -102,9 +106,14 @@ export default createEslintRule<Options, MESSAGE_ID>({
}

let settings = getSettings(context.settings)

let options = complete(context.options.at(0), settings, defaultOptions)

let sourceCode = getSourceCode(context)
let matchedContextOptions = getMatchingContextOptions({
nodeNames: nodeObject.properties
.map(property => getNodeName({ sourceCode, property }))
.filter(nodeName => nodeName !== null),
contextOptions: context.options,
})
let options = complete(matchedContextOptions, settings, defaultOptions)
validateGroupsConfiguration(
options.groups,
['multiline', 'method', 'unknown'],
Expand Down Expand Up @@ -182,7 +191,6 @@ export default createEslintRule<Options, MESSAGE_ID>({
return
}

let sourceCode = getSourceCode(context)
let eslintDisabledLines = getEslintDisabledLines({
ruleName: context.id,
sourceCode,
Expand Down Expand Up @@ -480,8 +488,8 @@ export default createEslintRule<Options, MESSAGE_ID>({
}
},
meta: {
schema: [
{
schema: {
items: {
properties: {
destructuredObjects: {
oneOf: [
Expand Down Expand Up @@ -528,6 +536,7 @@ export default createEslintRule<Options, MESSAGE_ID>({
type: 'boolean',
},
partitionByNewLine: partitionByNewLineJsonSchema,
useConfigurationIf: useConfigurationIfJsonSchema,
specialCharacters: specialCharactersJsonSchema,
newlinesBetween: newlinesBetweenJsonSchema,
customGroups: customGroupsJsonSchema,
Expand All @@ -540,7 +549,9 @@ export default createEslintRule<Options, MESSAGE_ID>({
additionalProperties: false,
type: 'object',
},
],
uniqueItems: true,
type: 'array',
},
messages: {
unexpectedObjectsGroupOrder:
'Expected "{{right}}" ({{rightGroup}}) to come before "{{left}}" ({{leftGroup}}).',
Expand All @@ -563,3 +574,24 @@ export default createEslintRule<Options, MESSAGE_ID>({
defaultOptions: [defaultOptions],
name: 'sort-objects',
})

let getNodeName = ({
sourceCode,
property,
}: {
property:
| TSESTree.ObjectLiteralElement
| TSESTree.RestElement
| TSESTree.Property
sourceCode: ReturnType<typeof getSourceCode>
}): string | null => {
if (property.type === 'SpreadElement' || property.type === 'RestElement') {
return null
}
if (property.key.type === 'Identifier') {
return property.key.name
} else if (property.key.type === 'Literal') {
return `${property.key.value}`
}
return sourceCode.getText(property.key)
}
44 changes: 44 additions & 0 deletions test/get-matching-context-options.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { describe, expect, it } from 'vitest'

import { getMatchingContextOptions } from '../utils/get-matching-context-options'

describe('get-matching-context-options', () => {
describe('`allNamesMatchPattern`', () => {
it('matches the appropriate context options with `allNamesMatchPattern`', () => {
let barContextOptions = buildContextOptions('bar')
let contextOptions = [buildContextOptions('foo'), barContextOptions]
let nodeNames = ['bar1', 'bar2']

expect(getMatchingContextOptions({ contextOptions, nodeNames })).toEqual(
barContextOptions,
)
})

it('returns `undefined` if no configuration matches', () => {
let contextOptions = [buildContextOptions('foo')]
let nodeNames = ['bar1', 'bar2']

expect(
getMatchingContextOptions({ contextOptions, nodeNames }),
).toBeUndefined()
})

it('returns the first context options if no filters are entered', () => {
let emptyContextOptions = buildContextOptions()
let contextOptions = [emptyContextOptions, buildContextOptions()]
let nodeNames = ['bar1', 'bar2']

expect(getMatchingContextOptions({ contextOptions, nodeNames })).toEqual(
emptyContextOptions,
)
})
})

let buildContextOptions = (
allNamesMatchPattern?: string,
): { useConfigurationIf: { allNamesMatchPattern?: string } } => ({
useConfigurationIf: {
...(allNamesMatchPattern ? { allNamesMatchPattern } : {}),
},
})
})
68 changes: 68 additions & 0 deletions test/sort-objects.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1850,6 +1850,74 @@ describe(ruleName, () => {
valid: [],
},
)

describe(`${ruleName}(${type}): allows to use 'useConfigurationIf'`, () => {
ruleTester.run(
`${ruleName}(${type}): allows to use 'allNamesMatchPattern'`,
rule,
{
invalid: [
{
errors: [
{
data: {
rightGroup: 'g',
leftGroup: 'b',
right: 'g',
left: 'b',
},
messageId: 'unexpectedObjectsGroupOrder',
},
{
data: {
rightGroup: 'r',
leftGroup: 'g',
right: 'r',
left: 'g',
},
messageId: 'unexpectedObjectsGroupOrder',
},
],
options: [
{
...options,
useConfigurationIf: {
allNamesMatchPattern: 'foo',
},
},
{
...options,
customGroups: {
r: 'r',
g: 'g',
b: 'b',
},
useConfigurationIf: {
allNamesMatchPattern: '^r|g|b$',
},
groups: ['r', 'g', 'b'],
},
],
output: dedent`
let obj = {
r: string,
g: string,
b: string
}
`,
code: dedent`
let obj = {
b: string,
g: string,
r: string
}
`,
},
],
valid: [],
},
)
})
})

describe(`${ruleName}: sorting by natural order`, () => {
Expand Down
10 changes: 10 additions & 0 deletions utils/common-json-schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,16 @@ export let newlinesBetweenJsonSchema: JSONSchema4 = {
type: 'string',
}

export let useConfigurationIfJsonSchema: JSONSchema4 = {
properties: {
allNamesMatchPattern: {
type: 'string',
},
},
additionalProperties: false,
type: 'object',
}

let customGroupSortJsonSchema: Record<string, JSONSchema4> = {
type: {
enum: ['alphabetical', 'line-length', 'natural', 'unsorted'],
Expand Down
22 changes: 22 additions & 0 deletions utils/get-matching-context-options.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { matches } from './matches'

interface Options {
useConfigurationIf?: {
allNamesMatchPattern?: string
}
}

export let getMatchingContextOptions = ({
contextOptions,
nodeNames,
}: {
contextOptions: Options[]
nodeNames: string[]
}): undefined | Options =>
contextOptions.find(options => {
let allNamesMatchPattern = options.useConfigurationIf?.allNamesMatchPattern
return (
!allNamesMatchPattern ||
nodeNames.every(nodeName => matches(nodeName, allNamesMatchPattern))
)
})

0 comments on commit a3ee3ff

Please sign in to comment.