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 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.2.0",
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.1"
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.14.0"
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": "71d56c769bf74139d828b181bec39fb148d9b5ca"
40
+ "gitHead": "3c7f6f40f11714a9dd6cb88dc243d1b6973c6a93"
41
41
  }
@@ -1,27 +1,42 @@
1
- const { generateTagFormatter } = require('../../libs/format_styled_components');
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
- 'InputFile$': 'InputFile$',
8
- 'RadioButton$': 'RadioButton$',
9
- 'RadioButtonPanel$': 'RadioButtonPanel$',
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
- 'TimePicker$': 'TimePicker$',
14
- 'DropZone$': 'DropZone$',
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 nodeName = node.name.name || '';
74
+ const { name, attributes } = node
75
+ const nodeName = name.name || ''
60
76
 
61
77
  if (nodeName.match(TARGET_TAG_NAME_REGEX)) {
62
- const nameAttr = node.attributes.find(findNameAttr)
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
- node.attributes.length === 0 ||
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
+ })