eslint-plugin-smarthr 1.5.1 → 1.6.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 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
+ ## [1.6.0](https://github.com/kufu/tamatebako/compare/eslint-plugin-smarthr-v1.5.1...eslint-plugin-smarthr-v1.6.0) (2025-05-20)
6
+
7
+
8
+ ### Features
9
+
10
+ * best-practice-for-async-current-targetルールを追加 ([#625](https://github.com/kufu/tamatebako/issues/625)) ([8cc72b4](https://github.com/kufu/tamatebako/commit/8cc72b4d4d552ed1efa9cfe7fbec04de0fcc369e))
11
+
5
12
  ## [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
13
 
7
14
 
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "eslint-plugin-smarthr",
3
- "version": "1.5.1",
3
+ "version": "1.6.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.15.0"
9
+ "node": ">=22.15.1"
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.32.0"
29
+ "typescript-eslint": "^8.32.1"
30
30
  },
31
31
  "peerDependencies": {
32
32
  "eslint": "^9"
@@ -37,5 +37,5 @@
37
37
  "eslintplugin",
38
38
  "smarthr"
39
39
  ],
40
- "gitHead": "e74469a80bae406252eea39f939efa557857bc88"
40
+ "gitHead": "92e589c6bfc8e311b4c873f7e4c65b29eed61304"
41
41
  }
@@ -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,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
+ })