eslint-plugin-smarthr 3.3.2 → 3.5.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 +15 -0
- package/README.md +2 -0
- package/package.json +3 -3
- package/rules/best-practice-for-optional-chaining/README.md +75 -0
- package/rules/best-practice-for-optional-chaining/index.js +40 -0
- package/rules/best-practice-for-rest-parameters/README.md +24 -8
- package/rules/best-practice-for-rest-parameters/index.js +23 -10
- package/rules/best-practice-for-unnesessary-early-return/README.md +161 -0
- package/rules/best-practice-for-unnesessary-early-return/index.js +92 -0
- package/test/best-practice-for-optional-chaining.js +48 -0
- package/test/best-practice-for-rest-parameters.js +15 -12
- package/test/best-practice-for-unnesessary-early-return.js +189 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,21 @@
|
|
|
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
|
+
## [3.5.0](https://github.com/kufu/tamatebako/compare/eslint-plugin-smarthr-v3.4.0...eslint-plugin-smarthr-v3.5.0) (2025-12-26)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* smarthr/best-practice-for-optional-chaining を追加 ([#979](https://github.com/kufu/tamatebako/issues/979)) ([ff18a55](https://github.com/kufu/tamatebako/commit/ff18a55970f026c2d828cf22f99e9969d90d7d82))
|
|
11
|
+
* smarthr/best-practice-for-unnesessary-early-return を追加 ([#977](https://github.com/kufu/tamatebako/issues/977)) ([cd0b1a2](https://github.com/kufu/tamatebako/commit/cd0b1a205e82f01ce2be4fe43b69555d86ab5fd1))
|
|
12
|
+
|
|
13
|
+
## [3.4.0](https://github.com/kufu/tamatebako/compare/eslint-plugin-smarthr-v3.3.2...eslint-plugin-smarthr-v3.4.0) (2025-12-24)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
### Features
|
|
17
|
+
|
|
18
|
+
* best-practice-for-rest-parameters の残余引数の命名規則をrestもしくはxxxRestにすることを促すように修正 ([#973](https://github.com/kufu/tamatebako/issues/973)) ([3824818](https://github.com/kufu/tamatebako/commit/3824818a68d0ab6b2b2dc04ff5292f9d8be03d9b))
|
|
19
|
+
|
|
5
20
|
## [3.3.2](https://github.com/kufu/tamatebako/compare/eslint-plugin-smarthr-v3.3.1...eslint-plugin-smarthr-v3.3.2) (2025-12-23)
|
|
6
21
|
|
|
7
22
|
|
package/README.md
CHANGED
|
@@ -21,11 +21,13 @@
|
|
|
21
21
|
- [best-practice-for-date](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/best-practice-for-date)
|
|
22
22
|
- [best-practice-for-layouts](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/best-practice-for-layouts)
|
|
23
23
|
- [best-practice-for-nested-attributes-array-index](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/best-practice-for-nested-attributes-array-index)
|
|
24
|
+
- [best-practice-for-optional-chaining](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/best-practice-for-optional-chaining)
|
|
24
25
|
- [best-practice-for-prohibit-import-smarthr-ui-local](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/best-practice-for-prohibit-import-smarthr-ui-local)
|
|
25
26
|
- [best-practice-for-remote-trigger-dialog](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/best-practice-for-remote-trigger-dialog)
|
|
26
27
|
- [best-practice-for-rest-parameters](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/best-practice-for-rest-parameters)
|
|
27
28
|
- [best-practice-for-tailwind-prohibit-root-margin](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/best-practice-for-tailwind-prohibit-root-margin)
|
|
28
29
|
- [best-practice-for-tailwind-variants](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/best-practice-for-tailwind-variants)
|
|
30
|
+
- [best-practice-for-unnesessary-early-return](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/best-practice-for-unnesessary-early-return)
|
|
29
31
|
- [design-system-guideline-prohibit-double-icons](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/design-system-guideline-prohibit-double-icons)
|
|
30
32
|
- [format-import-path](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/format-import-path)
|
|
31
33
|
- [format-translate-component](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/format-translate-component)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-smarthr",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.5.0",
|
|
4
4
|
"author": "SmartHR",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "A sharable ESLint plugin for SmartHR",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"json5": "^2.2.3"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
|
-
"typescript-eslint": "^8.50.
|
|
29
|
+
"typescript-eslint": "^8.50.1"
|
|
30
30
|
},
|
|
31
31
|
"peerDependencies": {
|
|
32
32
|
"eslint": "^9"
|
|
@@ -37,5 +37,5 @@
|
|
|
37
37
|
"eslintplugin",
|
|
38
38
|
"smarthr"
|
|
39
39
|
],
|
|
40
|
-
"gitHead": "
|
|
40
|
+
"gitHead": "2709611af1e9178d321a0fbfc6564e2cbc454c72"
|
|
41
41
|
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# smarthr/best-practice-for-optional-chaining
|
|
2
|
+
|
|
3
|
+
- optional chaining(xxx?.yyy記法)を使うことを促すルールです
|
|
4
|
+
|
|
5
|
+
## rules
|
|
6
|
+
|
|
7
|
+
```js
|
|
8
|
+
{
|
|
9
|
+
rules: {
|
|
10
|
+
'smarthr/best-practice-for-optional-chaining': 'error', // 'warn', 'off'
|
|
11
|
+
},
|
|
12
|
+
}
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## ❌ Incorrect
|
|
16
|
+
|
|
17
|
+
```jsx
|
|
18
|
+
if (action) action()
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
```jsx
|
|
22
|
+
if (obj.action) {
|
|
23
|
+
obj.action(hoge, fuga)
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
## ✅ Correct
|
|
29
|
+
|
|
30
|
+
```jsx
|
|
31
|
+
action?.()
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
```jsx
|
|
35
|
+
obj.action?.(hoge, fuga)
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
```jsx
|
|
39
|
+
// 実行する関数と無関係の条件の場合は許容
|
|
40
|
+
if (any) {
|
|
41
|
+
action()
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
```jsx
|
|
46
|
+
// 実行する関数の存在チェック以外が条件に含まれている場合は許容
|
|
47
|
+
if (action && any) {
|
|
48
|
+
action()
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
```jsx
|
|
53
|
+
// if - else などifに他条件が連なる場合は許容
|
|
54
|
+
if (action) {
|
|
55
|
+
action()
|
|
56
|
+
} else {
|
|
57
|
+
...
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
```jsx
|
|
62
|
+
// if内で関数呼び出し以外の処理が存在すれば許容
|
|
63
|
+
if (action) {
|
|
64
|
+
return action()
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
```jsx
|
|
69
|
+
// else if内の場合は許容
|
|
70
|
+
if (any) {
|
|
71
|
+
...
|
|
72
|
+
} else if (action) {
|
|
73
|
+
action()
|
|
74
|
+
}
|
|
75
|
+
```
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
const SCHEMA = []
|
|
2
|
+
|
|
3
|
+
const REPLACABLE_CALLEE = `[consequent.expression.callee.type='Identifier']`
|
|
4
|
+
const WITHOUT_BODY_IF_ID = `[test.type='Identifier']${REPLACABLE_CALLEE}`
|
|
5
|
+
const WITH_BODY_IF_ID = WITHOUT_BODY_IF_ID.replace(REPLACABLE_CALLEE, "[consequent.body.length=1][consequent.body.0.expression.callee.type='Identifier']")
|
|
6
|
+
const SELECTOR = `IfStatement[alternate=null]:not([parent.type='IfStatement']):matches(${WITHOUT_BODY_IF_ID},${WITHOUT_BODY_IF_ID.replaceAll("'Identifier'", "'MemberExpression'")},${WITH_BODY_IF_ID},${WITH_BODY_IF_ID.replaceAll("'Identifier'", "'MemberExpression'")})`
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @type {import('@typescript-eslint/utils').TSESLint.RuleModule<''>}
|
|
11
|
+
*/
|
|
12
|
+
module.exports = {
|
|
13
|
+
meta: {
|
|
14
|
+
type: 'suggestion',
|
|
15
|
+
fixable: 'code',
|
|
16
|
+
schema: SCHEMA,
|
|
17
|
+
},
|
|
18
|
+
create(context) {
|
|
19
|
+
return {
|
|
20
|
+
[SELECTOR]: (node) => {
|
|
21
|
+
const expression = node.consequent.expression || node.consequent.body[0].expression
|
|
22
|
+
const calleName = context.sourceCode.getText(expression.callee)
|
|
23
|
+
|
|
24
|
+
if (context.sourceCode.getText(node.test) === calleName) {
|
|
25
|
+
context.report({
|
|
26
|
+
node,
|
|
27
|
+
message: `optional chaining(xxx?.yyyy記法)を利用してください
|
|
28
|
+
- 詳細: https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/best-practice-for-optional-chaining`,
|
|
29
|
+
fix: (fixer) => fixer.replaceText(
|
|
30
|
+
node,
|
|
31
|
+
context.sourceCode.getText(expression).replace(new RegExp(`^${calleName}\\(`), `${calleName}\?\.(`),
|
|
32
|
+
),
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
}
|
|
39
|
+
module.exports.schema = SCHEMA
|
|
40
|
+
|
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
# smarthr/best-practice-for-rest-parameters
|
|
2
2
|
|
|
3
|
-
- 残余引数(rest parameters)
|
|
3
|
+
- 残余引数(rest parameters)の利用方法を設定するルールです
|
|
4
4
|
- https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Functions/rest_parameters
|
|
5
5
|
- 残余引数にはrestという名称を設定することを推奨します
|
|
6
6
|
- よく利用される `props` などを利用するとエラーになります
|
|
7
7
|
- コンポーネントが受け取れる属性を定義する際、多用される "Props" 型と勘違いされる可能性を減らすためです
|
|
8
8
|
- 残余引数の時点でコンポーネントが受け取れる属性全てではないことが確定するためpropsの利用を禁止しています
|
|
9
9
|
- `rest` という名称に揃えることで可読性を向上させることが出来ます
|
|
10
|
-
-
|
|
10
|
+
- restがすでに利用されており設定出来ない場合 `xxxRest` というフォーマットが利用出来ます
|
|
11
|
+
- 残余引数以外でrest、もしくはxxxRestというフォーマットの名称を設定することを禁止します
|
|
11
12
|
- restは rest parametersから命名されたjsのイディオムのため、残余引数以外の箇所で利用すると混乱を招くためです
|
|
12
|
-
- `xxxRest` のように他単語と組合されている場合はエラーになりません
|
|
13
13
|
- 残余引数内の属性を直接参照することを禁止します
|
|
14
14
|
- 例: `const hoge = rest.fuga`
|
|
15
|
+
- 例: `const { hoge } = rest`
|
|
15
16
|
- この条件を守る場合、残余引数がそのスコープ内で関心が薄い引数の集まりになり、可読性が向上します
|
|
16
17
|
|
|
17
18
|
## rules
|
|
@@ -27,7 +28,7 @@
|
|
|
27
28
|
## ❌ Incorrect
|
|
28
29
|
|
|
29
30
|
```js
|
|
30
|
-
// 残余引数にrest以外の名称が利用されているためNG
|
|
31
|
+
// 残余引数にrest、xxxRest以外の名称が利用されているためNG
|
|
31
32
|
const hoge = (a, b, ...props) => {
|
|
32
33
|
// any
|
|
33
34
|
}
|
|
@@ -37,15 +38,15 @@ const hoge = ({ a, b, ...props }) => {
|
|
|
37
38
|
// any
|
|
38
39
|
}
|
|
39
40
|
|
|
40
|
-
//
|
|
41
|
-
const hoge = (a,
|
|
41
|
+
// 残余引数ではない箇所で残余引数と勘違いしかねない命名がされているためNG
|
|
42
|
+
const hoge = (a, anyRest, b) => {
|
|
42
43
|
// any
|
|
43
44
|
}
|
|
44
45
|
// 引数以外の場合でも混乱するためNG
|
|
45
|
-
const
|
|
46
|
+
const hogeRest = { /* any */ }
|
|
46
47
|
|
|
47
48
|
// 残余引数内の属性を参照しているためNG
|
|
48
|
-
const
|
|
49
|
+
const ComponentA = ({ a, b, ...rest }) => {
|
|
49
50
|
...
|
|
50
51
|
|
|
51
52
|
if (rest.abc) {
|
|
@@ -54,6 +55,18 @@ const Component = ({ a, b, ...rest }) => {
|
|
|
54
55
|
|
|
55
56
|
...
|
|
56
57
|
}
|
|
58
|
+
const ComponentB = ({ a, b, ...rest }) => {
|
|
59
|
+
...
|
|
60
|
+
|
|
61
|
+
// 残余引数を構造分解することもNG
|
|
62
|
+
const { abc } = rest
|
|
63
|
+
|
|
64
|
+
if (abc) {
|
|
65
|
+
return <Children {...rest} />
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
...
|
|
69
|
+
}
|
|
57
70
|
```
|
|
58
71
|
|
|
59
72
|
## ✅ Correct
|
|
@@ -64,6 +77,9 @@ const Component = ({ a, b, ...rest }) => {
|
|
|
64
77
|
const hoge = (a, b, ...rest) => {
|
|
65
78
|
// any
|
|
66
79
|
}
|
|
80
|
+
const fuga = ({ a, b, ...anyRest }) => {
|
|
81
|
+
// any
|
|
82
|
+
}
|
|
67
83
|
|
|
68
84
|
// 残余引数ではない場合、rest以外の名称を許容
|
|
69
85
|
const hoge = (props) => {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const SCHEMA = []
|
|
2
2
|
|
|
3
|
-
const
|
|
3
|
+
const REST_REGEX = /(^r|R)est$/
|
|
4
|
+
const MEMBER_EXPRESSION_REST_REGEX = /^(r|[a-zA-Z0-9_]+R)est\./
|
|
4
5
|
const DETAIL_LINK = `
|
|
5
6
|
- 詳細: https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/best-practice-for-rest-parameters`
|
|
6
7
|
|
|
@@ -16,9 +17,9 @@ module.exports = {
|
|
|
16
17
|
const actionNotRest = (node) => {
|
|
17
18
|
context.report({
|
|
18
19
|
node,
|
|
19
|
-
message: `残余引数以外に
|
|
20
|
+
message: `残余引数以外に ${REST_REGEX} とマッチする名称を利用しないでください${DETAIL_LINK}
|
|
20
21
|
- 残余引数(rest parameters)と混同する可能性があるため別の名称に修正してください`,
|
|
21
|
-
})
|
|
22
|
+
})
|
|
22
23
|
}
|
|
23
24
|
const actionMemberExpressionName = (node) => {
|
|
24
25
|
if (node.parent.type === 'MemberExpression') {
|
|
@@ -29,20 +30,32 @@ module.exports = {
|
|
|
29
30
|
context.report({
|
|
30
31
|
node,
|
|
31
32
|
message: `残余引数内の属性を参照しないでください${DETAIL_LINK}`,
|
|
32
|
-
})
|
|
33
|
+
})
|
|
33
34
|
}
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
return {
|
|
37
|
-
[`
|
|
38
|
+
[`ObjectPattern[properties.length=1]>RestElement`]: (node) => {
|
|
38
39
|
context.report({
|
|
39
40
|
node,
|
|
40
|
-
message:
|
|
41
|
-
})
|
|
41
|
+
message: `意味のない残余引数のため、単一の引数に変更してください${DETAIL_LINK}`,
|
|
42
|
+
})
|
|
43
|
+
},
|
|
44
|
+
[`RestElement:not([argument.name=${REST_REGEX}])`]: (node) => {
|
|
45
|
+
context.report({
|
|
46
|
+
node,
|
|
47
|
+
message: `残余引数には ${REST_REGEX} とマッチする名称を指定してください${DETAIL_LINK}`,
|
|
48
|
+
})
|
|
49
|
+
},
|
|
50
|
+
[`:not(:matches(RestElement,JSXSpreadAttribute,JSXSpreadAttribute>TSAsExpression,SpreadElement,SpreadElement>TSAsExpression,MemberExpression,VariableDeclarator,ArrayExpression,CallExpression,ObjectPattern>Property,ObjectExpression>Property))>Identifier[name=${REST_REGEX}]`]: actionNotRest,
|
|
51
|
+
[`:matches(VariableDeclarator[id.name=${REST_REGEX}],ObjectPattern>Property[value.name=${REST_REGEX}],ObjectExpression>Property[key.name=${REST_REGEX}])`]: actionNotRest,
|
|
52
|
+
[`MemberExpression[object.name=${REST_REGEX}]`]: actionMemberExpressionName,
|
|
53
|
+
[`VariableDeclarator[id.type='ObjectPattern'][init.name=${REST_REGEX}]`]: (node) => {
|
|
54
|
+
context.report({
|
|
55
|
+
node,
|
|
56
|
+
message: `残余引数内の属性を参照しないでください${DETAIL_LINK}`,
|
|
57
|
+
})
|
|
42
58
|
},
|
|
43
|
-
[`:not(:matches(RestElement,JSXSpreadAttribute,JSXSpreadAttribute>TSAsExpression,SpreadElement,SpreadElement>TSAsExpression,MemberExpression,VariableDeclarator,ArrayExpression,CallExpression,ObjectPattern>Property,ObjectExpression>Property))>Identifier[name='rest']`]: actionNotRest,
|
|
44
|
-
[`:matches(VariableDeclarator[id.name='rest'],ObjectPattern>Property[value.name='rest'],ObjectExpression>Property[key.name='rest'])`]: actionNotRest,
|
|
45
|
-
[`MemberExpression[object.name='rest']`]: actionMemberExpressionName,
|
|
46
59
|
}
|
|
47
60
|
},
|
|
48
61
|
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# smarthr/best-practice-for-unnesessary-early-return
|
|
2
|
+
|
|
3
|
+
- 不必要な早期returnをチェックするルールです
|
|
4
|
+
- 不要な早期returnとは **直後に実行される処理の条件を逆転している早期return** を指します
|
|
5
|
+
|
|
6
|
+
```jsx
|
|
7
|
+
// 不要な早期returnの例
|
|
8
|
+
const anyAction = (a) => {
|
|
9
|
+
if (!a) {
|
|
10
|
+
return
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
otherAction()
|
|
14
|
+
}
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
- 上記の例の場合、実質 `otherActionを実行する条件だけが存在する` ため、実際にコードを読む際、条件を逆転させて考える余計なコストが発生します
|
|
18
|
+
- 下記の様に早期returnを利用せず書くことを推奨します
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
```jsx
|
|
22
|
+
const anyAction = (a) => {
|
|
23
|
+
if (a) {
|
|
24
|
+
otherAction()
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
- 上記のような条件に修正することで、本質的に実行したい処理の条件がわかりやすくなります
|
|
30
|
+
- このルールは以下の条件すべてに合致する場合、NGにします
|
|
31
|
+
- 早期return以降にifやelse、switch, tryがない
|
|
32
|
+
- 早期returnの条件ブロック以降に変数宣言がない
|
|
33
|
+
- 早期returnの条件ブロック以降にreturnがない
|
|
34
|
+
- 早期returnが値を返していない
|
|
35
|
+
|
|
36
|
+
## rules
|
|
37
|
+
|
|
38
|
+
```js
|
|
39
|
+
{
|
|
40
|
+
rules: {
|
|
41
|
+
'smarthr/best-practice-for-unnesessary-early-return': 'error', // 'warn', 'off'
|
|
42
|
+
},
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## ❌ Incorrect
|
|
47
|
+
|
|
48
|
+
```jsx
|
|
49
|
+
const anyAction = (a) => {
|
|
50
|
+
// otherActionの実行条件を逆転させているだけのためNG
|
|
51
|
+
if (!a) {
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
otherAction()
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
```jsx
|
|
60
|
+
const anyAction = (a, b) => {
|
|
61
|
+
// 早期returnの条件内容自体は無関係にチェックするためNG
|
|
62
|
+
if (!a || !b) {
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
otherAction()
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
```jsx
|
|
71
|
+
const anyAction = (a, b) => {
|
|
72
|
+
switch (a) {
|
|
73
|
+
...
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// 早期returnより前に条件などがあっても、条件が実質逆転しているためNG
|
|
77
|
+
if (!b) {
|
|
78
|
+
return
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
otherAction()
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
```jsx
|
|
86
|
+
const anyAction = (a, b) => {
|
|
87
|
+
if (!a) {
|
|
88
|
+
return
|
|
89
|
+
}
|
|
90
|
+
// 早期returnが分割されているため、前述の早期returnの条件とまとめるべきなのでNG
|
|
91
|
+
if (!b) {
|
|
92
|
+
return
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
otherAction()
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## ✅ Correct
|
|
100
|
+
|
|
101
|
+
```jsx
|
|
102
|
+
const anyAction = (a) => {
|
|
103
|
+
if (a) {
|
|
104
|
+
otherAction()
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
```jsx
|
|
110
|
+
// 許容する早期returnの例
|
|
111
|
+
const sample1 = (a) => {
|
|
112
|
+
if (a) {
|
|
113
|
+
// 早期returnで値を返しているため許容
|
|
114
|
+
return true
|
|
115
|
+
}
|
|
116
|
+
...
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const sample2 = (a) => {
|
|
120
|
+
if (!a) {
|
|
121
|
+
return
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// 早期return後に変数宣言しているため許容
|
|
125
|
+
const caculated = calc(a)
|
|
126
|
+
...
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const sample3 = (a) => {
|
|
130
|
+
if (!a) {
|
|
131
|
+
return
|
|
132
|
+
}
|
|
133
|
+
// 早期returnの条件以外に条件が存在するため許容
|
|
134
|
+
else if (a === any) {
|
|
135
|
+
...
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const sample4 = (a, b) => {
|
|
140
|
+
if (!a) {
|
|
141
|
+
return
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
otherAction1(a)
|
|
145
|
+
|
|
146
|
+
// この場合も別条件のため許容
|
|
147
|
+
if (b === any) {
|
|
148
|
+
return
|
|
149
|
+
}
|
|
150
|
+
...
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const sample5 = (a, b) => {
|
|
154
|
+
if (!a) {
|
|
155
|
+
return
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// 早期returnとは別のreturnが関数スコープのrootにあるため許容
|
|
159
|
+
return ...
|
|
160
|
+
}
|
|
161
|
+
```
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
const SCHEMA = []
|
|
2
|
+
|
|
3
|
+
const EARLY_RETURN_IF_STATEMENT = `:matches(ArrowFunctionExpression,FunctionExpression,FunctionDeclaration)>BlockStatement>IfStatement[alternate=null]:matches([consequent.type='ReturnStatement'],[consequent.body.length=1])>`
|
|
4
|
+
const NULL_RETURN_STATEMENT = 'ReturnStatement[argument=null]'
|
|
5
|
+
|
|
6
|
+
const FUNCTION_REGEX = /^(Arrow)?Function(Expression|Declaration)$/
|
|
7
|
+
|
|
8
|
+
const searchFunction = (node) => FUNCTION_REGEX.test(node.type) ? node : searchFunction(node.parent)
|
|
9
|
+
|
|
10
|
+
const getEarlyReturn = (b) => {
|
|
11
|
+
let ret = null
|
|
12
|
+
switch (b.consequent.type) {
|
|
13
|
+
case 'ReturnStatement':
|
|
14
|
+
ret = b.consequent
|
|
15
|
+
|
|
16
|
+
break
|
|
17
|
+
case 'BlockStatement':
|
|
18
|
+
if (b.consequent.body.length === 1 && b.consequent.body[0].type === 'ReturnStatement') {
|
|
19
|
+
ret = b.consequent.body[0]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
break
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return ret?.argument === null ? ret : null
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const DETAIL_LINK = `
|
|
29
|
+
- 詳細: https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/best-practice-for-unnesessary-early-return`
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @type {import('@typescript-eslint/utils').TSESLint.RuleModule<''>}
|
|
33
|
+
*/
|
|
34
|
+
module.exports = {
|
|
35
|
+
meta: {
|
|
36
|
+
type: 'suggestion',
|
|
37
|
+
schema: SCHEMA,
|
|
38
|
+
},
|
|
39
|
+
create(context) {
|
|
40
|
+
const action = (node) => {
|
|
41
|
+
const fn = searchFunction(node).body.body
|
|
42
|
+
// 0: 最初の早期returnを検索中
|
|
43
|
+
// 1: 最初の早期returnを発見、その後も連続して早期returnが存在する場合
|
|
44
|
+
// 2: 1の後、早期returnの連続が途切れた場合
|
|
45
|
+
let flg = 0
|
|
46
|
+
|
|
47
|
+
for (let i = 0; i < fn.length; i++) {
|
|
48
|
+
const b = fn[i]
|
|
49
|
+
|
|
50
|
+
if (!flg) {
|
|
51
|
+
if (b.type === 'IfStatement' && getEarlyReturn(b)) {
|
|
52
|
+
flg = 1
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
continue
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
switch (b.type) {
|
|
59
|
+
case 'VariableDeclaration':
|
|
60
|
+
case 'ReturnStatement':
|
|
61
|
+
case 'SwitchStatement':
|
|
62
|
+
case 'TryStatement':
|
|
63
|
+
return
|
|
64
|
+
case 'IfStatement':
|
|
65
|
+
if (flg === 1 && node === getEarlyReturn(b)) {
|
|
66
|
+
context.report({
|
|
67
|
+
node,
|
|
68
|
+
message: `早期returnのifが分割されています${DETAIL_LINK}
|
|
69
|
+
- 一つのifにまとめるよう、条件を調整してください`,
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
flg = 2
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
context.report({
|
|
79
|
+
node,
|
|
80
|
+
message: `後続の処理の逆の条件の早期returnのため修正してください。${DETAIL_LINK}
|
|
81
|
+
- 本質的に行いたい処理の条件とは逆がifに記述されているため、ロジックを確認する際条件を逆転させて考える余計な手間が発生しています
|
|
82
|
+
- 条件を逆転させたうえで後続の処理をifの内部に移動してください`,
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
[`${EARLY_RETURN_IF_STATEMENT}BlockStatement>${NULL_RETURN_STATEMENT}`]: action,
|
|
88
|
+
[`${EARLY_RETURN_IF_STATEMENT}${NULL_RETURN_STATEMENT}`]: action,
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
}
|
|
92
|
+
module.exports.schema = SCHEMA
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
const rule = require('../rules/best-practice-for-optional-chaining')
|
|
2
|
+
const RuleTester = require('eslint').RuleTester
|
|
3
|
+
|
|
4
|
+
const ruleTester = new RuleTester({
|
|
5
|
+
languageOptions: {
|
|
6
|
+
parserOptions: {
|
|
7
|
+
ecmaFeatures: {
|
|
8
|
+
jsx: true,
|
|
9
|
+
},
|
|
10
|
+
},
|
|
11
|
+
},
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
const ERROR = `optional chaining(xxx?.yyyy記法)を利用してください
|
|
15
|
+
- 詳細: https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/best-practice-for-optional-chaining`
|
|
16
|
+
|
|
17
|
+
ruleTester.run('best-practice-for-optional-chaining', rule, {
|
|
18
|
+
valid: [
|
|
19
|
+
{ code: `action?.()` },
|
|
20
|
+
{ code: `obj.action?.(hoge, fuga)` },
|
|
21
|
+
{ code: `if (any) { action() }` },
|
|
22
|
+
{ code: `if (action && any) { action() }` },
|
|
23
|
+
{ code: `if (action) { action() } else { any() }` },
|
|
24
|
+
{ code: `if (obj.hoge) obj.hoge.fuga.action()` },
|
|
25
|
+
{ code: `() => { if (action) return action() }` },
|
|
26
|
+
{ code: `if (any) {} else if (action) { action() }` },
|
|
27
|
+
],
|
|
28
|
+
invalid: [
|
|
29
|
+
{ code: `if (action) action()`, errors: [{ message: ERROR }], output: 'action?.()' },
|
|
30
|
+
{ code: `if (obj.action) { obj.action(hoge, fuga) }`, errors: [{ message: ERROR }], output: 'obj.action?.(hoge, fuga)' },
|
|
31
|
+
{ code: `if (obj.hoge.fuga.action) obj.hoge.fuga.action()`, errors: [{ message: ERROR }], output: 'obj.hoge.fuga.action?.()' },
|
|
32
|
+
{ code: `
|
|
33
|
+
if (obj.hoge.fuga.action) {
|
|
34
|
+
obj.hoge.fuga.action(
|
|
35
|
+
a,
|
|
36
|
+
b,
|
|
37
|
+
c
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
`, errors: [{ message: ERROR }], output: `
|
|
41
|
+
obj.hoge.fuga.action?.(
|
|
42
|
+
a,
|
|
43
|
+
b,
|
|
44
|
+
c
|
|
45
|
+
)
|
|
46
|
+
` },
|
|
47
|
+
],
|
|
48
|
+
})
|
|
@@ -13,37 +13,40 @@ const ruleTester = new RuleTester({
|
|
|
13
13
|
|
|
14
14
|
const DETAIL_LINK = `
|
|
15
15
|
- 詳細: https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/best-practice-for-rest-parameters`
|
|
16
|
-
const ERROR_REST_NAME = `残余引数には
|
|
17
|
-
const ERROR_NOT_REST_NAME = `残余引数以外に
|
|
16
|
+
const ERROR_REST_NAME = `残余引数には /(^r|R)est$/ とマッチする名称を指定してください${DETAIL_LINK}`
|
|
17
|
+
const ERROR_NOT_REST_NAME = `残余引数以外に /(^r|R)est$/ とマッチする名称を利用しないでください${DETAIL_LINK}
|
|
18
18
|
- 残余引数(rest parameters)と混同する可能性があるため別の名称に修正してください`
|
|
19
19
|
const ERROR_REST_CHILD_REF = `残余引数内の属性を参照しないでください${DETAIL_LINK}`
|
|
20
20
|
|
|
21
21
|
ruleTester.run('best-practice-for-rest-parameters', rule, {
|
|
22
22
|
valid: [
|
|
23
|
+
{ code: `const hoge = (...rest) => {}` },
|
|
23
24
|
{ code: `const hoge = (a, b, ...rest) => {}` },
|
|
24
|
-
{ code: `const hoge = ({ a, b, ...
|
|
25
|
+
{ code: `const hoge = ({ a, b, ...anyRest }) => {}` },
|
|
25
26
|
{ code: `const hoge = (props) => {}` },
|
|
26
27
|
{ code: `const props = {}` },
|
|
27
|
-
{ code: `const hogeRest = {}` },
|
|
28
28
|
{ code: `const hoge = { fuga: rest }` },
|
|
29
|
-
{ code: `const hoge =
|
|
29
|
+
{ code: `const hoge = hogeRest` },
|
|
30
30
|
{ code: `const hoge = hoge.rest.fuga` },
|
|
31
|
-
{ code: `const hoge = { ...
|
|
31
|
+
{ code: `const hoge = { ...anyRest }` },
|
|
32
32
|
{ code: `const hoge = [rest]` },
|
|
33
|
-
{ code: `hoge(
|
|
33
|
+
{ code: `hoge(fugaRest)` },
|
|
34
34
|
{ code: `<Any {...rest} />` },
|
|
35
35
|
],
|
|
36
36
|
invalid: [
|
|
37
|
-
{ code: `const hoge = (
|
|
38
|
-
{ code: `const hoge = (
|
|
37
|
+
{ code: `const hoge = ({ ...rest }) => {}`, errors: [ { message: `意味のない残余引数のため、単一の引数に変更してください${DETAIL_LINK}` } ] },
|
|
38
|
+
{ code: `const hoge = (a, b, ...any) => {}`, errors: [ { message: ERROR_REST_NAME } ] },
|
|
39
|
+
{ code: `const hoge = ({ a, b, ...restHoge }) => {}`, errors: [ { message: ERROR_REST_NAME } ] },
|
|
39
40
|
{ code: `const hoge = (rest) => {}`, errors: [ { message: ERROR_NOT_REST_NAME } ] },
|
|
41
|
+
{ code: `const hogeRest = {}`, errors: [ { message: ERROR_NOT_REST_NAME } ] },
|
|
40
42
|
{ code: `const hoge = (a, b, rest) => {}`, errors: [ { message: ERROR_NOT_REST_NAME } ] },
|
|
41
|
-
{ code: `const hoge = ({ a:
|
|
43
|
+
{ code: `const hoge = ({ a: anyRest, b }) => {}`, errors: [ { message: ERROR_NOT_REST_NAME } ] },
|
|
42
44
|
{ code: `const rest = {}`, errors: [ { message: ERROR_NOT_REST_NAME } ] },
|
|
43
|
-
{ code: `const hoge = {
|
|
45
|
+
{ code: `const hoge = { hogeRest }`, errors: [ { message: ERROR_NOT_REST_NAME } ] },
|
|
44
46
|
{ code: `const hoge = { rest: fuga }`, errors: [ { message: ERROR_NOT_REST_NAME } ] },
|
|
45
47
|
{ code: `const hoge = rest.hoge`, errors: [ { message: ERROR_REST_CHILD_REF } ] },
|
|
46
|
-
{ code: `const hoge =
|
|
48
|
+
{ code: `const hoge = anyRest.hoge.fuga`, errors: [ { message: ERROR_REST_CHILD_REF } ] },
|
|
49
|
+
{ code: `const { any } = rest`, errors: [ { message: ERROR_REST_CHILD_REF } ] },
|
|
47
50
|
]
|
|
48
51
|
})
|
|
49
52
|
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
const rule = require('../rules/best-practice-for-unnesessary-early-return')
|
|
2
|
+
const RuleTester = require('eslint').RuleTester
|
|
3
|
+
|
|
4
|
+
const ruleTester = new RuleTester({
|
|
5
|
+
languageOptions: {
|
|
6
|
+
parserOptions: {
|
|
7
|
+
ecmaFeatures: {
|
|
8
|
+
jsx: true,
|
|
9
|
+
},
|
|
10
|
+
},
|
|
11
|
+
},
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
const UNNESESSARY_EARLY_RETURN_ERROR = `後続の処理の逆の条件の早期returnのため修正してください。
|
|
15
|
+
- 詳細: https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/best-practice-for-unnesessary-early-return
|
|
16
|
+
- 本質的に行いたい処理の条件とは逆がifに記述されているため、ロジックを確認する際条件を逆転させて考える余計な手間が発生しています
|
|
17
|
+
- 条件を逆転させたうえで後続の処理をifの内部に移動してください`
|
|
18
|
+
const SPLIT_EARLY_RETURN_ERROR = `早期returnのifが分割されています
|
|
19
|
+
- 詳細: https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/best-practice-for-unnesessary-early-return
|
|
20
|
+
- 一つのifにまとめるよう、条件を調整してください`
|
|
21
|
+
|
|
22
|
+
ruleTester.run('best-practice-for-unnesessary-early-return', rule, {
|
|
23
|
+
valid: [
|
|
24
|
+
{ code: `
|
|
25
|
+
const anyAction = (a) => {
|
|
26
|
+
if (a) {
|
|
27
|
+
otherAction()
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
` },
|
|
31
|
+
{ code: `
|
|
32
|
+
const sample2 = (a) => {
|
|
33
|
+
if (!a) return
|
|
34
|
+
|
|
35
|
+
const caculated = calc(a)
|
|
36
|
+
}
|
|
37
|
+
` },
|
|
38
|
+
{ code: `
|
|
39
|
+
const anyAction = (a) => {
|
|
40
|
+
if (!a) {
|
|
41
|
+
return
|
|
42
|
+
} else {
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
` },
|
|
46
|
+
{ code: `
|
|
47
|
+
const anyAction = (a) => {
|
|
48
|
+
if (!a) {
|
|
49
|
+
return
|
|
50
|
+
} else if (any) {
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
` },
|
|
54
|
+
{ code: `
|
|
55
|
+
const sample4 = (a, b) => {
|
|
56
|
+
if (!a) {
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
otherAction1(a)
|
|
61
|
+
|
|
62
|
+
if (b === any) {
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
otherAction2(b)
|
|
67
|
+
}
|
|
68
|
+
` },
|
|
69
|
+
{ code: `
|
|
70
|
+
const sample4 = (a, b) => {
|
|
71
|
+
if (!a) {
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
switch(a) {
|
|
76
|
+
case 'hoge':
|
|
77
|
+
break
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
` },
|
|
81
|
+
{ code: `
|
|
82
|
+
const anyAction = (a) => {
|
|
83
|
+
if (!a) {
|
|
84
|
+
otherAction1()
|
|
85
|
+
return
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
otherAction2()
|
|
89
|
+
}
|
|
90
|
+
` },
|
|
91
|
+
{ code: `
|
|
92
|
+
const anyAction = (a) => {
|
|
93
|
+
if (!a) {
|
|
94
|
+
return
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return otherAction2()
|
|
98
|
+
}
|
|
99
|
+
` },
|
|
100
|
+
{ code: `
|
|
101
|
+
const anyAction = (a) => {
|
|
102
|
+
if (!a) {
|
|
103
|
+
return
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
otherAction()
|
|
108
|
+
} catch {
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
` },
|
|
112
|
+
],
|
|
113
|
+
invalid: [
|
|
114
|
+
{ code: `
|
|
115
|
+
const anyAction = (a) => {
|
|
116
|
+
if (!a) {
|
|
117
|
+
return
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
otherAction()
|
|
121
|
+
}
|
|
122
|
+
`, errors: [ { message: UNNESESSARY_EARLY_RETURN_ERROR } ] },
|
|
123
|
+
{ code: `
|
|
124
|
+
const anyAction = (a) => {
|
|
125
|
+
const hoge = any
|
|
126
|
+
|
|
127
|
+
if (!a) {
|
|
128
|
+
return
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
otherAction()
|
|
132
|
+
}
|
|
133
|
+
`, errors: [ { message: UNNESESSARY_EARLY_RETURN_ERROR } ] },
|
|
134
|
+
{ code: `
|
|
135
|
+
const anyAction = (a, b) => {
|
|
136
|
+
if (!a || !b) {
|
|
137
|
+
return
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
otherAction()
|
|
141
|
+
}
|
|
142
|
+
`, errors: [ { message: UNNESESSARY_EARLY_RETURN_ERROR } ] },
|
|
143
|
+
{ code: `
|
|
144
|
+
const anyAction = (a, b) => {
|
|
145
|
+
switch (a) {
|
|
146
|
+
case 'hoge':
|
|
147
|
+
break
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (!b) {
|
|
151
|
+
return
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
otherAction()
|
|
155
|
+
}
|
|
156
|
+
`, errors: [ { message: UNNESESSARY_EARLY_RETURN_ERROR } ] },
|
|
157
|
+
{ code: `
|
|
158
|
+
const anyAction = (a, b) => {
|
|
159
|
+
// 早期returnより前に非早期returnのif,switchがある場合
|
|
160
|
+
if (a && b) {
|
|
161
|
+
return 'hoge'
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// 早期return後にswitchがある場合強制終了されるが
|
|
165
|
+
// このswitchは早期return前のため後続でエラーになる
|
|
166
|
+
switch (a) {
|
|
167
|
+
case 'hoge':
|
|
168
|
+
break
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (!b) {
|
|
172
|
+
return
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
otherAction()
|
|
176
|
+
}
|
|
177
|
+
`, errors: [ { message: UNNESESSARY_EARLY_RETURN_ERROR } ] },
|
|
178
|
+
{ code: `
|
|
179
|
+
const anyAction = (a, b) => {
|
|
180
|
+
if (!a) return
|
|
181
|
+
if (!b) {
|
|
182
|
+
return
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
otherAction()
|
|
186
|
+
}
|
|
187
|
+
`, errors: [ SPLIT_EARLY_RETURN_ERROR ] },
|
|
188
|
+
]
|
|
189
|
+
})
|