eslint-plugin-smarthr 0.3.9 → 0.3.10
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 +9 -0
- package/libs/format_styled_components.js +28 -1
- package/package.json +1 -1
- package/rules/a11y-anchor-has-href-attribute/README.md +13 -1
- package/rules/a11y-anchor-has-href-attribute/index.js +35 -10
- package/rules/a11y-clickable-element-has-text/index.js +7 -1
- package/rules/a11y-heading-in-sectioning-content/index.js +44 -17
- package/rules/a11y-image-has-alt-attribute/README.md +15 -1
- package/rules/a11y-image-has-alt-attribute/index.js +33 -4
- package/rules/a11y-input-has-name-attribute/README.md +13 -1
- package/rules/a11y-input-has-name-attribute/index.js +27 -8
- package/rules/require-barrel-import/index.js +59 -53
- package/test/a11y-anchor-has-href-attribute.js +21 -18
- package/test/a11y-clickable-element-has-text.js +18 -0
- package/test/a11y-heading-in-sectioning-content.js +24 -0
- package/test/a11y-image-has-alt-attribute.js +22 -1
- package/test/a11y-input-has-name-attribute.js +9 -0
- package/test/best-practice-for-date.js +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,15 @@
|
|
|
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
|
+
### [0.3.10](https://github.com/kufu/eslint-plugin-smarthr/compare/v0.3.9...v0.3.10) (2023-09-20)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* a11y-heading-in-sectioning-contentでSectioningContentと予想される名前だがそれらの拡張ではないコンポーネントはエラーとする ([#77](https://github.com/kufu/eslint-plugin-smarthr/issues/77)) ([f7248d5](https://github.com/kufu/eslint-plugin-smarthr/commit/f7248d597cb06ba0bab1d3f0d51956efefd04aac))
|
|
11
|
+
* a11y-xxx-has-yyy-attributeにcheckTypeオプションを追加 ([#81](https://github.com/kufu/eslint-plugin-smarthr/issues/81)) ([94a511a](https://github.com/kufu/eslint-plugin-smarthr/commit/94a511a412e62431282eb1980941862313dfb777))
|
|
12
|
+
* a11y系ruleにstyled-componentsで既存のコンポーネントなどを拡張する際、誤検知が発生しそうな名称が設定されている場合はエラーにする機能を追加 ([#80](https://github.com/kufu/eslint-plugin-smarthr/issues/80)) ([727ff3f](https://github.com/kufu/eslint-plugin-smarthr/commit/727ff3fc6116fca017f8c3a3e62af569b76863da))
|
|
13
|
+
|
|
5
14
|
### [0.3.9](https://github.com/kufu/eslint-plugin-smarthr/compare/v0.3.8...v0.3.9) (2023-09-04)
|
|
6
15
|
|
|
7
16
|
|
|
@@ -3,8 +3,13 @@ const STYLED_COMPONENTS = `${STYLED_COMPONENTS_METHOD}-components`
|
|
|
3
3
|
|
|
4
4
|
const findInvalidImportNameNode = (s) => s.type === 'ImportDefaultSpecifier' && s.local.name !== STYLED_COMPONENTS_METHOD
|
|
5
5
|
|
|
6
|
-
const generateTagFormatter = ({ context, EXPECTED_NAMES }) => {
|
|
6
|
+
const generateTagFormatter = ({ context, EXPECTED_NAMES, UNEXPECTED_NAMES }) => {
|
|
7
7
|
const entriesesTagNames = Object.entries(EXPECTED_NAMES).map(([b, e]) => [ new RegExp(b), new RegExp(e) ])
|
|
8
|
+
const entriesesUnTagNames = UNEXPECTED_NAMES ? Object.entries(UNEXPECTED_NAMES).map(([b, e]) => {
|
|
9
|
+
const [ auctualE, messageTemplate ] = Array.isArray(e) ? e : [e, '']
|
|
10
|
+
|
|
11
|
+
return [ new RegExp(b), new RegExp(auctualE), messageTemplate ]
|
|
12
|
+
}) : []
|
|
8
13
|
|
|
9
14
|
return {
|
|
10
15
|
ImportDeclaration: (node) => {
|
|
@@ -63,6 +68,28 @@ const generateTagFormatter = ({ context, EXPECTED_NAMES }) => {
|
|
|
63
68
|
});
|
|
64
69
|
}
|
|
65
70
|
})
|
|
71
|
+
|
|
72
|
+
entriesesUnTagNames.forEach(([b, e, m]) => {
|
|
73
|
+
const matcher = extended.match(e)
|
|
74
|
+
|
|
75
|
+
if (matcher && !base.match(b)) {
|
|
76
|
+
const expected = matcher[1]
|
|
77
|
+
const isBareTag = base === base.toLowerCase()
|
|
78
|
+
const sampleFixBase = `styled${isBareTag ? `.${base}` : `(${base})`}`
|
|
79
|
+
|
|
80
|
+
context.report({
|
|
81
|
+
node,
|
|
82
|
+
message: m ? m
|
|
83
|
+
.replaceAll('{{extended}}', extended)
|
|
84
|
+
.replaceAll('{{expected}}', expected) : `${extended} は ${b.toString()} にmatchする名前のコンポーネントを拡張することを期待している名称になっています
|
|
85
|
+
- ${extended} の名称の末尾が"${expected}" という文字列ではない状態にしつつ、"${base}"を継承していることをわかる名称に変更してください
|
|
86
|
+
- もしくは"${base}"を"${extended}"の継承元であることがわかるような${isBareTag ? '適切なタグや別コンポーネントに差し替えてください' : '名称に変更するか、適切な別コンポーネントに差し替えてください'}
|
|
87
|
+
- 修正例1: const ${extended.replace(expected, '')}Xxxx = ${sampleFixBase}
|
|
88
|
+
- 修正例2: const ${extended}Xxxx = ${sampleFixBase}
|
|
89
|
+
- 修正例3: const ${extended} = styled(Xxxx${expected})`
|
|
90
|
+
})
|
|
91
|
+
}
|
|
92
|
+
})
|
|
66
93
|
}
|
|
67
94
|
},
|
|
68
95
|
}
|
package/package.json
CHANGED
|
@@ -6,13 +6,17 @@
|
|
|
6
6
|
- URL遷移を行う場合、hrefが設定されていないとキーボード操作やコンテキストメニューからの遷移ができなくなります
|
|
7
7
|
- これらの操作は href属性を参照します
|
|
8
8
|
- 無効化されたリンクであることを表したい場合 `href={undefined}` を設定してください
|
|
9
|
+
- checkTypeオプションに 'smart' を指定することで spread attributeが設定されている場合はcorrectに出来ます。
|
|
9
10
|
|
|
10
11
|
## rules
|
|
11
12
|
|
|
12
13
|
```js
|
|
13
14
|
{
|
|
14
15
|
rules: {
|
|
15
|
-
'smarthr/a11y-anchor-has-href-attribute':
|
|
16
|
+
'smarthr/a11y-anchor-has-href-attribute': [
|
|
17
|
+
'error', // 'warn', 'off'
|
|
18
|
+
// { checkType: 'always' } /* 'always' || 'smart' */
|
|
19
|
+
]
|
|
16
20
|
},
|
|
17
21
|
}
|
|
18
22
|
```
|
|
@@ -24,6 +28,10 @@
|
|
|
24
28
|
<XxxAnchor>any</XxxAnchor>
|
|
25
29
|
<XxxLink>any</XxxLink>
|
|
26
30
|
<XxxLink href>any</XxxLink>
|
|
31
|
+
|
|
32
|
+
// checkType: 'always'
|
|
33
|
+
<XxxAnchor {...args} />
|
|
34
|
+
<XxxLink {...args} any="any" />
|
|
27
35
|
```
|
|
28
36
|
|
|
29
37
|
## ✅ Correct
|
|
@@ -38,4 +46,8 @@
|
|
|
38
46
|
|
|
39
47
|
// react-router-domを利用している場合
|
|
40
48
|
<Link to={hoge}>any</Link>
|
|
49
|
+
|
|
50
|
+
// checkType: 'smart'
|
|
51
|
+
<XxxAnchor {...args} />
|
|
52
|
+
<XxxLink {...args} any="any" />
|
|
41
53
|
```
|
|
@@ -28,22 +28,35 @@ const EXPECTED_NAMES = {
|
|
|
28
28
|
'^a$': '(Anchor|Link)$',
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
const UNEXPECTED_NAMES = {
|
|
32
|
+
'(Anchor|^a)$': '(Anchor)$',
|
|
33
|
+
'(Link|^a)$': '(Link)$',
|
|
34
|
+
}
|
|
35
|
+
|
|
31
36
|
const REGEX_TARGET = /(Anchor|Link|^a)$/
|
|
32
|
-
const check = (node) => {
|
|
33
|
-
const result = baseCheck(node)
|
|
37
|
+
const check = (node, checkType) => {
|
|
38
|
+
const result = baseCheck(node, checkType)
|
|
34
39
|
|
|
35
|
-
return result && ((OPTION.nextjs && !nextCheck(node)) || (OPTION.react_router && !reactRouterCheck(node))) ? null : result
|
|
40
|
+
return result && ((OPTION.nextjs && !nextCheck(node, checkType)) || (OPTION.react_router && !reactRouterCheck(node))) ? null : result
|
|
36
41
|
}
|
|
37
|
-
const baseCheck = (node) => {
|
|
42
|
+
const baseCheck = (node, checkType) => {
|
|
38
43
|
const nodeName = node.name.name || ''
|
|
39
44
|
|
|
40
|
-
|
|
45
|
+
if (
|
|
46
|
+
nodeName.match(REGEX_TARGET) &&
|
|
47
|
+
checkExistAttribute(node, findHrefAttribute) &&
|
|
48
|
+
(checkType !== 'smart' || !node.attributes.some(findSpreadAttr))
|
|
49
|
+
) {
|
|
50
|
+
return nodeName
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return false
|
|
41
54
|
}
|
|
42
|
-
const nextCheck = (node) => {
|
|
55
|
+
const nextCheck = (node, checkType) => {
|
|
43
56
|
// HINT: next/link で `Link>a` という構造がありえるので直上のJSXElementを調べる
|
|
44
57
|
const target = node.parent.parent.openingElement
|
|
45
58
|
|
|
46
|
-
return target ? baseCheck(target) : false
|
|
59
|
+
return target ? baseCheck(target, checkType) : false
|
|
47
60
|
}
|
|
48
61
|
const reactRouterCheck = (node) => checkExistAttribute(node, findToAttribute)
|
|
49
62
|
|
|
@@ -57,6 +70,7 @@ const checkExistAttribute = (node, find) => {
|
|
|
57
70
|
)
|
|
58
71
|
}
|
|
59
72
|
const isNullTextHref = (attr) => attr.type === 'Literal' && (attr.value === '' || attr.value === '#')
|
|
73
|
+
const findSpreadAttr = (a) => a.type === 'JSXSpreadAttribute'
|
|
60
74
|
|
|
61
75
|
const findHrefAttribute = (a) => a.name?.name == 'href'
|
|
62
76
|
const findToAttribute = (a) => a.name?.name == 'to'
|
|
@@ -69,7 +83,15 @@ const MESSAGE_SUFFIX = ` に href 属性を正しく設定してください
|
|
|
69
83
|
- リンクが存在せず無効化されていることを表したい場合、href属性に undefined を設定してください
|
|
70
84
|
- button要素のdisabled属性が設定された場合に相当します`
|
|
71
85
|
|
|
72
|
-
const SCHEMA = [
|
|
86
|
+
const SCHEMA = [
|
|
87
|
+
{
|
|
88
|
+
type: 'object',
|
|
89
|
+
properties: {
|
|
90
|
+
checkType: { type: 'string', enum: ['always', 'smart'], default: 'always' },
|
|
91
|
+
},
|
|
92
|
+
additionalProperties: false,
|
|
93
|
+
}
|
|
94
|
+
]
|
|
73
95
|
|
|
74
96
|
module.exports = {
|
|
75
97
|
meta: {
|
|
@@ -77,10 +99,13 @@ module.exports = {
|
|
|
77
99
|
schema: SCHEMA,
|
|
78
100
|
},
|
|
79
101
|
create(context) {
|
|
102
|
+
const option = context.options[0] || {}
|
|
103
|
+
const checkType = option.checkType || 'always'
|
|
104
|
+
|
|
80
105
|
return {
|
|
81
|
-
...generateTagFormatter({ context, EXPECTED_NAMES }),
|
|
106
|
+
...generateTagFormatter({ context, EXPECTED_NAMES, UNEXPECTED_NAMES }),
|
|
82
107
|
JSXOpeningElement: (node) => {
|
|
83
|
-
const nodeName = check(node)
|
|
108
|
+
const nodeName = check(node, checkType)
|
|
84
109
|
|
|
85
110
|
if (nodeName) {
|
|
86
111
|
context.report({
|
|
@@ -20,6 +20,12 @@ const EXPECTED_NAMES = {
|
|
|
20
20
|
'^a$': '(Anchor|Link)$',
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
const UNEXPECTED_NAMES = {
|
|
24
|
+
'(B|^b)utton$': '(Button)$',
|
|
25
|
+
'(Anchor|^a)$': '(Anchor)$',
|
|
26
|
+
'(Link|^a)$': '(Link)$',
|
|
27
|
+
}
|
|
28
|
+
|
|
23
29
|
const REGEX_NLSP = /^\s*\n+\s*$/
|
|
24
30
|
const REGEX_CLICKABLE_ELEMENT = /^(a|(.*?)Anchor(Button)?|(.*?)Link|(b|B)utton)$/
|
|
25
31
|
const REGEX_SMARTHR_LOGO = /SmartHRLogo$/
|
|
@@ -47,7 +53,7 @@ module.exports = {
|
|
|
47
53
|
const componentsWithText = option.componentsWithText || []
|
|
48
54
|
|
|
49
55
|
return {
|
|
50
|
-
...generateTagFormatter({ context, EXPECTED_NAMES }),
|
|
56
|
+
...generateTagFormatter({ context, EXPECTED_NAMES, UNEXPECTED_NAMES }),
|
|
51
57
|
JSXElement: (parentNode) => {
|
|
52
58
|
// HINT: 閉じタグが存在しない === テキストノードが存在しない
|
|
53
59
|
if (!parentNode.closingElement) {
|
|
@@ -12,6 +12,30 @@ const EXPECTED_NAMES = {
|
|
|
12
12
|
'ModelessDialog$': 'ModelessDialog$',
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
const unexpectedMessageTemplate = `{{extended}} は smarthr-ui/{{expected}} をextendすることを期待する名称になっています
|
|
16
|
+
- childrenにHeadingを含まない場合、コンポーネントの名称から"{{expected}}"を取り除いてください
|
|
17
|
+
- childrenにHeadingを含み、アウトラインの範囲を指定するためのコンポーネントならば、smarthr-ui/{{expected}}をexendしてください
|
|
18
|
+
- "styled(Xxxx)" 形式の場合、拡張元であるXxxxコンポーネントの名称の末尾に"{{expected}}"を設定し、そのコンポーネント内でsmarthr-ui/{{expected}}を利用してください`
|
|
19
|
+
const UNEXPECTED_NAMES = {
|
|
20
|
+
'(Heading|^h(1|2|3|4|5|6))$': '(Heading)$',
|
|
21
|
+
'(A|^a)rticle$': [
|
|
22
|
+
'(Article)$',
|
|
23
|
+
unexpectedMessageTemplate,
|
|
24
|
+
],
|
|
25
|
+
'(A|^a)side$': [
|
|
26
|
+
'(Aside)$',
|
|
27
|
+
unexpectedMessageTemplate,
|
|
28
|
+
],
|
|
29
|
+
'(N|^n)av$': [
|
|
30
|
+
'(Nav)$',
|
|
31
|
+
unexpectedMessageTemplate,
|
|
32
|
+
],
|
|
33
|
+
'(S|^s)ection$': [
|
|
34
|
+
'(Section)$',
|
|
35
|
+
unexpectedMessageTemplate,
|
|
36
|
+
],
|
|
37
|
+
}
|
|
38
|
+
|
|
15
39
|
const headingRegex = /((^h(1|2|3|4|5|6))|Heading)$/
|
|
16
40
|
const declaratorHeadingRegex = /Heading$/
|
|
17
41
|
const sectioningRegex = /((A(rticle|side))|Nav|Section|^SectioningFragment)$/
|
|
@@ -28,6 +52,24 @@ const rootHeadingMessage = `${headingMessage}
|
|
|
28
52
|
const pageHeadingMessage = 'smarthr-ui/PageHeading が同一ファイル内に複数存在しています。PageHeadingはh1タグを出力するため最も重要な見出しにのみ利用してください。'
|
|
29
53
|
const pageHeadingInSectionMessage = 'smarthr-ui/PageHeadingはsmarthr-uiのArticle, Aside, Nav, Sectionで囲まないでください。囲んでしまうとページ全体の見出しではなくなってしまいます。'
|
|
30
54
|
|
|
55
|
+
const VariableDeclaratorBareToSHR = (context, node) => {
|
|
56
|
+
if (!node.init) {
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const tag = node.init.tag || node.init
|
|
61
|
+
|
|
62
|
+
if (tag.object?.name === 'styled') {
|
|
63
|
+
const message = reportMessageBareToSHR(tag.property.name, true)
|
|
64
|
+
|
|
65
|
+
if (message) {
|
|
66
|
+
context.report({
|
|
67
|
+
node,
|
|
68
|
+
message,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
31
73
|
const reportMessageBareToSHR = (tagName, visibleExample) => {
|
|
32
74
|
const matcher = tagName.match(bareTagRegex)
|
|
33
75
|
|
|
@@ -67,26 +109,11 @@ module.exports = {
|
|
|
67
109
|
create(context) {
|
|
68
110
|
let h1s = []
|
|
69
111
|
let sections = []
|
|
70
|
-
let { VariableDeclarator, ...formatter } = generateTagFormatter({ context, EXPECTED_NAMES })
|
|
112
|
+
let { VariableDeclarator, ...formatter } = generateTagFormatter({ context, EXPECTED_NAMES, UNEXPECTED_NAMES, unexpectedMessageTemplate })
|
|
71
113
|
|
|
72
114
|
formatter.VariableDeclarator = (node) => {
|
|
73
115
|
VariableDeclarator(node)
|
|
74
|
-
|
|
75
|
-
return
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const tag = node.init.tag || node.init
|
|
79
|
-
|
|
80
|
-
if (tag.object?.name === 'styled') {
|
|
81
|
-
const message = reportMessageBareToSHR(tag.property.name, true)
|
|
82
|
-
|
|
83
|
-
if (message) {
|
|
84
|
-
context.report({
|
|
85
|
-
node,
|
|
86
|
-
message,
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
}
|
|
116
|
+
VariableDeclaratorBareToSHR(context, node)
|
|
90
117
|
}
|
|
91
118
|
|
|
92
119
|
return {
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
# smarthr/a11y-image-has-alt-attribute
|
|
2
2
|
|
|
3
3
|
- 画像やアイコンにalt属性を設定することを強制するルールです
|
|
4
|
+
- checkTypeオプションに 'smart' を指定することで spread attributeが設定されている場合はcorrectに出来ます。
|
|
4
5
|
|
|
5
6
|
## rules
|
|
6
7
|
|
|
7
8
|
```js
|
|
8
9
|
{
|
|
9
10
|
rules: {
|
|
10
|
-
'smarthr/a11y-image-has-alt-attribute':
|
|
11
|
+
'smarthr/a11y-image-has-alt-attribute': [
|
|
12
|
+
'error', // 'warn', 'off'
|
|
13
|
+
// { checkType: 'always' } /* 'always' || 'smart' */
|
|
14
|
+
]
|
|
11
15
|
},
|
|
12
16
|
}
|
|
13
17
|
```
|
|
@@ -26,6 +30,11 @@
|
|
|
26
30
|
<Icon />
|
|
27
31
|
```
|
|
28
32
|
```jsx
|
|
33
|
+
// checkType: 'always'
|
|
34
|
+
<XxxImage {...args} />
|
|
35
|
+
<YyyIcon {...args} any="any" />
|
|
36
|
+
```
|
|
37
|
+
```jsx
|
|
29
38
|
import styled from 'styled-components'
|
|
30
39
|
|
|
31
40
|
const StyledHoge = styled.img``
|
|
@@ -47,6 +56,11 @@ const StyledPiyo = styled(Icon)``
|
|
|
47
56
|
<Icon alt="message" />
|
|
48
57
|
```
|
|
49
58
|
```jsx
|
|
59
|
+
// checkType: 'smart'
|
|
60
|
+
<XxxImage {...args} />
|
|
61
|
+
<YyyIcon {...args} any="any" />
|
|
62
|
+
```
|
|
63
|
+
```jsx
|
|
50
64
|
import styled from 'styled-components'
|
|
51
65
|
|
|
52
66
|
const StyledImage = styled.img``
|
|
@@ -7,9 +7,16 @@ const EXPECTED_NAMES = {
|
|
|
7
7
|
'^(img|svg)$': '(Img|Image|Icon)$',
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
const UNEXPECTED_NAMES = {
|
|
11
|
+
'(Img|^(img|svg))$': '(Img)$',
|
|
12
|
+
'(Image|^(img|svg))$': '(Image)$',
|
|
13
|
+
'(Icon|^(img|svg))$': '(Icon)$',
|
|
14
|
+
}
|
|
15
|
+
|
|
10
16
|
const REGEX_IMG = /(img|image)$/i // HINT: Iconは別途テキストが存在する場合が多いためチェックの対象外とする
|
|
11
17
|
|
|
12
18
|
const findAltAttr = (a) => a.name?.name === 'alt'
|
|
19
|
+
const findSpreadAttr = (a) => a.type === 'JSXSpreadAttribute'
|
|
13
20
|
const isWithinSvgJsxElement = (node) => {
|
|
14
21
|
if (
|
|
15
22
|
node.type === 'JSXElement' &&
|
|
@@ -28,14 +35,27 @@ const MESSAGE_NOT_EXIST_ALT = `画像にはalt属性を指定してください
|
|
|
28
35
|
const MESSAGE_NULL_ALT = `画像の情報をテキストにした代替テキスト('alt')を設定してください。
|
|
29
36
|
- 装飾目的の画像など、alt属性に指定すべき文字がない場合は背景画像にすることを検討してください。`
|
|
30
37
|
|
|
38
|
+
const SCHEMA = [
|
|
39
|
+
{
|
|
40
|
+
type: 'object',
|
|
41
|
+
properties: {
|
|
42
|
+
checkType: { type: 'string', enum: ['always', 'smart'], default: 'always' },
|
|
43
|
+
},
|
|
44
|
+
additionalProperties: false,
|
|
45
|
+
}
|
|
46
|
+
]
|
|
47
|
+
|
|
31
48
|
module.exports = {
|
|
32
49
|
meta: {
|
|
33
50
|
type: 'problem',
|
|
34
|
-
schema:
|
|
51
|
+
schema: SCHEMA,
|
|
35
52
|
},
|
|
36
53
|
create(context) {
|
|
54
|
+
const option = context.options[0] || {}
|
|
55
|
+
const checkType = option.checkType || 'always'
|
|
56
|
+
|
|
37
57
|
return {
|
|
38
|
-
...generateTagFormatter({ context, EXPECTED_NAMES }),
|
|
58
|
+
...generateTagFormatter({ context, EXPECTED_NAMES, UNEXPECTED_NAMES }),
|
|
39
59
|
JSXOpeningElement: (node) => {
|
|
40
60
|
if (node.name.name) {
|
|
41
61
|
const matcher = node.name.name.match(REGEX_IMG)
|
|
@@ -46,7 +66,16 @@ module.exports = {
|
|
|
46
66
|
let message = ''
|
|
47
67
|
|
|
48
68
|
if (!alt) {
|
|
49
|
-
if (
|
|
69
|
+
if (
|
|
70
|
+
(
|
|
71
|
+
matcher.input !== 'image' ||
|
|
72
|
+
!isWithinSvgJsxElement(node.parent)
|
|
73
|
+
) &&
|
|
74
|
+
(
|
|
75
|
+
checkType !== 'smart' ||
|
|
76
|
+
!node.attributes.some(findSpreadAttr)
|
|
77
|
+
)
|
|
78
|
+
) {
|
|
50
79
|
message = MESSAGE_NOT_EXIST_ALT
|
|
51
80
|
}
|
|
52
81
|
} else if (alt.value.value === '') {
|
|
@@ -65,4 +94,4 @@ module.exports = {
|
|
|
65
94
|
}
|
|
66
95
|
},
|
|
67
96
|
}
|
|
68
|
-
module.exports.schema =
|
|
97
|
+
module.exports.schema = SCHEMA
|
|
@@ -4,13 +4,17 @@
|
|
|
4
4
|
- input は name を設定することでブラウザの補完機能が有効になる可能性が高まります。
|
|
5
5
|
- 補完機能はブラウザによって異なるため、補完される可能性が上がるよう、name には半角英数の小文字・大文字と一部記号(`_ , [, ]`)のみ利用可能です。
|
|
6
6
|
- input[type="radio"] は name を適切に設定することでラジオグループが確立され、キーボード操作しやすくなる等のメリットがあります。
|
|
7
|
+
- checkTypeオプションに 'smart' を指定することで spread attributeが設定されている場合はcorrectに出来ます。
|
|
7
8
|
|
|
8
9
|
## rules
|
|
9
10
|
|
|
10
11
|
```js
|
|
11
12
|
{
|
|
12
13
|
rules: {
|
|
13
|
-
'smarthr/a11y-input-has-name-attribute':
|
|
14
|
+
'smarthr/a11y-input-has-name-attribute': [
|
|
15
|
+
'error', // 'warn', 'off'
|
|
16
|
+
// { checkType: 'always' } /* 'always' || 'smart' */
|
|
17
|
+
]
|
|
14
18
|
},
|
|
15
19
|
}
|
|
16
20
|
```
|
|
@@ -23,6 +27,10 @@
|
|
|
23
27
|
<input type="text" />
|
|
24
28
|
<Textarea />
|
|
25
29
|
<Select />
|
|
30
|
+
|
|
31
|
+
// checkType: 'always'
|
|
32
|
+
<AnyInput {...args} />
|
|
33
|
+
<AnyInput {...args} any="any" />
|
|
26
34
|
```
|
|
27
35
|
|
|
28
36
|
|
|
@@ -42,6 +50,10 @@ const StyledPiyo = styled(RadioButton)``;
|
|
|
42
50
|
<input type="text" name="any" />
|
|
43
51
|
<Textarea name="some" />
|
|
44
52
|
<Select name="piyo" />
|
|
53
|
+
|
|
54
|
+
// checkType: 'smart'
|
|
55
|
+
<AnyInput {...args} />
|
|
56
|
+
<AnyInput {...args} any="any" />
|
|
45
57
|
```
|
|
46
58
|
|
|
47
59
|
```jsx
|
|
@@ -14,8 +14,10 @@ const EXPECTED_NAMES = {
|
|
|
14
14
|
const TARGET_TAG_NAME_REGEX = new RegExp(`(${Object.keys(EXPECTED_NAMES).join('|')})`)
|
|
15
15
|
const INPUT_NAME_REGEX = /^[a-zA-Z0-9_\[\]]+$/
|
|
16
16
|
const INPUT_TAG_REGEX = /(i|I)nput$/
|
|
17
|
+
const RADIO_BUTTON_REGEX = /RadioButton$/
|
|
17
18
|
|
|
18
19
|
const findNameAttr = (a) => a?.name?.name === 'name'
|
|
20
|
+
const findSpreadAttr = (a) => a.type === 'JSXSpreadAttribute'
|
|
19
21
|
const findRadioInput = (a) => a.name?.name === 'type' && a.value.value === 'radio'
|
|
20
22
|
|
|
21
23
|
const MESSAGE_PART_FORMAT = `"${INPUT_NAME_REGEX.toString()}"にmatchするフォーマットで命名してください`
|
|
@@ -27,7 +29,15 @@ const MESSAGE_UNDEFINED_FOR_RADIO = `にグループとなる他のinput[radio]
|
|
|
27
29
|
const MESSAGE_UNDEFINED_FOR_NOT_RADIO = `にname属性を指定してください${MESSAGE_UNDEFINED_NAME_PART}`
|
|
28
30
|
const MESSAGE_NAME_FORMAT_SUFFIX = `はブラウザの自動補完が適切に行えない可能性があるため${MESSAGE_PART_FORMAT}`
|
|
29
31
|
|
|
30
|
-
const SCHEMA = [
|
|
32
|
+
const SCHEMA = [
|
|
33
|
+
{
|
|
34
|
+
type: 'object',
|
|
35
|
+
properties: {
|
|
36
|
+
checkType: { type: 'string', enum: ['always', 'smart'], default: 'always' },
|
|
37
|
+
},
|
|
38
|
+
additionalProperties: false,
|
|
39
|
+
}
|
|
40
|
+
]
|
|
31
41
|
|
|
32
42
|
module.exports = {
|
|
33
43
|
meta: {
|
|
@@ -35,6 +45,9 @@ module.exports = {
|
|
|
35
45
|
schema: SCHEMA,
|
|
36
46
|
},
|
|
37
47
|
create(context) {
|
|
48
|
+
const option = context.options[0] || {}
|
|
49
|
+
const checkType = option.checkType || 'always'
|
|
50
|
+
|
|
38
51
|
return {
|
|
39
52
|
...generateTagFormatter({ context, EXPECTED_NAMES }),
|
|
40
53
|
JSXOpeningElement: (node) => {
|
|
@@ -44,14 +57,20 @@ module.exports = {
|
|
|
44
57
|
const nameAttr = node.attributes.find(findNameAttr)
|
|
45
58
|
|
|
46
59
|
if (!nameAttr) {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
60
|
+
if (
|
|
61
|
+
node.attributes.length === 0 ||
|
|
62
|
+
checkType !== 'smart' ||
|
|
63
|
+
!node.attributes.some(findSpreadAttr)
|
|
64
|
+
) {
|
|
65
|
+
const isRadio =
|
|
66
|
+
nodeName.match(RADIO_BUTTON_REGEX) ||
|
|
67
|
+
(nodeName.match(INPUT_TAG_REGEX) && node.attributes.some(findRadioInput));
|
|
50
68
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
69
|
+
context.report({
|
|
70
|
+
node,
|
|
71
|
+
message: `${nodeName} ${isRadio ? MESSAGE_UNDEFINED_FOR_RADIO : MESSAGE_UNDEFINED_FOR_NOT_RADIO}`,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
55
74
|
} else {
|
|
56
75
|
const nameValue = nameAttr.value?.value || ''
|
|
57
76
|
|
|
@@ -1,19 +1,55 @@
|
|
|
1
1
|
const path = require('path')
|
|
2
2
|
const fs = require('fs')
|
|
3
3
|
const { replacePaths, rootPath } = require('../../libs/common')
|
|
4
|
+
|
|
5
|
+
const SCHEMA = [
|
|
6
|
+
{
|
|
7
|
+
type: 'object',
|
|
8
|
+
properties: {
|
|
9
|
+
allowedImports: {
|
|
10
|
+
type: 'object',
|
|
11
|
+
patternProperties: {
|
|
12
|
+
'.+': {
|
|
13
|
+
type: 'object',
|
|
14
|
+
patternProperties: {
|
|
15
|
+
'.+': {
|
|
16
|
+
type: ['boolean', 'array' ],
|
|
17
|
+
items: {
|
|
18
|
+
type: 'string',
|
|
19
|
+
},
|
|
20
|
+
additionalProperties: false
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
additionalProperties: true,
|
|
26
|
+
},
|
|
27
|
+
ignores: { type: 'array', items: { type: 'string' }, default: [] },
|
|
28
|
+
},
|
|
29
|
+
additionalProperties: false,
|
|
30
|
+
}
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
const entriedReplacePaths = Object.entries(replacePaths)
|
|
34
|
+
const CWD = process.cwd()
|
|
35
|
+
const REGEX_UNNECESSARY_SLASH = /(\/)+/g
|
|
36
|
+
const REGEX_ROOT_PATH = new RegExp(`^${rootPath}/index\.`)
|
|
37
|
+
const REGEX_INDEX_FILE = /\/index\.(ts|js)x?$/
|
|
38
|
+
const TARGET_EXTS = ['ts', 'tsx', 'js', 'jsx']
|
|
39
|
+
|
|
4
40
|
const calculateAbsoluteImportPath = (source) => {
|
|
5
41
|
if (source[0] === '/') {
|
|
6
42
|
return source
|
|
7
43
|
}
|
|
8
44
|
|
|
9
|
-
return
|
|
45
|
+
return entriedReplacePaths.reduce((prev, [key, values]) => {
|
|
10
46
|
if (source === prev) {
|
|
47
|
+
const regexp = new RegExp(`^${key}(.+)$`)
|
|
48
|
+
|
|
11
49
|
return values.reduce((p, v) => {
|
|
12
50
|
if (prev === p) {
|
|
13
|
-
const regexp = new RegExp(`^${key}(.+)$`)
|
|
14
|
-
|
|
15
51
|
if (prev.match(regexp)) {
|
|
16
|
-
return p.replace(regexp, `${path.resolve(`${
|
|
52
|
+
return p.replace(regexp, `${path.resolve(`${CWD}/${v}`)}/$1`)
|
|
17
53
|
}
|
|
18
54
|
}
|
|
19
55
|
|
|
@@ -25,14 +61,14 @@ const calculateAbsoluteImportPath = (source) => {
|
|
|
25
61
|
}, source)
|
|
26
62
|
}
|
|
27
63
|
const calculateReplacedImportPath = (source) => {
|
|
28
|
-
return
|
|
64
|
+
return entriedReplacePaths.reduce((prev, [key, values]) => {
|
|
29
65
|
if (source === prev) {
|
|
30
66
|
return values.reduce((p, v) => {
|
|
31
67
|
if (prev === p) {
|
|
32
|
-
const regexp = new RegExp(`^${path.resolve(`${
|
|
68
|
+
const regexp = new RegExp(`^${path.resolve(`${CWD}/${v}`)}(.+)$`)
|
|
33
69
|
|
|
34
70
|
if (prev.match(regexp)) {
|
|
35
|
-
return p.replace(regexp, `${key}/$1`).replace(
|
|
71
|
+
return p.replace(regexp, `${key}/$1`).replace(REGEX_UNNECESSARY_SLASH, '/')
|
|
36
72
|
}
|
|
37
73
|
}
|
|
38
74
|
|
|
@@ -43,34 +79,9 @@ const calculateReplacedImportPath = (source) => {
|
|
|
43
79
|
return prev
|
|
44
80
|
}, source)
|
|
45
81
|
}
|
|
46
|
-
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
type: 'object',
|
|
50
|
-
properties: {
|
|
51
|
-
allowedImports: {
|
|
52
|
-
type: 'object',
|
|
53
|
-
patternProperties: {
|
|
54
|
-
'.+': {
|
|
55
|
-
type: 'object',
|
|
56
|
-
patternProperties: {
|
|
57
|
-
'.+': {
|
|
58
|
-
type: ['boolean', 'array' ],
|
|
59
|
-
items: {
|
|
60
|
-
type: 'string',
|
|
61
|
-
},
|
|
62
|
-
additionalProperties: false
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
},
|
|
66
|
-
},
|
|
67
|
-
additionalProperties: true,
|
|
68
|
-
},
|
|
69
|
-
ignores: { type: 'array', items: { type: 'string' }, default: [] },
|
|
70
|
-
},
|
|
71
|
-
additionalProperties: false,
|
|
72
|
-
}
|
|
73
|
-
]
|
|
82
|
+
|
|
83
|
+
const pickImportedName = (s) => s.imported?.name
|
|
84
|
+
const findExistsSync = (p) => fs.existsSync(p)
|
|
74
85
|
|
|
75
86
|
module.exports = {
|
|
76
87
|
meta: {
|
|
@@ -81,16 +92,13 @@ module.exports = {
|
|
|
81
92
|
const option = context.options[0] || {}
|
|
82
93
|
const filename = context.getFilename()
|
|
83
94
|
|
|
84
|
-
if (
|
|
95
|
+
if (option.ignores && option.ignores.some((i) => !!filename.match(new RegExp(i)))) {
|
|
85
96
|
return {}
|
|
86
97
|
}
|
|
87
98
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
return d.join('/')
|
|
93
|
-
})()
|
|
99
|
+
let d = filename.split('/')
|
|
100
|
+
d.pop()
|
|
101
|
+
const dir = d.join('/')
|
|
94
102
|
const targetPathRegexs = Object.keys(option?.allowedImports || {})
|
|
95
103
|
const targetAllowedImports = targetPathRegexs.filter((regex) => !!filename.match(new RegExp(regex)))
|
|
96
104
|
|
|
@@ -105,7 +113,7 @@ module.exports = {
|
|
|
105
113
|
|
|
106
114
|
targetModules.forEach((targetModule) => {
|
|
107
115
|
const allowedModules = allowedOption[targetModule] || true
|
|
108
|
-
const actualTarget = targetModule[0] !== '.' ? targetModule : path.resolve(`${
|
|
116
|
+
const actualTarget = targetModule[0] !== '.' ? targetModule : path.resolve(`${CWD}/${targetModule}`)
|
|
109
117
|
let sourceValue = node.source.value
|
|
110
118
|
|
|
111
119
|
if (actualTarget[0] === '/') {
|
|
@@ -116,21 +124,19 @@ module.exports = {
|
|
|
116
124
|
return
|
|
117
125
|
}
|
|
118
126
|
|
|
119
|
-
|
|
120
127
|
if (!Array.isArray(allowedModules)) {
|
|
121
128
|
isDenyPath = true
|
|
122
129
|
deniedModules.push(true)
|
|
123
130
|
} else {
|
|
124
|
-
deniedModules.push(node.specifiers.map(
|
|
131
|
+
deniedModules.push(node.specifiers.map(pickImportedName).filter(i => allowedModules.indexOf(i) == -1))
|
|
125
132
|
}
|
|
126
133
|
})
|
|
127
134
|
})
|
|
128
135
|
|
|
129
|
-
if (
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
if (!isDenyPath && deniedModules.length === 1 && deniedModules[0].length === 0) {
|
|
136
|
+
if (
|
|
137
|
+
isDenyPath && deniedModules[0] === true ||
|
|
138
|
+
!isDenyPath && deniedModules.length === 1 && deniedModules[0].length === 0
|
|
139
|
+
) {
|
|
134
140
|
return
|
|
135
141
|
}
|
|
136
142
|
|
|
@@ -167,15 +173,15 @@ module.exports = {
|
|
|
167
173
|
break
|
|
168
174
|
}
|
|
169
175
|
|
|
170
|
-
barrel = TARGET_EXTS.map((e) => `${sourceValue}/index.${e}`).find(
|
|
176
|
+
barrel = TARGET_EXTS.map((e) => `${sourceValue}/index.${e}`).find(findExistsSync) || barrel
|
|
171
177
|
|
|
172
178
|
sources.pop()
|
|
173
179
|
sourceValue = sources.join('/')
|
|
174
180
|
}
|
|
175
181
|
|
|
176
|
-
if (barrel && !barrel.match(
|
|
182
|
+
if (barrel && !barrel.match(REGEX_ROOT_PATH)) {
|
|
177
183
|
barrel = calculateReplacedImportPath(barrel)
|
|
178
|
-
const noExt = barrel.replace(
|
|
184
|
+
const noExt = barrel.replace(REGEX_INDEX_FILE, '')
|
|
179
185
|
deniedModules = [...new Set(deniedModules.flat())]
|
|
180
186
|
|
|
181
187
|
context.report({
|
|
@@ -29,30 +29,31 @@ ruleTester.run('a11y-anchor-has-href-attribute', rule, {
|
|
|
29
29
|
{ code: 'const HogeLink = styled.a``' },
|
|
30
30
|
{ code: 'const HogeAnchor = styled(Anchor)``' },
|
|
31
31
|
{ code: 'const HogeLink = styled(Link)``' },
|
|
32
|
-
{
|
|
33
|
-
|
|
34
|
-
},
|
|
35
|
-
{
|
|
36
|
-
|
|
37
|
-
},
|
|
38
|
-
{
|
|
39
|
-
code: `<a href={undefined}>ほげ</a>`,
|
|
40
|
-
},
|
|
41
|
-
{
|
|
42
|
-
code: `<HogeAnchor href={hoge}>ほげ</HogeAnchor>`,
|
|
43
|
-
},
|
|
44
|
-
{
|
|
45
|
-
code: `<Link href="hoge">ほげ</Link>`,
|
|
46
|
-
},
|
|
47
|
-
{
|
|
48
|
-
code: `<Link href="#fuga">ほげ</Link>`,
|
|
49
|
-
},
|
|
32
|
+
{ code: `<a href="hoge">ほげ</a>` },
|
|
33
|
+
{ code: `<a href={hoge}>ほげ</a>` },
|
|
34
|
+
{ code: `<a href={undefined}>ほげ</a>` },
|
|
35
|
+
{ code: `<HogeAnchor href={hoge}>ほげ</HogeAnchor>` },
|
|
36
|
+
{ code: `<Link href="hoge">ほげ</Link>` },
|
|
37
|
+
{ code: `<Link href="#fuga">ほげ</Link>` },
|
|
38
|
+
{ code: '<AnyAnchor {...args1} />', options: [{ checkType: 'smart' }] },
|
|
50
39
|
],
|
|
51
40
|
invalid: [
|
|
52
41
|
{ code: `import hoge from 'styled-components'`, errors: [ { message: `styled-components をimportする際は、名称が"styled" となるようにしてください。例: "import styled from 'styled-components'"` } ] },
|
|
53
42
|
{ code: 'const Hoge = styled.a``', errors: [ { message: `Hogeを正規表現 "/(Anchor|Link)$/" がmatchする名称に変更してください` } ] },
|
|
54
43
|
{ code: 'const Hoge = styled(Anchor)``', errors: [ { message: `Hogeを正規表現 "/Anchor$/" がmatchする名称に変更してください` } ] },
|
|
55
44
|
{ code: 'const Hoge = styled(Link)``', errors: [ { message: `Hogeを正規表現 "/Link$/" がmatchする名称に変更してください` } ] },
|
|
45
|
+
{ code: 'const FugaAnchor = styled.div``', errors: [ { message: `FugaAnchor は /(Anchor|^a)$/ にmatchする名前のコンポーネントを拡張することを期待している名称になっています
|
|
46
|
+
- FugaAnchor の名称の末尾が"Anchor" という文字列ではない状態にしつつ、"div"を継承していることをわかる名称に変更してください
|
|
47
|
+
- もしくは"div"を"FugaAnchor"の継承元であることがわかるような適切なタグや別コンポーネントに差し替えてください
|
|
48
|
+
- 修正例1: const FugaXxxx = styled.div
|
|
49
|
+
- 修正例2: const FugaAnchorXxxx = styled.div
|
|
50
|
+
- 修正例3: const FugaAnchor = styled(XxxxAnchor)` } ] },
|
|
51
|
+
{ code: 'const FugaLink = styled.p``', errors: [ { message: `FugaLink は /(Link|^a)$/ にmatchする名前のコンポーネントを拡張することを期待している名称になっています
|
|
52
|
+
- FugaLink の名称の末尾が"Link" という文字列ではない状態にしつつ、"p"を継承していることをわかる名称に変更してください
|
|
53
|
+
- もしくは"p"を"FugaLink"の継承元であることがわかるような適切なタグや別コンポーネントに差し替えてください
|
|
54
|
+
- 修正例1: const FugaXxxx = styled.p
|
|
55
|
+
- 修正例2: const FugaLinkXxxx = styled.p
|
|
56
|
+
- 修正例3: const FugaLink = styled(XxxxLink)` } ] },
|
|
56
57
|
{ code: `<a></a>`, errors: [{ message: generateErrorText('a') }] },
|
|
57
58
|
{ code: `<a>hoge</a>`, errors: [{ message: generateErrorText('a') }] },
|
|
58
59
|
{ code: `<Anchor>hoge</Anchor>`, errors: [{ message: generateErrorText('Anchor') }] },
|
|
@@ -65,5 +66,7 @@ ruleTester.run('a11y-anchor-has-href-attribute', rule, {
|
|
|
65
66
|
{ code: `<HogeLink href={''}>hoge</HogeLink>`, errors: [{ message: generateErrorText('HogeLink') }] },
|
|
66
67
|
{ code: `<HogeLink href="#">hoge</HogeLink>`, errors: [{ message: generateErrorText('HogeLink') }] },
|
|
67
68
|
{ code: `<HogeLink href={'#'}>hoge</HogeLink>`, errors: [{ message: generateErrorText('HogeLink') }] },
|
|
69
|
+
{ code: '<AnyAnchor {...args1} />', errors: [{ message: generateErrorText('AnyAnchor') }] },
|
|
70
|
+
{ code: '<AnyAnchor {...args1} />', options: [{ checkType: 'always' }], errors: [{ message: generateErrorText('AnyAnchor') }] },
|
|
68
71
|
]
|
|
69
72
|
})
|
|
@@ -141,6 +141,24 @@ ruleTester.run('a11y-clickable-element-has-text', rule, {
|
|
|
141
141
|
{ code: 'const Piyo = styled(Anchor)(() => ``)', errors: [ { message: `Piyoを正規表現 "/Anchor$/" がmatchする名称に変更してください` } ] },
|
|
142
142
|
{ code: 'const Hoge = styled(Text)``', errors: [ { message: `Hogeを正規表現 "/Text$/" がmatchする名称に変更してください` } ] },
|
|
143
143
|
{ code: 'const Hoge = styled(HogeMessage)``', errors: [ { message: `Hogeを正規表現 "/Message$/" がmatchする名称に変更してください` } ] },
|
|
144
|
+
{ code: 'const StyledButton = styled.div``', errors: [ { message: `StyledButton は /(B|^b)utton$/ にmatchする名前のコンポーネントを拡張することを期待している名称になっています
|
|
145
|
+
- StyledButton の名称の末尾が"Button" という文字列ではない状態にしつつ、"div"を継承していることをわかる名称に変更してください
|
|
146
|
+
- もしくは"div"を"StyledButton"の継承元であることがわかるような適切なタグや別コンポーネントに差し替えてください
|
|
147
|
+
- 修正例1: const StyledXxxx = styled.div
|
|
148
|
+
- 修正例2: const StyledButtonXxxx = styled.div
|
|
149
|
+
- 修正例3: const StyledButton = styled(XxxxButton)` } ] },
|
|
150
|
+
{ code: 'const HogeAnchor = styled(Fuga)``', errors: [ { message: `HogeAnchor は /(Anchor|^a)$/ にmatchする名前のコンポーネントを拡張することを期待している名称になっています
|
|
151
|
+
- HogeAnchor の名称の末尾が"Anchor" という文字列ではない状態にしつつ、"Fuga"を継承していることをわかる名称に変更してください
|
|
152
|
+
- もしくは"Fuga"を"HogeAnchor"の継承元であることがわかるような名称に変更するか、適切な別コンポーネントに差し替えてください
|
|
153
|
+
- 修正例1: const HogeXxxx = styled(Fuga)
|
|
154
|
+
- 修正例2: const HogeAnchorXxxx = styled(Fuga)
|
|
155
|
+
- 修正例3: const HogeAnchor = styled(XxxxAnchor)` } ] },
|
|
156
|
+
{ code: 'const HogeLink = styled.p``', errors: [ { message: `HogeLink は /(Link|^a)$/ にmatchする名前のコンポーネントを拡張することを期待している名称になっています
|
|
157
|
+
- HogeLink の名称の末尾が"Link" という文字列ではない状態にしつつ、"p"を継承していることをわかる名称に変更してください
|
|
158
|
+
- もしくは"p"を"HogeLink"の継承元であることがわかるような適切なタグや別コンポーネントに差し替えてください
|
|
159
|
+
- 修正例1: const HogeXxxx = styled.p
|
|
160
|
+
- 修正例2: const HogeLinkXxxx = styled.p
|
|
161
|
+
- 修正例3: const HogeLink = styled(XxxxLink)` } ] },
|
|
144
162
|
{
|
|
145
163
|
code: `<a><img src="hoge.jpg" /></a>`,
|
|
146
164
|
errors: [{ message: defaultErrorMessage }]
|
|
@@ -58,6 +58,30 @@ ruleTester.run('a11y-heading-in-sectioning-content', rule, {
|
|
|
58
58
|
{ code: 'const StyledAside = styled.aside``', errors: [ { message: `"aside"を利用せず、smarthr-ui/Asideを拡張してください。Headingのレベルが自動計算されるようになります。(例: "styled.aside" -> "styled(Aside)")` } ] },
|
|
59
59
|
{ code: 'const StyledNav = styled.nav``', errors: [ { message: `"nav"を利用せず、smarthr-ui/Navを拡張してください。Headingのレベルが自動計算されるようになります。(例: "styled.nav" -> "styled(Nav)")` } ] },
|
|
60
60
|
{ code: 'const StyledSection = styled.section``', errors: [ { message: `"section"を利用せず、smarthr-ui/Sectionを拡張してください。Headingのレベルが自動計算されるようになります。(例: "styled.section" -> "styled(Section)")` } ] },
|
|
61
|
+
{ code: 'const StyledSection = styled.div``', errors: [ { message: `StyledSection は smarthr-ui/Section をextendすることを期待する名称になっています
|
|
62
|
+
- childrenにHeadingを含まない場合、コンポーネントの名称から"Section"を取り除いてください
|
|
63
|
+
- childrenにHeadingを含み、アウトラインの範囲を指定するためのコンポーネントならば、smarthr-ui/Sectionをexendしてください
|
|
64
|
+
- "styled(Xxxx)" 形式の場合、拡張元であるXxxxコンポーネントの名称の末尾に"Section"を設定し、そのコンポーネント内でsmarthr-ui/Sectionを利用してください` } ] },
|
|
65
|
+
{ code: 'const StyledArticle = styled(Hoge)``', errors: [ { message: `StyledArticle は smarthr-ui/Article をextendすることを期待する名称になっています
|
|
66
|
+
- childrenにHeadingを含まない場合、コンポーネントの名称から"Article"を取り除いてください
|
|
67
|
+
- childrenにHeadingを含み、アウトラインの範囲を指定するためのコンポーネントならば、smarthr-ui/Articleをexendしてください
|
|
68
|
+
- "styled(Xxxx)" 形式の場合、拡張元であるXxxxコンポーネントの名称の末尾に"Article"を設定し、そのコンポーネント内でsmarthr-ui/Articleを利用してください` } ] },
|
|
69
|
+
{ code: 'const StyledAside = styled(AsideXxxx)``', errors: [ { message: `StyledAside は smarthr-ui/Aside をextendすることを期待する名称になっています
|
|
70
|
+
- childrenにHeadingを含まない場合、コンポーネントの名称から"Aside"を取り除いてください
|
|
71
|
+
- childrenにHeadingを含み、アウトラインの範囲を指定するためのコンポーネントならば、smarthr-ui/Asideをexendしてください
|
|
72
|
+
- "styled(Xxxx)" 形式の場合、拡張元であるXxxxコンポーネントの名称の末尾に"Aside"を設定し、そのコンポーネント内でsmarthr-ui/Asideを利用してください` } ] },
|
|
73
|
+
{ code: 'const StyledHeading = styled(Hoge)``', errors: [ { message: `StyledHeading は /(Heading|^h(1|2|3|4|5|6))$/ にmatchする名前のコンポーネントを拡張することを期待している名称になっています
|
|
74
|
+
- StyledHeading の名称の末尾が"Heading" という文字列ではない状態にしつつ、"Hoge"を継承していることをわかる名称に変更してください
|
|
75
|
+
- もしくは"Hoge"を"StyledHeading"の継承元であることがわかるような名称に変更するか、適切な別コンポーネントに差し替えてください
|
|
76
|
+
- 修正例1: const StyledXxxx = styled(Hoge)
|
|
77
|
+
- 修正例2: const StyledHeadingXxxx = styled(Hoge)
|
|
78
|
+
- 修正例3: const StyledHeading = styled(XxxxHeading)` } ] },
|
|
79
|
+
{ code: 'const StyledHeading = styled.div``', errors: [ { message: `StyledHeading は /(Heading|^h(1|2|3|4|5|6))$/ にmatchする名前のコンポーネントを拡張することを期待している名称になっています
|
|
80
|
+
- StyledHeading の名称の末尾が"Heading" という文字列ではない状態にしつつ、"div"を継承していることをわかる名称に変更してください
|
|
81
|
+
- もしくは"div"を"StyledHeading"の継承元であることがわかるような適切なタグや別コンポーネントに差し替えてください
|
|
82
|
+
- 修正例1: const StyledXxxx = styled.div
|
|
83
|
+
- 修正例2: const StyledHeadingXxxx = styled.div
|
|
84
|
+
- 修正例3: const StyledHeading = styled(XxxxHeading)` } ] },
|
|
61
85
|
{ code: '<><PageHeading>hoge</PageHeading><PageHeading>fuga</PageHeading></>', errors: [ { message: pageMessage } ] },
|
|
62
86
|
{ code: '<Heading>hoge</Heading>', errors: [ { message } ] },
|
|
63
87
|
{ code: '<><Heading>hoge</Heading><Heading>fuga</Heading></>', errors: [ { message }, { message } ] },
|
|
@@ -32,12 +32,13 @@ ruleTester.run('a11y-image-has-alt-attribute', rule, {
|
|
|
32
32
|
{ code: 'const HogeIcon = styled.svg``' },
|
|
33
33
|
{ code: 'const HogeImg = styled(Img)``' },
|
|
34
34
|
{ code: 'const HogeImage = styled(Image)``' },
|
|
35
|
-
{ code: 'const HogeIcon = styled(
|
|
35
|
+
{ code: 'const HogeIcon = styled(Icon)``' },
|
|
36
36
|
{ code: '<img alt="hoge" />' },
|
|
37
37
|
{ code: '<HogeImg alt="hoge" />' },
|
|
38
38
|
{ code: '<HogeImage alt="hoge" />' },
|
|
39
39
|
{ code: '<HogeIcon />' },
|
|
40
40
|
{ code: '<svg><image /></svg>' },
|
|
41
|
+
{ code: '<AnyImg {...hoge} />', options: [{ checkType: 'smart' }] },
|
|
41
42
|
],
|
|
42
43
|
invalid: [
|
|
43
44
|
{ code: `import hoge from 'styled-components'`, errors: [ { message: `styled-components をimportする際は、名称が"styled" となるようにしてください。例: "import styled from 'styled-components'"` } ] },
|
|
@@ -46,8 +47,28 @@ ruleTester.run('a11y-image-has-alt-attribute', rule, {
|
|
|
46
47
|
{ code: 'const Hoge = styled(Icon)``', errors: [ { message: `Hogeを正規表現 "/Icon$/" がmatchする名称に変更してください` } ] },
|
|
47
48
|
{ code: 'const Hoge = styled(Img)``', errors: [ { message: `Hogeを正規表現 "/Img$/" がmatchする名称に変更してください` } ] },
|
|
48
49
|
{ code: 'const Hoge = styled(Image)``', errors: [ { message: `Hogeを正規表現 "/Image$/" がmatchする名称に変更してください` } ] },
|
|
50
|
+
{ code: 'const StyledImage = styled.span``', errors: [ { message: `StyledImage は /(Image|^(img|svg))$/ にmatchする名前のコンポーネントを拡張することを期待している名称になっています
|
|
51
|
+
- StyledImage の名称の末尾が"Image" という文字列ではない状態にしつつ、"span"を継承していることをわかる名称に変更してください
|
|
52
|
+
- もしくは"span"を"StyledImage"の継承元であることがわかるような適切なタグや別コンポーネントに差し替えてください
|
|
53
|
+
- 修正例1: const StyledXxxx = styled.span
|
|
54
|
+
- 修正例2: const StyledImageXxxx = styled.span
|
|
55
|
+
- 修正例3: const StyledImage = styled(XxxxImage)` } ] },
|
|
56
|
+
{ code: 'const StyledImg = styled(Hoge)``', errors: [ { message: `StyledImg は /(Img|^(img|svg))$/ にmatchする名前のコンポーネントを拡張することを期待している名称になっています
|
|
57
|
+
- StyledImg の名称の末尾が"Img" という文字列ではない状態にしつつ、"Hoge"を継承していることをわかる名称に変更してください
|
|
58
|
+
- もしくは"Hoge"を"StyledImg"の継承元であることがわかるような名称に変更するか、適切な別コンポーネントに差し替えてください
|
|
59
|
+
- 修正例1: const StyledXxxx = styled(Hoge)
|
|
60
|
+
- 修正例2: const StyledImgXxxx = styled(Hoge)
|
|
61
|
+
- 修正例3: const StyledImg = styled(XxxxImg)` } ] },
|
|
62
|
+
{ code: 'const FugaIcon = styled(Fuga)``', errors: [ { message: `FugaIcon は /(Icon|^(img|svg))$/ にmatchする名前のコンポーネントを拡張することを期待している名称になっています
|
|
63
|
+
- FugaIcon の名称の末尾が"Icon" という文字列ではない状態にしつつ、"Fuga"を継承していることをわかる名称に変更してください
|
|
64
|
+
- もしくは"Fuga"を"FugaIcon"の継承元であることがわかるような名称に変更するか、適切な別コンポーネントに差し替えてください
|
|
65
|
+
- 修正例1: const FugaXxxx = styled(Fuga)
|
|
66
|
+
- 修正例2: const FugaIconXxxx = styled(Fuga)
|
|
67
|
+
- 修正例3: const FugaIcon = styled(XxxxIcon)` } ] },
|
|
49
68
|
{ code: '<img />', errors: [ { message: messageNotExistAlt } ] },
|
|
50
69
|
{ code: '<HogeImage alt="" />', errors: [ { message: messageNullAlt } ] },
|
|
51
70
|
{ code: '<hoge><image /></hoge>', errors: [ { message: messageNotExistAlt } ] },
|
|
71
|
+
{ code: '<AnyImg {...hoge} />', errors: [ { message: messageNotExistAlt } ] },
|
|
72
|
+
{ code: '<AnyImg {...hoge} />', options: [{ checkType: 'always' }], errors: [ { message: messageNotExistAlt } ] },
|
|
52
73
|
]
|
|
53
74
|
})
|
|
@@ -37,6 +37,9 @@ ruleTester.run('a11y-input-has-name-attribute', rule, {
|
|
|
37
37
|
{ code: '<HogeTextarea name="hoge" />' },
|
|
38
38
|
{ code: '<select name="hoge" />' },
|
|
39
39
|
{ code: '<Select name="hoge[0][Fuga]" />' },
|
|
40
|
+
{ code: '<Input {...hoge} />', options: [{ checkType: 'smart' }] },
|
|
41
|
+
{ code: '<Input {...args1} {...args2} />', options: [{ checkType: 'smart' }] },
|
|
42
|
+
{ code: '<Input {...args} hoge="fuga" />', options: [{ checkType: 'smart' }] },
|
|
40
43
|
],
|
|
41
44
|
invalid: [
|
|
42
45
|
{ code: `import hoge from 'styled-components'`, errors: [ { message: `styled-components をimportする際は、名称が"styled" となるようにしてください。例: "import styled from 'styled-components'"` } ] },
|
|
@@ -56,5 +59,11 @@ ruleTester.run('a11y-input-has-name-attribute', rule, {
|
|
|
56
59
|
{ code: '<HogeTextarea />', errors: [ { message: `HogeTextarea にname属性を指定してください${MESSAGE_SUFFIX}` } ] },
|
|
57
60
|
{ code: '<input type="radio" name="ほげ" />', errors: [ { message: 'input のname属性の値(ほげ)はブラウザの自動補完が適切に行えない可能性があるため"/^[a-zA-Z0-9_\\[\\]]+$/"にmatchするフォーマットで命名してください' } ] },
|
|
58
61
|
{ code: '<select name="hoge[fuga][0][あいうえお]" />', errors: [ { message: 'select のname属性の値(hoge[fuga][0][あいうえお])はブラウザの自動補完が適切に行えない可能性があるため"/^[a-zA-Z0-9_\\[\\]]+$/"にmatchするフォーマットで命名してください' } ] },
|
|
62
|
+
{ code: '<Input {...hoge} />', errors: [ { message: `Input にname属性を指定してください
|
|
63
|
+
- ブラウザの自動補完が有効化されるなどのメリットがあります
|
|
64
|
+
- より多くのブラウザが自動補完を行える可能性を上げるため、\"/^[a-zA-Z0-9_\\[\\]]+$/\"にmatchするフォーマットで命名してください` } ] },
|
|
65
|
+
{ code: '<Input {...hoge} hoge="fuga" />', options: [{ checkType: 'always' }], errors: [ { message: `Input にname属性を指定してください
|
|
66
|
+
- ブラウザの自動補完が有効化されるなどのメリットがあります
|
|
67
|
+
- より多くのブラウザが自動補完を行える可能性を上げるため、\"/^[a-zA-Z0-9_\\[\\]]+$/\"にmatchするフォーマットで命名してください` } ] },
|
|
59
68
|
],
|
|
60
69
|
});
|
|
@@ -27,9 +27,9 @@ ruleTester.run('best-practice-for-date', rule, {
|
|
|
27
27
|
{ code: `const year = 2022; const month = 11; const date = 31; new Date(year, month, date)` },
|
|
28
28
|
],
|
|
29
29
|
invalid: [
|
|
30
|
-
{ code: 'new Date("2022/12/31")', errors: [ { message: errorNewDate } ] },
|
|
30
|
+
{ code: 'new Date("2022/12/31")', errors: [ { message: errorNewDate } ], output: 'new Date(2022, 12 - 1, 31)' },
|
|
31
31
|
{ code: 'const arg = "2022/12/31"; new Date(arg)', errors: [ { message: errorNewDate } ] },
|
|
32
|
-
{ code: 'Date.parse("2022/12/31")', errors: [ { message: errorDateParse } ] },
|
|
32
|
+
{ code: 'Date.parse("2022/12/31")', errors: [ { message: errorDateParse } ], output: 'new Date(2022, 12 - 1, 31).getTime()' },
|
|
33
33
|
{ code: 'const arg = "2022/12/31"; Date.parse(arg)', errors: [ { message: errorDateParse } ] },
|
|
34
34
|
]
|
|
35
35
|
})
|