Skip to content

Commit

Permalink
feat: add shorthand position option to sort-jsx-props rule
Browse files Browse the repository at this point in the history
  • Loading branch information
azat-io committed Jun 3, 2023
1 parent fc342d2 commit 416ffee
Show file tree
Hide file tree
Showing 5 changed files with 168 additions and 21 deletions.
7 changes: 7 additions & 0 deletions docs/rules/sort-jsx-props.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,13 @@ let Riko = () => (

- `boolean` (default: `false`) - only affects alphabetical and natural sorting. When `true` the rule ignores the case-sensitivity of the order.

### `shorthand`

- `enum` (default: `ignore`):
- `first` - enforce shorthand JSX props to be at the top of the list
- `ignore` - sort shorthand props in general order
- `last` - enforce shorthand JSX props to be at the end of the list

## ⚙️ Usage

### Legacy Config
Expand Down
14 changes: 12 additions & 2 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,12 @@ let createConfigWithOptions = (options: {
let recommendedRules: {
[key: string]: RuleDeclaration
} = {
[sortArrayIncludesName]: ['error', { 'spread-last': true }],
[sortArrayIncludesName]: [
'error',
{
'spread-last': true,
},
],
[sortEnumsName]: ['error'],
[sortImportsName]: [
'error',
Expand All @@ -49,7 +54,12 @@ let createConfigWithOptions = (options: {
},
],
[sortInterfacesName]: ['error'],
[sortJsxPropsName]: ['error'],
[sortJsxPropsName]: [
'error',
{
shorthand: 'ignore',
},
],
[sortMapElementsName]: ['error'],
[sortNamedExportsName]: ['error'],
[sortNamedImportsName]: ['error'],
Expand Down
89 changes: 71 additions & 18 deletions rules/sort-jsx-props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,25 @@ 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 = 'unexpectedJSXPropsOrder'

export enum Position {
'first' = 'first',
'last' = 'last',
'ignore' = 'ignore',
}

type SortingNodeWithPosition = SortingNode & { position: Position }

type Options = [
Partial<{
order: SortOrder
type: SortType
'ignore-case': boolean
shorthand: Position
}>,
]

Expand Down Expand Up @@ -53,6 +63,9 @@ export default createEslintRule<Options, MESSAGE_ID>({
type: 'boolean',
default: false,
},
shorthand: {
enum: [Position.first, Position.last, Position.ignore],
},
},
additionalProperties: false,
},
Expand All @@ -72,49 +85,89 @@ export default createEslintRule<Options, MESSAGE_ID>({
JSXElement: node => {
let options = complete(context.options.at(0), {
type: SortType.alphabetical,
shorthand: Position.ignore,
'ignore-case': false,
order: SortOrder.asc,
})

let source = context.getSourceCode()

let parts: TSESTree.JSXAttribute[][] =
let parts: SortingNodeWithPosition[][] =
node.openingElement.attributes.reduce(
(
accumulator: TSESTree.JSXAttribute[][],
accumulator: SortingNodeWithPosition[][],
attribute: TSESTree.JSXSpreadAttribute | TSESTree.JSXAttribute,
) => {
if (attribute.type === 'JSXAttribute') {
accumulator.at(-1)!.push(attribute)
} else {
if (attribute.type === AST_NODE_TYPES.JSXSpreadAttribute) {
accumulator.push([])
return accumulator
}

let position: Position = Position.ignore

if (
options.shorthand !== Position.ignore &&
attribute.value === null
) {
position = options.shorthand
}

let jsxNode = {
name:
attribute.name.type === AST_NODE_TYPES.JSXNamespacedName
? `${attribute.name.namespace.name}:${attribute.name.name.name}`
: attribute.name.name,
size: rangeToDiff(attribute.range),
node: attribute,
position,
}

accumulator.at(-1)!.push(jsxNode)

return accumulator
},
[[]],
)

parts.forEach(part => {
let nodes: SortingNode[] = part.map(attribute => ({
name:
attribute.name.type === AST_NODE_TYPES.JSXNamespacedName
? `${attribute.name.namespace.name}:${attribute.name.name.name}`
: attribute.name.name,
size: rangeToDiff(attribute.range),
node: attribute,
}))

parts.forEach(nodes => {
pairwise(nodes, (first, second) => {
if (compare(first, second, options)) {
let comparison: boolean

if (first.position === second.position) {
comparison = compare(first, second, options)
} else {
let positionPower = {
[Position.first]: 1,
[Position.ignore]: 0,
[Position.last]: -1,
}

comparison =
positionPower[first.position] < positionPower[second.position]
}

if (comparison) {
context.report({
messageId: 'unexpectedJSXPropsOrder',
data: {
first: first.name,
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 = [
sortNodes(getGroup(Position.first), options),
sortNodes(getGroup(Position.ignore), options),
sortNodes(getGroup(Position.last), options),
].flat()

return makeFixes(fixer, nodes, sortedNodes, source)
},
})
}
})
Expand Down
68 changes: 67 additions & 1 deletion test/sort-jsx-props.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { ESLintUtils } from '@typescript-eslint/utils'
import { describe, it } from 'vitest'
import { dedent } from 'ts-dedent'

import rule, { RULE_NAME } from '../rules/sort-jsx-props'
import rule, { RULE_NAME, Position } from '../rules/sort-jsx-props'
import { SortType, SortOrder } from '../typings'

describe(RULE_NAME, () => {
Expand Down Expand Up @@ -230,6 +230,72 @@ describe(RULE_NAME, () => {
],
})
})

it(`${RULE_NAME}(${type}): allows to put shorthand props to the end`, () => {
ruleTester.run(RULE_NAME, rule, {
valid: [
{
code: dedent`
let Spike = () => (
<Hunter
age={27}
bloodType={0}
origin="Mars"
isFromCowboyBebop
/>
)
`,
options: [
{
type: SortType.alphabetical,
shorthand: Position.last,
order: SortOrder.asc,
},
],
},
],
invalid: [
{
code: dedent`
let Spike = () => (
<Hunter
age={27}
bloodType={0}
isFromCowboyBebop
origin="Mars"
/>
)
`,
output: dedent`
let Spike = () => (
<Hunter
age={27}
bloodType={0}
origin="Mars"
isFromCowboyBebop
/>
)
`,
options: [
{
type: SortType.alphabetical,
shorthand: Position.last,
order: SortOrder.asc,
},
],
errors: [
{
messageId: 'unexpectedJSXPropsOrder',
data: {
first: 'isFromCowboyBebop',
second: 'origin',
},
},
],
},
],
})
})
})

describe(`${RULE_NAME}: sorting by natural order`, () => {
Expand Down
11 changes: 11 additions & 0 deletions utils/group-by.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export let groupBy = <T>(array: T[], predicate: (v: T) => string) =>
array.reduce((acc, value) => {
let computedValue = predicate(value)

if (!(computedValue in acc)) {
acc[computedValue] = []
}

acc[computedValue].push(value)
return acc
}, {} as { [key: string]: T[] })

0 comments on commit 416ffee

Please sign in to comment.