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.
- package/CHANGELOG.md +19 -0
- package/README.md +0 -1
- package/libs/format_styled_components.js +1 -1
- package/package.json +4 -4
- package/rules/a11y-anchor-has-href-attribute/index.js +44 -80
- package/rules/a11y-clickable-element-has-text/index.js +9 -76
- package/rules/a11y-image-has-alt-attribute/index.js +14 -51
- package/rules/a11y-input-has-name-attribute/index.js +34 -70
- package/rules/a11y-prohibit-input-maxlength-attribute/index.js +5 -11
- package/rules/a11y-prohibit-input-placeholder/index.js +31 -52
- package/rules/a11y-prohibit-useless-sectioning-fragment/index.js +9 -44
- package/rules/a11y-trigger-has-button/index.js +18 -36
- package/rules/best-practice-for-async-current-target/index.js +14 -21
- package/rules/best-practice-for-button-element/index.js +10 -23
- package/rules/best-practice-for-data-test-attribute/index.js +9 -12
- package/rules/best-practice-for-date/index.js +16 -29
- package/rules/best-practice-for-nested-attributes-array-index/index.js +7 -15
- package/rules/best-practice-for-remote-trigger-dialog/index.js +10 -23
- package/rules/best-practice-for-tailwind-prohibit-root-margin/index.js +5 -93
- package/rules/design-system-guideline-prohibit-double-icons/index.js +6 -31
- package/rules/prohibit-export-array-type/index.js +6 -9
- package/rules/require-i18n-text/README.md +123 -0
- package/rules/require-i18n-text/index.js +94 -0
- package/test/a11y-clickable-element-has-text.js +0 -4
- package/test/a11y-image-has-alt-attribute.js +0 -1
- package/test/a11y-prohibit-useless-sectioning-fragment.js +7 -7
- package/test/a11y-trigger-has-button.js +8 -7
- package/test/best-practice-for-button-element.js +0 -3
- package/test/best-practice-for-data-test-attribute.js +9 -8
- package/test/best-practice-for-remote-trigger-dialog.js +3 -9
- package/test/best-practice-for-tailwind-prohibit-root-margin.js +16 -7
- package/test/design-system-guideline-prohibit-double-icons.js +1 -3
- package/test/require-i18n-text.js +170 -0
- package/rules/a11y-replace-unreadable-symbol/README.md +0 -38
- package/rules/a11y-replace-unreadable-symbol/index.js +0 -31
- 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)
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-smarthr",
|
|
3
|
-
"version": "1.
|
|
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.
|
|
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.
|
|
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": "
|
|
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 (
|
|
8
|
-
|
|
9
|
-
}
|
|
7
|
+
if (fs.existsSync(file)) {
|
|
8
|
+
const json = JSON5.parse(fs.readFileSync(file))
|
|
10
9
|
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
13
|
+
let nextjs = false
|
|
14
|
+
let react_router = false
|
|
15
|
+
const result = () => ({
|
|
16
|
+
nextjs,
|
|
17
|
+
react_router,
|
|
18
|
+
})
|
|
23
19
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
20
|
+
for (let i = 0; i < dependencies.length; i++) {
|
|
21
|
+
switch (dependencies[i]) {
|
|
22
|
+
case 'next':
|
|
23
|
+
nextjs = true
|
|
28
24
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
25
|
+
if (react_router) {
|
|
26
|
+
return result()
|
|
27
|
+
}
|
|
32
28
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
29
|
+
break
|
|
30
|
+
case 'react-router':
|
|
31
|
+
react_router = true
|
|
36
32
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
33
|
+
if (nextjs) {
|
|
34
|
+
return result()
|
|
35
|
+
}
|
|
40
36
|
|
|
41
|
-
|
|
37
|
+
break
|
|
38
|
+
}
|
|
39
|
+
}
|
|
42
40
|
}
|
|
43
41
|
}
|
|
44
42
|
|
|
45
|
-
return
|
|
43
|
+
return {}
|
|
46
44
|
})()
|
|
47
45
|
|
|
48
|
-
const
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
122
|
-
|
|
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
|
|
12
|
-
const
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
|
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
|
|
33
|
+
const notHasSpreadAttr = option.checkType === 'allow-spread-attributes' ? ':not(:has(JSXSpreadAttribute))' : ''
|
|
47
34
|
|
|
48
35
|
return {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
|
21
|
-
const
|
|
22
|
-
const
|
|
23
|
-
const
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
},
|