Skip to content

Commit da515ce

Browse files
committed
feat: add ability to ignore jsx elements
1 parent 611f088 commit da515ce

File tree

3 files changed

+171
-92
lines changed

3 files changed

+171
-92
lines changed

docs/content/rules/sort-jsx-props.mdx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,14 @@ Example:
190190
}
191191
```
192192

193+
### ignorePattern
194+
195+
<sub>(default: `[]`)</sub>
196+
197+
If you need to ignore a rule for some JSX elements, you can specify their names or a pattern to ignore, for example: `'Table*'` to ignore all interfaces whose names begin with the word Component.
198+
199+
The [minimatch](https://github.com/isaacs/minimatch) library is used for pattern matching.
200+
193201
## Usage
194202

195203
<CodeTabs

rules/sort-jsx-props.ts

Lines changed: 114 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { TSESTree } from '@typescript-eslint/types'
22

3+
import { minimatch } from 'minimatch'
34
import path from 'node:path'
45

56
import type { SortingNode } from '../typings'
@@ -28,6 +29,7 @@ type Options<T extends string[]> = [
2829
customGroups: { [key in T[number]]: string[] | string }
2930
type: 'alphabetical' | 'line-length' | 'natural'
3031
groups: (Group<T>[] | Group<T>)[]
32+
ignorePattern: string[]
3133
order: 'desc' | 'asc'
3234
ignoreCase: boolean
3335
}>,
@@ -67,6 +69,12 @@ export default createEslintRule<Options<string[]>, MESSAGE_ID>({
6769
type: 'boolean',
6870
default: true,
6971
},
72+
ignorePattern: {
73+
items: {
74+
type: 'string',
75+
},
76+
type: 'array',
77+
},
7078
},
7179
additionalProperties: false,
7280
},
@@ -92,108 +100,124 @@ export default createEslintRule<Options<string[]>, MESSAGE_ID>({
92100
if (node.openingElement.attributes.length > 1) {
93101
let options = complete(context.options.at(0), {
94102
type: 'alphabetical',
103+
ignorePattern: [],
95104
ignoreCase: true,
96105
customGroups: {},
97106
order: 'asc',
98107
groups: [],
99108
} as const)
100109

101-
let parts: SortingNode[][] = node.openingElement.attributes.reduce(
102-
(
103-
accumulator: SortingNode[][],
104-
attribute: TSESTree.JSXSpreadAttribute | TSESTree.JSXAttribute,
105-
) => {
106-
if (attribute.type === 'JSXSpreadAttribute') {
107-
accumulator.push([])
108-
return accumulator
109-
}
110-
111-
let name =
112-
attribute.name.type === 'JSXNamespacedName'
113-
? `${attribute.name.namespace.name}:${attribute.name.name.name}`
114-
: attribute.name.name
115-
116-
let { getGroup, defineGroup, setCustomGroups } = useGroups(
117-
options.groups,
118-
)
119-
120-
setCustomGroups(options.customGroups, name)
110+
let shouldIgnore = false
111+
if (options.ignorePattern.length) {
112+
let tagName = context.sourceCode.text.slice(
113+
...node.openingElement.name.range,
114+
)
115+
shouldIgnore = options.ignorePattern.some(pattern =>
116+
minimatch(tagName, pattern),
117+
)
118+
}
121119

122-
if (attribute.value === null) {
123-
defineGroup('shorthand')
124-
}
120+
if (!shouldIgnore && node.openingElement.attributes.length > 1) {
121+
let parts: SortingNode[][] = node.openingElement.attributes.reduce(
122+
(
123+
accumulator: SortingNode[][],
124+
attribute: TSESTree.JSXSpreadAttribute | TSESTree.JSXAttribute,
125+
) => {
126+
if (attribute.type === 'JSXSpreadAttribute') {
127+
accumulator.push([])
128+
return accumulator
129+
}
130+
131+
let name =
132+
attribute.name.type === 'JSXNamespacedName'
133+
? `${attribute.name.namespace.name}:${attribute.name.name.name}`
134+
: attribute.name.name
135+
136+
let { getGroup, defineGroup, setCustomGroups } = useGroups(
137+
options.groups,
138+
)
139+
140+
setCustomGroups(options.customGroups, name)
141+
142+
if (attribute.value === null) {
143+
defineGroup('shorthand')
144+
}
145+
146+
if (attribute.loc.start.line !== attribute.loc.end.line) {
147+
defineGroup('multiline')
148+
}
149+
150+
let jsxNode = {
151+
size: rangeToDiff(attribute.range),
152+
group: getGroup(),
153+
node: attribute,
154+
name,
155+
}
156+
157+
accumulator.at(-1)!.push(jsxNode)
125158

126-
if (attribute.loc.start.line !== attribute.loc.end.line) {
127-
defineGroup('multiline')
128-
}
159+
return accumulator
160+
},
161+
[[]],
162+
)
163+
164+
for (let nodes of parts) {
165+
pairwise(nodes, (left, right) => {
166+
let leftNum = getGroupNumber(options.groups, left)
167+
let rightNum = getGroupNumber(options.groups, right)
168+
169+
if (
170+
leftNum > rightNum ||
171+
(leftNum === rightNum &&
172+
isPositive(compare(left, right, options)))
173+
) {
174+
context.report({
175+
messageId: 'unexpectedJSXPropsOrder',
176+
data: {
177+
left: left.name,
178+
right: right.name,
179+
},
180+
node: right.node,
181+
fix: fixer => {
182+
let grouped: {
183+
[key: string]: SortingNode[]
184+
} = {}
185+
186+
for (let currentNode of nodes) {
187+
let groupNum = getGroupNumber(
188+
options.groups,
189+
currentNode,
190+
)
129191

130-
let jsxNode = {
131-
size: rangeToDiff(attribute.range),
132-
group: getGroup(),
133-
node: attribute,
134-
name,
135-
}
192+
if (!(groupNum in grouped)) {
193+
grouped[groupNum] = [currentNode]
194+
} else {
195+
grouped[groupNum] = sortNodes(
196+
[...grouped[groupNum], currentNode],
197+
options,
198+
)
199+
}
200+
}
136201

137-
accumulator.at(-1)!.push(jsxNode)
202+
let sortedNodes: SortingNode[] = []
138203

139-
return accumulator
140-
},
141-
[[]],
142-
)
143-
144-
for (let nodes of parts) {
145-
pairwise(nodes, (left, right) => {
146-
let leftNum = getGroupNumber(options.groups, left)
147-
let rightNum = getGroupNumber(options.groups, right)
148-
149-
if (
150-
leftNum > rightNum ||
151-
(leftNum === rightNum &&
152-
isPositive(compare(left, right, options)))
153-
) {
154-
context.report({
155-
messageId: 'unexpectedJSXPropsOrder',
156-
data: {
157-
left: left.name,
158-
right: right.name,
159-
},
160-
node: right.node,
161-
fix: fixer => {
162-
let grouped: {
163-
[key: string]: SortingNode[]
164-
} = {}
165-
166-
for (let currentNode of nodes) {
167-
let groupNum = getGroupNumber(options.groups, currentNode)
168-
169-
if (!(groupNum in grouped)) {
170-
grouped[groupNum] = [currentNode]
171-
} else {
172-
grouped[groupNum] = sortNodes(
173-
[...grouped[groupNum], currentNode],
174-
options,
175-
)
204+
for (let group of Object.keys(grouped).sort(
205+
(a, b) => Number(a) - Number(b),
206+
)) {
207+
sortedNodes.push(...sortNodes(grouped[group], options))
176208
}
177-
}
178-
179-
let sortedNodes: SortingNode[] = []
180-
181-
for (let group of Object.keys(grouped).sort(
182-
(a, b) => Number(a) - Number(b),
183-
)) {
184-
sortedNodes.push(...sortNodes(grouped[group], options))
185-
}
186-
187-
return makeFixes(
188-
fixer,
189-
nodes,
190-
sortedNodes,
191-
context.sourceCode,
192-
)
193-
},
194-
})
195-
}
196-
})
209+
210+
return makeFixes(
211+
fixer,
212+
nodes,
213+
sortedNodes,
214+
context.sourceCode,
215+
)
216+
},
217+
})
218+
}
219+
})
220+
}
197221
}
198222
}
199223
},

test/sort-jsx-props.test.ts

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1364,6 +1364,7 @@ describe(RULE_NAME, () => {
13641364
...options,
13651365
customGroups: { top: ['d', 'e'] },
13661366
groups: ['top', 'unknown'],
1367+
ignoreCase: true,
13671368
},
13681369
],
13691370
},
@@ -1441,8 +1442,6 @@ describe(RULE_NAME, () => {
14411442
invalid: [],
14421443
},
14431444
)
1444-
1445-
// prettier-ignore
14461445
;['.svelte', '.astro', '.vue'].forEach(extension => {
14471446
ruleTester.run(`${RULE_NAME}: not works with ${extension} files`, rule, {
14481447
valid: [
@@ -1460,5 +1459,53 @@ describe(RULE_NAME, () => {
14601459
invalid: [],
14611460
})
14621461
})
1462+
1463+
ruleTester.run(`${RULE_NAME}: does not work with empty props`, rule, {
1464+
valid: [
1465+
dedent`
1466+
let Component = () => (
1467+
<Element />
1468+
)
1469+
`,
1470+
],
1471+
invalid: [],
1472+
})
1473+
1474+
ruleTester.run(`${RULE_NAME}: does not work with single prop`, rule, {
1475+
valid: [
1476+
dedent`
1477+
let Component = () => (
1478+
<Element a="a" />
1479+
)
1480+
`,
1481+
],
1482+
invalid: [],
1483+
})
1484+
1485+
ruleTester.run(
1486+
`${RULE_NAME}: allow to disable rule for some JSX elements`,
1487+
rule,
1488+
{
1489+
valid: [
1490+
{
1491+
code: dedent`
1492+
let Component = () => (
1493+
<Element
1494+
c="c"
1495+
b="bb"
1496+
a="aaa"
1497+
/>
1498+
)
1499+
`,
1500+
options: [
1501+
{
1502+
ignorePattern: ['Element'],
1503+
},
1504+
],
1505+
},
1506+
],
1507+
invalid: [],
1508+
},
1509+
)
14631510
})
14641511
})

0 commit comments

Comments
 (0)