eslint-plugin-smarthr 1.11.0 → 2.1.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.
Files changed (36) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +0 -1
  3. package/libs/format_styled_components.js +1 -1
  4. package/package.json +4 -4
  5. package/rules/a11y-anchor-has-href-attribute/index.js +44 -80
  6. package/rules/a11y-clickable-element-has-text/index.js +9 -76
  7. package/rules/a11y-image-has-alt-attribute/index.js +14 -51
  8. package/rules/a11y-input-has-name-attribute/index.js +34 -70
  9. package/rules/a11y-prohibit-input-maxlength-attribute/index.js +5 -11
  10. package/rules/a11y-prohibit-input-placeholder/index.js +31 -52
  11. package/rules/a11y-prohibit-useless-sectioning-fragment/index.js +9 -44
  12. package/rules/a11y-trigger-has-button/index.js +18 -36
  13. package/rules/best-practice-for-async-current-target/index.js +14 -21
  14. package/rules/best-practice-for-button-element/index.js +10 -23
  15. package/rules/best-practice-for-data-test-attribute/index.js +9 -12
  16. package/rules/best-practice-for-date/index.js +16 -29
  17. package/rules/best-practice-for-nested-attributes-array-index/index.js +7 -15
  18. package/rules/best-practice-for-remote-trigger-dialog/index.js +10 -23
  19. package/rules/best-practice-for-tailwind-prohibit-root-margin/index.js +5 -93
  20. package/rules/design-system-guideline-prohibit-double-icons/index.js +6 -31
  21. package/rules/prohibit-export-array-type/index.js +6 -9
  22. package/rules/require-i18n-text/README.md +123 -0
  23. package/rules/require-i18n-text/index.js +94 -0
  24. package/test/a11y-clickable-element-has-text.js +0 -4
  25. package/test/a11y-image-has-alt-attribute.js +0 -1
  26. package/test/a11y-prohibit-useless-sectioning-fragment.js +7 -7
  27. package/test/a11y-trigger-has-button.js +8 -7
  28. package/test/best-practice-for-button-element.js +0 -3
  29. package/test/best-practice-for-data-test-attribute.js +9 -8
  30. package/test/best-practice-for-remote-trigger-dialog.js +3 -9
  31. package/test/best-practice-for-tailwind-prohibit-root-margin.js +16 -7
  32. package/test/design-system-guideline-prohibit-double-icons.js +1 -3
  33. package/test/require-i18n-text.js +170 -0
  34. package/rules/a11y-replace-unreadable-symbol/README.md +0 -38
  35. package/rules/a11y-replace-unreadable-symbol/index.js +0 -31
  36. package/test/a11y-replace-unreadable-symbol.js +0 -35
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
+ ## [2.1.0](https://github.com/kufu/tamatebako/compare/eslint-plugin-smarthr-v2.0.0...eslint-plugin-smarthr-v2.1.0) (2025-10-29)
6
+
7
+
8
+ ### Features
9
+
10
+ * prohibit-export-array-typeの型チェックをArray<any>の場合も対象にする ([#869](https://github.com/kufu/tamatebako/issues/869)) ([ee4a647](https://github.com/kufu/tamatebako/commit/ee4a64755c36880d8f43c31ebd47244d71ea2fe9))
11
+ * コンポーネントの子要素やプロパティの文字列リテラルを多言語化の観点で検査するルールを追加 ([#815](https://github.com/kufu/tamatebako/issues/815)) ([9dc8bda](https://github.com/kufu/tamatebako/commit/9dc8bda98d1a978dd0e4d61b91c858ef1c117d9c))
12
+
13
+ ## [2.0.0](https://github.com/kufu/tamatebako/compare/eslint-plugin-smarthr-v1.11.0...eslint-plugin-smarthr-v2.0.0) (2025-10-16)
14
+
15
+
16
+ ### ⚠ BREAKING CHANGES
17
+
18
+ * eslint rule の smarthr/a11y-replace-unreadable-symbolを削除 ([#850](https://github.com/kufu/tamatebako/issues/850))
19
+
20
+ ### Code Refactoring
21
+
22
+ * eslint rule の smarthr/a11y-replace-unreadable-symbolを削除 ([#850](https://github.com/kufu/tamatebako/issues/850)) ([68336f8](https://github.com/kufu/tamatebako/commit/68336f8df42fcfe0c3a8375671377cc62b2be711))
23
+
5
24
  ## [1.11.0](https://github.com/kufu/tamatebako/compare/eslint-plugin-smarthr-v1.10.0...eslint-plugin-smarthr-v1.11.0) (2025-10-01)
6
25
 
7
26
 
package/README.md CHANGED
@@ -14,7 +14,6 @@
14
14
  - [a11y-prohibit-input-placeholder](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/a11y-prohibit-input-placeholder)
15
15
  - [a11y-prohibit-sectioning-content-in-form](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/a11y-prohibit-sectioning-content-in-form)
16
16
  - [a11y-prohibit-useless-sectioning-fragment](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/a11y-prohibit-useless-sectioning-fragment)
17
- - [a11y-replace-unreadable-symbol](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/a11y-replace-unreadable-symbol)
18
17
  - [a11y-required-layout-as-attribute](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/a11y-required-layout-as-attribute)
19
18
  - [a11y-trigger-has-button](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/a11y-trigger-has-button)
20
19
  - [best-practice-for-async-current-target](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/best-practice-for-async-current-target)
@@ -123,4 +123,4 @@ const generateTagFormatter = ({ context, EXPECTED_NAMES, UNEXPECTED_NAMES }) =>
123
123
  }
124
124
  }
125
125
 
126
- module.exports = { generateTagFormatter, checkImportStyledComponents, getStyledComponentBaseName }
126
+ module.exports = { generateTagFormatter }
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "eslint-plugin-smarthr",
3
- "version": "1.11.0",
3
+ "version": "2.1.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.18.0"
9
+ "node": ">=22.21.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.40.0"
29
+ "typescript-eslint": "^8.46.2"
30
30
  },
31
31
  "peerDependencies": {
32
32
  "eslint": "^9"
@@ -37,5 +37,5 @@
37
37
  "eslintplugin",
38
38
  "smarthr"
39
39
  ],
40
- "gitHead": "5877e7cf1fee3324112f56f65c5aa2f753dd2aca"
40
+ "gitHead": "41a58b02c8a6116db8f65bfed94283fa0187d53f"
41
41
  }
@@ -4,90 +4,52 @@ const fs = require('fs')
4
4
  const OPTION = (() => {
5
5
  const file = `${process.cwd()}/package.json`
6
6
 
7
- if (!fs.existsSync(file)) {
8
- return {}
9
- }
7
+ if (fs.existsSync(file)) {
8
+ const json = JSON5.parse(fs.readFileSync(file))
10
9
 
11
- const json = JSON5.parse(fs.readFileSync(file))
12
- const dependencies = [
13
- ...(json.dependencies ? Object.keys(json.dependencies) : []),
14
- ...(json.devDependencies ? Object.keys(json.devDependencies) : []),
15
- ]
10
+ if (json.dependencies){
11
+ const dependencies = Object.keys(json.dependencies)
16
12
 
17
- let nextjs = false
18
- let react_router = false
19
- const result = () => ({
20
- nextjs,
21
- react_router,
22
- })
13
+ let nextjs = false
14
+ let react_router = false
15
+ const result = () => ({
16
+ nextjs,
17
+ react_router,
18
+ })
23
19
 
24
- for (let i = 0; i < dependencies.length; i++) {
25
- switch (dependencies[i]) {
26
- case 'next':
27
- nextjs = true
20
+ for (let i = 0; i < dependencies.length; i++) {
21
+ switch (dependencies[i]) {
22
+ case 'next':
23
+ nextjs = true
28
24
 
29
- if (react_router) {
30
- return result()
31
- }
25
+ if (react_router) {
26
+ return result()
27
+ }
32
28
 
33
- break
34
- case 'react-router':
35
- react_router = true
29
+ break
30
+ case 'react-router':
31
+ react_router = true
36
32
 
37
- if (nextjs) {
38
- return result()
39
- }
33
+ if (nextjs) {
34
+ return result()
35
+ }
40
36
 
41
- break
37
+ break
38
+ }
39
+ }
42
40
  }
43
41
  }
44
42
 
45
- return result()
43
+ return {}
46
44
  })()
47
45
 
48
- const REGEX_TARGET = /(Anchor|Link|^a)$/
49
- const check = (node, checkType) => {
50
- const result = baseCheck(node, checkType)
51
-
52
- return result && ((OPTION.nextjs && !nextCheck(node, checkType)) || (OPTION.react_router && !reactRouterCheck(node))) ? null : result
53
- }
54
- const baseCheck = (node, checkType) => {
55
- const nodeName = node.name.name || ''
56
-
57
- if (
58
- REGEX_TARGET.test(nodeName) &&
59
- checkExistAttribute(node, findHrefAttribute) &&
60
- (checkType !== 'allow-spread-attributes' || !node.attributes.some(findSpreadAttr))
61
- ) {
62
- return nodeName
63
- }
64
-
65
- return false
66
- }
67
- const nextCheck = (node, checkType) => {
68
- // HINT: next/link で `Link>a` という構造がありえるので直上のJSXElementを調べる
69
- const target = node.parent.parent.openingElement
70
-
71
- return target ? baseCheck(target, checkType) : false
72
- }
73
- const reactRouterCheck = (node) => checkExistAttribute(node, findToAttribute)
46
+ const ANCHOR_ELEMENT = 'JSXOpeningElement[name.name=/(Anchor|Link|^a)$/]'
47
+ const HREF_ATTRIBUTE = `JSXAttribute[name.name=${OPTION.react_router ? '/^(href|to)$/' : '"href"'}]`
48
+ const NEXT_LINK_REGEX = /Link$/
49
+ // HINT: next/link で `Link>a` という構造がありえるので直上のJSXElementを調べる
50
+ const nextCheck = (node) => ((node.parent.parent.openingElement.name.name || '').test(NEXT_LINK_REGEX))
74
51
 
75
- const checkExistAttribute = (node, find) => {
76
- const attr = node.attributes.find(find)?.value
77
-
78
- return (
79
- !attr ||
80
- isNullTextHref(attr) ||
81
- (attr.type === 'JSXExpressionContainer' && isNullTextHref(attr.expression))
82
- )
83
- }
84
- const isNullTextHref = (attr) => attr.type === 'Literal' && (attr.value === '' || attr.value === '#')
85
- const findSpreadAttr = (a) => a.type === 'JSXSpreadAttribute'
86
-
87
- const findHrefAttribute = (a) => a.name?.name == 'href'
88
- const findToAttribute = (a) => a.name?.name == 'to'
89
-
90
- const MESSAGE_SUFFIX = ` に href 属性を正しく設定してください
52
+ const MESSAGE_SUFFIX = ` href${OPTION.react_router ? '、もしくはto' : ''} 属性を正しく設定してください
91
53
  - onClickなどでページ遷移する場合でもhref属性に遷移先のURIを設定してください
92
54
  - Cmd + clickなどのキーボードショートカットに対応出来ます
93
55
  - onClickなどの動作がURLの変更を行わない場合、button要素でマークアップすることを検討してください
@@ -115,19 +77,21 @@ module.exports = {
115
77
  },
116
78
  create(context) {
117
79
  const option = context.options[0] || {}
118
- const checkType = option.checkType || 'always'
80
+ const spreadAttributeSelector = option.checkType === 'allow-spread-attributes' ? ':not(:has(JSXSpreadAttribute))' : ''
81
+ const reporter = (node) => {
82
+ context.report({
83
+ node,
84
+ message: `${node.name.name}${MESSAGE_SUFFIX}`,
85
+ })
86
+ }
119
87
 
120
88
  return {
121
- JSXOpeningElement: (node) => {
122
- const nodeName = check(node, checkType)
123
-
124
- if (nodeName) {
125
- context.report({
126
- node,
127
- message: `${nodeName}${MESSAGE_SUFFIX}`,
128
- })
89
+ [`${ANCHOR_ELEMENT}:not(:has(${HREF_ATTRIBUTE}))${spreadAttributeSelector}`]: (node) => {
90
+ if (!OPTION.nextjs || !nextCheck(node)) {
91
+ reporter(node)
129
92
  }
130
93
  },
94
+ [`${ANCHOR_ELEMENT}:has(${HREF_ATTRIBUTE}:matches([value=null],:has(Literal[value=/^(|#)$/])))`]: reporter,
131
95
  }
132
96
  },
133
97
  }
@@ -8,14 +8,8 @@ const SCHEMA = [
8
8
  }
9
9
  ]
10
10
 
11
- const REGEX_NLSP = /^\s*\n+\s*$/
12
- const REGEX_CLICKABLE_ELEMENT = /^(a|(.*?)Anchor(Button)?|(.*?)Link|(b|B)utton)$/
13
- const REGEX_SMARTHR_LOGO = /SmartHRLogo$/
14
- const REGEX_TEXT_COMPONENT = /(Text|Message)$/
15
- const REGEX_JSX_TYPE = /^(JSXText|JSXExpressionContainer)$/
16
-
17
- const filterFalsyJSXText = (cs) => cs.filter(checkFalsyJSXText)
18
- const checkFalsyJSXText = (c) => c.type !== 'JSXText' || !REGEX_NLSP.test(c.value)
11
+ const CLICKABLE_ELEMENT = 'JSXElement[openingElement.name.name=/((^b|B)utton|Anchor|Link|^a)/]:has(JSXClosingElement)'
12
+ const TEXT_LIKE_ATTRIBUTE = 'JSXAttribute[name.name=/^(text|alt|aria-label(ledby)?)$/]:not(:matches([value=null],[value.value=""])))'
19
13
 
20
14
  const message = `a, buttonなどのクリッカブルな要素内にはテキストを設定してください
21
15
  - 要素内にアイコン、画像のみを設置する場合はaltなどの代替テキスト用属性を指定してください
@@ -33,76 +27,15 @@ module.exports = {
33
27
  create(context) {
34
28
  const option = context.options[0] || {}
35
29
  const componentsWithText = option.componentsWithText || []
30
+ // HINT: SmartHRLogo コンポーネントは内部でaltを持っているため対象外にする
31
+ const elementWithText = `JSXOpeningElement[name.name=/(SmartHRLogo|Text|Message|^(${componentsWithText.join('|')}))$/]`
36
32
 
37
33
  return {
38
- JSXElement: (parentNode) => {
39
- // HINT: 閉じタグが存在しない === テキストノードが存在しない
40
- if (!parentNode.closingElement) {
41
- return
42
- }
43
-
44
- const node = parentNode.openingElement
45
-
46
- if (!node.name.name || !REGEX_CLICKABLE_ELEMENT.test(node.name.name)) {
47
- return
48
- }
49
-
50
- const recursiveSearch = (c) => {
51
- if (REGEX_JSX_TYPE.test(c.type)) {
52
- return true
53
- }
54
-
55
- switch (c.type) {
56
- case 'JSXFragment': {
57
- return c.children && filterFalsyJSXText(c.children).some(recursiveSearch)
58
- }
59
- case 'JSXElement': {
60
- // // HINT: SmartHRLogo コンポーネントは内部でaltを持っているため対象外にする
61
- if (REGEX_SMARTHR_LOGO.test(c.openingElement.name.name)) {
62
- return true
63
- }
64
-
65
- const tagName = c.openingElement.name.name
66
-
67
- if (REGEX_TEXT_COMPONENT.test(tagName) || componentsWithText.includes(tagName)) {
68
- return true
69
- }
70
-
71
- // HINT: role & aria-label を同時に設定されている場合か、text属性が設定されている場合許可
72
- let existRole = false
73
- let existAriaLabel = false
74
- const result = c.openingElement.attributes.reduce((prev, a) => {
75
- const n = a.name?.name
76
-
77
- if (prev || n === 'text') {
78
- return true
79
- }
80
-
81
- const v = a.value?.value
82
-
83
- existRole = existRole || (n === 'role' && v === 'img')
84
- existAriaLabel = existAriaLabel || n === 'aria-label'
85
-
86
- if (existRole && existAriaLabel) {
87
- return true
88
- }
89
-
90
- return n === 'alt' && (v || a.value.type === 'JSXExpressionContainer') || false
91
- }, null)
92
-
93
- return result || (c.children && filterFalsyJSXText(c.children).some(recursiveSearch))
94
- }
95
- }
96
-
97
- return false
98
- }
99
-
100
- if (!filterFalsyJSXText(parentNode.children).find(recursiveSearch)) {
101
- context.report({
102
- node,
103
- message,
104
- });
105
- }
34
+ [`${CLICKABLE_ELEMENT}:not(:has(:matches(${TEXT_LIKE_ATTRIBUTE},JSXText,JSXExpressionContainer,${elementWithText}))`]: (node) => {
35
+ context.report({
36
+ node,
37
+ message,
38
+ });
106
39
  },
107
40
  }
108
41
  },
@@ -1,18 +1,5 @@
1
- const REGEX_IMG = /(img|image)$/i // HINT: Iconは別途テキストが存在する場合が多いためチェックの対象外とする
2
-
3
- const findAltAttr = (a) => a.name?.name === 'alt'
4
- const findAriaDescribedbyAttr = (a) => a.name?.name === 'aria-describedby'
5
- const findSpreadAttr = (a) => a.type === 'JSXSpreadAttribute'
6
- const isWithinSvgJsxElement = (node) => {
7
- if (
8
- node.type === 'JSXElement' &&
9
- node.openingElement.name?.name === 'svg'
10
- ) {
11
- return true
12
- }
13
-
14
- return node.parent ? isWithinSvgJsxElement(node.parent) : false
15
- }
1
+ const IMG_ELEMENT = 'JSXOpeningElement[name.name=/((i|I)mg|Image)$/]'
2
+ const ALT_LIKE_ATTRIBUTE = 'JSXAttribute[name.name=/^(alt|aria-describedby)$/]'
16
3
 
17
4
  const MESSAGE_NOT_EXIST_ALT = `画像にはalt属性を指定してください。
18
5
  - コンポーネントが画像ではない場合、img or image を末尾に持たない名称に変更してください。
@@ -43,44 +30,20 @@ module.exports = {
43
30
  },
44
31
  create(context) {
45
32
  const option = context.options[0] || {}
46
- const checkType = option.checkType || 'always'
33
+ const notHasSpreadAttr = option.checkType === 'allow-spread-attributes' ? ':not(:has(JSXSpreadAttribute))' : ''
47
34
 
48
35
  return {
49
- JSXOpeningElement: (node) => {
50
- if (node.name.name) {
51
- const matcher = node.name.name.match(REGEX_IMG)
52
-
53
- if (matcher) {
54
- const alt = node.attributes.find(findAltAttr)
55
-
56
- let message = ''
57
-
58
- if (!alt) {
59
- if (
60
- !node.attributes.find(findAriaDescribedbyAttr) &&
61
- (
62
- matcher.input !== 'image' ||
63
- !isWithinSvgJsxElement(node.parent)
64
- ) &&
65
- (
66
- checkType !== 'allow-spread-attributes' ||
67
- !node.attributes.some(findSpreadAttr)
68
- )
69
- ) {
70
- message = MESSAGE_NOT_EXIST_ALT
71
- }
72
- } else if (alt.value.value === '') {
73
- message = MESSAGE_NULL_ALT
74
- }
75
-
76
- if (message) {
77
- context.report({
78
- node,
79
- message,
80
- });
81
- }
82
- }
83
- }
36
+ [`${IMG_ELEMENT}:not(:has(${ALT_LIKE_ATTRIBUTE}))${notHasSpreadAttr}`]: (node) => {
37
+ context.report({
38
+ node,
39
+ message: MESSAGE_NOT_EXIST_ALT,
40
+ });
41
+ },
42
+ [`${IMG_ELEMENT}:has(${ALT_LIKE_ATTRIBUTE}:matches([value.value=""],[value=null]))${notHasSpreadAttr}`]: (node) => {
43
+ context.report({
44
+ node,
45
+ message: MESSAGE_NULL_ALT,
46
+ });
84
47
  },
85
48
  }
86
49
  },
@@ -5,31 +5,26 @@ const JSON5 = require('json5')
5
5
  const OPTION = (() => {
6
6
  const file = `${process.cwd()}/package.json`
7
7
 
8
- if (!fs.existsSync(file)) {
9
- return {}
8
+ if (fs.existsSync(file)) {
9
+ return {
10
+ react_hook_form: Object.keys(JSON5.parse(fs.readFileSync(file)).dependencies).includes('react-hook-form'),
11
+ }
10
12
  }
11
13
 
12
- const json = JSON5.parse(fs.readFileSync(file))
13
- const dependencies = [...Object.keys(json.dependencies || {}), ...Object.keys(json.devDependencies || {})]
14
-
15
- return {
16
- react_hook_form: dependencies.includes('react-hook-form'),
17
- }
14
+ return {}
18
15
  })()
19
16
 
20
- const TARGET_TAG_NAME_REGEX = /((I|^i)nput|(T|^t)extarea|(S|^s)elect|InputFile|RadioButton(Panel)?|(Check|Combo)(B|b)ox|(Date|Wareki|Time)Picker|DropZone)$/
21
- const INPUT_NAME_REGEX = /^[a-zA-Z0-9_\[\]]+$/
22
- const INPUT_TAG_REGEX = /(i|I)nput$/
23
- const RADIO_BUTTON_REGEX = /RadioButton(Panel)?$/
17
+ const INPUT_ELEMENT = 'JSXOpeningElement[name.name=/((I|^i)nput|(T|^t)extarea|(S|^s)elect|InputFile|RadioButton(Panel)?|(Check|Combo)(B|b)ox|(Date|Wareki|Time)Picker|DropZone)$/]'
18
+ const INPUT_ELEMENT_WITHOUT_RADIO = 'JSXOpeningElement[name.name=/((I|^i)nput|(T|^t)extarea|(S|^s)elect|InputFile|(Check|Combo)(B|b)ox|(Date|Wareki|Time)Picker|DropZone)$/]'
19
+ const RADIO_ELEMENT = 'JSXOpeningElement:matches([name.name=/RadioButton(Panel)?$/],[name.name=/(I|^i)nput?$/]:has(JSXAttribute[name.name="type"][value.value="radio"]))'
20
+ const NAME_ATTRIBUTE = 'JSXAttribute[name.name="name"]'
21
+
22
+ const INPUT_NAME_REGEX = /^[a-zA-Z0-9_\[\]]+$/.toString()
24
23
 
25
24
  const MESSAGE_PART_FORMAT = `"${INPUT_NAME_REGEX.toString()}"にmatchするフォーマットで命名してください`
26
25
  const MESSAGE_UNDEFINED_NAME_PART = `
27
26
  - ブラウザの自動補完が有効化されるなどのメリットがあります
28
27
  - より多くのブラウザが自動補完を行える可能性を上げるため、${MESSAGE_PART_FORMAT}`
29
- const MESSAGE_UNDEFINED_FOR_RADIO = `にグループとなる他のinput[radio]と同じname属性を指定してください
30
- - 適切に指定することで同じname属性を指定したinput[radio]とグループが確立され、適切なキーボード操作を行えるようになります${MESSAGE_UNDEFINED_NAME_PART}`
31
- const MESSAGE_UNDEFINED_FOR_NOT_RADIO = `にname属性を指定してください${MESSAGE_UNDEFINED_NAME_PART}`
32
- const MESSAGE_NAME_FORMAT_SUFFIX = `はブラウザの自動補完が適切に行えない可能性があるため${MESSAGE_PART_FORMAT}`
33
28
 
34
29
  const SCHEMA = [
35
30
  {
@@ -53,61 +48,30 @@ module.exports = {
53
48
  const option = context.options[0] || {}
54
49
  const checkType = option.checkType || 'always'
55
50
 
56
- return {
57
- JSXOpeningElement: (node) => {
58
- const { name, attributes } = node
59
- const nodeName = name.name || ''
60
-
61
- if (TARGET_TAG_NAME_REGEX.test(nodeName)) {
62
- let nameAttr = null
63
- let hasSpreadAttr = false
64
- let hasReactHookFormRegisterSpreadAttr = false
65
- let hasRadioInput = false
51
+ const notHasSpreadAttribute =
52
+ option.checkType == 'allow-spread-attributes'
53
+ ? ':not(:has(JSXSpreadAttribute))'
54
+ : OPTION.react_hook_form ? ':not(:has(JSXSpreadAttribute CallExpression[callee.name="register"]))' : ''
66
55
 
67
- attributes.forEach((a) => {
68
- if (a.type === 'JSXSpreadAttribute') {
69
- hasSpreadAttr = true
70
-
71
- if (hasReactHookFormRegisterSpreadAttr === false && a.argument?.callee?.name === 'register') {
72
- hasReactHookFormRegisterSpreadAttr = true
73
- }
74
- } else {
75
- switch (a.name?.name) {
76
- case 'name': {
77
- nameAttr = a
78
- break
79
- }
80
- case 'type': {
81
- if (a.value.value === 'radio') {
82
- hasRadioInput = true
83
- }
84
- break
85
- }
86
- }
87
- }
88
- })
89
-
90
- if (nameAttr) {
91
- const nameValue = nameAttr.value?.value
92
-
93
- if (nameValue && !INPUT_NAME_REGEX.test(nameValue)) {
94
- context.report({
95
- node,
96
- message: `${nodeName} のname属性の値(${nameValue})${MESSAGE_NAME_FORMAT_SUFFIX}`,
97
- })
98
- }
99
- } else if (
100
- !(OPTION.react_hook_form && hasReactHookFormRegisterSpreadAttr) &&
101
- (attributes.length === 0 || checkType !== 'allow-spread-attributes' || !hasSpreadAttr)
102
- ) {
103
- const isRadio = RADIO_BUTTON_REGEX.test(nodeName) || INPUT_TAG_REGEX.test(nodeName) && hasRadioInput
104
-
105
- context.report({
106
- node,
107
- message: `${nodeName} ${isRadio ? MESSAGE_UNDEFINED_FOR_RADIO : MESSAGE_UNDEFINED_FOR_NOT_RADIO}`,
108
- })
109
- }
110
- }
56
+ return {
57
+ [`${INPUT_ELEMENT_WITHOUT_RADIO}:not(:has(:matches(${NAME_ATTRIBUTE},JSXAttribute[name.name="type"][value.value="radio"])))${notHasSpreadAttribute}`]: (node) => {
58
+ context.report({
59
+ node,
60
+ message: `${node.name.name} にname属性を指定してください${MESSAGE_UNDEFINED_NAME_PART}`,
61
+ })
62
+ },
63
+ [`${RADIO_ELEMENT}:not(:has(${NAME_ATTRIBUTE}))${notHasSpreadAttribute}`]: (node) => {
64
+ context.report({
65
+ node,
66
+ message: `${node.name.name} にグループとなる他のinput[radio]と同じname属性を指定してください
67
+ - 適切に指定することで同じname属性を指定したinput[radio]とグループが確立され、適切なキーボード操作を行えるようになります${MESSAGE_UNDEFINED_NAME_PART}`,
68
+ })
69
+ },
70
+ [`${INPUT_ELEMENT}${notHasSpreadAttribute} ${NAME_ATTRIBUTE}:not([value.value=${INPUT_NAME_REGEX}])`]: (node) => {
71
+ context.report({
72
+ node,
73
+ message: `${node.parent.name.name} のname属性の値(${node.value.value})はブラウザの自動補完が適切に行えない可能性があるため${MESSAGE_PART_FORMAT}`,
74
+ })
111
75
  },
112
76
  }
113
77
  },
@@ -1,9 +1,5 @@
1
- const INPUT_COMPONENT_NAMES = /((I|^i)nput|(T|^t)extarea)$/
2
-
3
1
  const SCHEMA = []
4
2
 
5
- const checkHasMaxLength = (attr) => attr.name?.name === 'maxLength'
6
-
7
3
  /**
8
4
  * @type {import('@typescript-eslint/utils').TSESLint.RuleModule<''>}
9
5
  */
@@ -14,17 +10,15 @@ module.exports = {
14
10
  },
15
11
  create(context) {
16
12
  return {
17
- JSXOpeningElement: (node) => {
18
- if (node.name.type === 'JSXIdentifier' && INPUT_COMPONENT_NAMES.test(node.name.name) && node.attributes.find(checkHasMaxLength)) {
19
- context.report({
20
- node,
21
- message: `${node.name.name}にmaxLength属性を設定しないでください。
13
+ [`JSXOpeningElement[name.name=/((I|^i)nput|(T|^t)extarea)$/]:has(JSXAttribute[name.name="maxLength"])`]: (node) => {
14
+ context.report({
15
+ node,
16
+ message: `${node.name.name}にmaxLength属性を設定しないでください。
22
17
  - maxLength属性がついた要素に、テキストをペーストすると、maxLength属性の値を超えた範囲が意図せず切り捨てられてしまう場合があります
23
18
  - 以下のいずれかの方法で修正をおこなってください
24
19
  - 方法1: pattern属性とtitle属性を組み合わせ、form要素でラップする
25
20
  - 方法2: JavaScriptを用いたバリデーションを実装する`,
26
- })
27
- }
21
+ })
28
22
  },
29
23
  }
30
24
  },