eslint-plugin-smarthr 6.22.0 → 6.22.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
|
4
4
|
|
|
5
|
+
## [6.22.1](https://github.com/kufu/tamatebako/compare/eslint-plugin-smarthr-v6.22.0...eslint-plugin-smarthr-v6.22.1) (2026-07-03)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* best-practice-for-reduce-redundant-callsでJSX属性の差分数に基づいて検出するように改善 ([#1446](https://github.com/kufu/tamatebako/issues/1446)) ([b74f734](https://github.com/kufu/tamatebako/commit/b74f734dd1315f8bf6cd2ccab5f21f0571326c74))
|
|
11
|
+
|
|
5
12
|
## [6.22.0](https://github.com/kufu/tamatebako/compare/eslint-plugin-smarthr-v6.21.2...eslint-plugin-smarthr-v6.22.0) (2026-07-02)
|
|
6
13
|
|
|
7
14
|
|
package/package.json
CHANGED
|
@@ -106,22 +106,40 @@ const result = isAdmin ? notify('admin') : isModerator ? notify('moderator') : n
|
|
|
106
106
|
### パターン4: JSX/TSX
|
|
107
107
|
|
|
108
108
|
**子要素なし(セルフクロージングタグ)の場合:**
|
|
109
|
-
|
|
109
|
+
同じコンポーネント名で、属性の差分が**1つだけ**の場合に検出されます。
|
|
110
110
|
|
|
111
111
|
```jsx
|
|
112
|
-
//
|
|
112
|
+
// 属性の差分が1つ(role のみ異なる)→ 検出される
|
|
113
113
|
function Component() {
|
|
114
|
-
if (
|
|
115
|
-
return <UserCard
|
|
114
|
+
if (isModerator) {
|
|
115
|
+
return <UserCard role="moderator" />
|
|
116
116
|
} else {
|
|
117
|
-
return <UserCard
|
|
117
|
+
return <UserCard role="user" />
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// 属性の差分が1つ(role のみ異なる、他は同じ)→ 検出される
|
|
122
|
+
function Component({ data }) {
|
|
123
|
+
if (isModerator) {
|
|
124
|
+
return <UserCard role="moderator" hoge={data} />
|
|
125
|
+
} else {
|
|
126
|
+
return <UserCard role="user" hoge={data} />
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// boolean属性の差分が1つ → 検出される
|
|
131
|
+
function Component() {
|
|
132
|
+
if (condition) {
|
|
133
|
+
return <Component hoge={true} fuga={false} />
|
|
134
|
+
} else {
|
|
135
|
+
return <Component hoge={true} fuga={false} piyo />
|
|
118
136
|
}
|
|
119
137
|
}
|
|
120
138
|
|
|
121
139
|
// 三項演算子
|
|
122
|
-
const element =
|
|
123
|
-
? <UserCard
|
|
124
|
-
: <UserCard
|
|
140
|
+
const element = isModerator
|
|
141
|
+
? <UserCard role="moderator" hoge={data} />
|
|
142
|
+
: <UserCard role="user" hoge={data} />
|
|
125
143
|
```
|
|
126
144
|
|
|
127
145
|
**子要素あり の場合:**
|
|
@@ -254,6 +272,28 @@ if (condition) {
|
|
|
254
272
|
}
|
|
255
273
|
```
|
|
256
274
|
|
|
275
|
+
### JSX: 子要素なし、属性の差分が2つ以上の場合
|
|
276
|
+
|
|
277
|
+
```jsx
|
|
278
|
+
// 属性の差分が2つ以上(name と role が異なる)→ 意図的に分けていると判断
|
|
279
|
+
function Component() {
|
|
280
|
+
if (isAdmin) {
|
|
281
|
+
return <UserCard name="Admin" role="admin" />
|
|
282
|
+
} else {
|
|
283
|
+
return <UserCard name="User" role="user" />
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// FormattedMessage(id と defaultMessage が異なる = 差分2つ)
|
|
288
|
+
function Component({ searchKeyword }) {
|
|
289
|
+
return searchKeyword === '' ? (
|
|
290
|
+
<FormattedMessage id="userRoles/addDialog/noUser" defaultMessage="追加できるアカウントがありません" />
|
|
291
|
+
) : (
|
|
292
|
+
<FormattedMessage id="userRoles/addDialog/noResult" defaultMessage="該当するアカウントがありません" />
|
|
293
|
+
)
|
|
294
|
+
}
|
|
295
|
+
```
|
|
296
|
+
|
|
257
297
|
### JSX: 子要素あり、属性が異なる場合
|
|
258
298
|
|
|
259
299
|
```jsx
|
|
@@ -451,10 +491,10 @@ return (
|
|
|
451
491
|
|
|
452
492
|
- メソッドチェーンは完全一致で検出します。例えば、`api.post('/endpoint1').send(dataA)` と `api.post('/endpoint2').send(dataB)` は検出されません(エンドポイントが異なるため)。
|
|
453
493
|
- **JSXの検出条件:**
|
|
454
|
-
- **子要素なし(self-closing)の場合:**
|
|
494
|
+
- **子要素なし(self-closing)の場合:** 同じコンポーネント名で、属性の差分が1つだけの場合に検出します。差分が2つ以上の場合は、意図的に分けていると判断して検出しません。
|
|
455
495
|
- **子要素ありの場合:** 同じコンポーネント名、同じ属性(値を含む)で、子要素が異なる場合に検出します。
|
|
456
496
|
- **React.FragmentとFragmentは除外されます。** これらは単なるグループ化のためのものなので、検出対象外です。
|
|
457
|
-
- JSXのspread attributes(`{...props}
|
|
497
|
+
- JSXのspread attributes(`{...props}`)がある場合は、完全一致のみ検出します。spreadの変数名が異なる場合(`{...propsA}` vs `{...propsB}`)や、片方だけspreadがある場合は検出されません。
|
|
458
498
|
- **if文/switch文のearly returnパターン:** すべてのブランチが`return`で終わっている場合のみ、次のステートメントも検出対象に含まれます。`break`を使う場合は含まれません。
|
|
459
499
|
- **switch文のfall-through:** 空のcaseは次のcaseにfall-throughし、最初に見つかった処理を実行するものとして扱います。
|
|
460
500
|
- **switch文のdefaultケース:** defaultケースは最後のcaseなので、breakがなくても許容されます。
|
|
@@ -67,11 +67,45 @@ module.exports = {
|
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
const firstSelfClosing = jsxElements[0].openingElement.selfClosing
|
|
70
|
-
|
|
70
|
+
const firstAttrs = jsxElements[0].openingElement.attributes
|
|
71
|
+
|
|
72
|
+
// Spread attributesがある場合は厳密比較のみ
|
|
73
|
+
const hasSpread = firstAttrs.some((a) => a.type === 'JSXSpreadAttribute')
|
|
74
|
+
if (hasSpread) {
|
|
75
|
+
const firstOpeningTag = sourceCode.getText(jsxElements[0].openingElement)
|
|
76
|
+
|
|
77
|
+
for (let i = 1; i < jsxElements.length; i++) {
|
|
78
|
+
// Spreadがある場合は完全一致のみ許可、片方だけSpreadがある場合は除外
|
|
79
|
+
if (
|
|
80
|
+
jsxElements[i].openingElement.selfClosing !== firstSelfClosing ||
|
|
81
|
+
getJSXElementName(jsxElements[i]) !== firstComponentName ||
|
|
82
|
+
!jsxElements[i].openingElement.attributes.some((a) => a.type === 'JSXSpreadAttribute') ||
|
|
83
|
+
sourceCode.getText(jsxElements[i].openingElement) !== firstOpeningTag
|
|
84
|
+
) {
|
|
85
|
+
return
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
context.report({
|
|
90
|
+
node,
|
|
91
|
+
messageId: 'consolidateJSXElement',
|
|
92
|
+
data: { componentName: firstComponentName },
|
|
93
|
+
})
|
|
94
|
+
return
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// 最初の要素の属性Mapを作成(1回のみ)
|
|
98
|
+
const firstAttrMap = new Map()
|
|
99
|
+
for (const attr of firstAttrs) {
|
|
100
|
+
if (attr.type === 'JSXAttribute') {
|
|
101
|
+
const name = attr.name.name
|
|
102
|
+
const value = attr.value ? sourceCode.getText(attr.value) : 'true'
|
|
103
|
+
firstAttrMap.set(name, value)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
71
106
|
|
|
72
|
-
//
|
|
107
|
+
// 各要素と比較
|
|
73
108
|
for (let i = 1; i < jsxElements.length; i++) {
|
|
74
|
-
// selfClosingとコンポーネント名をチェック
|
|
75
109
|
if (
|
|
76
110
|
jsxElements[i].openingElement.selfClosing !== firstSelfClosing ||
|
|
77
111
|
getJSXElementName(jsxElements[i]) !== firstComponentName
|
|
@@ -79,14 +113,40 @@ module.exports = {
|
|
|
79
113
|
return
|
|
80
114
|
}
|
|
81
115
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
116
|
+
const currentAttrs = jsxElements[i].openingElement.attributes
|
|
117
|
+
|
|
118
|
+
// Spreadがある場合は除外
|
|
119
|
+
if (currentAttrs.some((a) => a.type === 'JSXSpreadAttribute')) {
|
|
120
|
+
return
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// 差分カウント(2以上で早期リターン)
|
|
124
|
+
let diffCount = 0
|
|
125
|
+
const checkedKeys = new Set()
|
|
126
|
+
|
|
127
|
+
// 現在の要素の属性を走査
|
|
128
|
+
for (const attr of currentAttrs) {
|
|
129
|
+
if (attr.type === 'JSXAttribute') {
|
|
130
|
+
const name = attr.name.name
|
|
131
|
+
const value = attr.value ? sourceCode.getText(attr.value) : 'true'
|
|
132
|
+
checkedKeys.add(name)
|
|
133
|
+
|
|
134
|
+
if (!firstAttrMap.has(name) || firstAttrMap.get(name) !== value) {
|
|
135
|
+
diffCount++
|
|
136
|
+
if (diffCount >= 2) {
|
|
137
|
+
return // 差分2以上 → 意図的に分けている
|
|
138
|
+
}
|
|
139
|
+
}
|
|
86
140
|
}
|
|
141
|
+
}
|
|
87
142
|
|
|
88
|
-
|
|
89
|
-
|
|
143
|
+
// 最初の要素にしかない属性もカウント
|
|
144
|
+
for (const key of firstAttrMap.keys()) {
|
|
145
|
+
if (!checkedKeys.has(key)) {
|
|
146
|
+
diffCount++
|
|
147
|
+
if (diffCount >= 2) {
|
|
148
|
+
return
|
|
149
|
+
}
|
|
90
150
|
}
|
|
91
151
|
}
|
|
92
152
|
}
|
|
@@ -94,6 +94,42 @@ ruleTester.run('best-practice-for-reduce-redundant-calls', rule, {
|
|
|
94
94
|
}
|
|
95
95
|
`,
|
|
96
96
|
},
|
|
97
|
+
// JSX: selfClosing、spread attributesの内容が異なる
|
|
98
|
+
{
|
|
99
|
+
code: `
|
|
100
|
+
function Component() {
|
|
101
|
+
if (condition) {
|
|
102
|
+
return <Component {...propsA} />
|
|
103
|
+
} else {
|
|
104
|
+
return <Component {...propsB} />
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
`,
|
|
108
|
+
},
|
|
109
|
+
// JSX: selfClosing、spreadがある/ない
|
|
110
|
+
{
|
|
111
|
+
code: `
|
|
112
|
+
function Component() {
|
|
113
|
+
if (condition) {
|
|
114
|
+
return <Component name="test" />
|
|
115
|
+
} else {
|
|
116
|
+
return <Component {...props} />
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
`,
|
|
120
|
+
},
|
|
121
|
+
// JSX: selfClosing、spread + 通常属性でspreadが異なる
|
|
122
|
+
{
|
|
123
|
+
code: `
|
|
124
|
+
function Component() {
|
|
125
|
+
if (condition) {
|
|
126
|
+
return <Component {...propsA} name="test" />
|
|
127
|
+
} else {
|
|
128
|
+
return <Component {...propsB} name="test" />
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
`,
|
|
132
|
+
},
|
|
97
133
|
// JSX: React.Fragment(同じ属性、異なる子要素)
|
|
98
134
|
{
|
|
99
135
|
code: `
|
|
@@ -367,6 +403,62 @@ ruleTester.run('best-practice-for-reduce-redundant-calls', rule, {
|
|
|
367
403
|
const element = condition ? <ComponentA>{child}</ComponentA> : <ComponentB>{child}</ComponentB>
|
|
368
404
|
`,
|
|
369
405
|
},
|
|
406
|
+
// JSX: selfClosing、属性が異なる(意図的に分けている)
|
|
407
|
+
{
|
|
408
|
+
code: `
|
|
409
|
+
function Component() {
|
|
410
|
+
if (isAdmin) {
|
|
411
|
+
return <UserCard name="Admin" role="admin" />
|
|
412
|
+
} else {
|
|
413
|
+
return <UserCard name="User" role="user" />
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
`,
|
|
417
|
+
},
|
|
418
|
+
// JSX: 三項演算子、selfClosing、属性が異なる
|
|
419
|
+
{
|
|
420
|
+
code: `
|
|
421
|
+
const element = isAdmin ? <UserCard name="Admin" role="admin" /> : <UserCard name="User" role="user" />
|
|
422
|
+
`,
|
|
423
|
+
},
|
|
424
|
+
// JSX: switch、selfClosing、属性が異なる
|
|
425
|
+
{
|
|
426
|
+
code: `
|
|
427
|
+
function Component() {
|
|
428
|
+
switch (type) {
|
|
429
|
+
case 'admin':
|
|
430
|
+
return <UserCard name="Admin" role="admin" />
|
|
431
|
+
case 'user':
|
|
432
|
+
return <UserCard name="User" role="user" />
|
|
433
|
+
default:
|
|
434
|
+
return <UserCard name="Guest" role="guest" />
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
`,
|
|
438
|
+
},
|
|
439
|
+
// JSX: selfClosing、FormattedMessage(属性が異なる)
|
|
440
|
+
{
|
|
441
|
+
code: `
|
|
442
|
+
function Component({ searchKeyword }) {
|
|
443
|
+
return searchKeyword === '' ? (
|
|
444
|
+
<FormattedMessage id="userRoles/addDialog/noUser" defaultMessage="追加できるアカウントがありません" />
|
|
445
|
+
) : (
|
|
446
|
+
<FormattedMessage id="userRoles/addDialog/noResult" defaultMessage="該当するアカウントがありません" />
|
|
447
|
+
)
|
|
448
|
+
}
|
|
449
|
+
`,
|
|
450
|
+
},
|
|
451
|
+
// JSX: early return、selfClosing、属性が異なる
|
|
452
|
+
{
|
|
453
|
+
code: `
|
|
454
|
+
function Component() {
|
|
455
|
+
if (isAdmin) {
|
|
456
|
+
return <UserCard name="Admin" role="admin" />
|
|
457
|
+
}
|
|
458
|
+
return <UserCard name="User" role="user" />
|
|
459
|
+
}
|
|
460
|
+
`,
|
|
461
|
+
},
|
|
370
462
|
],
|
|
371
463
|
invalid: [
|
|
372
464
|
// ========================================
|
|
@@ -474,23 +566,6 @@ ruleTester.run('best-practice-for-reduce-redundant-calls', rule, {
|
|
|
474
566
|
},
|
|
475
567
|
],
|
|
476
568
|
},
|
|
477
|
-
// early return: JSX
|
|
478
|
-
{
|
|
479
|
-
code: `
|
|
480
|
-
function Component() {
|
|
481
|
-
if (isAdmin) {
|
|
482
|
-
return <UserCard name="Admin" role="admin" />
|
|
483
|
-
}
|
|
484
|
-
return <UserCard name="User" role="user" />
|
|
485
|
-
}
|
|
486
|
-
`,
|
|
487
|
-
errors: [
|
|
488
|
-
{
|
|
489
|
-
messageId: 'consolidateJSXElement',
|
|
490
|
-
data: { componentName: 'UserCard', detailLink: DETAIL_LINK },
|
|
491
|
-
},
|
|
492
|
-
],
|
|
493
|
-
},
|
|
494
569
|
// early return: メソッドチェーン
|
|
495
570
|
{
|
|
496
571
|
code: `
|
|
@@ -592,18 +667,6 @@ ruleTester.run('best-practice-for-reduce-redundant-calls', rule, {
|
|
|
592
667
|
},
|
|
593
668
|
],
|
|
594
669
|
},
|
|
595
|
-
// ネストした三項演算子でJSX(子要素なし)
|
|
596
|
-
{
|
|
597
|
-
code: `
|
|
598
|
-
const element = isAdmin ? <UserCard role="admin" /> : isModerator ? <UserCard role="moderator" /> : <UserCard role="user" />
|
|
599
|
-
`,
|
|
600
|
-
errors: [
|
|
601
|
-
{
|
|
602
|
-
messageId: 'consolidateJSXElement',
|
|
603
|
-
data: { componentName: 'UserCard', detailLink: DETAIL_LINK },
|
|
604
|
-
},
|
|
605
|
-
],
|
|
606
|
-
},
|
|
607
670
|
|
|
608
671
|
// ========================================
|
|
609
672
|
// switch文
|
|
@@ -941,24 +1004,6 @@ ruleTester.run('best-practice-for-reduce-redundant-calls', rule, {
|
|
|
941
1004
|
// ========================================
|
|
942
1005
|
// JSX
|
|
943
1006
|
// ========================================
|
|
944
|
-
// JSX: 子要素なし、属性が異なる
|
|
945
|
-
{
|
|
946
|
-
code: `
|
|
947
|
-
function Component() {
|
|
948
|
-
if (isAdmin) {
|
|
949
|
-
return <UserCard name="Admin" role="admin" />
|
|
950
|
-
} else {
|
|
951
|
-
return <UserCard name="User" role="user" />
|
|
952
|
-
}
|
|
953
|
-
}
|
|
954
|
-
`,
|
|
955
|
-
errors: [
|
|
956
|
-
{
|
|
957
|
-
messageId: 'consolidateJSXElement',
|
|
958
|
-
data: { componentName: 'UserCard', detailLink: DETAIL_LINK },
|
|
959
|
-
},
|
|
960
|
-
],
|
|
961
|
-
},
|
|
962
1007
|
// 基本的なJSX(同じコンポーネント、同じ属性、異なる子要素)
|
|
963
1008
|
{
|
|
964
1009
|
code: `
|
|
@@ -1007,18 +1052,6 @@ ruleTester.run('best-practice-for-reduce-redundant-calls', rule, {
|
|
|
1007
1052
|
},
|
|
1008
1053
|
],
|
|
1009
1054
|
},
|
|
1010
|
-
// JSX: 三項演算子、子要素なし、属性が異なる
|
|
1011
|
-
{
|
|
1012
|
-
code: `
|
|
1013
|
-
const element = isAdmin ? <UserCard name="Admin" role="admin" /> : <UserCard name="User" role="user" />
|
|
1014
|
-
`,
|
|
1015
|
-
errors: [
|
|
1016
|
-
{
|
|
1017
|
-
messageId: 'consolidateJSXElement',
|
|
1018
|
-
data: { componentName: 'UserCard', detailLink: DETAIL_LINK },
|
|
1019
|
-
},
|
|
1020
|
-
],
|
|
1021
|
-
},
|
|
1022
1055
|
// JSX: spread attributesが同じ
|
|
1023
1056
|
{
|
|
1024
1057
|
code: `
|
|
@@ -1096,20 +1129,59 @@ ruleTester.run('best-practice-for-reduce-redundant-calls', rule, {
|
|
|
1096
1129
|
},
|
|
1097
1130
|
],
|
|
1098
1131
|
},
|
|
1099
|
-
// JSX:
|
|
1132
|
+
// JSX: ネストした三項演算子、selfClosing、属性の差分が1つ
|
|
1133
|
+
{
|
|
1134
|
+
code: `
|
|
1135
|
+
const element = isAdmin ? <UserCard role="admin" /> : isModerator ? <UserCard role="moderator" /> : <UserCard role="user" />
|
|
1136
|
+
`,
|
|
1137
|
+
errors: [
|
|
1138
|
+
{
|
|
1139
|
+
messageId: 'consolidateJSXElement',
|
|
1140
|
+
data: { componentName: 'UserCard', detailLink: DETAIL_LINK },
|
|
1141
|
+
},
|
|
1142
|
+
],
|
|
1143
|
+
},
|
|
1144
|
+
// JSX: selfClosing、属性の差分が1つ、他の属性は同じ
|
|
1145
|
+
{
|
|
1146
|
+
code: `
|
|
1147
|
+
function Component({ data }) {
|
|
1148
|
+
if (isModerator) {
|
|
1149
|
+
return <UserCard role="moderator" hoge={data} />
|
|
1150
|
+
} else {
|
|
1151
|
+
return <UserCard role="user" hoge={data} />
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
`,
|
|
1155
|
+
errors: [
|
|
1156
|
+
{
|
|
1157
|
+
messageId: 'consolidateJSXElement',
|
|
1158
|
+
data: { componentName: 'UserCard', detailLink: DETAIL_LINK },
|
|
1159
|
+
},
|
|
1160
|
+
],
|
|
1161
|
+
},
|
|
1162
|
+
// JSX: selfClosing、boolean属性の差分が1つ
|
|
1100
1163
|
{
|
|
1101
1164
|
code: `
|
|
1102
1165
|
function Component() {
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
return <UserCard name="User" role="user" />
|
|
1108
|
-
default:
|
|
1109
|
-
return <UserCard name="Guest" role="guest" />
|
|
1166
|
+
if (condition) {
|
|
1167
|
+
return <Component hoge={true} fuga={false} />
|
|
1168
|
+
} else {
|
|
1169
|
+
return <Component hoge={true} fuga={false} piyo />
|
|
1110
1170
|
}
|
|
1111
1171
|
}
|
|
1112
1172
|
`,
|
|
1173
|
+
errors: [
|
|
1174
|
+
{
|
|
1175
|
+
messageId: 'consolidateJSXElement',
|
|
1176
|
+
data: { componentName: 'Component', detailLink: DETAIL_LINK },
|
|
1177
|
+
},
|
|
1178
|
+
],
|
|
1179
|
+
},
|
|
1180
|
+
// JSX: 三項演算子、属性の差分が1つ
|
|
1181
|
+
{
|
|
1182
|
+
code: `
|
|
1183
|
+
const element = isModerator ? <UserCard role="moderator" hoge={data} /> : <UserCard role="user" hoge={data} />
|
|
1184
|
+
`,
|
|
1113
1185
|
errors: [
|
|
1114
1186
|
{
|
|
1115
1187
|
messageId: 'consolidateJSXElement',
|