eslint-plugin-smarthr 1.2.0 → 1.3.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 +12 -0
- package/README.md +1 -0
- package/package.json +4 -4
- package/rules/a11y-input-has-name-attribute/index.js +63 -24
- package/rules/best-practice-for-tailwind-variants/README.md +53 -0
- package/rules/best-practice-for-tailwind-variants/index.js +78 -0
- package/test/best-practice-for-tailwind-variants.js +28 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
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.3.0](https://github.com/kufu/tamatebako/compare/eslint-plugin-smarthr-v1.2.0...eslint-plugin-smarthr-v1.3.0) (2025-02-21)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* tailwind-variantsの使い方をチェックするルールを追加 ([#480](https://github.com/kufu/tamatebako/issues/480)) ([1501f05](https://github.com/kufu/tamatebako/commit/1501f05e84492e7671a4dc95ef08b15360a1c309))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* [a11y-input-has-name-attribute]Inputにreact-hook-formのregisterが指定されている場合はエラーにならないようにする ([#490](https://github.com/kufu/tamatebako/issues/490)) ([2fc6abe](https://github.com/kufu/tamatebako/commit/2fc6abe9515a06dbf7e823da556e045402298a1b))
|
|
16
|
+
|
|
5
17
|
## [1.2.0](https://github.com/kufu/tamatebako/compare/eslint-plugin-smarthr-v1.1.0...eslint-plugin-smarthr-v1.2.0) (2025-01-09)
|
|
6
18
|
|
|
7
19
|
|
package/README.md
CHANGED
|
@@ -21,6 +21,7 @@
|
|
|
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-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)
|
|
24
25
|
- [design-system-guideline-prohibit-double-icons](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/design-system-guideline-prohibit-double-icons)
|
|
25
26
|
- [format-import-path](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/format-import-path)
|
|
26
27
|
- [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.3.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": ">=20.18.
|
|
9
|
+
"node": ">=20.18.2"
|
|
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.19.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": "3c7f6f40f11714a9dd6cb88dc243d1b6973c6a93"
|
|
41
41
|
}
|
|
@@ -1,27 +1,42 @@
|
|
|
1
|
-
const
|
|
1
|
+
const fs = require('fs')
|
|
2
|
+
|
|
3
|
+
const JSON5 = require('json5')
|
|
4
|
+
|
|
5
|
+
const { generateTagFormatter } = require('../../libs/format_styled_components')
|
|
6
|
+
|
|
7
|
+
const OPTION = (() => {
|
|
8
|
+
const file = `${process.cwd()}/package.json`
|
|
9
|
+
|
|
10
|
+
if (!fs.existsSync(file)) {
|
|
11
|
+
return {}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const json = JSON5.parse(fs.readFileSync(file))
|
|
15
|
+
const dependencies = [...Object.keys(json.dependencies || {}), ...Object.keys(json.devDependencies || {})]
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
react_hook_form: dependencies.includes('react-hook-form'),
|
|
19
|
+
}
|
|
20
|
+
})()
|
|
2
21
|
|
|
3
22
|
const EXPECTED_NAMES = {
|
|
4
23
|
'(i|I)nput$': 'Input$',
|
|
5
24
|
'(t|T)extarea$': 'Textarea$',
|
|
6
25
|
'(s|S)elect$': 'Select$',
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
26
|
+
InputFile$: 'InputFile$',
|
|
27
|
+
RadioButton$: 'RadioButton$',
|
|
28
|
+
RadioButtonPanel$: 'RadioButtonPanel$',
|
|
10
29
|
'Check(b|B)ox$': 'CheckBox$',
|
|
11
30
|
'Combo(b|B)ox$': 'ComboBox$',
|
|
12
31
|
'(Date|Wareki)Picker$': '(Date|Wareki)Picker$',
|
|
13
|
-
|
|
14
|
-
|
|
32
|
+
TimePicker$: 'TimePicker$',
|
|
33
|
+
DropZone$: 'DropZone$',
|
|
15
34
|
}
|
|
16
35
|
const TARGET_TAG_NAME_REGEX = new RegExp(`(${Object.keys(EXPECTED_NAMES).join('|')})`)
|
|
17
36
|
const INPUT_NAME_REGEX = /^[a-zA-Z0-9_\[\]]+$/
|
|
18
37
|
const INPUT_TAG_REGEX = /(i|I)nput$/
|
|
19
38
|
const RADIO_BUTTON_REGEX = /RadioButton(Panel)?$/
|
|
20
39
|
|
|
21
|
-
const findNameAttr = (a) => a?.name?.name === 'name'
|
|
22
|
-
const findSpreadAttr = (a) => a.type === 'JSXSpreadAttribute'
|
|
23
|
-
const findRadioInput = (a) => a.name?.name === 'type' && a.value.value === 'radio'
|
|
24
|
-
|
|
25
40
|
const MESSAGE_PART_FORMAT = `"${INPUT_NAME_REGEX.toString()}"にmatchするフォーマットで命名してください`
|
|
26
41
|
const MESSAGE_UNDEFINED_NAME_PART = `
|
|
27
42
|
- ブラウザの自動補完が有効化されるなどのメリットがあります
|
|
@@ -38,7 +53,7 @@ const SCHEMA = [
|
|
|
38
53
|
checkType: { type: 'string', enum: ['always', 'allow-spread-attributes'], default: 'always' },
|
|
39
54
|
},
|
|
40
55
|
additionalProperties: false,
|
|
41
|
-
}
|
|
56
|
+
},
|
|
42
57
|
]
|
|
43
58
|
|
|
44
59
|
/**
|
|
@@ -56,25 +71,49 @@ module.exports = {
|
|
|
56
71
|
return {
|
|
57
72
|
...generateTagFormatter({ context, EXPECTED_NAMES }),
|
|
58
73
|
JSXOpeningElement: (node) => {
|
|
59
|
-
const
|
|
74
|
+
const { name, attributes } = node
|
|
75
|
+
const nodeName = name.name || ''
|
|
60
76
|
|
|
61
77
|
if (nodeName.match(TARGET_TAG_NAME_REGEX)) {
|
|
62
|
-
|
|
78
|
+
let nameAttr = null
|
|
79
|
+
let hasSpreadAttr = false
|
|
80
|
+
let hasReactHookFormRegisterSpreadAttr = false
|
|
81
|
+
let hasRadioInput = false
|
|
82
|
+
|
|
83
|
+
attributes.forEach((a) => {
|
|
84
|
+
if (a.type === 'JSXSpreadAttribute') {
|
|
85
|
+
hasSpreadAttr = true
|
|
86
|
+
|
|
87
|
+
if (hasReactHookFormRegisterSpreadAttr === false && a.argument?.callee?.name === 'register') {
|
|
88
|
+
hasReactHookFormRegisterSpreadAttr = true
|
|
89
|
+
}
|
|
90
|
+
} else {
|
|
91
|
+
switch (a.name?.name) {
|
|
92
|
+
case 'name': {
|
|
93
|
+
nameAttr = a
|
|
94
|
+
break
|
|
95
|
+
}
|
|
96
|
+
case 'type': {
|
|
97
|
+
if (a.value.value === 'radio') {
|
|
98
|
+
hasRadioInput = true
|
|
99
|
+
}
|
|
100
|
+
break
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
})
|
|
63
105
|
|
|
64
106
|
if (!nameAttr) {
|
|
65
107
|
if (
|
|
66
|
-
|
|
67
|
-
checkType !== 'allow-spread-attributes' ||
|
|
68
|
-
!node.attributes.some(findSpreadAttr)
|
|
108
|
+
!(OPTION.react_hook_form && hasReactHookFormRegisterSpreadAttr) &&
|
|
109
|
+
(attributes.length === 0 || checkType !== 'allow-spread-attributes' || !hasSpreadAttr)
|
|
69
110
|
) {
|
|
70
|
-
const isRadio =
|
|
71
|
-
nodeName.match(RADIO_BUTTON_REGEX) ||
|
|
72
|
-
(nodeName.match(INPUT_TAG_REGEX) && node.attributes.some(findRadioInput));
|
|
111
|
+
const isRadio = nodeName.match(RADIO_BUTTON_REGEX) || (nodeName.match(INPUT_TAG_REGEX) && hasRadioInput)
|
|
73
112
|
|
|
74
113
|
context.report({
|
|
75
114
|
node,
|
|
76
115
|
message: `${nodeName} ${isRadio ? MESSAGE_UNDEFINED_FOR_RADIO : MESSAGE_UNDEFINED_FOR_NOT_RADIO}`,
|
|
77
|
-
})
|
|
116
|
+
})
|
|
78
117
|
}
|
|
79
118
|
} else {
|
|
80
119
|
const nameValue = nameAttr.value?.value || ''
|
|
@@ -83,12 +122,12 @@ module.exports = {
|
|
|
83
122
|
context.report({
|
|
84
123
|
node,
|
|
85
124
|
message: `${nodeName} のname属性の値(${nameValue})${MESSAGE_NAME_FORMAT_SUFFIX}`,
|
|
86
|
-
})
|
|
125
|
+
})
|
|
87
126
|
}
|
|
88
127
|
}
|
|
89
128
|
}
|
|
90
129
|
},
|
|
91
|
-
}
|
|
130
|
+
}
|
|
92
131
|
},
|
|
93
|
-
}
|
|
94
|
-
module.exports.schema = SCHEMA
|
|
132
|
+
}
|
|
133
|
+
module.exports.schema = SCHEMA
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# smarthr/best-practice-for-tailwind-variants
|
|
2
|
+
|
|
3
|
+
- tailwind-variantsの記法をチェックするルールです
|
|
4
|
+
- tailwind-variantsの使い方に一定のルールを課すことで、実装のブレを無くし、レビューコストなどを下げることが目的です
|
|
5
|
+
- tv関数の実行結果を格納する変数の名称を一定のルールにすることで可読性を向上させます
|
|
6
|
+
- また、上記関数を実行する際、一律useMemoを利用することを促します
|
|
7
|
+
- useMemo内にtailwind系の実行をまとめることで将来的にtailwind-variantsから乗り換える際の作業を楽にする、などmemo化以外の効果もあります
|
|
8
|
+
- まとめると現状以下のチェックを行います
|
|
9
|
+
- tailwind-variants(tv) のimport時の名称をtvに固定しているか (asなどでの名称変更の禁止)
|
|
10
|
+
- tv の実行結果を格納する変数名を統一 (classNameGenerator、もしくはxxxClassNameGenerator)
|
|
11
|
+
- tvで生成した関数の実行をuseMemo hook でメモ化しているか
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
## rules
|
|
15
|
+
|
|
16
|
+
```js
|
|
17
|
+
{
|
|
18
|
+
rules: {
|
|
19
|
+
'smarthr/best-practice-for-tailwind-variants': 'error', // 'warn', 'off'
|
|
20
|
+
},
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## ❌ Incorrect
|
|
25
|
+
|
|
26
|
+
```jsx
|
|
27
|
+
import { tv as hoge } from 'tailwind-variants'
|
|
28
|
+
```
|
|
29
|
+
```jsx
|
|
30
|
+
import { tv } from 'tailwind-variants'
|
|
31
|
+
|
|
32
|
+
const xxx = tv({
|
|
33
|
+
...
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
...
|
|
37
|
+
|
|
38
|
+
const yyyy = xxx()
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## ✅ Correct
|
|
42
|
+
|
|
43
|
+
```jsx
|
|
44
|
+
import { tv } from 'tailwind-variants'
|
|
45
|
+
|
|
46
|
+
const classNameGenerator = tv({
|
|
47
|
+
...
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
...
|
|
51
|
+
|
|
52
|
+
const yyyy = useMemo(() => classNameGenerator(), [])
|
|
53
|
+
```
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
const SCHEMA = []
|
|
2
|
+
|
|
3
|
+
const TV_COMPONENTS_METHOD = 'tv'
|
|
4
|
+
const TV_COMPONENTS = 'tailwind-variants'
|
|
5
|
+
const TV_RESULT_CONST_NAME_REGEX = /(C|c)lassNameGenerator$/
|
|
6
|
+
|
|
7
|
+
const findValidImportNameNode = (s) => s.type === 'ImportSpecifier' && s.local.name === TV_COMPONENTS_METHOD
|
|
8
|
+
|
|
9
|
+
const checkImportTailwindVariants = (node, context) => {
|
|
10
|
+
}
|
|
11
|
+
const findNodeHasId = (node) => {
|
|
12
|
+
if (node.id) {
|
|
13
|
+
return node
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (node.parent) {
|
|
17
|
+
return findNodeHasId(node.parent)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return null
|
|
21
|
+
}
|
|
22
|
+
const findNodeUseMemo = (node) => {
|
|
23
|
+
if (node.type === 'CallExpression' && node.callee.name === 'useMemo') {
|
|
24
|
+
return node
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (node.parent) {
|
|
28
|
+
return findNodeUseMemo(node.parent)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return null
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @type {import('@typescript-eslint/utils').TSESLint.RuleModule<''>}
|
|
36
|
+
*/
|
|
37
|
+
module.exports = {
|
|
38
|
+
meta: {
|
|
39
|
+
type: 'problem',
|
|
40
|
+
schema: SCHEMA,
|
|
41
|
+
},
|
|
42
|
+
create(context) {
|
|
43
|
+
return {
|
|
44
|
+
ImportDeclaration: (node) => {
|
|
45
|
+
if (node.source.value === TV_COMPONENTS) {
|
|
46
|
+
if (!node.specifiers.some(findValidImportNameNode)) {
|
|
47
|
+
context.report({
|
|
48
|
+
node,
|
|
49
|
+
message: `${TV_COMPONENTS} をimportする際は、名称が"${TV_COMPONENTS_METHOD}" となるようにしてください。例: "import { ${TV_COMPONENTS_METHOD} } from '${TV_COMPONENTS}'"`,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
CallExpression: (node) => {
|
|
55
|
+
if (node.callee.name === TV_COMPONENTS_METHOD) {
|
|
56
|
+
const idNode = findNodeHasId(node.parent)
|
|
57
|
+
|
|
58
|
+
if (idNode && !TV_RESULT_CONST_NAME_REGEX.test(idNode.id.name)) {
|
|
59
|
+
context.report({
|
|
60
|
+
node: idNode,
|
|
61
|
+
message: `${TV_COMPONENTS_METHOD}の実行結果を格納する変数名は "${idNode.id.name}" ではなく "${TV_RESULT_CONST_NAME_REGEX}"にmatchする名称に統一してください。`,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
} else if (TV_RESULT_CONST_NAME_REGEX.test(node.callee.name)) {
|
|
65
|
+
const useMemoNode = findNodeUseMemo(node.parent)
|
|
66
|
+
|
|
67
|
+
if (!useMemoNode) {
|
|
68
|
+
context.report({
|
|
69
|
+
node,
|
|
70
|
+
message: `"${node.callee.name}" を実行する際、useMemoでラップし、メモ化してください`,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
}
|
|
78
|
+
module.exports.schema = SCHEMA
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
const rule = require('../rules/best-practice-for-tailwind-variants')
|
|
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
|
+
ruleTester.run('best-practice-for-button-element', rule, {
|
|
15
|
+
valid: [
|
|
16
|
+
{ code: `import { tv } from 'tailwind-variants'` },
|
|
17
|
+
{ code: `const classNameGenerator = tv()` },
|
|
18
|
+
{ code: `const xxxClassNameGenerator = tv()` },
|
|
19
|
+
{ code: `const hoge = useMemo(() => classNameGenerator(), [])` },
|
|
20
|
+
{ code: `const xxx = useMemo(() => hogeClassNameGenerator(), [])` },
|
|
21
|
+
],
|
|
22
|
+
invalid: [
|
|
23
|
+
{ code: `import { tv as hoge } from 'tailwind-variants'`, errors: [ { message: `tailwind-variants をimportする際は、名称が"tv" となるようにしてください。例: "import { tv } from 'tailwind-variants'"` } ] },
|
|
24
|
+
{ code: `const hoge = tv()`, errors: [ { message: `tvの実行結果を格納する変数名は "hoge" ではなく "/(C|c)lassNameGenerator$/"にmatchする名称に統一してください。` } ] },
|
|
25
|
+
{ code: `const hoge = classNameGenerator()`, errors: [ { message: `"classNameGenerator" を実行する際、useMemoでラップし、メモ化してください` } ] },
|
|
26
|
+
{ code: `const hoge = hogeClassNameGenerator()`, errors: [ { message: `"hogeClassNameGenerator" を実行する際、useMemoでラップし、メモ化してください` } ] },
|
|
27
|
+
]
|
|
28
|
+
})
|