eslint-plugin-smarthr 6.18.0 → 6.20.0
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 +21 -0
- package/README.md +1 -0
- package/package.json +1 -1
- package/rules/best-practice-for-lazy-variable/index.js +2 -1
- package/rules/best-practice-for-no-unnecessary-variable/index.js +79 -11
- package/rules/best-practice-for-rest-parameters/index.js +25 -15
- package/rules/best-practice-for-unstable-dependencies/README.md +287 -0
- package/rules/best-practice-for-unstable-dependencies/index.js +116 -0
- package/rules/require-barrel-import/README.md +114 -5
- package/rules/require-barrel-import/index.js +250 -157
- package/rules/require-barrel-import/utils.js +254 -0
- package/test/best-practice-for-lazy-variable.js +51 -0
- package/test/best-practice-for-no-unnecessary-variable.js +77 -0
- package/test/best-practice-for-rest-parameters.js +13 -0
- package/test/best-practice-for-unstable-dependencies.js +284 -0
- package/test/require-barrel-import.js +288 -3
- package/rules/require-barrel-import/barrel-purity-checker.js +0 -58
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,27 @@
|
|
|
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.20.0](https://github.com/kufu/tamatebako/compare/eslint-plugin-smarthr-v6.19.0...eslint-plugin-smarthr-v6.20.0) (2026-06-16)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* **best-practice-for-no-unnecessary-variable:** export { xxx as yyy } パターンで適切なメッセージを表示 ([#1375](https://github.com/kufu/tamatebako/issues/1375)) ([52d90fa](https://github.com/kufu/tamatebako/commit/52d90fa6eb682c956d71a12269d2cc2c4173d3dd))
|
|
11
|
+
* 依存配列に不安定な参照を含めることを禁止するルールを追加 ([#1376](https://github.com/kufu/tamatebako/issues/1376)) ([af7c18e](https://github.com/kufu/tamatebako/commit/af7c18e74ce38391078a0bd14bf815529e852f8a))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
### Bug Fixes
|
|
15
|
+
|
|
16
|
+
* best-practice-for-rest-parametersでin演算子も内部属性アクセスとして検知 ([#1372](https://github.com/kufu/tamatebako/issues/1372)) ([086b414](https://github.com/kufu/tamatebako/commit/086b414ef731ebd27aee935f2d31aa5d303dd9fd))
|
|
17
|
+
* lazy-variable/no-unnecessary-variableでjsxタグ名を正しく扱う ([#1382](https://github.com/kufu/tamatebako/issues/1382)) ([391fe7e](https://github.com/kufu/tamatebako/commit/391fe7ea655e030ded3d2a3d45ae511c2da75dd1))
|
|
18
|
+
|
|
19
|
+
## [6.19.0](https://github.com/kufu/tamatebako/compare/eslint-plugin-smarthr-v6.18.0...eslint-plugin-smarthr-v6.19.0) (2026-06-10)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
### Features
|
|
23
|
+
|
|
24
|
+
* require-barrel-import に同一ディレクトリ内の重複export検出機能を追加 ([#1350](https://github.com/kufu/tamatebako/issues/1350)) ([313449a](https://github.com/kufu/tamatebako/commit/313449a088aec3fa984f8bf1ba87e24bad1f10d6))
|
|
25
|
+
|
|
5
26
|
## [6.18.0](https://github.com/kufu/tamatebako/compare/eslint-plugin-smarthr-v6.17.0...eslint-plugin-smarthr-v6.18.0) (2026-06-09)
|
|
6
27
|
|
|
7
28
|
|
package/README.md
CHANGED
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
- [best-practice-for-tailwind-variants](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/best-practice-for-tailwind-variants)
|
|
41
41
|
- [best-practice-for-text-component](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/best-practice-for-text-component)
|
|
42
42
|
- [best-practice-for-unnesessary-early-return](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/best-practice-for-unnesessary-early-return)
|
|
43
|
+
- [best-practice-for-unstable-dependencies](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/best-practice-for-unstable-dependencies)
|
|
43
44
|
- [component-name](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/component-name)
|
|
44
45
|
- [design-system-guideline-bulk-action-row-button](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/design-system-guideline-bulk-action-row-button)
|
|
45
46
|
- [design-system-guideline-prohibit-dialog-button-icon](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/design-system-guideline-prohibit-dialog-button-icon)
|
package/package.json
CHANGED
|
@@ -84,7 +84,8 @@ function getVariableUsages(sourceCode, varName, declarationNode) {
|
|
|
84
84
|
if (!node || typeof node !== 'object') return
|
|
85
85
|
|
|
86
86
|
switch (node.type) {
|
|
87
|
-
case 'Identifier':
|
|
87
|
+
case 'Identifier':
|
|
88
|
+
case 'JSXIdentifier': {
|
|
88
89
|
// 変数名が一致し、変数参照である場合のみ収集(宣言自体は除外)
|
|
89
90
|
if (node.name === varName && node !== declarationNode.id && isVariableReference(node)) {
|
|
90
91
|
usages.push(node)
|
|
@@ -50,9 +50,7 @@ function shouldSkipVariableExceptComplexity(node) {
|
|
|
50
50
|
// UPPER_SNAKE_CASE形式の定数は除外(慣習的な定数命名)
|
|
51
51
|
UPPER_SNAKE_CASE_PATTERN.test(node.id.name) ||
|
|
52
52
|
// 関数式、TaggedTemplateExpressionは除外
|
|
53
|
-
EXCLUDED_INIT_TYPES.includes(node.init.type)
|
|
54
|
-
// React Hooks(useXxxで始まる関数)で初期化される変数は対象外
|
|
55
|
-
(node.init.type === 'CallExpression' && node.init.callee.type === 'Identifier' && node.init.callee.name.startsWith('use'))
|
|
53
|
+
EXCLUDED_INIT_TYPES.includes(node.init.type)
|
|
56
54
|
) {
|
|
57
55
|
return true
|
|
58
56
|
}
|
|
@@ -69,6 +67,16 @@ function shouldSkipVariableExceptComplexity(node) {
|
|
|
69
67
|
return false
|
|
70
68
|
}
|
|
71
69
|
|
|
70
|
+
/**
|
|
71
|
+
* React Hooksで初期化されているかを判定
|
|
72
|
+
*/
|
|
73
|
+
function isReactHookCall(node) {
|
|
74
|
+
return node.init &&
|
|
75
|
+
node.init.type === 'CallExpression' &&
|
|
76
|
+
node.init.callee.type === 'Identifier' &&
|
|
77
|
+
node.init.callee.name.startsWith('use')
|
|
78
|
+
}
|
|
79
|
+
|
|
72
80
|
/**
|
|
73
81
|
* 型注釈のテキストを取得
|
|
74
82
|
* @param {object} sourceCode - SourceCode
|
|
@@ -165,7 +173,7 @@ function getVariableUsagesInScope(sourceCode, varName, declarationNode) {
|
|
|
165
173
|
* 変数の使用箇所として収集すべきIdentifierかチェック
|
|
166
174
|
*/
|
|
167
175
|
function isTargetIdentifier(node) {
|
|
168
|
-
return node.type === 'Identifier' &&
|
|
176
|
+
return (node.type === 'Identifier' || node.type === 'JSXIdentifier') &&
|
|
169
177
|
node.name === varName &&
|
|
170
178
|
node !== declarationNode.id &&
|
|
171
179
|
!isInsideInit(node)
|
|
@@ -195,7 +203,11 @@ function getVariableUsagesInScope(sourceCode, varName, declarationNode) {
|
|
|
195
203
|
|
|
196
204
|
// 変数名が一致するIdentifierを収集(宣言自体と初期化式内は除外)
|
|
197
205
|
if (isTargetIdentifier(node)) {
|
|
198
|
-
|
|
206
|
+
// 同じノードオブジェクトを重複してカウントしないようにする
|
|
207
|
+
// (export { foo } の場合、localとexportedが同じノードを指すため)
|
|
208
|
+
if (!usages.includes(node)) {
|
|
209
|
+
usages.push(node)
|
|
210
|
+
}
|
|
199
211
|
return
|
|
200
212
|
}
|
|
201
213
|
|
|
@@ -371,6 +383,24 @@ function isAtStartOfExpressionStatement(usageNode) {
|
|
|
371
383
|
return false
|
|
372
384
|
}
|
|
373
385
|
|
|
386
|
+
/**
|
|
387
|
+
* 使用箇所がexport { xxx as yyy }のExportSpecifierの中にあるかチェック
|
|
388
|
+
*/
|
|
389
|
+
function isInExportSpecifier(usageNode) {
|
|
390
|
+
let current = usageNode.parent
|
|
391
|
+
while (current) {
|
|
392
|
+
if (current.type === 'ExportSpecifier') {
|
|
393
|
+
return true
|
|
394
|
+
}
|
|
395
|
+
// ExportNamedDeclarationまで到達したら終了
|
|
396
|
+
if (current.type === 'ExportNamedDeclaration') {
|
|
397
|
+
return false
|
|
398
|
+
}
|
|
399
|
+
current = current.parent
|
|
400
|
+
}
|
|
401
|
+
return false
|
|
402
|
+
}
|
|
403
|
+
|
|
374
404
|
/**
|
|
375
405
|
* 複雑さをチェックしてインライン化可能かを判定
|
|
376
406
|
*/
|
|
@@ -413,6 +443,19 @@ function analyzeVariable(sourceCode, node, options = {}) {
|
|
|
413
443
|
}
|
|
414
444
|
|
|
415
445
|
const usage = usages[0]
|
|
446
|
+
|
|
447
|
+
// JSXIdentifierとして使用されている場合は対象外(JSXタグ名はインライン化できない)
|
|
448
|
+
if (usage.type === 'JSXIdentifier') {
|
|
449
|
+
return null
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const isExportSpec = isInExportSpecifier(usage)
|
|
453
|
+
|
|
454
|
+
// React Hooks呼び出しは対象外(ただしexport { xxx as yyy }パターンは対象)
|
|
455
|
+
if (!isExportSpec && isReactHookCall(node)) {
|
|
456
|
+
return null
|
|
457
|
+
}
|
|
458
|
+
|
|
416
459
|
const typeAnnotation = getTypeAnnotationText(sourceCode, node)
|
|
417
460
|
|
|
418
461
|
// 複雑さチェック
|
|
@@ -425,6 +468,7 @@ function analyzeVariable(sourceCode, node, options = {}) {
|
|
|
425
468
|
varName,
|
|
426
469
|
usage,
|
|
427
470
|
typeAnnotation,
|
|
471
|
+
isExportSpecifier: isExportSpec,
|
|
428
472
|
}
|
|
429
473
|
}
|
|
430
474
|
|
|
@@ -438,6 +482,7 @@ module.exports = {
|
|
|
438
482
|
schema: SCHEMA,
|
|
439
483
|
messages: {
|
|
440
484
|
inlineVariable: '変数"{{name}}"は一度しか使用されていません。直接使用してください。',
|
|
485
|
+
exportDirectly: '変数"{{name}}"は一度しか使用されていません。export const {{exportedName}} = ... の形式で直接エクスポートしてください。',
|
|
441
486
|
},
|
|
442
487
|
},
|
|
443
488
|
create(context) {
|
|
@@ -450,12 +495,35 @@ module.exports = {
|
|
|
450
495
|
const analysis = analyzeVariable(sourceCode, node, options)
|
|
451
496
|
|
|
452
497
|
if (analysis) {
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
498
|
+
// export { xxx as yyy } パターンの場合は異なるメッセージを使用
|
|
499
|
+
if (analysis.isExportSpecifier) {
|
|
500
|
+
// ExportSpecifierからエクスポート名を取得
|
|
501
|
+
let exportedName = analysis.varName
|
|
502
|
+
let current = analysis.usage.parent
|
|
503
|
+
while (current && current.type !== 'ExportSpecifier') {
|
|
504
|
+
current = current.parent
|
|
505
|
+
}
|
|
506
|
+
if (current && current.type === 'ExportSpecifier' && current.exported) {
|
|
507
|
+
exportedName = current.exported.name
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
context.report({
|
|
511
|
+
node: analysis.node,
|
|
512
|
+
messageId: 'exportDirectly',
|
|
513
|
+
data: {
|
|
514
|
+
name: analysis.varName,
|
|
515
|
+
exportedName,
|
|
516
|
+
},
|
|
517
|
+
fix: fix ? createInlineFixer(sourceCode, analysis.node, analysis.usage, analysis.typeAnnotation) : null,
|
|
518
|
+
})
|
|
519
|
+
} else {
|
|
520
|
+
context.report({
|
|
521
|
+
node: analysis.node,
|
|
522
|
+
messageId: 'inlineVariable',
|
|
523
|
+
data: { name: analysis.varName },
|
|
524
|
+
fix: fix ? createInlineFixer(sourceCode, analysis.node, analysis.usage, analysis.typeAnnotation) : null,
|
|
525
|
+
})
|
|
526
|
+
}
|
|
459
527
|
}
|
|
460
528
|
},
|
|
461
529
|
}
|
|
@@ -2,6 +2,15 @@ const SCHEMA = []
|
|
|
2
2
|
|
|
3
3
|
const REST_REGEX = /(^r|R)est$/
|
|
4
4
|
const MEMBER_EXPRESSION_REST_REGEX = /^(r|[a-zA-Z0-9_]+R)est\./
|
|
5
|
+
|
|
6
|
+
const UNFORMAT_NAME__REST_ELEMENT_SELECTOR = `RestElement:not([argument.name=${REST_REGEX}])`
|
|
7
|
+
const CONFUSING_NAME_VARIABLE_SELECTOR_1 = `:not(:matches(RestElement,JSXSpreadAttribute,JSXSpreadAttribute > TSAsExpression,SpreadElement,SpreadElement > TSAsExpression,MemberExpression,BinaryExpression,VariableDeclarator,ArrayExpression,CallExpression,ObjectPattern > Property,ObjectExpression > Property,ReturnStatement,ArrowFunctionExpression)) > Identifier[name=${REST_REGEX}]`
|
|
8
|
+
const CONFUSING_NAME_VARIABLE_SELECTOR_2 = `:matches(VariableDeclarator[id.name=${REST_REGEX}],ObjectPattern > Property[value.name=${REST_REGEX}],ObjectExpression > Property[key.name=${REST_REGEX}])`
|
|
9
|
+
const ARROW_FUNCTION_PARAMS_SELECTOR = `ArrowFunctionExpression > Identifier[name=${REST_REGEX}]`
|
|
10
|
+
const USE_REST_VIORATION_SELECTOR_MEMBER_EXPRESSION = `MemberExpression[object.name=${REST_REGEX}]`
|
|
11
|
+
const USE_REST_VIORATION_SELECTOR_IN_OPERATOR = `BinaryExpression[operator="in"][right.name=${REST_REGEX}]`
|
|
12
|
+
const USE_REST_VIORATION_OBJECT_PATTERN = `VariableDeclarator[id.type='ObjectPattern'][init.name=${REST_REGEX}]`
|
|
13
|
+
|
|
5
14
|
const DETAIL_LINK = `
|
|
6
15
|
- 詳細: https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/best-practice-for-rest-parameters`
|
|
7
16
|
|
|
@@ -21,46 +30,47 @@ module.exports = {
|
|
|
21
30
|
- 残余引数(rest parameters)と混同する可能性があるため別の名称に修正してください`,
|
|
22
31
|
})
|
|
23
32
|
}
|
|
33
|
+
const actionUseRestVioration = (node) => {
|
|
34
|
+
context.report({
|
|
35
|
+
node,
|
|
36
|
+
message: `残余引数内の属性を参照しないでください${DETAIL_LINK}`,
|
|
37
|
+
})
|
|
38
|
+
}
|
|
24
39
|
const actionMemberExpressionName = (node) => {
|
|
25
40
|
if (node.parent.type === 'MemberExpression') {
|
|
26
41
|
return actionMemberExpressionName(node.parent)
|
|
27
42
|
}
|
|
28
43
|
|
|
29
44
|
if (MEMBER_EXPRESSION_REST_REGEX.test(context.sourceCode.getText(node))){
|
|
30
|
-
|
|
31
|
-
node,
|
|
32
|
-
message: `残余引数内の属性を参照しないでください${DETAIL_LINK}`,
|
|
33
|
-
})
|
|
45
|
+
actionUseRestVioration(node)
|
|
34
46
|
}
|
|
35
47
|
}
|
|
36
48
|
|
|
37
49
|
return {
|
|
38
|
-
|
|
50
|
+
'ObjectPattern[properties.length=1]>RestElement': (node) => {
|
|
39
51
|
context.report({
|
|
40
52
|
node,
|
|
41
53
|
message: `意味のない残余引数のため、単一の引数に変更してください${DETAIL_LINK}`,
|
|
42
54
|
})
|
|
43
55
|
},
|
|
44
|
-
[
|
|
56
|
+
[UNFORMAT_NAME__REST_ELEMENT_SELECTOR]: (node) => {
|
|
45
57
|
context.report({
|
|
46
58
|
node,
|
|
47
59
|
message: `残余引数には ${REST_REGEX} とマッチする名称を指定してください${DETAIL_LINK}`,
|
|
48
60
|
})
|
|
49
61
|
},
|
|
50
|
-
[
|
|
51
|
-
[
|
|
52
|
-
[
|
|
53
|
-
[`ArrowFunctionExpression > Identifier[name=${REST_REGEX}]`]: (node) => {
|
|
62
|
+
[CONFUSING_NAME_VARIABLE_SELECTOR_1]: actionNotRest,
|
|
63
|
+
[CONFUSING_NAME_VARIABLE_SELECTOR_2]: actionNotRest,
|
|
64
|
+
[ARROW_FUNCTION_PARAMS_SELECTOR]: (node) => {
|
|
54
65
|
if (node !== node.parent.body) {
|
|
55
66
|
actionNotRest(node)
|
|
56
67
|
}
|
|
57
68
|
},
|
|
58
|
-
[
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
message: `残余引数内の属性を参照しないでください${DETAIL_LINK}`,
|
|
62
|
-
})
|
|
69
|
+
[USE_REST_VIORATION_SELECTOR_MEMBER_EXPRESSION]: actionMemberExpressionName,
|
|
70
|
+
[USE_REST_VIORATION_SELECTOR_IN_OPERATOR]: (node) => {
|
|
71
|
+
actionUseRestVioration(node.right)
|
|
63
72
|
},
|
|
73
|
+
[USE_REST_VIORATION_OBJECT_PATTERN]: actionUseRestVioration,
|
|
64
74
|
}
|
|
65
75
|
},
|
|
66
76
|
}
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
# best-practice-for-unstable-dependencies
|
|
2
|
+
|
|
3
|
+
React Hooksの依存配列に不安定な参照(オブジェクト、配列、関数、ReactNodeなど)を含めることを禁止します。
|
|
4
|
+
|
|
5
|
+
これらの値は参照が頻繁に変わるため、依存配列に含めると不要な再実行や無限ループの原因となります。
|
|
6
|
+
|
|
7
|
+
**デフォルトでは `children` のみを検出します。**他の変数名を検出したい場合は、オプションで追加してください。
|
|
8
|
+
|
|
9
|
+
## 不安定な参照の種類(オプションで追加可能)
|
|
10
|
+
|
|
11
|
+
- **ReactNode**: `children`(デフォルト)、`icon`、`prefix`、`suffix`など
|
|
12
|
+
- **オブジェクト**: `object`、`config`、`options`、`settings`など
|
|
13
|
+
- **配列**: `items`、`list`、`data`、`records`など
|
|
14
|
+
- **関数**: `callback`、`handler`、`onClick`、`onChange`、`onSubmit`など
|
|
15
|
+
|
|
16
|
+
## ❌ NG
|
|
17
|
+
|
|
18
|
+
### children(デフォルトで検出)
|
|
19
|
+
|
|
20
|
+
```javascript
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
console.log(children)
|
|
23
|
+
}, [children])
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
```javascript
|
|
27
|
+
const memoized = useMemo(() => {
|
|
28
|
+
return children.length
|
|
29
|
+
}, [children])
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### オプション追加時の例
|
|
33
|
+
|
|
34
|
+
以下は `additionalUnstableNames` オプションで変数名を追加した場合の検出例です。
|
|
35
|
+
|
|
36
|
+
**オブジェクト:**
|
|
37
|
+
|
|
38
|
+
```javascript
|
|
39
|
+
// additionalUnstableNames: ["object"] を設定した場合
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
console.log(object.key)
|
|
42
|
+
}, [object])
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
**配列:**
|
|
46
|
+
|
|
47
|
+
```javascript
|
|
48
|
+
// additionalUnstableNames: ["items"] を設定した場合
|
|
49
|
+
const memoized = useMemo(() => {
|
|
50
|
+
return items.map(i => i.value)
|
|
51
|
+
}, [items])
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
**関数:**
|
|
55
|
+
|
|
56
|
+
```javascript
|
|
57
|
+
// additionalUnstableNames: ["callback"] を設定した場合
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
callback()
|
|
60
|
+
}, [callback])
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## ✅ OK
|
|
64
|
+
|
|
65
|
+
### 方法1: refを使用する(最も推奨)
|
|
66
|
+
|
|
67
|
+
依存配列で再実行する必要がない変数は、refに保存して依存配列から除外します。
|
|
68
|
+
|
|
69
|
+
**children(DOM要素のrefを使う場合):**
|
|
70
|
+
|
|
71
|
+
```javascript
|
|
72
|
+
const childrenRef = useRef()
|
|
73
|
+
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
console.log(childrenRef.current)
|
|
76
|
+
}, [/* childrenを含めない */])
|
|
77
|
+
|
|
78
|
+
...
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<Any ref={childrenRef}>{children}</Any>
|
|
82
|
+
)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
refは任意の値を保持でき、更新してもコンポーネントの再レンダリングをトリガーしません。
|
|
86
|
+
|
|
87
|
+
```javascript
|
|
88
|
+
const valueRef = useRef()
|
|
89
|
+
valueRef.current = value // レンダリングごとに最新の値に更新
|
|
90
|
+
|
|
91
|
+
useEffect(() => {
|
|
92
|
+
// valueRef.currentを使用
|
|
93
|
+
console.log(valueRef.current)
|
|
94
|
+
}, [/* valueを含めない */])
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### 方法2: MutationObserverを使用する
|
|
98
|
+
|
|
99
|
+
`children` の変更を検知する必要がある場合は、DOM要素にrefを設定してMutationObserverで監視します。
|
|
100
|
+
|
|
101
|
+
```javascript
|
|
102
|
+
const containerRef = useRef()
|
|
103
|
+
|
|
104
|
+
useEffect(() => {
|
|
105
|
+
const observer = new MutationObserver(() => {
|
|
106
|
+
// children内のDOM要素が追加/削除されたときの処理
|
|
107
|
+
console.log('子要素が変更されました')
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
if (containerRef.current) {
|
|
111
|
+
// containerRef内の子要素の変更を監視
|
|
112
|
+
observer.observe(containerRef.current, {
|
|
113
|
+
childList: true, // 直接の子要素の追加/削除を監視
|
|
114
|
+
subtree: true, // 子孫要素の変更も監視(必要に応じて)
|
|
115
|
+
})
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return () => observer.disconnect()
|
|
119
|
+
}, []) // childrenは依存配列に含めない
|
|
120
|
+
|
|
121
|
+
...
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<div ref={containerRef}>
|
|
125
|
+
{children}
|
|
126
|
+
</div>
|
|
127
|
+
)
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### 方法3: プリミティブ値のみを依存配列に含める
|
|
131
|
+
|
|
132
|
+
オブジェクトや配列の場合は、必要なプリミティブ値のみを依存配列に含めます。
|
|
133
|
+
|
|
134
|
+
```javascript
|
|
135
|
+
// ❌ 配列全体を依存配列に含める
|
|
136
|
+
useEffect(() => {
|
|
137
|
+
console.log(items.length)
|
|
138
|
+
}, [items])
|
|
139
|
+
|
|
140
|
+
// ✅ 必要な値のみを依存配列に含める
|
|
141
|
+
useEffect(() => {
|
|
142
|
+
console.log(items.length)
|
|
143
|
+
}, [items.length])
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
**スプレッド構文を使っている場合:**
|
|
147
|
+
|
|
148
|
+
スプレッド構文(`...config`など)で展開している箇所は、実際に必要な値のみをベタ書きできないか検証してください。
|
|
149
|
+
|
|
150
|
+
```javascript
|
|
151
|
+
// ❌ オブジェクト全体を展開
|
|
152
|
+
useEffect(() => {
|
|
153
|
+
const newConfig = { ...config, newProp: value }
|
|
154
|
+
doSomething(newConfig)
|
|
155
|
+
}, [config, value])
|
|
156
|
+
|
|
157
|
+
// ✅ 必要な値のみをベタ書き
|
|
158
|
+
useEffect(() => {
|
|
159
|
+
const newConfig = {
|
|
160
|
+
apiUrl: config.apiUrl,
|
|
161
|
+
timeout: config.timeout,
|
|
162
|
+
newProp: value
|
|
163
|
+
}
|
|
164
|
+
doSomething(newConfig)
|
|
165
|
+
}, [config.apiUrl, config.timeout, value])
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
**ヒント:** 多くの場合、スプレッド構文で展開しているオブジェクトは、実際には一部のプロパティしか使っていないことがあります。使用箇所を確認して、必要な値のみを明示的に指定することで、依存配列を最小限に抑えられます。配列の場合も、実際に必要なのは`length`や特定のインデックスの値だけかもしれません。
|
|
169
|
+
|
|
170
|
+
### 方法4: useCallbackやuseMemoでメモ化する
|
|
171
|
+
|
|
172
|
+
不安定な参照を安定化させるために、useCallbackやuseMemoでラップします。
|
|
173
|
+
|
|
174
|
+
**関数の場合(useCallback):**
|
|
175
|
+
|
|
176
|
+
```javascript
|
|
177
|
+
// ❌ 関数を直接依存配列に含める
|
|
178
|
+
useEffect(() => {
|
|
179
|
+
callback()
|
|
180
|
+
}, [callback])
|
|
181
|
+
|
|
182
|
+
// ✅ useCallbackでメモ化
|
|
183
|
+
const memoizedCallback = useCallback(callback, [/* callbackの依存関係 */])
|
|
184
|
+
|
|
185
|
+
useEffect(() => {
|
|
186
|
+
memoizedCallback()
|
|
187
|
+
}, [memoizedCallback])
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
**オブジェクトの場合(useMemo):**
|
|
191
|
+
|
|
192
|
+
```javascript
|
|
193
|
+
// ❌ オブジェクトを直接依存配列に含める
|
|
194
|
+
useEffect(() => {
|
|
195
|
+
console.log(config.apiUrl)
|
|
196
|
+
}, [config])
|
|
197
|
+
|
|
198
|
+
// ✅ useMemoでメモ化
|
|
199
|
+
const memoizedConfig = useMemo(() => config, [/* configの依存関係 */])
|
|
200
|
+
|
|
201
|
+
useEffect(() => {
|
|
202
|
+
console.log(memoizedConfig.apiUrl)
|
|
203
|
+
}, [memoizedConfig])
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
**注意:** メモ化は依存関係が明確な場合に有効です。依存関係が不明確な場合は、方法1のrefを使用することを推奨します。
|
|
207
|
+
|
|
208
|
+
## オプション
|
|
209
|
+
|
|
210
|
+
### additionalUnstableNames
|
|
211
|
+
|
|
212
|
+
デフォルトでは `children` のみをチェックしますが、他の変数名を追加できます。
|
|
213
|
+
|
|
214
|
+
```javascript
|
|
215
|
+
{
|
|
216
|
+
"smarthr/best-practice-for-unstable-dependencies": ["error", {
|
|
217
|
+
"additionalUnstableNames": ["icon", "prefix", "object", "items", "callback"]
|
|
218
|
+
}]
|
|
219
|
+
}
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### additionalTargetHooks
|
|
223
|
+
|
|
224
|
+
デフォルトでは `useEffect`, `useLayoutEffect`, `useCallback`, `useMemo` をチェックしますが、カスタムフックを追加できます。
|
|
225
|
+
|
|
226
|
+
```javascript
|
|
227
|
+
{
|
|
228
|
+
"smarthr/best-practice-for-unstable-dependencies": ["error", {
|
|
229
|
+
"additionalTargetHooks": ["useCustomHook", "useMyEffect"]
|
|
230
|
+
}]
|
|
231
|
+
}
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
## 検出対象のHooks(デフォルト)
|
|
235
|
+
|
|
236
|
+
- `useEffect`
|
|
237
|
+
- `useCallback`
|
|
238
|
+
- `useMemo`
|
|
239
|
+
- `useLayoutEffect`
|
|
240
|
+
|
|
241
|
+
## 使用例
|
|
242
|
+
|
|
243
|
+
### 不安定な参照の追加
|
|
244
|
+
|
|
245
|
+
プロジェクトでよく使われる不安定な参照を追加することで、チーム全体で一貫したコードを書くことができます。
|
|
246
|
+
|
|
247
|
+
```javascript
|
|
248
|
+
{
|
|
249
|
+
"smarthr/best-practice-for-unstable-dependencies": ["error", {
|
|
250
|
+
"additionalUnstableNames": [
|
|
251
|
+
// ReactNode
|
|
252
|
+
"icon",
|
|
253
|
+
"prefix",
|
|
254
|
+
"suffix",
|
|
255
|
+
// オブジェクト
|
|
256
|
+
"object",
|
|
257
|
+
"config",
|
|
258
|
+
"options",
|
|
259
|
+
"settings",
|
|
260
|
+
// 配列
|
|
261
|
+
"items",
|
|
262
|
+
"list",
|
|
263
|
+
"data",
|
|
264
|
+
"records",
|
|
265
|
+
// 関数
|
|
266
|
+
"callback",
|
|
267
|
+
"handler",
|
|
268
|
+
"onClick",
|
|
269
|
+
"onChange",
|
|
270
|
+
"onSubmit"
|
|
271
|
+
]
|
|
272
|
+
}]
|
|
273
|
+
}
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
### カスタムフックの追加
|
|
277
|
+
|
|
278
|
+
プロジェクト固有のカスタムフックも検出対象に追加できます。
|
|
279
|
+
|
|
280
|
+
```javascript
|
|
281
|
+
{
|
|
282
|
+
"smarthr/best-practice-for-unstable-dependencies": ["error", {
|
|
283
|
+
"additionalUnstableNames": ["icon", "items"],
|
|
284
|
+
"additionalTargetHooks": ["useCustomEffect", "useMyMemo"]
|
|
285
|
+
}]
|
|
286
|
+
}
|
|
287
|
+
```
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
const SCHEMA = [
|
|
2
|
+
{
|
|
3
|
+
type: 'object',
|
|
4
|
+
properties: {
|
|
5
|
+
additionalUnstableNames: {
|
|
6
|
+
type: 'array',
|
|
7
|
+
items: { type: 'string' },
|
|
8
|
+
default: [],
|
|
9
|
+
},
|
|
10
|
+
additionalTargetHooks: {
|
|
11
|
+
type: 'array',
|
|
12
|
+
items: { type: 'string' },
|
|
13
|
+
default: [],
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
additionalProperties: false,
|
|
17
|
+
},
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
const DOLLAR_SIGN_REGEX = /\$/g
|
|
21
|
+
const DEFAULT_UNSTABLE_NAMES = ['children']
|
|
22
|
+
const DEFAULT_TARGET_HOOKS = ['useEffect', 'useLayoutEffect', 'useCallback', 'useMemo']
|
|
23
|
+
|
|
24
|
+
const DETAIL_LINK = `
|
|
25
|
+
- 詳細: https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/best-practice-for-unstable-dependencies`
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 名前の配列から正規表現を生成する($をエスケープして処理)
|
|
29
|
+
* @param {string[]} names - 名前の配列
|
|
30
|
+
* @returns {RegExp} 生成された正規表現
|
|
31
|
+
*/
|
|
32
|
+
function buildRegex(names) {
|
|
33
|
+
const pattern = names.reduce((acc, name, i) =>
|
|
34
|
+
acc + (i > 0 ? '|' : '') + name.replace(DOLLAR_SIGN_REGEX, '\\$'), '')
|
|
35
|
+
return new RegExp(`^(${pattern})$`)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 依存配列内の識別子を取得する
|
|
40
|
+
* @param {object} dependenciesArray - 依存配列のArrayExpressionノード
|
|
41
|
+
* @returns {Array<{node: object, name: string}>} 識別子の配列
|
|
42
|
+
*/
|
|
43
|
+
function getDependencyIdentifiers(dependenciesArray) {
|
|
44
|
+
const identifiers = []
|
|
45
|
+
|
|
46
|
+
if (dependenciesArray.type === 'ArrayExpression') {
|
|
47
|
+
for (const element of dependenciesArray.elements) {
|
|
48
|
+
if (element?.type === 'Identifier') {
|
|
49
|
+
// [children]
|
|
50
|
+
identifiers.push({
|
|
51
|
+
node: element,
|
|
52
|
+
name: element.name,
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return identifiers
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* @type {import('@typescript-eslint/utils').TSESLint.RuleModule<''>}
|
|
63
|
+
*/
|
|
64
|
+
module.exports = {
|
|
65
|
+
meta: {
|
|
66
|
+
type: 'problem',
|
|
67
|
+
schema: SCHEMA,
|
|
68
|
+
messages: {
|
|
69
|
+
unstableDependency: '依存配列に不安定な参照と予想される"{{name}}"が含まれています。オブジェクトやReactNodeなどの参照は頻繁に変わるため、不要な再実行や無限ループの原因となります。{{detailLink}}',
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
create(context) {
|
|
73
|
+
const options = context.options[0] || {}
|
|
74
|
+
const unstableNames = [...DEFAULT_UNSTABLE_NAMES, ...(options.additionalUnstableNames || [])]
|
|
75
|
+
const targetHooks = [...DEFAULT_TARGET_HOOKS, ...(options.additionalTargetHooks || [])]
|
|
76
|
+
|
|
77
|
+
const unstableNamesRegex = buildRegex(unstableNames)
|
|
78
|
+
const targetHooksRegex = buildRegex(targetHooks)
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
CallExpression(node) {
|
|
82
|
+
// 対象のHooksかチェック
|
|
83
|
+
if (
|
|
84
|
+
node.callee.type !== 'Identifier' ||
|
|
85
|
+
!targetHooksRegex.test(node.callee.name)
|
|
86
|
+
) {
|
|
87
|
+
return
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// 第2引数(依存配列)を取得
|
|
91
|
+
const dependenciesArray = node.arguments[1]
|
|
92
|
+
if (!dependenciesArray) {
|
|
93
|
+
return
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// 依存配列内の識別子を取得
|
|
97
|
+
const identifiers = getDependencyIdentifiers(dependenciesArray)
|
|
98
|
+
|
|
99
|
+
// 不安定な参照と予想される名前が含まれているかチェック
|
|
100
|
+
for (const identifier of identifiers) {
|
|
101
|
+
if (unstableNamesRegex.test(identifier.name)) {
|
|
102
|
+
context.report({
|
|
103
|
+
node: identifier.node,
|
|
104
|
+
messageId: 'unstableDependency',
|
|
105
|
+
data: {
|
|
106
|
+
name: identifier.name,
|
|
107
|
+
detailLink: DETAIL_LINK,
|
|
108
|
+
},
|
|
109
|
+
})
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
}
|
|
116
|
+
module.exports.schema = SCHEMA
|