eslint-plugin-smarthr 1.2.0 → 1.4.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,25 @@
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.4.0](https://github.com/kufu/tamatebako/compare/eslint-plugin-smarthr-v1.3.0...eslint-plugin-smarthr-v1.4.0) (2025-03-06)
6
+
7
+
8
+ ### Features
9
+
10
+ * best-practice-for-tailwind-prohibit-root-margin ([#523](https://github.com/kufu/tamatebako/issues/523)) ([b14700a](https://github.com/kufu/tamatebako/commit/b14700a17b1c5496e50c6c920f0281bf3ec1d7da))
11
+
12
+ ## [1.3.0](https://github.com/kufu/tamatebako/compare/eslint-plugin-smarthr-v1.2.0...eslint-plugin-smarthr-v1.3.0) (2025-02-21)
13
+
14
+
15
+ ### Features
16
+
17
+ * tailwind-variantsの使い方をチェックするルールを追加 ([#480](https://github.com/kufu/tamatebako/issues/480)) ([1501f05](https://github.com/kufu/tamatebako/commit/1501f05e84492e7671a4dc95ef08b15360a1c309))
18
+
19
+
20
+ ### Bug Fixes
21
+
22
+ * [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))
23
+
5
24
  ## [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
25
 
7
26
 
package/README.md CHANGED
@@ -21,6 +21,8 @@
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)
25
+ - [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)
24
26
  - [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
27
  - [format-import-path](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/format-import-path)
26
28
  - [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.4.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.3"
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": "49a1837e61d27cf7f1476d2026a63164542df55d"
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,73 @@
1
+ # best-practice-for-tailwind-prohibit-root-margin
2
+
3
+ tailwindcss を使用したコンポーネントのルート要素に外側の余白(margin)を設定することを禁止します。
4
+
5
+ ## ルールの目的
6
+
7
+ コンポーネントは、それ自体が外側に余白を持つべきではありません。外側の余白の制御は、コンポーネントを使用する側の責任とすべきです。
8
+
9
+ これにより、以下のメリットがあります。
10
+
11
+ - コンポーネントの再利用性が向上します
12
+ - レイアウトの一貫性が保たれます
13
+ - コンポーネント間の余白の調整が容易になります
14
+
15
+ ## 対象となるクラス名
16
+
17
+ 以下のパターンのクラス名が検出対象となります。
18
+
19
+ - `shr-m-`
20
+ - `shr-mt-`
21
+ - `shr-mr-`
22
+ - `shr-mb-`
23
+ - `shr-ml-`
24
+
25
+ クラス名のプレフィックス(`shr-`) の通り、SmartHR UI と合わせて使用されることを前提としています。
26
+
27
+ ## NG例
28
+
29
+ ```jsx
30
+ const Button = () => {
31
+ return <button className="shr-m-4">Click me</button>
32
+ }
33
+
34
+ const Card = () => {
35
+ return (
36
+ <div className="shr-mt-4">
37
+ <p>Content</p>
38
+ </div>
39
+ )
40
+ }
41
+ ```
42
+
43
+ ## OK例
44
+
45
+ ```jsx
46
+ const Button = ({ className }) => {
47
+ return <button className={className}>Click me</button>
48
+ }
49
+
50
+ const Card = () => {
51
+ return (
52
+ <div>
53
+ <p>Content</p>
54
+ </div>
55
+ )
56
+ }
57
+
58
+ // 使用する側で余白を制御
59
+ const Page = () => {
60
+ return (
61
+ <div>
62
+ <Button className="shr-m-4" />
63
+ <div className="shr-mt-4">
64
+ <Card />
65
+ </div>
66
+ </div>
67
+ )
68
+ }
69
+ ```
70
+
71
+ ## オプション
72
+
73
+ このルールにはオプションはありません。
@@ -0,0 +1,103 @@
1
+ const { AST_NODE_TYPES } = require('@typescript-eslint/utils')
2
+
3
+ const SCHEMA = []
4
+ const MARGIN_CLASS_PATTERNS = /shr-m[trbl]?-/ // mt-, mr-, mb-, ml-, m-
5
+
6
+ /**
7
+ * コンポーネントのルート要素を渡し、該当の余白クラスが存在すればそれを、なければNULLを返す
8
+ * @param {import('@typescript-eslint/utils').TSESTree.Node} node
9
+ * @returns {import('@typescript-eslint/utils').TSESTree.Literal | null}
10
+ */
11
+ const findSpacingClassInRootElement = (node) => {
12
+ // JSX でなければ対象外
13
+ if (node.type !== AST_NODE_TYPES.JSXElement) return null
14
+
15
+ // className属性がなければ対象外
16
+ const classNameAttr = node.openingElement.attributes.find(
17
+ (attr) => attr.type === AST_NODE_TYPES.JSXAttribute && attr.name.name === 'className',
18
+ )
19
+ if (!classNameAttr) return null
20
+
21
+ // className属性の値がリテラルでなければ対象外
22
+ if (classNameAttr?.value?.type !== AST_NODE_TYPES.Literal || typeof classNameAttr.value.value !== 'string') return null
23
+
24
+ // className属性の値に余白クラスが含まれていればそれを返す
25
+ return MARGIN_CLASS_PATTERNS.test(classNameAttr.value.value) ? classNameAttr.value : null
26
+ }
27
+
28
+ /**
29
+ * ブロックステートメント内から、JSX要素を返す ReturnStatement を返す
30
+ * @param {import('@typescript-eslint/utils').TSESTree.BlockStatement} block
31
+ * @returns {import('@typescript-eslint/utils').TSESTree.ReturnStatement | null}
32
+ */
33
+ const findJSXReturnStatement = (block) => {
34
+ for (const statement of block.body) {
35
+ if (statement.type === AST_NODE_TYPES.ReturnStatement && statement.argument?.type === AST_NODE_TYPES.JSXElement) {
36
+ return statement
37
+ }
38
+ }
39
+ return null
40
+ }
41
+
42
+ /**
43
+ * @type {import('@typescript-eslint/utils').TSESLint.RuleModule}
44
+ */
45
+ module.exports = {
46
+ meta: {
47
+ type: 'problem',
48
+ schema: SCHEMA,
49
+ messages: {
50
+ noRootSpacing:
51
+ 'コンポーネントのルート要素に外側への余白(margin)を設定しないでください。外側の余白は使用する側で制御するべきです。',
52
+ },
53
+ },
54
+ create(context) {
55
+ /**
56
+ * 関数本体をチェックし、ルート要素で余白クラスが設定されたJSXを返している場合、エラーを報告する
57
+ * @param {import('@typescript-eslint/utils').TSESTree.Node} body
58
+ */
59
+ const checkFunctionBody = (body) => {
60
+ // 関数がブロックを持たずに直接JSXを返すパターン
61
+ if (body.type === AST_NODE_TYPES.JSXElement) {
62
+ const spacingClass = findSpacingClassInRootElement(body)
63
+ if (spacingClass) {
64
+ context.report({
65
+ node: spacingClass,
66
+ messageId: 'noRootSpacing',
67
+ })
68
+ }
69
+ return
70
+ }
71
+
72
+ // 関数がブロック内で JSX を return するパターン
73
+ if (body.type === AST_NODE_TYPES.BlockStatement) {
74
+ const returnStatement = findJSXReturnStatement(body)
75
+ if (returnStatement?.argument) {
76
+ const spacingClass = findSpacingClassInRootElement(returnStatement.argument)
77
+ if (spacingClass) {
78
+ context.report({
79
+ node: spacingClass,
80
+ messageId: 'noRootSpacing',
81
+ })
82
+ }
83
+ }
84
+ }
85
+ }
86
+
87
+ return {
88
+ // アロー関数式のコンポーネントをチェック
89
+ ArrowFunctionExpression: (node) => {
90
+ if (node.parent?.type === AST_NODE_TYPES.VariableDeclarator) {
91
+ checkFunctionBody(node.body)
92
+ }
93
+ },
94
+
95
+ // function宣言のコンポーネントをチェック
96
+ FunctionDeclaration: (node) => {
97
+ checkFunctionBody(node.body)
98
+ },
99
+ }
100
+ },
101
+ }
102
+
103
+ 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,111 @@
1
+ const rule = require('../rules/best-practice-for-tailwind-prohibit-root-margin')
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 =
15
+ 'コンポーネントのルート要素に外側への余白(margin)を設定しないでください。外側の余白は使用する側で制御するべきです。'
16
+
17
+ ruleTester.run('best-practice-for-tailwind-prohibit-root-margin', rule, {
18
+ valid: [
19
+ // 余白のないコンポーネント
20
+ {
21
+ code: `
22
+ const Button = () => {
23
+ return <button className="shr-bg-blue-500">Click me</button>
24
+ }
25
+ `,
26
+ },
27
+ // コンポーネントのルート以外の要素での余白使用
28
+ {
29
+ code: `
30
+ const Card = () => {
31
+ return (
32
+ <div className="shr-bg-white">
33
+ <h2 className="shr-mt-4">Title</h2>
34
+ <p className="shr-mb-2 shr-pt-2">Content</p>
35
+ </div>
36
+ )
37
+ }
38
+ `,
39
+ },
40
+ // shr-min などの、shr-m から始まるほかのクラス
41
+ { code: `const Button = () => <button className="shr-min-w-100">Click me</button>` },
42
+ { code: `const Button = () => <button className="shr-min-h-100">Click me</button>` },
43
+ { code: `const Button = () => <button className="shr-max-w-100">Click me</button>` },
44
+ { code: `const Button = () => <button className="shr-max-h-100">Click me</button>` },
45
+ // padding は許可
46
+ { code: `const Button = () => <button className="shr-p-4">Click me</button>` },
47
+ { code: `const Button = () => <button className="shr-pt-4">Click me</button>` },
48
+ { code: `const Button = () => <button className="shr-pb-4">Click me</button>` },
49
+ { code: `const Button = () => <button className="shr-pl-4">Click me</button>` },
50
+ { code: `const Button = () => <button className="shr-pr-4">Click me</button>` },
51
+ // リテラルでないクラス名
52
+ { code: `const Button = () => <button className={shr-mt-2}>Click me</button>` },
53
+ { code: `const Button = () => <button className={shr-pb-4}>Click me</button>` },
54
+ ],
55
+ invalid: [
56
+ // マージンクラスを持つコンポーネント
57
+ {
58
+ code: `const Button = () => { return <button className="shr-m-4">Click me</button> }`,
59
+ errors: [{ message: errorMessage }],
60
+ },
61
+ {
62
+ code: `const Button = () => <button className="shr-mt-4">Click me</button>`,
63
+ errors: [{ message: errorMessage }],
64
+ },
65
+ {
66
+ code: `const Button = () => { return <button className="shr-mb-4">Click me</button> }`,
67
+ errors: [{ message: errorMessage }],
68
+ },
69
+ {
70
+ code: `const Button = () => <button className="shr-ml-4">Click me</button>`,
71
+ errors: [{ message: errorMessage }],
72
+ },
73
+ {
74
+ code: `const Button = () => { return <button className="shr-mr-4">Click me</button> }`,
75
+ errors: [{ message: errorMessage }],
76
+ },
77
+ // 複数の余白クラスを持つコンポーネント
78
+ {
79
+ code: `
80
+ const Card = () => {
81
+ return (
82
+ <div className="shr-mt-4 shr-mb-2 shr-ml-2 shr-mr-2">
83
+ <p>Content</p>
84
+ </div>
85
+ )
86
+ }
87
+ `,
88
+ errors: [{ message: errorMessage }],
89
+ },
90
+ // 他のクラスと組み合わせた余白
91
+ {
92
+ code: `
93
+ const Box = () => (
94
+ <div className="shr-bg-gray-100 shr-ml-2">
95
+ <p>Content</p>
96
+ </div>
97
+ )
98
+ `,
99
+ errors: [{ message: errorMessage }],
100
+ },
101
+ // function 宣言によるコンポーネント
102
+ {
103
+ code: `
104
+ function Button() {
105
+ return <button className="shr-m-4">Click me</button>
106
+ }
107
+ `,
108
+ errors: [{ message: errorMessage }],
109
+ },
110
+ ],
111
+ })
@@ -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
+ })