eslint-plugin-smarthr 0.1.1 → 0.2.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/.github/CODEOWNERS +3 -0
- package/CHANGELOG.md +27 -0
- package/README.md +97 -12
- package/github/CODEOWNERS +3 -0
- package/libs/format_styled_components.js +57 -0
- package/package.json +1 -1
- package/rules/a11y-clickable-element-has-text.js +71 -0
- package/rules/a11y-image-has-alt-attribute.js +48 -0
- package/rules/a11y-trigger-has-button.js +74 -0
- package/rules/format-translate-component.js +97 -0
- package/rules/redundant-name.js +14 -4
- package/rules/require-barrel-import.js +19 -22
- package/test/a11y-clickable-element-has-text.js +142 -0
- package/test/a11y-image-has-alt-attribute.js +44 -0
- package/test/a11y-trigger-has-button.js +50 -0
- package/test/format-translate-component.js +37 -0
- package/rules/a11y-icon-button-has-name.js +0 -56
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,33 @@
|
|
|
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.2.0](https://github.com/kufu/eslint-plugin-smarthr/compare/v0.1.2...v0.2.0) (2022-07-05)
|
|
6
|
+
|
|
7
|
+
### ⚠ BREAKING CHANGES
|
|
8
|
+
|
|
9
|
+
* BREAKING CHANGE: a11-xxxx-has-text を a11-clickable-element-has-text に統一する ([#16](https://github.com/kufu/eslint-plugin-smarthr/pull/16)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
### [0.1.3](https://github.com/kufu/eslint-plugin-smarthr/compare/v0.1.2...v0.1.3) (2022-07-05)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
### Features
|
|
16
|
+
|
|
17
|
+
* add rules format-translate-component ([#19](https://github.com/kufu/eslint-plugin-smarthr/issues/19)) ([a429e9e](https://github.com/kufu/eslint-plugin-smarthr/commit/a429e9ef31779deb8f08499cfb8cbf00322c58b8))
|
|
18
|
+
* リンク要素内にテキストが設定されていない場合、エラーとなるルールを追加する ([#15](https://github.com/kufu/eslint-plugin-smarthr/issues/15)) ([4bbb9c1](https://github.com/kufu/eslint-plugin-smarthr/commit/4bbb9c1204a8edd068fabcdca497d94ecc1db4a4))
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
### Bug Fixes
|
|
22
|
+
|
|
23
|
+
* redundant-nameのバグを修正する ([#20](https://github.com/kufu/eslint-plugin-smarthr/issues/20)) ([b733f18](https://github.com/kufu/eslint-plugin-smarthr/commit/b733f1835293c3b478f6d9bb3ebe944041c67038))
|
|
24
|
+
|
|
25
|
+
### [0.1.2](https://github.com/kufu/eslint-plugin-smarthr/compare/v0.1.1...v0.1.2) (2022-03-09)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
### Bug Fixes
|
|
29
|
+
|
|
30
|
+
* require-barrel-import修正(barrelファイルが複数存在する場合、一番親に当たるファイルを検知する) ([#14](https://github.com/kufu/eslint-plugin-smarthr/issues/14)) ([87a6724](https://github.com/kufu/eslint-plugin-smarthr/commit/87a67240f31c9408faad6784741bbf6a2f7ef47b))
|
|
31
|
+
|
|
5
32
|
### [0.1.1](https://github.com/kufu/eslint-plugin-smarthr/compare/v0.1.0...v0.1.1) (2022-03-08)
|
|
6
33
|
|
|
7
34
|
|
package/README.md
CHANGED
|
@@ -1,47 +1,67 @@
|
|
|
1
1
|
# eslint-plugin-smarthr
|
|
2
2
|
|
|
3
|
-
## smarthr/a11y-
|
|
3
|
+
## smarthr/a11y-clickable-element-has-text
|
|
4
4
|
|
|
5
|
-
-
|
|
5
|
+
- ButtonやAnchor,Link コンポーネントにテキスト要素が設定されていない場合、アクセシビリティの問題が発生する可能性を防ぐルールです
|
|
6
6
|
|
|
7
7
|
### rules
|
|
8
8
|
|
|
9
9
|
```js
|
|
10
10
|
{
|
|
11
11
|
rules: {
|
|
12
|
-
'smarthr/a11y-
|
|
12
|
+
'smarthr/a11y-clickable-element-has-text': 'error', // 'warn', 'off'
|
|
13
13
|
},
|
|
14
14
|
}
|
|
15
15
|
```
|
|
16
16
|
|
|
17
17
|
### ❌ Incorrect
|
|
18
18
|
|
|
19
|
+
```jsx
|
|
20
|
+
<XxxAnchor>
|
|
21
|
+
<Xxx />
|
|
22
|
+
</XxxAnchor>
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
```jsx
|
|
26
|
+
<XxxLink>
|
|
27
|
+
<Yyy />
|
|
28
|
+
</XxxLink>
|
|
29
|
+
```
|
|
30
|
+
|
|
19
31
|
```jsx
|
|
20
32
|
<XxxButton>
|
|
21
|
-
<
|
|
33
|
+
<Zzz />
|
|
22
34
|
</XxxButton>
|
|
23
35
|
```
|
|
24
36
|
|
|
25
37
|
### ✅ Correct
|
|
26
38
|
|
|
27
39
|
```jsx
|
|
28
|
-
<
|
|
29
|
-
<YyyIcon />
|
|
40
|
+
<XxxAnchor>
|
|
30
41
|
Hoge
|
|
31
|
-
</
|
|
42
|
+
</XxxAnchor>
|
|
32
43
|
```
|
|
33
44
|
```jsx
|
|
34
|
-
<
|
|
45
|
+
<XxxLink>
|
|
35
46
|
<YyyIcon />
|
|
36
|
-
|
|
37
|
-
</
|
|
47
|
+
Fuga
|
|
48
|
+
</XxxLink>
|
|
38
49
|
```
|
|
39
50
|
```jsx
|
|
40
|
-
<
|
|
51
|
+
<XxxAnchor>>
|
|
41
52
|
<YyyIcon visuallyHiddenText="hoge" />
|
|
53
|
+
</XxxAnchor>
|
|
54
|
+
```
|
|
55
|
+
```jsx
|
|
56
|
+
<XxxButton>
|
|
57
|
+
<YyyImage alt="fuga" />
|
|
42
58
|
</XxxButton>
|
|
43
59
|
```
|
|
44
60
|
|
|
61
|
+
```jsx
|
|
62
|
+
<YyyAnchoor />
|
|
63
|
+
```
|
|
64
|
+
|
|
45
65
|
## smarthr/format-import-path
|
|
46
66
|
|
|
47
67
|
- importする際のpathをフォーマットするruleです
|
|
@@ -413,6 +433,8 @@ import { Item } from './Page/parts/Menu'
|
|
|
413
433
|
- ディレクトリ名から生成されるキーワードに含めたくない文字列を指定します
|
|
414
434
|
- betterNames
|
|
415
435
|
- 対象の名前を修正する候補を指定します
|
|
436
|
+
- allowedNames
|
|
437
|
+
- 許可する名前を指定します
|
|
416
438
|
- suffix:
|
|
417
439
|
- type のみ指定出来ます
|
|
418
440
|
- type のsuffixを指定します
|
|
@@ -444,6 +466,9 @@ const betterNames = {
|
|
|
444
466
|
names: ['index'],
|
|
445
467
|
},
|
|
446
468
|
}
|
|
469
|
+
// const allowedNames = {
|
|
470
|
+
// '\/views\/crews\/histories\/': ['crewId'],
|
|
471
|
+
// }
|
|
447
472
|
|
|
448
473
|
{
|
|
449
474
|
rules: {
|
|
@@ -452,7 +477,7 @@ const betterNames = {
|
|
|
452
477
|
{
|
|
453
478
|
type: { ignorekeywords, suffix: ['Props', 'Type'] },
|
|
454
479
|
file: { ignorekeywords, betternames },
|
|
455
|
-
// property: { ignorekeywords },
|
|
480
|
+
// property: { ignorekeywords, allowedNames },
|
|
456
481
|
// function: { ignorekeywords },
|
|
457
482
|
// variable: { ignorekeywords },
|
|
458
483
|
// class: { ignorekeywords },
|
|
@@ -489,3 +514,63 @@ type ItemProps = { hoge: string }
|
|
|
489
514
|
type IndexProps = { hoge: () => any }
|
|
490
515
|
type ResponseType = { hoge: () => any }
|
|
491
516
|
``
|
|
517
|
+
|
|
518
|
+
## smarthr/format-translate-component
|
|
519
|
+
|
|
520
|
+
- 翻訳用コンポーネントを適用する際のルールを定めます
|
|
521
|
+
|
|
522
|
+
### rules
|
|
523
|
+
|
|
524
|
+
```js
|
|
525
|
+
{
|
|
526
|
+
rules: {
|
|
527
|
+
'smarthr/format-translate-component': [
|
|
528
|
+
'error', // 'warn', 'off'
|
|
529
|
+
{
|
|
530
|
+
componentName: 'Translate',
|
|
531
|
+
// componentPath: '@/any/path/Translate',
|
|
532
|
+
// prohibitAttributies: ['data-translate'],
|
|
533
|
+
}
|
|
534
|
+
]
|
|
535
|
+
},
|
|
536
|
+
}
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
### ❌ Incorrect
|
|
540
|
+
|
|
541
|
+
```jsx
|
|
542
|
+
<Translate><Any>ほげ</Any></Translate>
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
```jsx
|
|
546
|
+
<Translate><Any /></Translate>
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
```jsx
|
|
550
|
+
<Translate></Translate>
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
```jsx
|
|
554
|
+
// prohibitAttributies: ['data-translate'],
|
|
555
|
+
<Any data-translate="true">...</Any>
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
### ✅ Correct
|
|
559
|
+
|
|
560
|
+
```jsx
|
|
561
|
+
<Translate>ほげ</Translate>
|
|
562
|
+
```
|
|
563
|
+
```jsx
|
|
564
|
+
<Translate>ほげ<br />ふが</Translate>
|
|
565
|
+
```
|
|
566
|
+
```jsx
|
|
567
|
+
<Translate>{any}</Translate>
|
|
568
|
+
```
|
|
569
|
+
```jsx
|
|
570
|
+
<Translate dangerouslySetInnerHTML={{ __html: "ほげ" }} />
|
|
571
|
+
```
|
|
572
|
+
```jsx
|
|
573
|
+
// prohibitAttributies: ['data-translate'],
|
|
574
|
+
<Any data-hoge="true">...</Any>
|
|
575
|
+
```
|
|
576
|
+
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
const generateTagFormatter = ({ context, EXPECTED_NAMES }) => ({
|
|
2
|
+
ImportDeclaration: (node) => {
|
|
3
|
+
if (node.source.value !== 'styled-components') {
|
|
4
|
+
return
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const invalidNameNode = node.specifiers.find((s) => s.type === 'ImportDefaultSpecifier' && s.local.name !== 'styled')
|
|
8
|
+
|
|
9
|
+
if (invalidNameNode) {
|
|
10
|
+
context.report({
|
|
11
|
+
node: invalidNameNode,
|
|
12
|
+
messageId: 'format-styled-components',
|
|
13
|
+
data: {
|
|
14
|
+
message: "styled-components をimportする際は、名称が`styled` となるようにしてください。例: `import styled from 'styled-components'`",
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
TaggedTemplateExpression: (node) => {
|
|
20
|
+
const { tag } = node
|
|
21
|
+
const base = (() => {
|
|
22
|
+
if (tag.type === 'CallExpression' && tag.callee.name === 'styled') {
|
|
23
|
+
return tag.arguments[0].name
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (tag?.object?.name === 'styled') {
|
|
27
|
+
return tag.property.name
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return null
|
|
31
|
+
})()
|
|
32
|
+
|
|
33
|
+
if (!base) {
|
|
34
|
+
return
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const extended = node.parent.id.name
|
|
38
|
+
|
|
39
|
+
Object.entries(EXPECTED_NAMES).forEach(([b, e]) => {
|
|
40
|
+
if (base.match(new RegExp(b))) {
|
|
41
|
+
const extendedregex = new RegExp(e)
|
|
42
|
+
|
|
43
|
+
if (!extended.match(extendedregex)) {
|
|
44
|
+
context.report({
|
|
45
|
+
node: node.parent,
|
|
46
|
+
messageId: 'format-styled-components',
|
|
47
|
+
data: {
|
|
48
|
+
message: `${extended}を正規表現 "${extendedregex.toString()}" がmatchする名称に変更してください`,
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
})
|
|
54
|
+
},
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
module.exports = { generateTagFormatter }
|
package/package.json
CHANGED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
const { generateTagFormatter } = require('../libs/format_styled_components')
|
|
2
|
+
|
|
3
|
+
const EXPECTED_NAMES = {
|
|
4
|
+
'(b|B)utton$': 'Button$',
|
|
5
|
+
'Anchor$': 'Anchor$',
|
|
6
|
+
'Link$': 'Link$',
|
|
7
|
+
'^a$': '(Anchor|Link)$',
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const filterFalsyJSXText = (cs) => cs.filter((c) => (
|
|
11
|
+
!(c.type === 'JSXText' && c.value.match(/^\s*\n+\s*$/))
|
|
12
|
+
))
|
|
13
|
+
|
|
14
|
+
module.exports = {
|
|
15
|
+
meta: {
|
|
16
|
+
type: 'suggestion',
|
|
17
|
+
messages: {
|
|
18
|
+
'format-styled-components': '{{ message }}',
|
|
19
|
+
'a11y-clickable-element-has-text': '{{ message }}',
|
|
20
|
+
},
|
|
21
|
+
schema: [],
|
|
22
|
+
},
|
|
23
|
+
create(context) {
|
|
24
|
+
return {
|
|
25
|
+
...generateTagFormatter({ context, EXPECTED_NAMES }),
|
|
26
|
+
JSXElement: (parentNode) => {
|
|
27
|
+
// HINT: 閉じタグが存在しない === テキストノードが存在しない
|
|
28
|
+
if (!parentNode.closingElement) {
|
|
29
|
+
return
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const node = parentNode.openingElement
|
|
33
|
+
|
|
34
|
+
if (!node.name.name || !node.name.name.match(/^(a|(.*?)Anchor(Button)?|(.*?)Link|(b|B)utton)$/)) {
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const recursiveSearch = (c) => {
|
|
39
|
+
if (['JSXText', 'JSXExpressionContainer'].includes(c.type)) {
|
|
40
|
+
return true
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (c.type === 'JSXElement') {
|
|
44
|
+
if (c.openingElement.attributes.some((a) => (['visuallyHiddenText', 'alt'].includes(a.name.name) && !!a.value.value))) {
|
|
45
|
+
return true
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (c.children && filterFalsyJSXText(c.children).some(recursiveSearch)) {
|
|
49
|
+
return true
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return false
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const child = filterFalsyJSXText(parentNode.children).find(recursiveSearch)
|
|
57
|
+
|
|
58
|
+
if (!child) {
|
|
59
|
+
context.report({
|
|
60
|
+
node,
|
|
61
|
+
messageId: 'a11y-clickable-element-has-text',
|
|
62
|
+
data: {
|
|
63
|
+
message: 'a, button要素にはテキストを設定してください。要素内にアイコン、画像のみを設置する場合はSmartHR UIのvisuallyHiddenText、通常のHTML要素にはaltなどの代替テキスト用属性を指定してください',
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
}
|
|
71
|
+
module.exports.schema = []
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
const { generateTagFormatter } = require('../libs/format_styled_components')
|
|
2
|
+
|
|
3
|
+
const EXPECTED_NAMES = {
|
|
4
|
+
'Img$': 'Img$',
|
|
5
|
+
'Image$': 'Image$',
|
|
6
|
+
'Icon$': 'Icon$',
|
|
7
|
+
'^(img|svg)$': '(Img|Image|Icon)$',
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
module.exports = {
|
|
11
|
+
meta: {
|
|
12
|
+
type: 'suggestion',
|
|
13
|
+
messages: {
|
|
14
|
+
'format-styled-components': '{{ message }}',
|
|
15
|
+
'a11y-image-has-alt-attribute': '{{ message }}',
|
|
16
|
+
},
|
|
17
|
+
schema: [],
|
|
18
|
+
},
|
|
19
|
+
create(context) {
|
|
20
|
+
return {
|
|
21
|
+
...generateTagFormatter({ context, EXPECTED_NAMES }),
|
|
22
|
+
JSXOpeningElement: (node) => {
|
|
23
|
+
if ((node.name.name || '').match(/(img|image)$/i)) { // HINT: Iconは別途テキストが存在する場合が多いためチェックの対象外とする
|
|
24
|
+
const alt = node.attributes.find((a) => a.name.name === 'alt')
|
|
25
|
+
|
|
26
|
+
let message = ''
|
|
27
|
+
|
|
28
|
+
if (!alt) {
|
|
29
|
+
message = '画像にはalt属性を指定してください。SVG component の場合、altを属性として受け取れるようにした上で `<svg role="img" aria-label={alt}>` のように指定してください。画像ではない場合、img or image を末尾に持たない名称に変更してください。'
|
|
30
|
+
} else if (alt.value.value === '') {
|
|
31
|
+
message = '画像の情報をテキストにした代替テキスト(`alt`)を設定してください。装飾目的の画像など、alt属性に指定すべき文字がない場合は背景画像にすることを検討してください。'
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (message) {
|
|
35
|
+
context.report({
|
|
36
|
+
node,
|
|
37
|
+
messageId: 'a11y-image-has-alt-attribute',
|
|
38
|
+
data: {
|
|
39
|
+
message,
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
}
|
|
48
|
+
module.exports.schema = []
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
const { generateTagFormatter } = require('../libs/format_styled_components')
|
|
2
|
+
|
|
3
|
+
const EXPECTED_NAMES = {
|
|
4
|
+
'DropdownTrigger$': 'DropdownTrigger$',
|
|
5
|
+
'DialogTrigger$': 'DialogTrigger$',
|
|
6
|
+
'(b|B)utton$': 'Button$',
|
|
7
|
+
'AnchorButton$': 'AnchorButton$',
|
|
8
|
+
'ButtonAnchor$': 'ButtonAnchor$',
|
|
9
|
+
'Anchor$': 'Anchor$',
|
|
10
|
+
'Link$': 'Link$',
|
|
11
|
+
'^a$': '(Anchor|Link)$',
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const filterFalsyJSXText = (cs) => cs.filter((c) => (
|
|
15
|
+
!(c.type === 'JSXText' && c.value.match(/^\s*\n+\s*$/))
|
|
16
|
+
))
|
|
17
|
+
|
|
18
|
+
module.exports = {
|
|
19
|
+
meta: {
|
|
20
|
+
type: 'suggestion',
|
|
21
|
+
messages: {
|
|
22
|
+
'format-styled-components': '{{ message }}',
|
|
23
|
+
'a11y-trigger-has-button': '{{ message }}',
|
|
24
|
+
},
|
|
25
|
+
schema: [],
|
|
26
|
+
},
|
|
27
|
+
create(context) {
|
|
28
|
+
return {
|
|
29
|
+
...generateTagFormatter({ context, EXPECTED_NAMES }),
|
|
30
|
+
JSXElement: (parentNode) => {
|
|
31
|
+
// HINT: 閉じタグが存在しない === 子が存在しない
|
|
32
|
+
// 子を持っていない場合はおそらく固定の要素を吐き出すコンポーネントと考えられるため
|
|
33
|
+
// その中身をチェックすることで担保できるのでskipする
|
|
34
|
+
if (!parentNode.closingElement) {
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const node = parentNode.openingElement
|
|
39
|
+
|
|
40
|
+
if (!node.name.name) {
|
|
41
|
+
return
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const match = node.name.name.match(/(Dropdown|Dialog)Trigger$/)
|
|
45
|
+
|
|
46
|
+
if (!match || node.name.name.match(/HelpDialogTrigger$/)) {
|
|
47
|
+
return
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
filterFalsyJSXText(parentNode.children).forEach((c) => {
|
|
51
|
+
// `<DialogTrigger>{button}</DialogTrigger>` のような場合は許可する
|
|
52
|
+
if (c.type === 'JSXExpressionContainer') {
|
|
53
|
+
return false
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (
|
|
57
|
+
c.type !== 'JSXElement' ||
|
|
58
|
+
!c.openingElement.name.name.match(/(b|B)utton$/) ||
|
|
59
|
+
c.openingElement.name.name.match(/AnchorButton?/)
|
|
60
|
+
) {
|
|
61
|
+
context.report({
|
|
62
|
+
node: c,
|
|
63
|
+
messageId: 'a11y-trigger-has-button',
|
|
64
|
+
data: {
|
|
65
|
+
message: `${match[1]}Trigger の直下にはbuttonコンポーネントのみ設置してください`,
|
|
66
|
+
},
|
|
67
|
+
})
|
|
68
|
+
}
|
|
69
|
+
})
|
|
70
|
+
},
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
}
|
|
74
|
+
module.exports.schema = []
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
const SCHEMA = [
|
|
2
|
+
{
|
|
3
|
+
type: 'object',
|
|
4
|
+
required: [
|
|
5
|
+
'componentName',
|
|
6
|
+
],
|
|
7
|
+
properties: {
|
|
8
|
+
componentPath: { type: 'string', default: '' },
|
|
9
|
+
componentName: { type: 'string' },
|
|
10
|
+
prohibitAttributies: { type: 'array', items: { type: 'string' }, default: [] },
|
|
11
|
+
},
|
|
12
|
+
additionalProperties: false,
|
|
13
|
+
}
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
module.exports = {
|
|
17
|
+
meta: {
|
|
18
|
+
type: 'suggestion',
|
|
19
|
+
messages: {
|
|
20
|
+
'format-translate-component': '{{ message }}',
|
|
21
|
+
},
|
|
22
|
+
schema: SCHEMA,
|
|
23
|
+
},
|
|
24
|
+
create(context) {
|
|
25
|
+
const { componentPath, componentName, prohibitAttributies } = context.options[0]
|
|
26
|
+
let JSXAttribute = () => {}
|
|
27
|
+
|
|
28
|
+
if (prohibitAttributies) {
|
|
29
|
+
JSXAttribute = (node) => {
|
|
30
|
+
const hit = prohibitAttributies.find((a) => a === node.name.name)
|
|
31
|
+
|
|
32
|
+
if (hit) {
|
|
33
|
+
context.report({
|
|
34
|
+
node,
|
|
35
|
+
messageId: 'format-translate-component',
|
|
36
|
+
data: {
|
|
37
|
+
message: `${hit} 属性は使用せず、 ${componentPath || componentName} コンポーネントを利用してください`,
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
JSXAttribute,
|
|
46
|
+
JSXOpeningElement: (node) => {
|
|
47
|
+
// HINT: 翻訳コンポーネントはテキストとbrのみ許容する
|
|
48
|
+
if (node.name.name === componentName) {
|
|
49
|
+
let existValidChild = false
|
|
50
|
+
let existNotBrElement = false
|
|
51
|
+
|
|
52
|
+
node.parent.children.forEach((c) => {
|
|
53
|
+
switch (c.type) {
|
|
54
|
+
case 'JSXText':
|
|
55
|
+
// HINT: 空白と改行のみの場合はテキストが存在する扱いにはしない
|
|
56
|
+
if (c.value.replace(/(\s|\n)+/g, '')) {
|
|
57
|
+
existValidChild = true
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
break
|
|
61
|
+
case 'JSXExpressionContainer':
|
|
62
|
+
// TODO 変数がstringのみか判定できるなら対応したい
|
|
63
|
+
existValidChild = true
|
|
64
|
+
|
|
65
|
+
break
|
|
66
|
+
case 'JSXElement':
|
|
67
|
+
if (c.openingElement.name.name !== 'br') {
|
|
68
|
+
existNotBrElement = true
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
break
|
|
72
|
+
}
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
const message = (() => {
|
|
76
|
+
if (existNotBrElement) {
|
|
77
|
+
return `${componentName} 内では <br /> 以外のタグは使えません`
|
|
78
|
+
} else if (!existValidChild && !node.attributes.some((a) => a.name.name === 'dangerouslySetInnerHTML')) {
|
|
79
|
+
return `${componentName} 内には必ずテキストを設置してください`
|
|
80
|
+
}
|
|
81
|
+
})()
|
|
82
|
+
|
|
83
|
+
if (message) {
|
|
84
|
+
context.report({
|
|
85
|
+
node,
|
|
86
|
+
messageId: 'format-translate-component',
|
|
87
|
+
data: {
|
|
88
|
+
message,
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
}
|
|
97
|
+
module.exports.schema = SCHEMA
|
package/rules/redundant-name.js
CHANGED
|
@@ -48,6 +48,10 @@ const DEFAULT_SCHEMA_PROPERTY = {
|
|
|
48
48
|
},
|
|
49
49
|
},
|
|
50
50
|
},
|
|
51
|
+
allowedNames: {
|
|
52
|
+
type: 'array',
|
|
53
|
+
items: 'string',
|
|
54
|
+
},
|
|
51
55
|
}
|
|
52
56
|
|
|
53
57
|
const SCHEMA = [
|
|
@@ -91,9 +95,11 @@ const generateRedundantKeywords = ({ args, key, terminalImportName }) => {
|
|
|
91
95
|
return prev
|
|
92
96
|
}
|
|
93
97
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
98
|
+
return [...prev, ...uniq([
|
|
99
|
+
Inflector.pluralize(keyword),
|
|
100
|
+
keyword,
|
|
101
|
+
Inflector.singularize(keyword),
|
|
102
|
+
])]
|
|
97
103
|
}, [])
|
|
98
104
|
}
|
|
99
105
|
const handleReportBetterName = ({
|
|
@@ -113,7 +119,11 @@ const handleReportBetterName = ({
|
|
|
113
119
|
return (node) => {
|
|
114
120
|
const name = fetchName(node)
|
|
115
121
|
|
|
116
|
-
if (
|
|
122
|
+
if (
|
|
123
|
+
!name ||
|
|
124
|
+
option.allowedNames &&
|
|
125
|
+
Object.entries(option.allowedNames).find(([regex, calcs]) => filename.match(new RegExp(regex)) && calcs.find((c) => c === name))
|
|
126
|
+
) {
|
|
117
127
|
return
|
|
118
128
|
}
|
|
119
129
|
|
|
@@ -43,6 +43,7 @@ const calculateReplacedImportPath = (source) => {
|
|
|
43
43
|
return prev
|
|
44
44
|
}, source)
|
|
45
45
|
}
|
|
46
|
+
const TARGET_EXTS = ['ts', 'tsx', 'js', 'jsx']
|
|
46
47
|
|
|
47
48
|
module.exports = {
|
|
48
49
|
meta: {
|
|
@@ -55,11 +56,6 @@ module.exports = {
|
|
|
55
56
|
create(context) {
|
|
56
57
|
const filename = context.getFilename()
|
|
57
58
|
|
|
58
|
-
// HINT: indexファイルがある == barrelであるとする
|
|
59
|
-
if (filename.match(/\/index\.(js|ts)x?$/)) {
|
|
60
|
-
return {}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
59
|
const dir = (() => {
|
|
64
60
|
const d = filename.split('/')
|
|
65
61
|
d.pop()
|
|
@@ -82,39 +78,40 @@ module.exports = {
|
|
|
82
78
|
}
|
|
83
79
|
|
|
84
80
|
const sources = sourceValue.split('/')
|
|
85
|
-
|
|
86
|
-
|
|
81
|
+
|
|
82
|
+
// HINT: directoryの場合、indexファイルからimportしていることは自明であるため、一階層上からチェックする
|
|
83
|
+
if (fs.existsSync(sourceValue) && fs.statSync(sourceValue).isDirectory()) {
|
|
84
|
+
sources.pop()
|
|
85
|
+
sourceValue = sources.join('/')
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
let barrel = undefined
|
|
87
89
|
|
|
88
90
|
while (sources.length > 0) {
|
|
89
91
|
// HINT: 以下の場合は即終了
|
|
90
92
|
// - import元以下のimportだった場合
|
|
91
93
|
// - rootまで捜索した場合
|
|
92
|
-
if (
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
ext = ['ts', 'tsx', 'js', 'jsx'].find((e) => fs.existsSync(`${sources.join('/')}/index.${e}`))
|
|
97
|
-
|
|
98
|
-
if (ext) {
|
|
94
|
+
if (
|
|
95
|
+
dir === rootPath ||
|
|
96
|
+
dir.match(new RegExp(`^${sourceValue}`))
|
|
97
|
+
) {
|
|
99
98
|
break
|
|
100
99
|
}
|
|
101
100
|
|
|
101
|
+
barrel = TARGET_EXTS.map((e) => `${sourceValue}/index.${e}`).find((p) => fs.existsSync(p)) || barrel
|
|
102
|
+
|
|
102
103
|
sources.pop()
|
|
103
|
-
|
|
104
|
+
sourceValue = sources.join('/')
|
|
104
105
|
}
|
|
105
106
|
|
|
106
|
-
if (
|
|
107
|
-
|
|
108
|
-
sourceValue !== joinedSources &&
|
|
109
|
-
!dir.match(new RegExp(`^${joinedSources}/`))
|
|
110
|
-
) {
|
|
111
|
-
const replacedSources = calculateReplacedImportPath(joinedSources)
|
|
107
|
+
if (barrel) {
|
|
108
|
+
barrel = calculateReplacedImportPath(barrel)
|
|
112
109
|
|
|
113
110
|
context.report({
|
|
114
111
|
node,
|
|
115
112
|
messageId: 'require-barrel-import',
|
|
116
113
|
data: {
|
|
117
|
-
message: `${
|
|
114
|
+
message: `${barrel.replace(/\/index\.(ts|js)x?$/, '')} からimportするか、${barrel} を削除してください`,
|
|
118
115
|
},
|
|
119
116
|
});
|
|
120
117
|
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
const rule = require('../rules/a11y-clickable-element-has-text')
|
|
2
|
+
const RuleTester = require('eslint').RuleTester
|
|
3
|
+
|
|
4
|
+
const ruleTester = new RuleTester({
|
|
5
|
+
parserOptions: {
|
|
6
|
+
ecmaVersion: 2018,
|
|
7
|
+
ecmaFeatures: {
|
|
8
|
+
experimentalObjectRestSpread: true,
|
|
9
|
+
jsx: true,
|
|
10
|
+
},
|
|
11
|
+
sourceType: 'module',
|
|
12
|
+
},
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
const defaultErrorMessage = 'a, button要素にはテキストを設定してください。要素内にアイコン、画像のみを設置する場合はSmartHR UIのvisuallyHiddenText、通常のHTML要素にはaltなどの代替テキスト用属性を指定してください'
|
|
16
|
+
|
|
17
|
+
ruleTester.run('a11y-clickable-element-has-text', rule, {
|
|
18
|
+
valid: [
|
|
19
|
+
{ code: `import styled from 'styled-components'` },
|
|
20
|
+
{ code: `import styled, { css } from 'styled-components'` },
|
|
21
|
+
{ code: `import { css } from 'styled-components'` },
|
|
22
|
+
{ code: 'const HogeAnchor = styled.a``' },
|
|
23
|
+
{ code: 'const HogeLink = styled.a``' },
|
|
24
|
+
{ code: 'const HogeButton = styled.button``' },
|
|
25
|
+
{ code: 'const HogeAnchor = styled(Anchor)``' },
|
|
26
|
+
{ code: 'const HogeLink = styled(Link)``' },
|
|
27
|
+
{ code: 'const HogeButton = styled(Button)``' },
|
|
28
|
+
{ code: 'const FugaAnchor = styled(HogeAnchor)``' },
|
|
29
|
+
{
|
|
30
|
+
code: `<a>ほげ</a>`,
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
code: `<Link>ほげ</Link>`,
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
code: `<HogeLink>ほげ</HogeLink>`,
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
code: `<Anchor>ほげ</Anchor>`,
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
code: `<FugaAnchor>ほげ</FugaAnchor>`,
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
code: `<AnchorButton>ほげ</AnchorButton>`,
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
code: `<HogaAnchorButton>ほげ</HogaAnchorButton>`,
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
code: `<Button>ほげ</Button>`,
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
code: `<a />`,
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
code: `<button />`,
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
code: `<Anchor />`,
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
code: `<Link />`,
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
code: `<Button />`,
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
code: `<HogeButton />`,
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
code: `<a><span>ほげ</span></a>`,
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
code: `<a><AnyComponent>ほげ</AnyComponent></a>`,
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
code: `<a><img src="hoge.jpg" alt="ほげ" /></a>`,
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
code: `<a><Icon visuallyHiddenText="ほげ" /></a>`,
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
code: `<a><AnyComponent><Icon visuallyHiddenText="ほげ" /></AnyComponent></a>`,
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
code: `<a>{any}</a>`,
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
code: `<a><span>{any}</span></a>`,
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
invalid: [
|
|
94
|
+
{ code: `import hoge from 'styled-components'`, errors: [ { message: "styled-components をimportする際は、名称が`styled` となるようにしてください。例: `import styled from 'styled-components'`" } ] },
|
|
95
|
+
{ code: 'const Hoge = styled.a``', errors: [ { message: `Hogeを正規表現 "/(Anchor|Link)$/" がmatchする名称に変更してください` } ] },
|
|
96
|
+
{ code: 'const Hoge = styled.button``', errors: [ { message: `Hogeを正規表現 "/Button$/" がmatchする名称に変更してください` } ] },
|
|
97
|
+
{ code: 'const Hoge = styled(Anchor)``', errors: [ { message: `Hogeを正規表現 "/Anchor$/" がmatchする名称に変更してください` } ] },
|
|
98
|
+
{ code: 'const Hoge = styled(Link)``', errors: [ { message: `Hogeを正規表現 "/Link$/" がmatchする名称に変更してください` } ] },
|
|
99
|
+
{ code: 'const Hoge = styled(Button)``', errors: [ { message: `Hogeを正規表現 "/Button$/" がmatchする名称に変更してください` } ] },
|
|
100
|
+
{ code: 'const Fuga = styled(HogeAnchor)``', errors: [ { message: `Fugaを正規表現 "/Anchor$/" がmatchする名称に変更してください` } ] },
|
|
101
|
+
{
|
|
102
|
+
code: `<a><img src="hoge.jpg" /></a>`,
|
|
103
|
+
errors: [{ message: defaultErrorMessage }]
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
code: `<a><Any /></a>`,
|
|
107
|
+
errors: [{ message: defaultErrorMessage }]
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
code: `<a><span><Any /></span></a>`,
|
|
111
|
+
errors: [{ message: defaultErrorMessage }]
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
code: `<a><img src="hoge.jpg" alt="" /></a>`,
|
|
115
|
+
errors: [{ message: defaultErrorMessage }]
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
code: `<a><AnyComponent><Icon visuallyHiddenText="" /></AnyComponent></a>`,
|
|
119
|
+
errors: [{ message: defaultErrorMessage }]
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
code: `<button><img src="hoge.jpg" /></button>`,
|
|
123
|
+
errors: [{ message: defaultErrorMessage }]
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
code: `<button><Any /></button>`,
|
|
127
|
+
errors: [{ message: defaultErrorMessage }]
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
code: `<button><span><Any /></span></button>`,
|
|
131
|
+
errors: [{ message: defaultErrorMessage }]
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
code: `<button><img src="hoge.jpg" alt="" /></button>`,
|
|
135
|
+
errors: [{ message: defaultErrorMessage }]
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
code: `<button><AnyComponent><Icon visuallyHiddenText="" /></AnyComponent></button>`,
|
|
139
|
+
errors: [{ message: defaultErrorMessage }]
|
|
140
|
+
},
|
|
141
|
+
]
|
|
142
|
+
})
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
const rule = require('../rules/a11y-image-has-alt-attribute')
|
|
2
|
+
const RuleTester = require('eslint').RuleTester
|
|
3
|
+
|
|
4
|
+
const ruleTester = new RuleTester({
|
|
5
|
+
parserOptions: {
|
|
6
|
+
ecmaVersion: 2018,
|
|
7
|
+
ecmaFeatures: {
|
|
8
|
+
experimentalObjectRestSpread: true,
|
|
9
|
+
jsx: true,
|
|
10
|
+
},
|
|
11
|
+
sourceType: 'module',
|
|
12
|
+
},
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
ruleTester.run('a11y-image-has-alt-attribute', rule, {
|
|
16
|
+
valid: [
|
|
17
|
+
{ code: `import styled from 'styled-components'` },
|
|
18
|
+
{ code: `import styled, { css } from 'styled-components'` },
|
|
19
|
+
{ code: `import { css } from 'styled-components'` },
|
|
20
|
+
{ code: 'const HogeImg = styled.img``' },
|
|
21
|
+
{ code: 'const HogeImage = styled.img``' },
|
|
22
|
+
{ code: 'const HogeIcon = styled.img``' },
|
|
23
|
+
{ code: 'const HogeImg = styled.svg``' },
|
|
24
|
+
{ code: 'const HogeImage = styled.svg``' },
|
|
25
|
+
{ code: 'const HogeIcon = styled.svg``' },
|
|
26
|
+
{ code: 'const HogeImg = styled(Img)``' },
|
|
27
|
+
{ code: 'const HogeImage = styled(Image)``' },
|
|
28
|
+
{ code: 'const HogeIcon = styled(ICon)``' },
|
|
29
|
+
{ code: '<img alt="hoge" />' },
|
|
30
|
+
{ code: '<HogeImg alt="hoge" />' },
|
|
31
|
+
{ code: '<HogeImage alt="hoge" />' },
|
|
32
|
+
{ code: '<HogeIcon />' },
|
|
33
|
+
],
|
|
34
|
+
invalid: [
|
|
35
|
+
{ code: `import hoge from 'styled-components'`, errors: [ { message: "styled-components をimportする際は、名称が`styled` となるようにしてください。例: `import styled from 'styled-components'`" } ] },
|
|
36
|
+
{ code: 'const Hoge = styled.img``', errors: [ { message: `Hogeを正規表現 "/(Img|Image|Icon)$/" がmatchする名称に変更してください` } ] },
|
|
37
|
+
{ code: 'const Hoge = styled.svg``', errors: [ { message: `Hogeを正規表現 "/(Img|Image|Icon)$/" がmatchする名称に変更してください` } ] },
|
|
38
|
+
{ code: 'const Hoge = styled(Icon)``', errors: [ { message: `Hogeを正規表現 "/Icon$/" がmatchする名称に変更してください` } ] },
|
|
39
|
+
{ code: 'const Hoge = styled(Img)``', errors: [ { message: `Hogeを正規表現 "/Img$/" がmatchする名称に変更してください` } ] },
|
|
40
|
+
{ code: 'const Hoge = styled(Image)``', errors: [ { message: `Hogeを正規表現 "/Image$/" がmatchする名称に変更してください` } ] },
|
|
41
|
+
{ code: '<img />', errors: [ { message: '画像にはalt属性を指定してください。SVG component の場合、altを属性として受け取れるようにした上で `<svg role="img" aria-label={alt}>` のように指定してください。画像ではない場合、img or image を末尾に持たない名称に変更してください。' } ] },
|
|
42
|
+
{ code: '<HogeImage alt="" />', errors: [ { message: '画像の情報をテキストにした代替テキスト(`alt`)を設定してください。装飾目的の画像など、alt属性に指定すべき文字がない場合は背景画像にすることを検討してください。' } ] },
|
|
43
|
+
]
|
|
44
|
+
})
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
const rule = require('../rules/a11y-trigger-has-button')
|
|
2
|
+
const RuleTester = require('eslint').RuleTester
|
|
3
|
+
|
|
4
|
+
const ruleTester = new RuleTester({
|
|
5
|
+
parserOptions: {
|
|
6
|
+
ecmaVersion: 2018,
|
|
7
|
+
ecmaFeatures: {
|
|
8
|
+
experimentalObjectRestSpread: true,
|
|
9
|
+
jsx: true,
|
|
10
|
+
},
|
|
11
|
+
sourceType: 'module',
|
|
12
|
+
},
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
ruleTester.run('a11y-trigger-has-button', rule, {
|
|
16
|
+
valid: [
|
|
17
|
+
{ code: `import styled from 'styled-components'` },
|
|
18
|
+
{ code: `import styled, { css } from 'styled-components'` },
|
|
19
|
+
{ code: `import { css } from 'styled-components'` },
|
|
20
|
+
{ code: 'const HogeButton = styled.button``' },
|
|
21
|
+
{ code: 'const HogeAnchor = styled.a``' },
|
|
22
|
+
{ code: 'const HogeLink = styled.a``' },
|
|
23
|
+
{ code: 'const HogeButton = styled(Button)``' },
|
|
24
|
+
{ code: 'const HogeButtonAnchor = styled(ButtonAnchor)``' },
|
|
25
|
+
{ code: 'const HogeAnchorButton = styled(AnchorButton)``' },
|
|
26
|
+
{ code: 'const HogeLink = styled(FugaLink)``' },
|
|
27
|
+
{ code: 'const HogeAnchor = styled(FugaAnchor)``' },
|
|
28
|
+
{ code: 'const HogeDialogTrigger = styled(DialogTrigger)``' },
|
|
29
|
+
{ code: 'const HogeDropdownTrigger = styled(DropdownTrigger)``' },
|
|
30
|
+
{ code: '<DropdownTrigger><button>hoge</button></DropdownTrigger>' },
|
|
31
|
+
{ code: '<DialogTrigger><button>{hoge}</button></DialogTrigger>' },
|
|
32
|
+
{ code: '<DropdownTrigger>{hoge}</DropdownTrigger>' },
|
|
33
|
+
],
|
|
34
|
+
invalid: [
|
|
35
|
+
{ code: `import hoge from 'styled-components'`, errors: [ { message: "styled-components をimportする際は、名称が`styled` となるようにしてください。例: `import styled from 'styled-components'`" } ] },
|
|
36
|
+
{ code: 'const Hoge = styled.button``', errors: [ { message: `Hogeを正規表現 "/Button$/" がmatchする名称に変更してください` } ] },
|
|
37
|
+
{ code: 'const Hoge = styled.a``', errors: [ { message: `Hogeを正規表現 "/(Anchor|Link)$/" がmatchする名称に変更してください` } ] },
|
|
38
|
+
{ code: 'const Hoge = styled(Button)``', errors: [ { message: `Hogeを正規表現 "/Button$/" がmatchする名称に変更してください` } ] },
|
|
39
|
+
{ code: 'const Hoge = styled(AnchorButton)``', errors: [ { message: `Hogeを正規表現 "/Button$/" がmatchする名称に変更してください` },{ message: `Hogeを正規表現 "/AnchorButton$/" がmatchする名称に変更してください` } ] },
|
|
40
|
+
{ code: 'const Hoge = styled(ButtonAnchor)``', errors: [ { message: `Hogeを正規表現 "/ButtonAnchor$/" がmatchする名称に変更してください` }, { message: `Hogeを正規表現 "/Anchor$/" がmatchする名称に変更してください` } ] },
|
|
41
|
+
{ code: 'const Hoge = styled(Anchor)``', errors: [ { message: `Hogeを正規表現 "/Anchor$/" がmatchする名称に変更してください` } ] },
|
|
42
|
+
{ code: 'const Hoge = styled(Link)``', errors: [ { message: `Hogeを正規表現 "/Link$/" がmatchする名称に変更してください` } ] },
|
|
43
|
+
{ code: 'const Hoge = styled(DropdownTrigger)``', errors: [ { message: `Hogeを正規表現 "/DropdownTrigger$/" がmatchする名称に変更してください` } ] },
|
|
44
|
+
{ code: 'const Hoge = styled(DialogTrigger)``', errors: [ { message: `Hogeを正規表現 "/DialogTrigger$/" がmatchする名称に変更してください` } ] },
|
|
45
|
+
{ code: '<DropdownTrigger>ほげ</DropdownTrigger>', errors: [ { message: 'DropdownTrigger の直下にはbuttonコンポーネントのみ設置してください' } ] },
|
|
46
|
+
{ code: '<DialogTrigger><span><Button>ほげ</Button></span></DialogTrigger>', errors: [ { message: 'DialogTrigger の直下にはbuttonコンポーネントのみ設置してください' } ] },
|
|
47
|
+
{ code: '<DropdownTrigger><AnchorButton>ほげ</AnchorButton></DropdownTrigger>', errors: [ { message: 'DropdownTrigger の直下にはbuttonコンポーネントのみ設置してください' } ] },
|
|
48
|
+
{ code: '<DropdownTrigger><ButtonAnchor>ほげ</ButtonAnchor></DropdownTrigger>', errors: [ { message: 'DropdownTrigger の直下にはbuttonコンポーネントのみ設置してください' } ] },
|
|
49
|
+
]
|
|
50
|
+
})
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
const rule = require('../rules/format-translate-component')
|
|
2
|
+
const RuleTester = require('eslint').RuleTester
|
|
3
|
+
|
|
4
|
+
const ruleTester = new RuleTester({
|
|
5
|
+
parserOptions: {
|
|
6
|
+
ecmaVersion: 2018,
|
|
7
|
+
ecmaFeatures: {
|
|
8
|
+
experimentalObjectRestSpread: true,
|
|
9
|
+
jsx: true,
|
|
10
|
+
},
|
|
11
|
+
sourceType: 'module',
|
|
12
|
+
},
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
const options = [
|
|
16
|
+
{
|
|
17
|
+
componentPath: '@/any/path/Translate',
|
|
18
|
+
componentName: 'Translate',
|
|
19
|
+
prohibitAttributies: ['data-translate'],
|
|
20
|
+
}
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
ruleTester.run('format-translate-component', rule, {
|
|
24
|
+
valid: [
|
|
25
|
+
{ code: '<Any data-wovn-enable="true">ほげ</Any>', options },
|
|
26
|
+
{ code: '<Translate>ほげ</Translate>', options },
|
|
27
|
+
{ code: '<Translate>ほげ<br />ふが</Translate>', options },
|
|
28
|
+
{ code: '<Translate>{any}</Translate>', options },
|
|
29
|
+
{ code: '<Translate dangerouslySetInnerHTML={{ __html: "ほげ" }} />', options },
|
|
30
|
+
],
|
|
31
|
+
invalid: [
|
|
32
|
+
{ code: '<Any data-translate="true">ほげ</Any>', options, errors: [ { message: 'data-translate 属性は使用せず、 @/any/path/Translate コンポーネントを利用してください' } ] },
|
|
33
|
+
{ code: '<Translate><Any>ほげ</Any></Translate>', options, errors: [ { message: 'Translate 内では <br /> 以外のタグは使えません' } ] },
|
|
34
|
+
{ code: '<Translate><Any /></Translate>', options, errors: [ { message: 'Translate 内では <br /> 以外のタグは使えません' } ] },
|
|
35
|
+
{ code: '<Translate></Translate>', options, errors: [ { message: 'Translate 内には必ずテキストを設置してください' } ] },
|
|
36
|
+
]
|
|
37
|
+
})
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
module.exports = {
|
|
2
|
-
meta: {
|
|
3
|
-
type: 'suggestion',
|
|
4
|
-
messages: {
|
|
5
|
-
'a11y-icon-button-has-name': '{{ message }}',
|
|
6
|
-
},
|
|
7
|
-
schema: [],
|
|
8
|
-
},
|
|
9
|
-
create(context) {
|
|
10
|
-
return {
|
|
11
|
-
JSXOpeningElement: (node) => {
|
|
12
|
-
if (!node.name.name || !node.name.name.match(/Button(Anchor)?$/)) {
|
|
13
|
-
return
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const children = node.parent.children.filter((c) => (
|
|
17
|
-
!(c.type === 'JSXText' && c.value.match(/^\s*\n+\s*$/))
|
|
18
|
-
))
|
|
19
|
-
|
|
20
|
-
if (children.length === 0) {
|
|
21
|
-
return
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
let existIcon = false
|
|
25
|
-
let existNoIcon = false
|
|
26
|
-
let targetNode = node
|
|
27
|
-
|
|
28
|
-
const child = children.find((c) => {
|
|
29
|
-
if (c.type === 'JSXElement' && c.openingElement.name.name.match(/Icon$/)) {
|
|
30
|
-
existIcon = true
|
|
31
|
-
targetNode = c
|
|
32
|
-
|
|
33
|
-
if (!existNoIcon) {
|
|
34
|
-
existNoIcon = c.openingElement.attributes.some((a) => a.name.name === 'visuallyHiddenText')
|
|
35
|
-
}
|
|
36
|
-
} else {
|
|
37
|
-
existNoIcon = true
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
return existIcon && !existNoIcon
|
|
41
|
-
})
|
|
42
|
-
|
|
43
|
-
if (child) {
|
|
44
|
-
context.report({
|
|
45
|
-
node: targetNode,
|
|
46
|
-
messageId: 'a11y-icon-button-has-name',
|
|
47
|
-
data: {
|
|
48
|
-
message: `Button 内に Icon のみを設置する場合、Icon に visuallyHiddenText props を指定してください`,
|
|
49
|
-
},
|
|
50
|
-
});
|
|
51
|
-
}
|
|
52
|
-
},
|
|
53
|
-
}
|
|
54
|
-
},
|
|
55
|
-
}
|
|
56
|
-
module.exports.schema = []
|