diff --git a/docs/content/rules/sort-objects.mdx b/docs/content/rules/sort-objects.mdx index 29369fc66..b509d2f3a 100644 --- a/docs/content/rules/sort-objects.mdx +++ b/docs/content/rules/sort-objects.mdx @@ -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 + + + type: `{ allNamesMatchPattern?: string }` + +default: `{}` + +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 @@ -391,8 +427,11 @@ const user = { partitionByComment: false, partitionByNewLine: false, newlinesBetween: 'ignore', + objectDeclarations: true, + destructuredObjects: true, styledComponents: true, ignorePattern: [], + useConfigurationIf: {}, groups: [], customGroups: {}, }, @@ -422,8 +461,11 @@ const user = { partitionByComment: false, partitionByNewLine: false, newlinesBetween: 'ignore', + objectDeclarations: true, + destructuredObjects: true, styledComponents: true, ignorePattern: [], + useConfigurationIf: {}, groups: [], customGroups: {}, }, diff --git a/rules/sort-objects.ts b/rules/sort-objects.ts index a0b3fc7ec..c79643e11 100644 --- a/rules/sort-objects.ts +++ b/rules/sort-objects.ts @@ -5,6 +5,7 @@ import type { SortingNodeWithDependencies } from '../utils/sort-nodes-by-depende import { partitionByCommentJsonSchema, partitionByNewLineJsonSchema, + useConfigurationIfJsonSchema, specialCharactersJsonSchema, newlinesBetweenJsonSchema, customGroupsJsonSchema, @@ -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' @@ -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 - partitionByComment: string[] | boolean | string - newlinesBetween: 'ignore' | 'always' | 'never' - specialCharacters: 'remove' | 'trim' | 'keep' - locales: NonNullable - 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 + partitionByComment: string[] | boolean | string + newlinesBetween: 'ignore' | 'always' | 'never' + specialCharacters: 'remove' | 'trim' | 'keep' + locales: NonNullable + 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' @@ -83,6 +86,7 @@ let defaultOptions: Required = { objectDeclarations: true, styledComponents: true, destructureOnly: false, + useConfigurationIf: {}, type: 'alphabetical', ignorePattern: [], ignoreCase: true, @@ -102,9 +106,14 @@ export default createEslintRule({ } 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'], @@ -182,7 +191,6 @@ export default createEslintRule({ return } - let sourceCode = getSourceCode(context) let eslintDisabledLines = getEslintDisabledLines({ ruleName: context.id, sourceCode, @@ -480,8 +488,8 @@ export default createEslintRule({ } }, meta: { - schema: [ - { + schema: { + items: { properties: { destructuredObjects: { oneOf: [ @@ -528,6 +536,7 @@ export default createEslintRule({ type: 'boolean', }, partitionByNewLine: partitionByNewLineJsonSchema, + useConfigurationIf: useConfigurationIfJsonSchema, specialCharacters: specialCharactersJsonSchema, newlinesBetween: newlinesBetweenJsonSchema, customGroups: customGroupsJsonSchema, @@ -540,7 +549,9 @@ export default createEslintRule({ additionalProperties: false, type: 'object', }, - ], + uniqueItems: true, + type: 'array', + }, messages: { unexpectedObjectsGroupOrder: 'Expected "{{right}}" ({{rightGroup}}) to come before "{{left}}" ({{leftGroup}}).', @@ -563,3 +574,24 @@ export default createEslintRule({ defaultOptions: [defaultOptions], name: 'sort-objects', }) + +let getNodeName = ({ + sourceCode, + property, +}: { + property: + | TSESTree.ObjectLiteralElement + | TSESTree.RestElement + | TSESTree.Property + sourceCode: ReturnType +}): 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) +} diff --git a/test/get-matching-context-options.test.ts b/test/get-matching-context-options.test.ts new file mode 100644 index 000000000..a673705f7 --- /dev/null +++ b/test/get-matching-context-options.test.ts @@ -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 } : {}), + }, + }) +}) diff --git a/test/sort-objects.test.ts b/test/sort-objects.test.ts index d1d482677..b82eff75a 100644 --- a/test/sort-objects.test.ts +++ b/test/sort-objects.test.ts @@ -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`, () => { diff --git a/utils/common-json-schemas.ts b/utils/common-json-schemas.ts index efdd07838..29564ece4 100644 --- a/utils/common-json-schemas.ts +++ b/utils/common-json-schemas.ts @@ -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 = { type: { enum: ['alphabetical', 'line-length', 'natural', 'unsorted'], diff --git a/utils/get-matching-context-options.ts b/utils/get-matching-context-options.ts new file mode 100644 index 000000000..0237729c9 --- /dev/null +++ b/utils/get-matching-context-options.ts @@ -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)) + ) + })