Skip to content

Commit 2a6653d

Browse files
hugop95azat-io
authored andcommitted
feat(sort-objects): add numeric keys detection option
1 parent b707549 commit 2a6653d

File tree

5 files changed

+159
-6
lines changed

5 files changed

+159
-6
lines changed

docs/content/rules/sort-objects.mdx

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,7 @@ Specifies whether this rule should be applied to styled-components like librarie
310310
declarationMatchesPattern?: string | string[] | { pattern: string; flags: string } | { pattern: string; flags: string }[]
311311
declarationCommentMatchesPattern?: string | string[] | { pattern: string; flags: string } | { pattern: string; flags: string }[]
312312
objectType?: 'destructured' | 'non-destructured'
313+
hasNumericKeysOnly?: boolean
313314
}
314315
```
315316
</sub>
@@ -433,6 +434,29 @@ Example configuration:
433434
}
434435
```
435436

437+
- `hasNumericKeysOnly` — If `true`, matches only objects that have exclusively numeric keys.
438+
439+
This option only detects unquoted numeric literal keys (e.g., `1`, `42`).
440+
Quoted strings like `"1"`, array-wrapped keys like `[1]`, or computed expressions are not detected as numeric keys.
441+
442+
Example configuration:
443+
```ts
444+
{
445+
'perfectionist/sort-objects': [
446+
'error',
447+
{
448+
type: 'natural', // Sort numeric keys naturally (by numeric value)
449+
useConfigurationIf: {
450+
hasNumericKeysOnly: true,
451+
},
452+
},
453+
{
454+
type: 'alphabetical' // Fallback configuration
455+
}
456+
],
457+
}
458+
```
459+
436460
### groups
437461

438462
<sub>

rules/sort-object-types.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@ import {
2020
groupsJsonSchema,
2121
regexJsonSchema,
2222
} from '../utils/common-json-schemas'
23+
import {
24+
singleCustomGroupJsonSchema,
25+
sortByJsonSchema,
26+
allModifiers,
27+
allSelectors,
28+
} from './sort-object-types/types'
2329
import {
2430
MISSED_SPACING_ERROR,
2531
EXTRA_SPACING_ERROR,
@@ -28,11 +34,6 @@ import {
2834
} from '../utils/report-errors'
2935
import { validateNewlinesAndPartitionConfiguration } from '../utils/validate-newlines-and-partition-configuration'
3036
import { filterOptionsByDeclarationCommentMatches } from '../utils/filter-options-by-declaration-comment-matches'
31-
import {
32-
singleCustomGroupJsonSchema,
33-
allModifiers,
34-
allSelectors,
35-
} from './sort-object-types/types'
3637
import { validateGeneratedGroupsConfiguration } from '../utils/validate-generated-groups-configuration'
3738
import { getCustomGroupsCompareOptions } from './sort-object-types/get-custom-groups-compare-options'
3839
import { validateCustomSortConfiguration } from '../utils/validate-custom-sort-configuration'
@@ -45,7 +46,6 @@ import { doesCustomGroupMatch } from '../utils/does-custom-group-match'
4546
import { isNodeFunctionType } from '../utils/is-node-function-type'
4647
import { sortNodesByGroups } from '../utils/sort-nodes-by-groups'
4748
import { createEslintRule } from '../utils/create-eslint-rule'
48-
import { sortByJsonSchema } from './sort-object-types/types'
4949
import { reportAllErrors } from '../utils/report-all-errors'
5050
import { shouldPartition } from '../utils/should-partition'
5151
import { computeGroup } from '../utils/compute-group'

rules/sort-objects.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { TSESLint } from '@typescript-eslint/utils'
22

3+
import { AST_NODE_TYPES } from '@typescript-eslint/utils'
34
import { TSESTree } from '@typescript-eslint/types'
45

56
import type { SortingNodeWithDependencies } from '../utils/sort-nodes-by-dependencies'
@@ -393,6 +394,11 @@ export default createEslintRule<Options, MessageId>({
393394
enum: ['destructured', 'non-destructured'],
394395
type: 'string',
395396
},
397+
hasNumericKeysOnly: {
398+
description:
399+
'Specifies whether to only match objects that have exclusively numeric keys.',
400+
type: 'boolean',
401+
},
396402
declarationCommentMatchesPattern: regexJsonSchema,
397403
callingFunctionNamePattern: regexJsonSchema,
398404
declarationMatchesPattern: regexJsonSchema,
@@ -528,6 +534,13 @@ function computeMatchedContextOptions({
528534
}
529535
}
530536

537+
if (
538+
options.useConfigurationIf.hasNumericKeysOnly &&
539+
!hasNumericKeysOnly(nodeObject)
540+
) {
541+
return false
542+
}
543+
531544
return true
532545
})
533546
}
@@ -637,6 +650,25 @@ function isStyledComponents(styledNode: TSESTree.Node): boolean {
637650
)
638651
}
639652

653+
function hasNumericKeysOnly(
654+
object: TSESTree.ObjectExpression | TSESTree.ObjectPattern,
655+
): boolean {
656+
switch (object.type) {
657+
case AST_NODE_TYPES.ObjectExpression:
658+
return object.properties.every(
659+
property =>
660+
property.type === AST_NODE_TYPES.Property &&
661+
property.key.type === 'Literal' &&
662+
typeof property.key.value === 'number',
663+
)
664+
case AST_NODE_TYPES.ObjectPattern:
665+
return false
666+
/* v8 ignore next 2 */
667+
default:
668+
throw new UnreachableCaseError(object)
669+
}
670+
}
671+
640672
function getNodeName({
641673
sourceCode,
642674
property,

rules/sort-objects/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,12 @@ export type Options = Partial<
5959
* rule is only applied when all property names match this pattern.
6060
*/
6161
allNamesMatchPattern?: RegexOption
62+
63+
/**
64+
* Specifies whether to only match objects that have exclusively numeric
65+
* keys.
66+
*/
67+
hasNumericKeysOnly?: boolean
6268
}
6369

6470
/**

test/rules/sort-objects.test.ts

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2534,6 +2534,97 @@ describe('sort-objects', () => {
25342534
})
25352535
})
25362536

2537+
it('applies configuration when object only has numeric keys', async () => {
2538+
await valid({
2539+
options: [
2540+
{
2541+
useConfigurationIf: {
2542+
hasNumericKeysOnly: true,
2543+
},
2544+
type: 'unsorted',
2545+
},
2546+
],
2547+
code: dedent`
2548+
const obj = {
2549+
5: 'five',
2550+
2: {},
2551+
3: 'three',
2552+
8: 'eight',
2553+
}
2554+
`,
2555+
})
2556+
2557+
await invalid({
2558+
options: [
2559+
{
2560+
useConfigurationIf: {
2561+
hasNumericKeysOnly: true,
2562+
},
2563+
type: 'unsorted',
2564+
},
2565+
options,
2566+
],
2567+
errors: [
2568+
{
2569+
data: {
2570+
right: '1',
2571+
left: '2',
2572+
},
2573+
messageId: 'unexpectedObjectsOrder',
2574+
},
2575+
],
2576+
output: dedent`
2577+
let obj = {
2578+
'1': 1,
2579+
2: 2,
2580+
}
2581+
`,
2582+
code: dedent`
2583+
let obj = {
2584+
2: 2,
2585+
'1': 1,
2586+
}
2587+
`,
2588+
})
2589+
2590+
await invalid({
2591+
options: [
2592+
{
2593+
useConfigurationIf: {
2594+
hasNumericKeysOnly: true,
2595+
},
2596+
type: 'unsorted',
2597+
},
2598+
options,
2599+
],
2600+
errors: [
2601+
{
2602+
data: {
2603+
right: 'a',
2604+
left: 'b',
2605+
},
2606+
messageId: 'unexpectedObjectsOrder',
2607+
},
2608+
],
2609+
output: dedent`
2610+
let Func = ({
2611+
a,
2612+
b,
2613+
}) => {
2614+
// ...
2615+
}
2616+
`,
2617+
code: dedent`
2618+
let Func = ({
2619+
b,
2620+
a,
2621+
}) => {
2622+
// ...
2623+
}
2624+
`,
2625+
})
2626+
})
2627+
25372628
it('skips object declarations when objectType is "destructured"', async () => {
25382629
await valid({
25392630
options: [

0 commit comments

Comments
 (0)