eslint-plugin-smarthr 0.3.5 → 0.3.7

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 CHANGED
@@ -2,6 +2,20 @@
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.7](https://github.com/kufu/eslint-plugin-smarthr/compare/v0.3.6...v0.3.7) (2023-08-24)
6
+
7
+
8
+ ### Features
9
+
10
+ * a11y-clickable-element-has-text のチェック時、リンク内部に名称の末尾がTextがつくコンポーネントがある場合、チェックを通過するように修正 ([#69](https://github.com/kufu/eslint-plugin-smarthr/issues/69)) ([182b5d5](https://github.com/kufu/eslint-plugin-smarthr/commit/182b5d5e52c1faee26011572c48271e4c03512e1))
11
+
12
+ ### [0.3.6](https://github.com/kufu/eslint-plugin-smarthr/compare/v0.3.5...v0.3.6) (2023-08-20)
13
+
14
+
15
+ ### Features
16
+
17
+ * .eslintrc.js などの設定からparserOptions.project が設定されている場合、tsconfig.jsonの読み込み先を変更する ([#68](https://github.com/kufu/eslint-plugin-smarthr/issues/68)) ([3897faf](https://github.com/kufu/eslint-plugin-smarthr/commit/3897fafbf3bf8ccdc42a06700ff832ec97dc7ff1))
18
+
5
19
  ### [0.3.5](https://github.com/kufu/eslint-plugin-smarthr/compare/v0.3.4...v0.3.5) (2023-07-28)
6
20
 
7
21
 
package/libs/common.js CHANGED
@@ -3,13 +3,29 @@ const path = require('path')
3
3
  const fs = require('fs')
4
4
 
5
5
  const replacePaths = (() => {
6
- const tsconfig = fs.readFileSync(`${process.cwd()}/tsconfig.json`)
6
+ const cwd = process.cwd()
7
+ const eslintrc = (() => {
8
+ let file = `${cwd}/.eslintrc.js`
9
+ if (fs.existsSync(file)) {
10
+ return require(file)
11
+ }
12
+
13
+ file = `${cwd}/.eslintrc`
14
+
15
+ if (fs.existsSync(file)) {
16
+ return JSON5.parse(fs.readFileSync(file))
17
+ }
18
+
19
+ return {}
20
+ })()
21
+
22
+ const tsconfigFile = `${cwd}/${eslintrc.parserOptions?.project || 'tsconfig.json'}`
7
23
 
8
- if (!tsconfig) {
9
- throw new Error('プロジェクトルートにtsconfig.json を設置してください')
24
+ if (!fs.existsSync(tsconfigFile)) {
25
+ throw new Error(`${tsconfigFile} を設置してください`)
10
26
  }
11
27
 
12
- const { compilerOptions } = JSON5.parse(tsconfig)
28
+ const { compilerOptions } = JSON5.parse(fs.readFileSync(tsconfigFile))
13
29
 
14
30
  if (!compilerOptions || !compilerOptions.paths) {
15
31
  throw new Error('tsconfig.json の compilerOptions.paths に `"@/*": ["any_path/*"]` 形式でフロントエンドのroot dir を指定してください')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-smarthr",
3
- "version": "0.3.5",
3
+ "version": "0.3.7",
4
4
  "author": "SmartHR",
5
5
  "license": "MIT",
6
6
  "description": "A sharable ESLint plugin for SmartHR",
@@ -37,6 +37,12 @@
37
37
  </XxxButton>
38
38
  ```
39
39
 
40
+ ```jsx
41
+ <XxxAnchor>>
42
+ <XxxTextYyyy />
43
+ </XxxAnchor>
44
+ ```
45
+
40
46
  ## ✅ Correct
41
47
 
42
48
  ```jsx
@@ -65,19 +71,25 @@
65
71
  <YyyAnchoor />
66
72
  ```
67
73
 
74
+ ```jsx
75
+ <XxxAnchor>>
76
+ <XxxText />
77
+ </XxxAnchor>
78
+ ```
79
+
68
80
  ```jsx
69
81
  /*
70
82
  rules: {
71
83
  'smarthr/a11y-clickable-element-has-text': [
72
84
  'error',
73
85
  {
74
- componentsWithText: ['AnyComponent'],
86
+ componentsWithText: ['Hoge'],
75
87
  },
76
88
  ]
77
89
  },
78
90
  */
79
91
 
80
92
  <XxxButton>
81
- <AnyComponent />
93
+ <Hoge />
82
94
  </XxxButton>
83
95
  ```
@@ -15,12 +15,27 @@ const EXPECTED_NAMES = {
15
15
  '(b|B)utton$': 'Button$',
16
16
  'Anchor$': 'Anchor$',
17
17
  'Link$': 'Link$',
18
+ 'Text$': 'Text$',
19
+ 'Message$': 'Message$',
18
20
  '^a$': '(Anchor|Link)$',
19
21
  }
20
22
 
21
- const filterFalsyJSXText = (cs) => cs.filter((c) => (
22
- !(c.type === 'JSXText' && c.value.match(/^\s*\n+\s*$/))
23
- ))
23
+ const REGEX_NLSP = /^\s*\n+\s*$/
24
+ const REGEX_CLICKABLE_ELEMENT = /^(a|(.*?)Anchor(Button)?|(.*?)Link|(b|B)utton)$/
25
+ const REGEX_SMARTHR_LOGO = /SmartHRLogo$/
26
+ const REGEX_TEXT_COMPONENT = /(Text|Message)$/
27
+
28
+ const HIT_TYPES_RECURSICVE_SEARCH = ['JSXText', 'JSXExpressionContainer']
29
+ const HIT_TEXT_ATTRS = ['visuallyHiddenText', 'alt']
30
+
31
+ const filterFalsyJSXText = (cs) => cs.filter(checkFalsyJSXText)
32
+ const checkFalsyJSXText = (c) => (
33
+ !(c.type === 'JSXText' && c.value.match(REGEX_NLSP))
34
+ )
35
+
36
+ const message = `a, buttonなどのクリッカブルな要素内にはテキストを設定してください。
37
+ - 要素内にアイコン、画像のみを設置する場合はaltなどの代替テキスト用属性を指定してください
38
+ - クリッカブルな要素内に設置しているコンポーネントがテキストを含んでいる場合、"XxxxText" のように末尾に "Text" もしくは "Message" という名称を設定してください`
24
39
 
25
40
  module.exports = {
26
41
  meta: {
@@ -41,69 +56,65 @@ module.exports = {
41
56
 
42
57
  const node = parentNode.openingElement
43
58
 
44
- if (!node.name.name || !node.name.name.match(/^(a|(.*?)Anchor(Button)?|(.*?)Link|(b|B)utton)$/)) {
59
+ if (!node.name.name || !node.name.name.match(REGEX_CLICKABLE_ELEMENT)) {
45
60
  return
46
61
  }
47
62
 
48
63
  const recursiveSearch = (c) => {
49
- if (['JSXText', 'JSXExpressionContainer'].includes(c.type)) {
64
+ if (HIT_TYPES_RECURSICVE_SEARCH.includes(c.type)) {
50
65
  return true
51
66
  }
52
67
 
53
- if (c.type === 'JSXFragment') {
54
- if (c.children && filterFalsyJSXText(c.children).some(recursiveSearch)) {
55
- return true
56
- }
57
-
58
- return false
59
- }
60
-
61
- if (c.type === 'JSXElement') {
62
- // // HINT: SmartHRLogo コンポーネントは内部でaltを持っているため対象外にする
63
- if (c.openingElement.name.name.match(/SmartHRLogo$/)) {
64
- return true
65
- }
66
-
67
- if (componentsWithText.includes(c.openingElement.name.name)) {
68
- return true
68
+ switch (c.type) {
69
+ case 'JSXFragment': {
70
+ return c.children && filterFalsyJSXText(c.children).some(recursiveSearch)
69
71
  }
72
+ case 'JSXElement': {
73
+ // // HINT: SmartHRLogo コンポーネントは内部でaltを持っているため対象外にする
74
+ if (c.openingElement.name.name.match(REGEX_SMARTHR_LOGO)) {
75
+ return true
76
+ }
70
77
 
71
- // HINT: role & aria-label を同時に設定されている場合は許可
72
- let existRole = false
73
- let existAriaLabel = false
74
- const result = c.openingElement.attributes.reduce((prev, a) => {
75
- existRole = existRole || (a.name.name === 'role' && a.value.value === 'img')
76
- existAriaLabel = existAriaLabel || a.name.name === 'aria-label'
78
+ const tagName = c.openingElement.name.name
77
79
 
78
- if (prev) {
79
- return prev
80
+ if (tagName.match(REGEX_TEXT_COMPONENT) || componentsWithText.includes(tagName)) {
81
+ return true
80
82
  }
81
83
 
82
- if (!['visuallyHiddenText', 'alt'].includes(a.name.name)) {
83
- return prev
84
+ // HINT: role & aria-label を同時に設定されている場合は許可
85
+ let existRole = false
86
+ let existAriaLabel = false
87
+ const result = c.openingElement.attributes.reduce((prev, a) => {
88
+ existRole = existRole || (a.name.name === 'role' && a.value.value === 'img')
89
+ existAriaLabel = existAriaLabel || a.name.name === 'aria-label'
90
+
91
+ if (
92
+ prev ||
93
+ !HIT_TEXT_ATTRS.includes(a.name.name)
94
+ ) {
95
+ return prev
96
+ }
97
+
98
+ return (!!a.value.value || a.value.type === 'JSXExpressionContainer') ? a : prev
99
+ }, null)
100
+
101
+ if (
102
+ result ||
103
+ (existRole && existAriaLabel) ||
104
+ (c.children && filterFalsyJSXText(c.children).some(recursiveSearch))
105
+ ) {
106
+ return true
84
107
  }
85
-
86
- return (!!a.value.value || a.value.type === 'JSXExpressionContainer') ? a : prev
87
- }, null)
88
-
89
- if (result || (existRole && existAriaLabel)) {
90
- return true
91
- }
92
-
93
- if (c.children && filterFalsyJSXText(c.children).some(recursiveSearch)) {
94
- return true
95
108
  }
96
109
  }
97
110
 
98
111
  return false
99
112
  }
100
113
 
101
- const child = filterFalsyJSXText(parentNode.children).find(recursiveSearch)
102
-
103
- if (!child) {
114
+ if (!filterFalsyJSXText(parentNode.children).find(recursiveSearch)) {
104
115
  context.report({
105
116
  node,
106
- message: 'a, button要素にはテキストを設定してください。要素内にアイコン、画像のみを設置する場合はSmartHR UIのvisuallyHiddenText、通常のHTML要素にはaltなどの代替テキスト用属性を指定してください',
117
+ message,
107
118
  });
108
119
  }
109
120
  },
@@ -7,6 +7,9 @@ const EXPECTED_NAMES = {
7
7
  '^(img|svg)$': '(Img|Image|Icon)$',
8
8
  }
9
9
 
10
+ const REGEX_IMG = /(img|image)$/i // HINT: Iconは別途テキストが存在する場合が多いためチェックの対象外とする
11
+
12
+ const findAltAttr = (a) => a.name?.name === 'alt'
10
13
  const isWithinSvgJsxElement = (node) => {
11
14
  if (
12
15
  node.type === 'JSXElement' &&
@@ -15,13 +18,16 @@ const isWithinSvgJsxElement = (node) => {
15
18
  return true
16
19
  }
17
20
 
18
- if (!node.parent) {
19
- return false
20
- }
21
-
22
- return isWithinSvgJsxElement(node.parent)
21
+ return node.parent ? isWithinSvgJsxElement(node.parent) : false
23
22
  }
24
23
 
24
+ const MESSAGE_NOT_EXIST_ALT = `画像にはalt属性を指定してください。
25
+ - コンポーネントが画像ではない場合、img or image を末尾に持たない名称に変更してください。
26
+ - ボタンやリンクの先頭・末尾などに設置するアイコンとしての役割を持つ画像の場合、コンポーネント名の末尾を "Icon" に変更してください。
27
+ - SVG component の場合、altを属性として受け取れるようにした上で '<svg role="img" aria-label={alt}>' のように指定してください。`
28
+ const MESSAGE_NULL_ALT = `画像の情報をテキストにした代替テキスト('alt')を設定してください。
29
+ - 装飾目的の画像など、alt属性に指定すべき文字がない場合は背景画像にすることを検討してください。`
30
+
25
31
  module.exports = {
26
32
  meta: {
27
33
  type: 'problem',
@@ -31,25 +37,28 @@ module.exports = {
31
37
  return {
32
38
  ...generateTagFormatter({ context, EXPECTED_NAMES }),
33
39
  JSXOpeningElement: (node) => {
34
- const matcher = (node.name.name || '').match(/(img|image)$/i) // HINT: Iconは別途テキストが存在する場合が多いためチェックの対象外とする
35
- if (matcher) {
36
- const alt = node.attributes.find((a) => a.name?.name === 'alt')
40
+ if (node.name.name) {
41
+ const matcher = node.name.name.match(REGEX_IMG)
37
42
 
38
- let message = ''
43
+ if (matcher) {
44
+ const alt = node.attributes.find(findAltAttr)
39
45
 
40
- if (!alt) {
41
- if (matcher.input !== 'image' || !isWithinSvgJsxElement(node.parent)) {
42
- message = '画像にはalt属性を指定してください。SVG component の場合、altを属性として受け取れるようにした上で `<svg role="img" aria-label={alt}>` のように指定してください。画像ではない場合、img or image を末尾に持たない名称に変更してください。'
46
+ let message = ''
47
+
48
+ if (!alt) {
49
+ if (matcher.input !== 'image' || !isWithinSvgJsxElement(node.parent)) {
50
+ message = MESSAGE_NOT_EXIST_ALT
51
+ }
52
+ } else if (alt.value.value === '') {
53
+ message = MESSAGE_NULL_ALT
43
54
  }
44
- } else if (alt.value.value === '') {
45
- message = '画像の情報をテキストにした代替テキスト(`alt`)を設定してください。装飾目的の画像など、alt属性に指定すべき文字がない場合は背景画像にすることを検討してください。'
46
- }
47
55
 
48
- if (message) {
49
- context.report({
50
- node,
51
- message,
52
- });
56
+ if (message) {
57
+ context.report({
58
+ node,
59
+ message,
60
+ });
61
+ }
53
62
  }
54
63
  }
55
64
  },
@@ -8,6 +8,8 @@
8
8
  ## config
9
9
 
10
10
  - tsconfig.json の compilerOptions.pathsに '@/*', もしくは '~/*' としてroot path を指定する必要があります
11
+ - tsconfig.json はデフォルトではコマンド実行をしたディレクトリから読み込みます
12
+ - tsconfig.json の設置ディレクトリを変更したい場合、 `.eslintrc` などのeslint設定ファイルに `parserOptions.project` を設定してください
11
13
  - ドメインを識別するために以下の設定を記述する必要があります
12
14
  - globalModuleDir
13
15
  - 全体で利用するファイルを収めているディレクトリを相対パスで指定します
@@ -7,6 +7,8 @@
7
7
  ## config
8
8
 
9
9
  - tsconfig.json の compilerOptions.pathsに '@/*', もしくは '~/*' としてroot path を指定する必要があります
10
+ - tsconfig.json はデフォルトではコマンド実行をしたディレクトリから読み込みます
11
+ - tsconfig.json の設置ディレクトリを変更したい場合、 `.eslintrc` などのeslint設定ファイルに `parserOptions.project` を設定してください
10
12
  - ドメインを識別するために以下の設定を記述する必要があります
11
13
  - globalModuleDir
12
14
  - 全体で利用するファイルを収めているディレクトリを相対パスで指定します
@@ -6,6 +6,8 @@
6
6
  ## config
7
7
 
8
8
  - tsconfig.json の compilerOptions.pathsに '@/*', もしくは '~/*' としてroot path を指定する必要があります
9
+ - tsconfig.json はデフォルトではコマンド実行をしたディレクトリから読み込みます
10
+ - tsconfig.json の設置ディレクトリを変更したい場合、 `.eslintrc` などのeslint設定ファイルに `parserOptions.project` を設定してください
9
11
  - 以下の設定を行えます。全て省略可能です。
10
12
  - ignoreKeywords
11
13
  - ディレクトリ名から生成されるキーワードに含めたくない文字列を指定します
@@ -1,6 +1,8 @@
1
1
  # smarthr/require-barrel-import
2
2
 
3
3
  - tsconfig.json の compilerOptions.pathsに '@/*', もしくは '~/*' としてroot path を指定する必要があります
4
+ - tsconfig.json はデフォルトではコマンド実行をしたディレクトリから読み込みます
5
+ - tsconfig.json の設置ディレクトリを変更したい場合、 `.eslintrc` などのeslint設定ファイルに `parserOptions.project` を設定してください
4
6
  - importした対象が本来exportされているべきであるbarrel(index.tsなど)が有る場合、import pathの変更を促します
5
7
  - 例: Page/parts/Menu/Item の import は Page/parts/Menu から行わせたい
6
8
  - ディレクトリ内のindexファイルを捜査し、対象を決定します
@@ -12,7 +12,9 @@ const ruleTester = new RuleTester({
12
12
  },
13
13
  })
14
14
 
15
- const defaultErrorMessage = 'a, button要素にはテキストを設定してください。要素内にアイコン、画像のみを設置する場合はSmartHR UIのvisuallyHiddenText、通常のHTML要素にはaltなどの代替テキスト用属性を指定してください'
15
+ const defaultErrorMessage = `a, buttonなどのクリッカブルな要素内にはテキストを設定してください。
16
+ - 要素内にアイコン、画像のみを設置する場合はaltなどの代替テキスト用属性を指定してください
17
+ - クリッカブルな要素内に設置しているコンポーネントがテキストを含んでいる場合、"XxxxText" のように末尾に "Text" もしくは "Message" という名称を設定してください`
16
18
 
17
19
  ruleTester.run('a11y-clickable-element-has-text', rule, {
18
20
  valid: [
@@ -30,6 +32,8 @@ ruleTester.run('a11y-clickable-element-has-text', rule, {
30
32
  { code: 'const HogeAnchor = styled.a(() => ``)' },
31
33
  { code: 'const HogeAnchor = styled("a")(() => ``)' },
32
34
  { code: 'const HogeAnchor = styled(Anchor)(() => ``)' },
35
+ { code: 'const FugaText = styled(HogeText)(() => ``)' },
36
+ { code: 'const FugaMessage = styled(HogeMessage)(() => ``)' },
33
37
  {
34
38
  code: `<a>ほげ</a>`,
35
39
  },
@@ -105,6 +109,15 @@ ruleTester.run('a11y-clickable-element-has-text', rule, {
105
109
  {
106
110
  code: `<a><svg role="img" aria-label="hoge" /></a>`,
107
111
  },
112
+ {
113
+ code: `<a><Text /></a>`,
114
+ },
115
+ {
116
+ code: `<a><HogeText /></a>`,
117
+ },
118
+ {
119
+ code: `<a><FormattedMessage /></a>`,
120
+ },
108
121
  {
109
122
  code: `<a><AnyComponent /></a>`,
110
123
  options: [{
@@ -126,6 +139,8 @@ ruleTester.run('a11y-clickable-element-has-text', rule, {
126
139
  { code: 'const Piyo = styled("a")(() => ``)', errors: [ { message: `Piyoを正規表現 "/(Anchor|Link)$/" がmatchする名称に変更してください` } ] },
127
140
  { code: 'const Piyo = styled("a")``', errors: [ { message: `Piyoを正規表現 "/(Anchor|Link)$/" がmatchする名称に変更してください` } ] },
128
141
  { code: 'const Piyo = styled(Anchor)(() => ``)', errors: [ { message: `Piyoを正規表現 "/Anchor$/" がmatchする名称に変更してください` } ] },
142
+ { code: 'const Hoge = styled(Text)``', errors: [ { message: `Hogeを正規表現 "/Text$/" がmatchする名称に変更してください` } ] },
143
+ { code: 'const Hoge = styled(HogeMessage)``', errors: [ { message: `Hogeを正規表現 "/Message$/" がmatchする名称に変更してください` } ] },
129
144
  {
130
145
  code: `<a><img src="hoge.jpg" /></a>`,
131
146
  errors: [{ message: defaultErrorMessage }]
@@ -174,6 +189,10 @@ ruleTester.run('a11y-clickable-element-has-text', rule, {
174
189
  code: `<a><div role="article" aria-label="hoge" /></a>`,
175
190
  errors: [{ message: defaultErrorMessage }]
176
191
  },
192
+ {
193
+ code: `<a><TextWithHoge /></a>`,
194
+ errors: [{ message: defaultErrorMessage }]
195
+ },
177
196
  {
178
197
  code: `<a><AnyComponent /></a>`,
179
198
  options: [{
@@ -12,6 +12,13 @@ const ruleTester = new RuleTester({
12
12
  },
13
13
  })
14
14
 
15
+ const messageNotExistAlt = `画像にはalt属性を指定してください。
16
+ - コンポーネントが画像ではない場合、img or image を末尾に持たない名称に変更してください。
17
+ - ボタンやリンクの先頭・末尾などに設置するアイコンとしての役割を持つ画像の場合、コンポーネント名の末尾を "Icon" に変更してください。
18
+ - SVG component の場合、altを属性として受け取れるようにした上で '<svg role="img" aria-label={alt}>' のように指定してください。`
19
+ const messageNullAlt = `画像の情報をテキストにした代替テキスト('alt')を設定してください。
20
+ - 装飾目的の画像など、alt属性に指定すべき文字がない場合は背景画像にすることを検討してください。`
21
+
15
22
  ruleTester.run('a11y-image-has-alt-attribute', rule, {
16
23
  valid: [
17
24
  { code: `import styled from 'styled-components'` },
@@ -39,8 +46,8 @@ ruleTester.run('a11y-image-has-alt-attribute', rule, {
39
46
  { code: 'const Hoge = styled(Icon)``', errors: [ { message: `Hogeを正規表現 "/Icon$/" がmatchする名称に変更してください` } ] },
40
47
  { code: 'const Hoge = styled(Img)``', errors: [ { message: `Hogeを正規表現 "/Img$/" がmatchする名称に変更してください` } ] },
41
48
  { code: 'const Hoge = styled(Image)``', errors: [ { message: `Hogeを正規表現 "/Image$/" がmatchする名称に変更してください` } ] },
42
- { code: '<img />', errors: [ { message: '画像にはalt属性を指定してください。SVG component の場合、altを属性として受け取れるようにした上で `<svg role="img" aria-label={alt}>` のように指定してください。画像ではない場合、img or image を末尾に持たない名称に変更してください。' } ] },
43
- { code: '<HogeImage alt="" />', errors: [ { message: '画像の情報をテキストにした代替テキスト(`alt`)を設定してください。装飾目的の画像など、alt属性に指定すべき文字がない場合は背景画像にすることを検討してください。' } ] },
44
- { code: '<hoge><image /></hoge>', errors: [ { message: '画像にはalt属性を指定してください。SVG component の場合、altを属性として受け取れるようにした上で `<svg role="img" aria-label={alt}>` のように指定してください。画像ではない場合、img or image を末尾に持たない名称に変更してください。' } ] },
49
+ { code: '<img />', errors: [ { message: messageNotExistAlt } ] },
50
+ { code: '<HogeImage alt="" />', errors: [ { message: messageNullAlt } ] },
51
+ { code: '<hoge><image /></hoge>', errors: [ { message: messageNotExistAlt } ] },
45
52
  ]
46
53
  })