eslint-plugin-smarthr 0.1.0 → 0.1.3

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.
@@ -0,0 +1,3 @@
1
+ # https://help.github.com/en/articles/about-code-owners
2
+
3
+ * @kufu/group-dev-front-linter-reviewers
package/CHANGELOG.md CHANGED
@@ -2,28 +2,37 @@
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.1.0](https://github.com/kufu/eslint-plugin-smarthr/compare/v0.0.1...v0.1.0) (2022-02-09)
5
+ ### [0.1.3](https://github.com/kufu/eslint-plugin-smarthr/compare/v0.1.2...v0.1.3) (2022-07-05)
6
6
 
7
7
 
8
- ### ⚠ BREAKING CHANGES
8
+ ### Features
9
+
10
+ * 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))
11
+ * リンク要素内にテキストが設定されていない場合、エラーとなるルールを追加する ([#15](https://github.com/kufu/eslint-plugin-smarthr/issues/15)) ([4bbb9c1](https://github.com/kufu/eslint-plugin-smarthr/commit/4bbb9c1204a8edd068fabcdca497d94ecc1db4a4))
12
+
13
+
14
+ ### Bug Fixes
9
15
 
10
- * update prohibit-imoprt option
16
+ * redundant-nameのバグを修正する ([#20](https://github.com/kufu/eslint-plugin-smarthr/issues/20)) ([b733f18](https://github.com/kufu/eslint-plugin-smarthr/commit/b733f1835293c3b478f6d9bb3ebe944041c67038))
11
17
 
12
- * chore: require-importの対象ファイル指定方法をoptionの第一階層のkeyにする
18
+ ### [0.1.2](https://github.com/kufu/eslint-plugin-smarthr/compare/v0.1.1...v0.1.2) (2022-03-09)
13
19
 
14
- * chore: update README.md
15
20
 
16
- * chore: require-importのdefaultReportMessageを修正
21
+ ### Bug Fixes
22
+
23
+ * require-barrel-import修正(barrelファイルが複数存在する場合、一番親に当たるファイルを検知する) ([#14](https://github.com/kufu/eslint-plugin-smarthr/issues/14)) ([87a6724](https://github.com/kufu/eslint-plugin-smarthr/commit/87a67240f31c9408faad6784741bbf6a2f7ef47b))
17
24
 
18
- * chore: update yarn.lock
25
+ ### [0.1.1](https://github.com/kufu/eslint-plugin-smarthr/compare/v0.1.0...v0.1.1) (2022-03-08)
19
26
 
20
- * chore: fix test
21
27
 
22
- * chore: add test
28
+ ### Features
23
29
 
24
- * chore: refactoring prohibit-import
30
+ * add require-barrel-import rule ([#13](https://github.com/kufu/eslint-plugin-smarthr/issues/13)) ([79ee88d](https://github.com/kufu/eslint-plugin-smarthr/commit/79ee88d355e01bb8344dc95bd65157e2fbcf916e))
25
31
 
26
- * chore: add test
32
+ ## [0.1.0](https://github.com/kufu/eslint-plugin-smarthr/compare/v0.0.1...v0.1.0) (2022-02-09)
33
+
34
+
35
+ ### ⚠ BREAKING CHANGES
27
36
 
28
37
  * BREAKING CHANGE: add require-import & update prohibit-import (#12) ([e6c5c44](https://github.com/kufu/eslint-plugin-smarthr/commit/e6c5c445a21620d4b796ded00a685e5da367c7bb)), closes [#12](https://github.com/kufu/eslint-plugin-smarthr/issues/12)
29
38
 
package/README.md CHANGED
@@ -1,47 +1,67 @@
1
1
  # eslint-plugin-smarthr
2
2
 
3
- ## smarthr/a11y-icon-button-has-name
3
+ ## smarthr/a11y-clickable-element-has-text
4
4
 
5
- - ボタンの中にIconのみが設置されている場合、アクセシビリティの問題が発生する可能性を防ぐルールです
5
+ - ButtonやAnchor,Link コンポーネントにテキスト要素が設定されていない場合、アクセシビリティの問題が発生する可能性を防ぐルールです
6
6
 
7
7
  ### rules
8
8
 
9
9
  ```js
10
10
  {
11
11
  rules: {
12
- 'smarthr/a11y-icon-button-has-name': 'error', // 'warn', 'off'
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
- <YyyIcon />
33
+ <Zzz />
22
34
  </XxxButton>
23
35
  ```
24
36
 
25
37
  ### ✅ Correct
26
38
 
27
39
  ```jsx
28
- <XxxButton>
29
- <YyyIcon />
40
+ <XxxAnchor>
30
41
  Hoge
31
- </XxxButton>
42
+ </XxxAnchor>
32
43
  ```
33
44
  ```jsx
34
- <XxxButton>
45
+ <XxxLink>
35
46
  <YyyIcon />
36
- <AnyComponent />
37
- </XxxButton>
47
+ Fuga
48
+ </XxxLink>
38
49
  ```
39
50
  ```jsx
40
- <XxxButton>
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です
@@ -359,6 +379,46 @@ import { SecondaryButton } from 'smarthr-ui'
359
379
  import useTitle from '.hooks/useTitle'
360
380
  ```
361
381
 
382
+ ## smarthr/require-barrel-import
383
+
384
+ - tsconfig.json の compilerOptions.pathsに '@/*' としてroot path を指定する必要があります
385
+ - importした対象が本来exportされているべきであるbarrel(index.tsなど)が有る場合、import pathの変更を促します
386
+ - 例: Page/parts/Menu/Item の import は Page/parts/Menu から行わせたい
387
+ - ディレクトリ内のindexファイルを捜査し、対象を決定します
388
+
389
+ ### rules
390
+
391
+ ```js
392
+ {
393
+ rules: {
394
+ 'smarthr/require-barrel-import': 'error',
395
+ },
396
+ }
397
+ ```
398
+
399
+ ### ❌ Incorrect
400
+
401
+ ```js
402
+ // client/src/views/Page/parts/Menu/index.ts
403
+ export { Menu } from './Menu'
404
+ export { Item } from './Item'
405
+
406
+ // client/src/App.tsx
407
+ import { Item } from './Page/parts/Menu/Item'
408
+ ```
409
+
410
+ ### ✅ Correct
411
+
412
+
413
+ ```js
414
+ // client/src/views/Page/parts/Menu/index.ts
415
+ export { Menu } from './Menu'
416
+ export { Item } from './Item'
417
+
418
+ // client/src/App.tsx
419
+ import { Item } from './Page/parts/Menu'
420
+ ```
421
+
362
422
 
363
423
  ## smarthr/redundant-name
364
424
 
@@ -373,6 +433,8 @@ import useTitle from '.hooks/useTitle'
373
433
  - ディレクトリ名から生成されるキーワードに含めたくない文字列を指定します
374
434
  - betterNames
375
435
  - 対象の名前を修正する候補を指定します
436
+ - allowedNames
437
+ - 許可する名前を指定します
376
438
  - suffix:
377
439
  - type のみ指定出来ます
378
440
  - type のsuffixを指定します
@@ -404,6 +466,9 @@ const betterNames = {
404
466
  names: ['index'],
405
467
  },
406
468
  }
469
+ // const allowedNames = {
470
+ // '\/views\/crews\/histories\/': ['crewId'],
471
+ // }
407
472
 
408
473
  {
409
474
  rules: {
@@ -412,7 +477,7 @@ const betterNames = {
412
477
  {
413
478
  type: { ignorekeywords, suffix: ['Props', 'Type'] },
414
479
  file: { ignorekeywords, betternames },
415
- // property: { ignorekeywords },
480
+ // property: { ignorekeywords, allowedNames },
416
481
  // function: { ignorekeywords },
417
482
  // variable: { ignorekeywords },
418
483
  // class: { ignorekeywords },
@@ -449,3 +514,63 @@ type ItemProps = { hoge: string }
449
514
  type IndexProps = { hoge: () => any }
450
515
  type ResponseType = { hoge: () => any }
451
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,3 @@
1
+ # https://help.github.com/en/articles/about-code-owners
2
+
3
+ * @kufu/group-dev-front-linter-reviewers
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-smarthr",
3
- "version": "0.1.0",
3
+ "version": "0.1.3",
4
4
  "author": "SmartHR",
5
5
  "license": "MIT",
6
6
  "description": "A sharable ESLint plugin for SmartHR",
@@ -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
@@ -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
- const singularized = Inflector.singularize(keyword)
95
-
96
- return singularized === keyword ? [...prev, keyword] : [...prev, keyword, singularized]
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 (!name) {
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
 
@@ -0,0 +1,122 @@
1
+ const path = require('path')
2
+ const fs = require('fs')
3
+ const { replacePaths, rootPath } = require('../libs/common')
4
+ const calculateAbsoluteImportPath = (source) => {
5
+ if (source[0] === '/') {
6
+ return source
7
+ }
8
+
9
+ return Object.entries(replacePaths).reduce((prev, [key, values]) => {
10
+ if (source === prev) {
11
+ return values.reduce((p, v) => {
12
+ if (prev === p) {
13
+ const regexp = new RegExp(`^${key}(.+)$`)
14
+
15
+ if (prev.match(regexp)) {
16
+ return p.replace(regexp, `${path.resolve(`${process.cwd()}/${v}`)}/$1`)
17
+ }
18
+ }
19
+
20
+ return p
21
+ }, prev)
22
+ }
23
+
24
+ return prev
25
+ }, source)
26
+ }
27
+ const calculateReplacedImportPath = (source) => {
28
+ return Object.entries(replacePaths).reduce((prev, [key, values]) => {
29
+ if (source === prev) {
30
+ return values.reduce((p, v) => {
31
+ if (prev === p) {
32
+ const regexp = new RegExp(`^${path.resolve(`${process.cwd()}/${v}`)}(.+)$`)
33
+
34
+ if (prev.match(regexp)) {
35
+ return p.replace(regexp, `${key}/$1`).replace(/(\/)+/g, '/')
36
+ }
37
+ }
38
+
39
+ return p
40
+ }, prev)
41
+ }
42
+
43
+ return prev
44
+ }, source)
45
+ }
46
+ const TARGET_EXTS = ['ts', 'tsx', 'js', 'jsx']
47
+
48
+ module.exports = {
49
+ meta: {
50
+ type: 'suggestion',
51
+ messages: {
52
+ 'require-barrel-import': '{{ message }}',
53
+ },
54
+ schema: [],
55
+ },
56
+ create(context) {
57
+ const filename = context.getFilename()
58
+
59
+ const dir = (() => {
60
+ const d = filename.split('/')
61
+ d.pop()
62
+
63
+ return d.join('/')
64
+ })()
65
+
66
+ return {
67
+ ImportDeclaration: (node) => {
68
+ let sourceValue = node.source.value
69
+
70
+ if (sourceValue[0] === '.') {
71
+ sourceValue = path.resolve(`${dir}/${sourceValue}`)
72
+ }
73
+
74
+ sourceValue = calculateAbsoluteImportPath(sourceValue)
75
+
76
+ if (sourceValue[0] !== '/') {
77
+ return
78
+ }
79
+
80
+ const sources = sourceValue.split('/')
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
89
+
90
+ while (sources.length > 0) {
91
+ // HINT: 以下の場合は即終了
92
+ // - import元以下のimportだった場合
93
+ // - rootまで捜索した場合
94
+ if (
95
+ dir === rootPath ||
96
+ dir.match(new RegExp(`^${sourceValue}`))
97
+ ) {
98
+ break
99
+ }
100
+
101
+ barrel = TARGET_EXTS.map((e) => `${sourceValue}/index.${e}`).find((p) => fs.existsSync(p)) || barrel
102
+
103
+ sources.pop()
104
+ sourceValue = sources.join('/')
105
+ }
106
+
107
+ if (barrel) {
108
+ barrel = calculateReplacedImportPath(barrel)
109
+
110
+ context.report({
111
+ node,
112
+ messageId: 'require-barrel-import',
113
+ data: {
114
+ message: `${barrel.replace(/\/index\.(ts|js)x?$/, '')} からimportするか、${barrel} を削除してください`,
115
+ },
116
+ });
117
+ }
118
+ },
119
+ }
120
+ },
121
+ }
122
+ module.exports.schema = []
@@ -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 = []