diff --git a/docs/rules/sort-objects.md b/docs/rules/sort-objects.md
index a9a4129d..fb4a39bc 100644
--- a/docs/rules/sort-objects.md
+++ b/docs/rules/sort-objects.md
@@ -71,6 +71,7 @@ interface Options {
type?: 'alphabetical' | 'natural' | 'natural'
order?: 'asc' | 'desc'
'ignore-case'?: boolean
+ 'always-on-top'?: string[]
}
```
@@ -95,6 +96,12 @@ interface Options {
Only affects alphabetical and natural sorting. When `true` the rule ignores the case-sensitivity of the order.
+### always-on-top
+
+(default: `[]`)
+
+You can set a list of key names that will always go at the beginning of the object. For example: `['id', 'name']`
+
## ⚙️ Usage
### Legacy Config
diff --git a/index.ts b/index.ts
index 491b91e2..6eb83a12 100644
--- a/index.ts
+++ b/index.ts
@@ -66,7 +66,12 @@ let createConfigWithOptions = (options: {
[sortMapElementsName]: ['error'],
[sortNamedExportsName]: ['error'],
[sortNamedImportsName]: ['error'],
- [sortObjectsName]: ['error'],
+ [sortObjectsName]: [
+ 'error',
+ {
+ 'always-on-top': [],
+ },
+ ],
[sortUnionTypesName]: ['error'],
}
return {
diff --git a/rules/sort-objects.ts b/rules/sort-objects.ts
index a40d84dc..c4542ee1 100644
--- a/rules/sort-objects.ts
+++ b/rules/sort-objects.ts
@@ -10,12 +10,21 @@ import { sortNodes } from '../utils/sort-nodes'
import { makeFixes } from '../utils/make-fixes'
import { complete } from '../utils/complete'
import { pairwise } from '../utils/pairwise'
+import { groupBy } from '../utils/group-by'
import { compare } from '../utils/compare'
type MESSAGE_ID = 'unexpectedObjectsOrder'
+export enum Position {
+ 'exception' = 'exception',
+ 'ignore' = 'ignore',
+}
+
+type SortingNodeWithPosition = SortingNode & { position: Position }
+
type Options = [
Partial<{
+ 'always-on-top': string[]
'ignore-case': boolean
order: SortOrder
type: SortType
@@ -53,6 +62,10 @@ export default createEslintRule({
type: 'boolean',
default: false,
},
+ 'always-on-top': {
+ type: 'array',
+ default: [],
+ },
},
additionalProperties: false,
},
@@ -75,21 +88,23 @@ export default createEslintRule({
type: SortType.alphabetical,
'ignore-case': false,
order: SortOrder.asc,
+ 'always-on-top': [],
})
let source = context.getSourceCode()
let formatProperties = (
props: TSESTree.ObjectLiteralElement[],
- ): SortingNode[][] =>
+ ): SortingNodeWithPosition[][] =>
props.reduce(
- (accumulator: SortingNode[][], prop) => {
+ (accumulator: SortingNodeWithPosition[][], prop) => {
if (prop.type === AST_NODE_TYPES.SpreadElement) {
accumulator.push([])
return accumulator
}
let name: string
+ let position: Position = Position.ignore
if (prop.key.type === AST_NODE_TYPES.Identifier) {
;({ name } = prop.key)
@@ -99,9 +114,17 @@ export default createEslintRule({
name = source.text.slice(...prop.key.range)
}
+ if (
+ prop.key.type === AST_NODE_TYPES.Identifier &&
+ options['always-on-top'].includes(prop.key.name)
+ ) {
+ position = Position.exception
+ }
+
let value = {
size: rangeToDiff(prop.range),
node: prop,
+ position,
name,
}
@@ -114,7 +137,28 @@ export default createEslintRule({
formatProperties(node.properties).forEach(nodes => {
pairwise(nodes, (first, second) => {
- if (compare(first, second, options)) {
+ let comparison: boolean
+
+ if (
+ first.position === Position.exception &&
+ second.position === Position.exception
+ ) {
+ comparison =
+ options['always-on-top'].indexOf(first.name) >
+ options['always-on-top'].indexOf(second.name)
+ } else if (first.position === second.position) {
+ comparison = compare(first, second, options)
+ } else {
+ let positionPower = {
+ [Position.exception]: 1,
+ [Position.ignore]: 0,
+ }
+
+ comparison =
+ positionPower[first.position] < positionPower[second.position]
+ }
+
+ if (comparison) {
context.report({
messageId: 'unexpectedObjectsOrder',
data: {
@@ -122,8 +166,23 @@ export default createEslintRule({
second: second.name,
},
node: second.node,
- fix: fixer =>
- makeFixes(fixer, nodes, sortNodes(nodes, options), source),
+ fix: fixer => {
+ let groups = groupBy(nodes, ({ position }) => position)
+
+ let getGroup = (index: string) =>
+ index in groups ? groups[index] : []
+
+ let sortedNodes = [
+ getGroup(Position.exception).sort(
+ (aNode, bNode) =>
+ options['always-on-top'].indexOf(aNode.name) -
+ options['always-on-top'].indexOf(bNode.name),
+ ),
+ sortNodes(getGroup(Position.ignore), options),
+ ].flat()
+
+ return makeFixes(fixer, nodes, sortedNodes, source)
+ },
})
}
})
diff --git a/test/sort-objects.test.ts b/test/sort-objects.test.ts
index ab7b6789..0eff3be4 100644
--- a/test/sort-objects.test.ts
+++ b/test/sort-objects.test.ts
@@ -293,6 +293,79 @@ describe(RULE_NAME, () => {
],
})
})
+
+ it(`${RULE_NAME}(${type}): allows to set priority keys`, () => {
+ ruleTester.run(RULE_NAME, rule, {
+ valid: [
+ {
+ code: dedent`
+ let terrorInResonance = {
+ name: 'Terror in Resonance',
+ id: 'de4d12c2-200c-49bf-a2c8-14f5b4576299',
+ episodes: 11,
+ genres: ['drama', 'mystery', 'psychological', 'thriller'],
+ romaji: 'Zankyou no Terror',
+ studio: 'Mappa'
+ }
+ `,
+ options: [
+ {
+ 'always-on-top': ['name', 'id'],
+ type: SortType.alphabetical,
+ order: SortOrder.asc,
+ },
+ ],
+ },
+ ],
+ invalid: [
+ {
+ code: dedent`
+ let terrorInResonance = {
+ episodes: 11,
+ genres: ['drama', 'mystery', 'psychological', 'thriller'],
+ id: 'de4d12c2-200c-49bf-a2c8-14f5b4576299',
+ name: 'Terror in Resonance',
+ romaji: 'Zankyou no Terror',
+ studio: 'Mappa'
+ }
+ `,
+ output: dedent`
+ let terrorInResonance = {
+ name: 'Terror in Resonance',
+ id: 'de4d12c2-200c-49bf-a2c8-14f5b4576299',
+ episodes: 11,
+ genres: ['drama', 'mystery', 'psychological', 'thriller'],
+ romaji: 'Zankyou no Terror',
+ studio: 'Mappa'
+ }
+ `,
+ options: [
+ {
+ 'always-on-top': ['name', 'id'],
+ type: SortType.alphabetical,
+ order: SortOrder.asc,
+ },
+ ],
+ errors: [
+ {
+ messageId: 'unexpectedObjectsOrder',
+ data: {
+ first: 'genres',
+ second: 'id',
+ },
+ },
+ {
+ messageId: 'unexpectedObjectsOrder',
+ data: {
+ first: 'id',
+ second: 'name',
+ },
+ },
+ ],
+ },
+ ],
+ })
+ })
})
describe(`${RULE_NAME}: sorting by natural order`, () => {
@@ -578,6 +651,79 @@ describe(RULE_NAME, () => {
],
})
})
+
+ it(`${RULE_NAME}(${type}): allows to set priority keys`, () => {
+ ruleTester.run(RULE_NAME, rule, {
+ valid: [
+ {
+ code: dedent`
+ let terrorInResonance = {
+ name: 'Terror in Resonance',
+ id: 'de4d12c2-200c-49bf-a2c8-14f5b4576299',
+ episodes: 11,
+ genres: ['drama', 'mystery', 'psychological', 'thriller'],
+ romaji: 'Zankyou no Terror',
+ studio: 'Mappa'
+ }
+ `,
+ options: [
+ {
+ 'always-on-top': ['name', 'id'],
+ type: SortType.natural,
+ order: SortOrder.asc,
+ },
+ ],
+ },
+ ],
+ invalid: [
+ {
+ code: dedent`
+ let terrorInResonance = {
+ episodes: 11,
+ genres: ['drama', 'mystery', 'psychological', 'thriller'],
+ id: 'de4d12c2-200c-49bf-a2c8-14f5b4576299',
+ name: 'Terror in Resonance',
+ romaji: 'Zankyou no Terror',
+ studio: 'Mappa'
+ }
+ `,
+ output: dedent`
+ let terrorInResonance = {
+ name: 'Terror in Resonance',
+ id: 'de4d12c2-200c-49bf-a2c8-14f5b4576299',
+ episodes: 11,
+ genres: ['drama', 'mystery', 'psychological', 'thriller'],
+ romaji: 'Zankyou no Terror',
+ studio: 'Mappa'
+ }
+ `,
+ options: [
+ {
+ 'always-on-top': ['name', 'id'],
+ type: SortType.natural,
+ order: SortOrder.asc,
+ },
+ ],
+ errors: [
+ {
+ messageId: 'unexpectedObjectsOrder',
+ data: {
+ first: 'genres',
+ second: 'id',
+ },
+ },
+ {
+ messageId: 'unexpectedObjectsOrder',
+ data: {
+ first: 'id',
+ second: 'name',
+ },
+ },
+ ],
+ },
+ ],
+ })
+ })
})
describe(`${RULE_NAME}: sorting by line length`, () => {
@@ -863,6 +1009,86 @@ describe(RULE_NAME, () => {
],
})
})
+
+ it(`${RULE_NAME}(${type}): allows to set priority keys`, () => {
+ ruleTester.run(RULE_NAME, rule, {
+ valid: [
+ {
+ code: dedent`
+ let terrorInResonance = {
+ name: 'Terror in Resonance',
+ id: 'de4d12c2-200c-49bf-a2c8-14f5b4576299',
+ genres: ['drama', 'mystery', 'psychological', 'thriller'],
+ romaji: 'Zankyou no Terror',
+ studio: 'Mappa',
+ episodes: 11,
+ }
+ `,
+ options: [
+ {
+ 'always-on-top': ['name', 'id'],
+ type: SortType['line-length'],
+ order: SortOrder.desc,
+ },
+ ],
+ },
+ ],
+ invalid: [
+ {
+ code: dedent`
+ let terrorInResonance = {
+ episodes: 11,
+ genres: ['drama', 'mystery', 'psychological', 'thriller'],
+ id: 'de4d12c2-200c-49bf-a2c8-14f5b4576299',
+ name: 'Terror in Resonance',
+ romaji: 'Zankyou no Terror',
+ studio: 'Mappa'
+ }
+ `,
+ output: dedent`
+ let terrorInResonance = {
+ name: 'Terror in Resonance',
+ id: 'de4d12c2-200c-49bf-a2c8-14f5b4576299',
+ genres: ['drama', 'mystery', 'psychological', 'thriller'],
+ romaji: 'Zankyou no Terror',
+ studio: 'Mappa',
+ episodes: 11
+ }
+ `,
+ options: [
+ {
+ 'always-on-top': ['name', 'id'],
+ type: SortType['line-length'],
+ order: SortOrder.desc,
+ },
+ ],
+ errors: [
+ {
+ messageId: 'unexpectedObjectsOrder',
+ data: {
+ first: 'episodes',
+ second: 'genres',
+ },
+ },
+ {
+ messageId: 'unexpectedObjectsOrder',
+ data: {
+ first: 'genres',
+ second: 'id',
+ },
+ },
+ {
+ messageId: 'unexpectedObjectsOrder',
+ data: {
+ first: 'id',
+ second: 'name',
+ },
+ },
+ ],
+ },
+ ],
+ })
+ })
})
describe(`${RULE_NAME}: misc`, () => {