eslint-plugin-smarthr 1.5.1 → 1.7.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 +4 -1
- package/package.json +4 -4
- package/rules/a11y-help-link-with-support-href/README.md +34 -0
- package/rules/a11y-help-link-with-support-href/index.js +82 -0
- package/rules/best-practice-for-async-current-target/README.md +77 -0
- package/rules/best-practice-for-async-current-target/index.js +88 -0
- package/rules/best-practice-for-nested-attributes-array-index/README.md +91 -0
- package/rules/best-practice-for-nested-attributes-array-index/index.js +35 -0
- package/rules/component-name/index.js +1 -0
- package/test/a11y-help-link-with-support-href.js +32 -0
- package/test/best-practice-for-async-current-target.js +55 -0
- package/test/best-practice-for-nested-attributes-array-index.js +31 -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
|
+
## [1.7.0](https://github.com/kufu/tamatebako/compare/eslint-plugin-smarthr-v1.6.0...eslint-plugin-smarthr-v1.7.0) (2025-06-02)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* a11y-help-link-with-support-href を追加 ([#646](https://github.com/kufu/tamatebako/issues/646)) ([662ca33](https://github.com/kufu/tamatebako/commit/662ca33120acac3fb9acdc7331d187fba9be86ef))
|
|
11
|
+
* best-practice-for-nested-attributes-array-index ruleを追加 ([#648](https://github.com/kufu/tamatebako/issues/648)) ([8774d84](https://github.com/kufu/tamatebako/commit/8774d84f46fcaff9360aa76cb3773f644d6d5529))
|
|
12
|
+
|
|
13
|
+
## [1.6.0](https://github.com/kufu/tamatebako/compare/eslint-plugin-smarthr-v1.5.1...eslint-plugin-smarthr-v1.6.0) (2025-05-20)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
### Features
|
|
17
|
+
|
|
18
|
+
* best-practice-for-async-current-targetルールを追加 ([#625](https://github.com/kufu/tamatebako/issues/625)) ([8cc72b4](https://github.com/kufu/tamatebako/commit/8cc72b4d4d552ed1efa9cfe7fbec04de0fcc369e))
|
|
19
|
+
|
|
5
20
|
## [1.5.1](https://github.com/kufu/tamatebako/compare/eslint-plugin-smarthr-v1.5.0...eslint-plugin-smarthr-v1.5.1) (2025-05-12)
|
|
6
21
|
|
|
7
22
|
|
package/README.md
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
- [a11y-delegate-element-has-role-presentation](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/a11y-delegate-element-has-role-presentation)
|
|
6
6
|
- [a11y-form-control-in-form](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/a11y-form-control-in-form)
|
|
7
7
|
- [a11y-heading-in-sectioning-content](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/a11y-heading-in-sectioning-content)
|
|
8
|
+
- [a11y-help-link-with-support-href](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/a11y-help-link-with-support-href)
|
|
8
9
|
- [a11y-image-has-alt-attribute](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/a11y-image-has-alt-attribute)
|
|
9
10
|
- [a11y-input-has-name-attribute](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/a11y-input-has-name-attribute)
|
|
10
11
|
- [a11y-input-in-form-control](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/a11y-input-in-form-control)
|
|
@@ -16,13 +17,15 @@
|
|
|
16
17
|
- [a11y-replace-unreadable-symbol](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/a11y-replace-unreadable-symbol)
|
|
17
18
|
- [a11y-required-layout-as-attribute](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/a11y-required-layout-as-attribute)
|
|
18
19
|
- [a11y-trigger-has-button](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/a11y-trigger-has-button)
|
|
20
|
+
- [best-practice-for-async-current-target](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/best-practice-for-async-current-target)
|
|
19
21
|
- [best-practice-for-button-element](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/best-practice-for-button-element)
|
|
20
22
|
- [best-practice-for-data-test-attribute](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/best-practice-for-data-test-attribute)
|
|
21
23
|
- [best-practice-for-date](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/best-practice-for-date)
|
|
22
24
|
- [best-practice-for-layouts](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/best-practice-for-layouts)
|
|
25
|
+
- [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)
|
|
23
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)
|
|
24
|
-
- [best-practice-for-tailwind-variants](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/best-practice-for-tailwind-variants)
|
|
25
27
|
- [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
|
+
- [best-practice-for-tailwind-variants](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/best-practice-for-tailwind-variants)
|
|
26
29
|
- [design-system-guideline-prohibit-double-icons](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/design-system-guideline-prohibit-double-icons)
|
|
27
30
|
- [format-import-path](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/format-import-path)
|
|
28
31
|
- [format-translate-component](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/format-translate-component)
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-smarthr",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0",
|
|
4
4
|
"author": "SmartHR",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "A sharable ESLint plugin for SmartHR",
|
|
7
7
|
"main": "index.js",
|
|
8
8
|
"engines": {
|
|
9
|
-
"node": ">=22.
|
|
9
|
+
"node": ">=22.16.0"
|
|
10
10
|
},
|
|
11
11
|
"scripts": {
|
|
12
12
|
"test": "jest"
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"json5": "^2.2.3"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
|
-
"typescript-eslint": "^8.
|
|
29
|
+
"typescript-eslint": "^8.33.0"
|
|
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": "12babfba8f3ff1aa9bc9d159136ef15ffce24fe0"
|
|
41
41
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# smarthr/a11y-help-link-with-support-href
|
|
2
|
+
|
|
3
|
+
- [ヘルプページ](https://support.smarthr.jp/) へのリンクはsmarthr-ui/HelpLinkを使うことを促すルールです
|
|
4
|
+
- ヘルプページへのリンクは、通常のテキストリンクと設定するべき属性が異なるため、専用のコンポーネントが用意されています
|
|
5
|
+
|
|
6
|
+
## rules
|
|
7
|
+
|
|
8
|
+
```js
|
|
9
|
+
{
|
|
10
|
+
rules: {
|
|
11
|
+
'smarthr/a11y-help-link-with-support-href': [
|
|
12
|
+
'error', // 'warn', 'off'
|
|
13
|
+
]
|
|
14
|
+
},
|
|
15
|
+
}
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## ❌ Incorrect
|
|
19
|
+
|
|
20
|
+
```jsx
|
|
21
|
+
<TextLink href="https://support.smarthr.jp/xxxxx">any</TextLink>
|
|
22
|
+
<a href={`//support.smarthr.jp/${hoge}`}>any</a>
|
|
23
|
+
<AnchorButton href={supportURL}>any</a>
|
|
24
|
+
<AnyAnchor href={path.support.xxxx.yyyy} />
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## ✅ Correct
|
|
28
|
+
|
|
29
|
+
```jsx
|
|
30
|
+
<HelpLink href="https://support.smarthr.jp/xxxxx">any</HelpLink>
|
|
31
|
+
<HelpLink href={`//support.smarthr.jp/${hoge}`}>any</HelpLink>
|
|
32
|
+
<HelpLink href={supportURL}>any</HelpLink>
|
|
33
|
+
<HogeHelpLink href={path.support.xxxx.yyyy} />
|
|
34
|
+
```
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
const SCHEMA = []
|
|
2
|
+
|
|
3
|
+
const SUPPORT_URL_PREFIX_REGEX = /(\/|\.)support\./
|
|
4
|
+
const SUPPORT_IDENTIFIER_REGEX = /(S|(^|_)s)upport(.*)(H(ref|REF)|U(rl|URL))$/
|
|
5
|
+
const PATH_OBJ_REGEX = /^((p|P)ath|PATH)\./
|
|
6
|
+
const SUPPORT_PATH_MEMBER_REGEX = /\.support\./
|
|
7
|
+
|
|
8
|
+
const ANCHER_LIKE_REGEX = /(Anchor(Button)?|Link|^a)$/
|
|
9
|
+
const HELP_LINK_REGEX = /HelpLink$/
|
|
10
|
+
|
|
11
|
+
const checkSupportURL = (node, context) => {
|
|
12
|
+
switch (node.type) {
|
|
13
|
+
case 'Literal': {
|
|
14
|
+
return SUPPORT_URL_PREFIX_REGEX.test(node.value)
|
|
15
|
+
}
|
|
16
|
+
case 'JSXExpressionContainer': {
|
|
17
|
+
return checkSupportURLExpression(node.expression, context)
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return false
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const checkSupportURLExpression = (node, context) => {
|
|
25
|
+
switch (node.type) {
|
|
26
|
+
case 'Literal': {
|
|
27
|
+
return SUPPORT_URL_PREFIX_REGEX.test(node.value)
|
|
28
|
+
}
|
|
29
|
+
case 'Identifier': {
|
|
30
|
+
return SUPPORT_IDENTIFIER_REGEX.test(node.name)
|
|
31
|
+
}
|
|
32
|
+
case 'CallExpression':
|
|
33
|
+
case 'ChainExpression':
|
|
34
|
+
case 'MemberExpression': {
|
|
35
|
+
const fullName = context.sourceCode.getText(node)
|
|
36
|
+
|
|
37
|
+
return PATH_OBJ_REGEX.test(fullName) && SUPPORT_PATH_MEMBER_REGEX.test(fullName)
|
|
38
|
+
}
|
|
39
|
+
case 'TemplateLiteral': {
|
|
40
|
+
const someChecker = (e) => checkSupportURLExpression(e, context)
|
|
41
|
+
|
|
42
|
+
return node.expressions.some(someChecker) || node.quasis.some(someChecker)
|
|
43
|
+
}
|
|
44
|
+
case 'TemplateElement': {
|
|
45
|
+
return node.value.raw && SUPPORT_URL_PREFIX_REGEX.test(node.value.raw)
|
|
46
|
+
}
|
|
47
|
+
case 'TSAsExpression': {
|
|
48
|
+
return checkSupportURLExpression(node.expression, context)
|
|
49
|
+
}
|
|
50
|
+
case 'LogicalExpression': {
|
|
51
|
+
return checkSupportURLExpression(node.left, context) || checkSupportURLExpression(node.right, context)
|
|
52
|
+
}
|
|
53
|
+
case 'ConditionalExpression': {
|
|
54
|
+
return checkSupportURLExpression(node.alternate, context) || checkSupportURLExpression(node.consequent, context)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return false
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* @type {import('@typescript-eslint/utils').TSESLint.RuleModule<''>}
|
|
63
|
+
*/
|
|
64
|
+
module.exports = {
|
|
65
|
+
meta: {
|
|
66
|
+
type: 'problem',
|
|
67
|
+
schema: SCHEMA,
|
|
68
|
+
},
|
|
69
|
+
create(context) {
|
|
70
|
+
return {
|
|
71
|
+
JSXAttribute: (node) => {
|
|
72
|
+
if (node.name.name === 'href' && ANCHER_LIKE_REGEX.test(node.parent.name.name) && !HELP_LINK_REGEX.test(node.parent.name.name) && checkSupportURL(node.value, context)) {
|
|
73
|
+
context.report({
|
|
74
|
+
node,
|
|
75
|
+
message: `ヘルプページ用のリンクは smarthr-ui/HelpLink コンポーネントを利用してください`,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
}
|
|
82
|
+
module.exports.schema = SCHEMA
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# smarthr/best-practice-for-async-current-target
|
|
2
|
+
|
|
3
|
+
- jsのイベントのcurrentTargetの参照するタイミングをチェックするルールです
|
|
4
|
+
- currentTarget属性はイベントの実行中のみ参照可能な値であり、それ以外のタイミングで参照するとエラーになる可能性があります
|
|
5
|
+
- https://developer.mozilla.org/ja/docs/Web/API/Event/currentTarget
|
|
6
|
+
- イベントハンドラーの先頭でcurrentTarget関連の参照を変数に格納することを推奨します
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
## rules
|
|
10
|
+
|
|
11
|
+
```js
|
|
12
|
+
{
|
|
13
|
+
rules: {
|
|
14
|
+
'smarthr/best-practice-for-async-current-target': 'error', // 'warn', 'off'
|
|
15
|
+
},
|
|
16
|
+
}
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## ❌ Incorrect
|
|
20
|
+
|
|
21
|
+
```jsx
|
|
22
|
+
// async-awaitにより、イベント処理中ではなくなっているため、currentTargetがnullの可能性がある
|
|
23
|
+
const onChange = async (e) => {
|
|
24
|
+
await anyAction()
|
|
25
|
+
|
|
26
|
+
const value = e.currentTarget.value
|
|
27
|
+
...
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
```jsx
|
|
32
|
+
// setItemはReactのuseStateのset関数
|
|
33
|
+
// useStateのset関数は非同期のためcurrentTargetがnullの可能性がある
|
|
34
|
+
const onSelect = (e) => {
|
|
35
|
+
setItem((item) => ({ ...item, value : e.currentTarget.value }))
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
```jsx
|
|
40
|
+
// 処理が非同期の可能性はあるため、イベントハンドラ用関数のスコープ直下以外から参照する場合はエラーになります
|
|
41
|
+
const onInput = (e) => {
|
|
42
|
+
anyAction(() => {
|
|
43
|
+
const currentTarget = e.currentTarget
|
|
44
|
+
...
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## ✅ Correct
|
|
50
|
+
|
|
51
|
+
```jsx
|
|
52
|
+
const onChange = async (e) => {
|
|
53
|
+
const value = e.currentTarget.value
|
|
54
|
+
|
|
55
|
+
await anyAction()
|
|
56
|
+
...
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
```jsx
|
|
61
|
+
const onSelect = (e) => {
|
|
62
|
+
const value = e.currentTarget.value
|
|
63
|
+
|
|
64
|
+
setItem((item) => ({ ...item, value }))
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
```jsx
|
|
69
|
+
const onInput = (e) => {
|
|
70
|
+
const currentTarget = e.currentTarget
|
|
71
|
+
|
|
72
|
+
anyAction(() => {
|
|
73
|
+
const value = currentTarget.value
|
|
74
|
+
...
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
```
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
const SCHEMA = []
|
|
2
|
+
|
|
3
|
+
const FUNCTION_EXPRESSION_REGEX = /FunctionExpression$/
|
|
4
|
+
const CURRENT_TARGET_AFTER_AWAIT_REGEX = /(\s|\(|;|^)await\s.+\.currentTarget(\.|;|\?|\s|\)|$)/
|
|
5
|
+
const NL_REGEX = /\n/g
|
|
6
|
+
|
|
7
|
+
const checkFunctionTopVariable = (node, eventObjectName, getSourceCodeText) => {
|
|
8
|
+
if (FUNCTION_EXPRESSION_REGEX.test(node.type)) {
|
|
9
|
+
if (node.params.find((p) => p.name === eventObjectName)) {
|
|
10
|
+
// HINT: currentTargetの参照より前にawait宣言がある場合はエラー
|
|
11
|
+
return CURRENT_TARGET_AFTER_AWAIT_REGEX.test(getSourceCodeText(node.body).replace(NL_REGEX, ';')) ? 1 : 0
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return 2
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const nextNode = node.parent
|
|
18
|
+
|
|
19
|
+
// HINT: 0の場合はrootまで検索して見つからない場合となる
|
|
20
|
+
// パターンとしてはe.currentTargetがイベントハンドラ外で定義されている場合があり得る
|
|
21
|
+
return nextNode ? checkFunctionTopVariable(nextNode, eventObjectName, getSourceCodeText) : 0
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @type {import('@typescript-eslint/utils').TSESLint.RuleModule<''>}
|
|
26
|
+
*/
|
|
27
|
+
module.exports = {
|
|
28
|
+
meta: {
|
|
29
|
+
type: 'problem',
|
|
30
|
+
schema: SCHEMA,
|
|
31
|
+
},
|
|
32
|
+
create(context) {
|
|
33
|
+
const getSourceCodeText = (node) => context.sourceCode.getText(node)
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
MemberExpression: (node) => {
|
|
37
|
+
if (node.property && node.property.name === 'currentTarget') {
|
|
38
|
+
const eventObjectName = node.object.name
|
|
39
|
+
|
|
40
|
+
if (eventObjectName) {
|
|
41
|
+
switch (checkFunctionTopVariable(node.parent, eventObjectName, getSourceCodeText)) {
|
|
42
|
+
case 1:
|
|
43
|
+
context.report({
|
|
44
|
+
node,
|
|
45
|
+
message: `currentTargetはイベント処理中以外に参照するとnullになる場合があります。awaitの宣言より前にcurrentTarget、もしくはcurrentTarget以下の属性を含む値を変数として宣言してください
|
|
46
|
+
- 参考: https://developer.mozilla.org/ja/docs/Web/API/Event/currentTarget
|
|
47
|
+
- NG例:
|
|
48
|
+
const onChange = async (e) => {
|
|
49
|
+
await hoge()
|
|
50
|
+
fuga(e.currentTarget.value)
|
|
51
|
+
}
|
|
52
|
+
- 修正例:
|
|
53
|
+
const onChange = async (e) => {
|
|
54
|
+
const value = e.currentTarget.value
|
|
55
|
+
await hoge()
|
|
56
|
+
fuga(value)
|
|
57
|
+
}`,
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
break
|
|
61
|
+
case 2:
|
|
62
|
+
context.report({
|
|
63
|
+
node,
|
|
64
|
+
message: `currentTargetはイベント処理中以外に参照するとnullになる場合があります。イベントハンドラ用関数のスコープ直下でcurrentTarget、もしくはcurrentTarget以下の属性を含む値を変数として宣言してください
|
|
65
|
+
- 参考: https://developer.mozilla.org/ja/docs/Web/API/Event/currentTarget
|
|
66
|
+
- React/useStateのsetterは第一引数に関数を渡すと非同期処理になるためこの問題が起きる可能性があります
|
|
67
|
+
- イベントハンドラ内で関数を定義すると参照タイミングがずれる可能性があるため、イベントハンドラ直下のスコープ内にcurrentTarget関連の参照を変数に残すことをオススメします
|
|
68
|
+
- NG例:
|
|
69
|
+
const onSelect = (e) => {
|
|
70
|
+
setItem((current) => ({ ...current, value: e.currentTarget.value }))
|
|
71
|
+
}
|
|
72
|
+
- 修正例:
|
|
73
|
+
const onSelect = (e) => {
|
|
74
|
+
const value = e.currentTarget.value
|
|
75
|
+
setItem((current) => ({ ...current, value }))
|
|
76
|
+
}`,
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
break
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
}
|
|
88
|
+
module.exports.schema = SCHEMA
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# smarthr/best-practice-for-nested-attributes-array-index
|
|
2
|
+
|
|
3
|
+
- 入力要素のname属性で、配列に当たる部分の連番を指定しない場合(例: a[xxx][][yyy])、配列内アイテムの属性が意図せず入れ替わってしまう場合がありえるため、常にindexを設定してすることを促すルールです
|
|
4
|
+
- 前述例のyyyに当たる値が配列内の別アイテムに紐づいてしまう場合があります
|
|
5
|
+
|
|
6
|
+
## indexを設定しない場合に起こり得る問題の詳細
|
|
7
|
+
|
|
8
|
+
配列に当たる部分の連番を指定しない場合(例: a[xxx][][yyy])、配列のアイテムの値はどこからどこまでが1つ目、ここから2つ目... というように区切る処理が自動的に行われます。
|
|
9
|
+
これらの区切りは `全く同じnameが出てきたら区切る` という処理が行われます。
|
|
10
|
+
|
|
11
|
+
```js
|
|
12
|
+
a[xxx][][id]='1'
|
|
13
|
+
a[xxx][][value]='hoge'
|
|
14
|
+
a[xxx][][id]='2' // `a[xxx][][id]`が被ったのでここで別オブジェクトになる
|
|
15
|
+
a[xxx][][value]='fuga'
|
|
16
|
+
|
|
17
|
+
/* 送信される値の概念モデル
|
|
18
|
+
a: {
|
|
19
|
+
xxx: [
|
|
20
|
+
{ id: 1, value: 'hoge' },
|
|
21
|
+
{ id: 2, value: 'fuga' },
|
|
22
|
+
]
|
|
23
|
+
}
|
|
24
|
+
*/
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
この挙動は、例えば以下のようなパターンで問題になります。
|
|
28
|
+
|
|
29
|
+
```js
|
|
30
|
+
// 問題あるパターン
|
|
31
|
+
// 新規作成でa[xxx][][id]=undefinedなので非表示にしたと仮定
|
|
32
|
+
a[xxx][][value]='hoge'
|
|
33
|
+
a[xxx][][id]='2' // 本来↓のvalueと紐づくべき値が↑に紐づいてしまう
|
|
34
|
+
a[xxx][][value]='fuga' // `a[xxx][][value]`が被ったのでここで別オブジェクトになる
|
|
35
|
+
|
|
36
|
+
/* 送信される値の概念モデル
|
|
37
|
+
a: {
|
|
38
|
+
xxx: [
|
|
39
|
+
{ id: 2, value: 'hoge' },
|
|
40
|
+
{ id: nil, value: 'fuga' }, // fugaだったもののidがhogeに紐づいてしまう
|
|
41
|
+
]
|
|
42
|
+
}
|
|
43
|
+
*/
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
この問題は正しくnameにindexを含めることで回避できます
|
|
47
|
+
|
|
48
|
+
```js
|
|
49
|
+
a[xxx][0][value]='hoge'
|
|
50
|
+
a[xxx][1][id]='2'
|
|
51
|
+
a[xxx][1][value]='fuga'
|
|
52
|
+
|
|
53
|
+
/* 送信される値の概念モデル
|
|
54
|
+
a: {
|
|
55
|
+
xxx: [
|
|
56
|
+
{ id: nil, value: 'hoge' },
|
|
57
|
+
{ id: 2, value: 'fuga' },
|
|
58
|
+
]
|
|
59
|
+
}
|
|
60
|
+
*/
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## rules
|
|
64
|
+
|
|
65
|
+
```js
|
|
66
|
+
{
|
|
67
|
+
rules: {
|
|
68
|
+
'smarthr/best-practice-for-nested-attributes-array-index': 'error', // 'warn', 'off'
|
|
69
|
+
},
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## ❌ Incorrect
|
|
74
|
+
|
|
75
|
+
```jsx
|
|
76
|
+
<Input name="a[xxxx][][yyy]" />
|
|
77
|
+
<Input name={"${any}[][xxx]"} />
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
// 文字列の変数などでも `[][` のような指定が出てくるのはほぼname属性に対する指定として
|
|
81
|
+
// 利用される可能性が高いためチェック対象です
|
|
82
|
+
const namePrefix = 'a[xxx][][yyy]'
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## ✅ Correct
|
|
86
|
+
|
|
87
|
+
```jsx
|
|
88
|
+
<Input name="a[xxxx][0][yyy]" />
|
|
89
|
+
<Input name={"${any}[${index}][xxx]"} />
|
|
90
|
+
const namePrefix = `a[xxx][${index}][yyy]`
|
|
91
|
+
```
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
const SCHEMA = []
|
|
2
|
+
|
|
3
|
+
const NOINDEX_ARRAY_REGEX = /\[\]\[/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @type {import('@typescript-eslint/utils').TSESLint.RuleModule<''>}
|
|
7
|
+
*/
|
|
8
|
+
module.exports = {
|
|
9
|
+
meta: {
|
|
10
|
+
type: 'problem',
|
|
11
|
+
schema: SCHEMA,
|
|
12
|
+
},
|
|
13
|
+
create(context) {
|
|
14
|
+
const checker = (node, value) => {
|
|
15
|
+
if (NOINDEX_ARRAY_REGEX.test(value)) {
|
|
16
|
+
context.report({
|
|
17
|
+
node,
|
|
18
|
+
message: `入力要素のname属性に対して、配列に当たる部分の連番を指定しない場合(例: a[xxx][][yyy] )、配列内アイテムの属性が意図せず入れ替わってしまう場合がありえるため、常にindexを設定してください。
|
|
19
|
+
- 例のyyyに当たる値が配列内の別アイテムに紐づいてしまう場合があります。
|
|
20
|
+
- 詳しくは https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/best-practice-for-nested-attributes-array-index を参照してください`,
|
|
21
|
+
})
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
Literal: (node) => {
|
|
27
|
+
checker(node, node.value)
|
|
28
|
+
},
|
|
29
|
+
TemplateElement: (node) => {
|
|
30
|
+
checker(node, node.value.cooked)
|
|
31
|
+
},
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
}
|
|
35
|
+
module.exports.schema = SCHEMA
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
const rule = require('../rules/a11y-help-link-with-support-href')
|
|
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
|
+
const errorText = `ヘルプページ用のリンクは smarthr-ui/HelpLink コンポーネントを利用してください`
|
|
14
|
+
|
|
15
|
+
ruleTester.run('a11y-help-link-with-support-href', rule, {
|
|
16
|
+
valid: [
|
|
17
|
+
{ code: `<HelpLink href="//support.hoge">any</HelpLink>` },
|
|
18
|
+
{ code: `<Hoge href="//support.hoge">any</Hoge>` },
|
|
19
|
+
{ code: `<HelpLink href="//support.hoge" />` },
|
|
20
|
+
{ code: `<HelpLink href={"//support.hoge"} />` },
|
|
21
|
+
{ code: `<HelpLink href={path.support.hoge} />` },
|
|
22
|
+
{ code: `<HelpLink href={supportUrl} />` },
|
|
23
|
+
],
|
|
24
|
+
invalid: [
|
|
25
|
+
{ code: `<Anchor href="//support.hoge">any</Anchor>`, errors: [{ message: errorText }] },
|
|
26
|
+
{ code: `<HogeLink href={path.support.hoge} />`, errors: [{ message: errorText }] },
|
|
27
|
+
{ code: `<a href={supportUrl}>ほげ</a>`, errors: [{ message: errorText }] },
|
|
28
|
+
{ code: `<HogeLink href={path.support.hoge?.fuga} />`, errors: [{ message: errorText }] },
|
|
29
|
+
{ code: `<HogeAnchor href={a ? path.support.hoge?.fuga : null} />`, errors: [{ message: errorText }] },
|
|
30
|
+
{ code: `<HogeLink href={a ? undefined : supportHogeHref} />`, errors: [{ message: errorText }] },
|
|
31
|
+
]
|
|
32
|
+
})
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
const rule = require('../rules/best-practice-for-async-current-target')
|
|
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 ERRORMESSAGE_NORMAL = `currentTargetはイベント処理中以外に参照するとnullになる場合があります。イベントハンドラ用関数のスコープ直下でcurrentTarget、もしくはcurrentTarget以下の属性を含む値を変数として宣言してください
|
|
15
|
+
- 参考: https://developer.mozilla.org/ja/docs/Web/API/Event/currentTarget
|
|
16
|
+
- React/useStateのsetterは第一引数に関数を渡すと非同期処理になるためこの問題が起きる可能性があります
|
|
17
|
+
- イベントハンドラ内で関数を定義すると参照タイミングがずれる可能性があるため、イベントハンドラ直下のスコープ内にcurrentTarget関連の参照を変数に残すことをオススメします
|
|
18
|
+
- NG例:
|
|
19
|
+
const onSelect = (e) => {
|
|
20
|
+
setItem((current) => ({ ...current, value: e.currentTarget.value }))
|
|
21
|
+
}
|
|
22
|
+
- 修正例:
|
|
23
|
+
const onSelect = (e) => {
|
|
24
|
+
const value = e.currentTarget.value
|
|
25
|
+
setItem((current) => ({ ...current, value }))
|
|
26
|
+
}`
|
|
27
|
+
const ERRORMESSAGE_AWAIT = `currentTargetはイベント処理中以外に参照するとnullになる場合があります。awaitの宣言より前にcurrentTarget、もしくはcurrentTarget以下の属性を含む値を変数として宣言してください
|
|
28
|
+
- 参考: https://developer.mozilla.org/ja/docs/Web/API/Event/currentTarget
|
|
29
|
+
- NG例:
|
|
30
|
+
const onChange = async (e) => {
|
|
31
|
+
await hoge()
|
|
32
|
+
fuga(e.currentTarget.value)
|
|
33
|
+
}
|
|
34
|
+
- 修正例:
|
|
35
|
+
const onChange = async (e) => {
|
|
36
|
+
const value = e.currentTarget.value
|
|
37
|
+
await hoge()
|
|
38
|
+
fuga(value)
|
|
39
|
+
}`
|
|
40
|
+
|
|
41
|
+
ruleTester.run('best-practice-for-async-current-target', rule, {
|
|
42
|
+
valid: [
|
|
43
|
+
{ code: `(e) => { setValue(e.currentTarget) }` },
|
|
44
|
+
{ code: `const action = function(e) { setValue(e.currentTarget) }` },
|
|
45
|
+
{ code: `async (e) => { const value = e.currentTarget.value; await any(); action(value) }` },
|
|
46
|
+
{ code: `const action = async function(e) { const value = e.currentTarget.value; await any(); action(value) }` },
|
|
47
|
+
],
|
|
48
|
+
invalid: [
|
|
49
|
+
{ code: `(e) => { setItem(() => { e.currentTarget }) }`, errors: [ { message: ERRORMESSAGE_NORMAL } ] },
|
|
50
|
+
{ code: `(function(e) { setItem(() => { e.currentTarget }) })`, errors: [ { message: ERRORMESSAGE_NORMAL } ] },
|
|
51
|
+
{ code: `async (e) => { await any();const value = e.currentTarget.value; action(value) }`, errors: [ { message: ERRORMESSAGE_AWAIT } ] },
|
|
52
|
+
{ code: `const action = async function(e) { await any();const value = e.currentTarget.value;action(value) }`, errors: [ { message: ERRORMESSAGE_AWAIT } ] },
|
|
53
|
+
{ code: `async (e) => { await any(e.currentTarget.value); }`, errors: [ { message: ERRORMESSAGE_AWAIT } ] },
|
|
54
|
+
]
|
|
55
|
+
})
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
const rule = require('../rules/best-practice-for-nested-attributes-array-index')
|
|
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_MESSAGE = `入力要素のname属性に対して、配列に当たる部分の連番を指定しない場合(例: a[xxx][][yyy] )、配列内アイテムの属性が意図せず入れ替わってしまう場合がありえるため、常にindexを設定してください。
|
|
15
|
+
- 例のyyyに当たる値が配列内の別アイテムに紐づいてしまう場合があります。
|
|
16
|
+
- 詳しくは https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/best-practice-for-nested-attributes-array-index を参照してください`
|
|
17
|
+
|
|
18
|
+
ruleTester.run('best-practice-for-nested-attributes-array-index', rule, {
|
|
19
|
+
valid: [
|
|
20
|
+
{ code: `<Input name="a[xxxx][0][yyy]" />` },
|
|
21
|
+
{ code: '`<Input name="a[xxxx][${index}][yyy]" />`' },
|
|
22
|
+
{ code: `const hoge = 'a[xxxx][0][id]'`},
|
|
23
|
+
{ code: 'const hoge = `${prefix}[${index}][id]`'},
|
|
24
|
+
],
|
|
25
|
+
invalid: [
|
|
26
|
+
{ code: `<Input name="a[xxxx][][yyy]" />`, errors: [ { message: ERROR_MESSAGE } ] },
|
|
27
|
+
{ code: '<Input name={`${any}[][yyy]`} />', errors: [ { message: ERROR_MESSAGE } ] },
|
|
28
|
+
{ code: `const hoge = 'a[xxxx][][id]'`, errors: [ { message: ERROR_MESSAGE } ] },
|
|
29
|
+
{ code: 'const hoge = `${prefix}[][id]`', errors: [ { message: ERROR_MESSAGE } ] },
|
|
30
|
+
]
|
|
31
|
+
})
|