eslint-plugin-smarthr 6.2.3 → 6.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,30 @@
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
+ ## [6.4.0](https://github.com/kufu/tamatebako/compare/eslint-plugin-smarthr-v6.3.0...eslint-plugin-smarthr-v6.4.0) (2026-02-16)
6
+
7
+
8
+ ### Features
9
+
10
+ * **best-practice-for-unnesessary-early-return:** 早期return直後に単独のifが存在する場合、一つのifにまとめるように促すチェックを追加 ([#1082](https://github.com/kufu/tamatebako/issues/1082)) ([e7a6d70](https://github.com/kufu/tamatebako/commit/e7a6d709a5e633c0fbb65f9f0331746cfd752124))
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * **best-practice-for-layouts:** flatMapメソッドの場合も正しくチェックできるように修正する ([#1079](https://github.com/kufu/tamatebako/issues/1079)) ([838af61](https://github.com/kufu/tamatebako/commit/838af61523370a321fc95272aca742072a04c235))
16
+
17
+ ## [6.3.0](https://github.com/kufu/tamatebako/compare/eslint-plugin-smarthr-v6.2.3...eslint-plugin-smarthr-v6.3.0) (2026-02-06)
18
+
19
+
20
+ ### Features
21
+
22
+ * **a11y-heading-in-sectioning-content:** tag属性がunrecommendedTagにrenameされたためチェック対象を調整 ([#1066](https://github.com/kufu/tamatebako/issues/1066)) ([5b524f3](https://github.com/kufu/tamatebako/commit/5b524f3a712e48953ec6aad18028f3f19d6b86ca))
23
+
24
+
25
+ ### Bug Fixes
26
+
27
+ * selectorでhas(>...)をやめ、直接属性で絞り込むように調整 ([#1069](https://github.com/kufu/tamatebako/issues/1069)) ([540426f](https://github.com/kufu/tamatebako/commit/540426f0ea90ba9b8cd67242067dcc8e4c855b5f))
28
+
5
29
  ## [6.2.3](https://github.com/kufu/tamatebako/compare/eslint-plugin-smarthr-v6.2.2...eslint-plugin-smarthr-v6.2.3) (2026-02-05)
6
30
 
7
31
 
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "eslint-plugin-smarthr",
3
- "version": "6.2.3",
3
+ "version": "6.4.0",
4
4
  "author": "SmartHR",
5
5
  "license": "MIT",
6
6
  "description": "A sharable ESLint plugin for SmartHR",
7
7
  "main": "index.js",
8
8
  "engines": {
9
- "node": ">=22.21.1"
9
+ "node": ">=22.22.0"
10
10
  },
11
11
  "scripts": {
12
12
  "test": "jest"
@@ -26,7 +26,7 @@
26
26
  "json5": "^2.2.3"
27
27
  },
28
28
  "devDependencies": {
29
- "typescript-eslint": "^8.51.0"
29
+ "typescript-eslint": "^8.54.0"
30
30
  },
31
31
  "peerDependencies": {
32
32
  "eslint": "^9"
@@ -37,5 +37,5 @@
37
37
  "eslintplugin",
38
38
  "smarthr"
39
39
  ],
40
- "gitHead": "4e8f1798eb458a4e9fd9ca57d8b9b349bcee5384"
40
+ "gitHead": "dbca48a75081a64d1f28acaccc411f0886569ca0"
41
41
  }
@@ -39,7 +39,7 @@ next/link コンポーネント直下のa要素にhref属性が指定されて
39
39
  <Link href={hoge}><a>any</a></Link>
40
40
  ```
41
41
 
42
- ### spread-attributesが設定されているなら許容したい場合
42
+ ## spread-attributesが設定されているなら許容したい場合
43
43
 
44
44
  下記の様にspread attributesが設定されていれば、href属性が設定されている扱いにしたい場合、lintのoptionとして `checkType` に `allow-spread-attributes` を設定してください。
45
45
 
@@ -50,7 +50,7 @@ next/link コンポーネント直下のa要素にhref属性が指定されて
50
50
  ```
51
51
 
52
52
  便利な設定ではありますが、**href属性が実際に設定されているかは判定出来ていないため、チェック漏れが発生する可能性があります。**
53
- 設定される場合は慎重に検討してください。
53
+ 設定する場合は慎重に検討してください。
54
54
 
55
55
  ## rules
56
56
 
@@ -46,7 +46,7 @@ const OPTION = (() => {
46
46
  const ANCHOR_ELEMENT = 'JSXOpeningElement[name.name=/(Anchor|Link|^a)$/]'
47
47
  const HREF_ATTRIBUTE = `JSXAttribute[name.name=${OPTION.react_router ? '/^(href|to)$/' : '"href"'}]`
48
48
  const NULL_HREF_ATTRIBUTE_VALUES = `${HREF_ATTRIBUTE}:matches(${['#', ''].reduce((prev, v) => {
49
- return `${prev},:has( > Literal[value="${v}"]),:has( > JSXExpressionContainer[expression.value="${v}"])`
49
+ return `${prev},[value.type="Literal"][value.value="${v}"],[value.type="JSXExpressionContainer"][value.expression.value="${v}"]`
50
50
  }, '[value=null]')})`
51
51
  const NEXT_LINK_REGEX = /Link$/
52
52
  // HINT: next/link で `Link > a` という構造がありえるので直上のJSXElementを調べる
@@ -11,6 +11,7 @@ const noHeadingTagNamesRegex = /^(span|legend)$/
11
11
  const ignoreHeadingCheckParentTypeRegex = /^(Program|ExportNamedDeclaration)$/
12
12
  const headingAttributeRegex = /^(heading|title)$/
13
13
  const ariaLabelRegex = /^aria-label(ledby)?$/
14
+ const tagAttrRegex = /^(tag|unrecommendedTag)$/
14
15
 
15
16
  const includeSectioningAsAttr = (a) => asRegex.test(a.name?.name) && bareTagRegex.test(a.value.value)
16
17
 
@@ -23,9 +24,9 @@ const pageHeadingMessage = `smarthr-ui/PageHeading が同一ファイル内に
23
24
  - 詳細: https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/a11y-heading-in-sectioning-content`
24
25
  const pageHeadingInSectionMessage = `smarthr-ui/PageHeadingはsmarthr-uiのArticle, Aside, Nav, Sectionで囲まないでください。囲んでしまうとページ全体の見出しではなくなってしまいます。
25
26
  - 詳細: https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/a11y-heading-in-sectioning-content`
26
- const noTagAttrMessage = `tag属性を指定せず、smarthr-uiのArticle, Aside, Nav, Sectionのいずれかの自動レベル計算に任せるよう、tag属性を削除してください。
27
+ const noTagAttrMessage = `tag属性、unrecommendedTag属性を指定せず、smarthr-uiのArticle, Aside, Nav, Sectionのいずれかの自動レベル計算に任せるよう、tag属性、unrecommendedTag属性を削除してください。
27
28
  - 詳細: https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/a11y-heading-in-sectioning-content
28
- - tag属性を指定することで意図しないレベルに固定されてしまう可能性があります。`
29
+ - tag属性、unrecommendedTag属性を指定することで意図しないレベルに固定されてしまう可能性があります。`
29
30
 
30
31
  const VariableDeclaratorBareToSHR = (context, node) => {
31
32
  if (!node.init) {
@@ -177,7 +178,7 @@ const forInSearchChildren = (ary) => {
177
178
  return r
178
179
  }
179
180
 
180
- const findTagAttr = (a) => a.name?.name == 'tag'
181
+ const findTagAttr = (a) => tagAttrRegex.test(a.name?.name)
181
182
 
182
183
  /**
183
184
  * @type {import('@typescript-eslint/utils').TSESLint.RuleModule<''>}
@@ -1,7 +1,37 @@
1
1
  # smarthr/a11y-help-link-with-support-href
2
2
 
3
- - [ヘルプページ](https://support.smarthr.jp/) へのリンクはsmarthr-ui/HelpLinkを使うことを促すルールです
4
- - ヘルプページへのリンクは、通常のテキストリンクと設定するべき属性が異なるため、専用のコンポーネントが用意されています
3
+ [ヘルプページ](https://support.smarthr.jp/)へのリンクは[smarthr-ui/HelpLink](https://smarthr.design/products/components/text-link/help-link/)を使うことを促すルールです
4
+
5
+ ## なぜ[ヘルプページ](https://support.smarthr.jp/)へのリンクはsmarthr-ui/HelpLinkを使う必要があるのか
6
+ ヘルプページへのリンクは、通常のテキストリンクと設定するべき属性が異なるため、専用のコンポーネントとして[smarthr-ui/HelpLink](https://smarthr.design/products/components/text-link/help-link/)が定義されています。<br />
7
+ 例えば `rel="help"` は `現在のページとリンク先の関係を表す` ものであり `ヘルプページへのリンク` であることが明確になります。
8
+
9
+ ## [ヘルプページ](https://support.smarthr.jp/)へのリンクとして扱われる条件
10
+
11
+ a要素、もしくはa要素と思われるコンポーネントのhref属性に `support` 関連と思われる値が設定さている場合がチェック対象になります。
12
+
13
+ ### a要素、もしくはa要素と思われるコンポーネントの判定方法
14
+
15
+ a要素として判定されるコンポーネントの条件は以下のいずれかに合致する場合です。
16
+
17
+ - a要素
18
+ - コンポーネント名の末尾が `Link` `Anchor` `AnchorButton` のいずれかであること
19
+
20
+ ### hrefがsupport関連のものかどうかの判定方法
21
+
22
+ hrefに設定されたURLがsupport関連であると判定される条件は以下のいずれかに合致する場合です。
23
+
24
+ - ドメイン部分に `support` という文言を含む場合
25
+ - `path` もしくは `xxxPath` というオブジェクト以下から `support` という値を参照する場合
26
+ - 例:
27
+ - path.support
28
+ - path.support.xxxx.yyyy
29
+ - path.xxxx.support.yyyy
30
+ - 変数の場合、`support` と `href` もしくは `url` という単語を含む場合
31
+ - 大文字小文字の組み合わせも許容されています
32
+ - 例:
33
+ - supportURL
34
+ - xxxSupportYyyHref
5
35
 
6
36
  ## rules
7
37
 
@@ -18,17 +48,37 @@
18
48
  ## ❌ Incorrect
19
49
 
20
50
  ```jsx
51
+ // supportという文字列を含むドメインの場合、smarthr-ui/HelpLinkを利用する必要がある
21
52
  <TextLink href="https://support.smarthr.jp/xxxxx">any</TextLink>
22
53
  <a href={`//support.smarthr.jp/${hoge}`}>any</a>
23
- <AnchorButton href={supportURL}>any</a>
54
+ ```
55
+
56
+ ```jsx
57
+ // path、xxxPath形式のオブジェクト以下で `support` という値を参照する場合もHelpLinkの利用が必要
24
58
  <AnyAnchor href={path.support.xxxx.yyyy} />
59
+ <AnyAnchor href={path.xxxx.support.yyyy} />
60
+ ```
61
+
62
+ ```jsx
63
+ // supportという単語が含まれる変数名が設定されている場合も同様にHelpLinkを利用する必要がある
64
+ <AnchorButton href={supportURL}>any</AnchorButton>
65
+ <AnchorButton href={xxxSupportYyyHref}>any</AnchorButton>
25
66
  ```
26
67
 
68
+
27
69
  ## ✅ Correct
28
70
 
29
71
  ```jsx
30
72
  <HelpLink href="https://support.smarthr.jp/xxxxx">any</HelpLink>
31
73
  <HelpLink href={`//support.smarthr.jp/${hoge}`}>any</HelpLink>
32
- <HelpLink href={supportURL}>any</HelpLink>
33
- <HogeHelpLink href={path.support.xxxx.yyyy} />
74
+ ```
75
+
76
+ ```jsx
77
+ <AnyHelpLink href={path.support.xxxx.yyyy} />
78
+ <AnyHelpLink href={path.xxxx.support.yyyy} />
79
+ ```
80
+
81
+ ```jsx
82
+ <StyledHelpLink href={supportURL}>any</StyledHelpLink>
83
+ <StyledHelpLink href={xxxSupportYyyHref}>any</StyledHelpLink>
34
84
  ```
@@ -1,7 +1,49 @@
1
1
  # smarthr/a11y-image-has-alt-attribute
2
2
 
3
- - 画像やアイコンにalt属性を設定することを強制するルールです
4
- - checkTypeオプションに 'allow-spread-attributes' を指定することで spread attributeが設定されている場合はcorrectに出来ます。
3
+ 画像にalt属性(代替テキスト)を設定することを強制するルールです
4
+
5
+ ## なぜ画像に代替テキストが必要なのか
6
+
7
+ スクリーンリーダーなどの一部のブラウザで**対象画像が何を表しているのか?という情報が欠落することを防ぐ**目的があります。<br />
8
+ 閲覧可能なUI上では十分な情報が存在していても、テキスト読み上げなどでは情報が不足することがあるため、**画像が何を表しているのかを説明する必要があります。**
9
+
10
+ [詳細: 画像に代替テキストが付与されている](https://smarthr.design/accessibility/check-list/alternative-text/)
11
+
12
+
13
+ ### 画像に代替テキストが不要な場合について
14
+
15
+ 例外的に**装飾目的の画像はaltに空文字を指定する**ことが許容されます。(例: `<XxxImage alt="" />`)<br />
16
+ **ただしこのルールでは上記のようなalt指定もエラーになります。**<br />
17
+ 理由は**SmartHRのプロダクトにおいて装飾目的の画像はほぼ全てアイコンとなる**ためです。<br />
18
+ smarthr-uiが提供するIconではないアイコンのコンポーネントを定義する場合、コンポーネント名の末尾を `Icon` にすることでこのチェックを回避出来ます。<br />
19
+ SmartHRのプロダクトにおいて、アイコンを単一で使う可能性は低く、利用する場合でも[a11y-clickable-element-has-text](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/a11y-clickable-element-has-text)で別途チェックされます。
20
+
21
+ [詳細: 装飾目的の画像が無視できるようになっている](https://smarthr.design/accessibility/check-list/decorative-image/)
22
+
23
+ ## alt以外の代替テキストの設定方法について
24
+
25
+ **前提として可能な限りalt属性を利用してください。** 迷うような条件の場合、alt属性を利用すれば大きな問題は発生しないでしょう。<br />
26
+ 例外として以下の場合 `aria-describedby` 属性を利用して代替テキストを設定することが出来ます。
27
+
28
+ 1. 設定するべき代替テキストが、すでに他要素でテキスト、もしくは代替テキストとして存在している
29
+ 2. 1であり、かつその要素と直接関係性がある場合
30
+
31
+ わかり易い例として以下の様なパターンが考えられます。
32
+
33
+ - Tableに値が一覧されており、それをグラフ画像として表示する場合
34
+ - 別アイコンにチェックアイコンを重ねる場合
35
+
36
+ ## spread-attributesが設定されているなら許容したい場合
37
+
38
+ 下記の様にspread attributesが設定されていれば、代替テキストが設定されている扱いにしたい場合、lintのoptionとして `checkType` に `allow-spread-attributes` を設定してください。
39
+
40
+ ```jsx
41
+ // checkType: 'allow-spread-attributes'
42
+ <XxxImage {...args} />
43
+ ```
44
+
45
+ 便利な設定ではありますが、**代替テキストが実際に設定されているかは判定出来ていないため、チェック漏れが発生する可能性があります。**
46
+ 設定する場合は慎重に検討してください。
5
47
 
6
48
  ## rules
7
49
 
@@ -19,27 +61,24 @@
19
61
  ## ❌ Incorrect
20
62
 
21
63
  ```jsx
64
+ // 画像に代替テキストが設定されていないためNG
22
65
  <Img />
23
66
  ```
67
+
24
68
  ```jsx
69
+ // 子要素が存在する場合でも同様にNG
70
+ // この場合、Imageは画像要素ではない場合がありますが
71
+ // その場合は画像と勘違いしない名称に変更してください
25
72
  <Image>
26
73
  <Any />
27
74
  </Image>
28
75
  ```
76
+
29
77
  ```jsx
30
- <Icon />
31
- ```
32
- ```jsx
78
+ // spread attributesにに代替テキスト用の属性が含まれていてもNG
79
+
33
80
  // checkType: 'always'
34
81
  <XxxImage {...args} />
35
- <YyyIcon {...args} any="any" />
36
- ```
37
- ```jsx
38
- import styled from 'styled-components'
39
-
40
- const StyledHoge = styled.img``
41
- const StyledFuga = styled(Img)``
42
- const StyledPiyo = styled(Icon)``
43
82
  ```
44
83
 
45
84
  ## ✅ Correct
@@ -47,23 +86,19 @@ const StyledPiyo = styled(Icon)``
47
86
  ```jsx
48
87
  <Img alt="message" />
49
88
  ```
89
+
50
90
  ```jsx
51
91
  <Image alt="message">
52
92
  <Any />
53
93
  </Image>
54
94
  ```
95
+
55
96
  ```jsx
56
- <Icon alt="message" />
97
+ // Iconは画像として扱われますが、例外的に代替テキストを設定しなくてもOK
98
+ <Icon />
57
99
  ```
100
+
58
101
  ```jsx
59
102
  // checkType: 'allow-spread-attributes'
60
103
  <XxxImage {...args} />
61
- <YyyIcon {...args} any="any" />
62
- ```
63
- ```jsx
64
- import styled from 'styled-components'
65
-
66
- const StyledImage = styled.img``
67
- const StyledImg = styled(Img)``
68
- const StyledIcon = styled(Icon)``
69
104
  ```
@@ -1,10 +1,32 @@
1
1
  # smarthr/a11y-input-has-name-attribute
2
2
 
3
- - input, textarea, select など入力要素に name 属性を設定することを強制するルールです。
4
- - 入力要素は name を設定することでブラウザの補完機能が有効になる可能性が高まります。
5
- - 補完機能はブラウザによって異なるため、補完される可能性が上がるよう、name には半角英数の小文字・大文字と一部記号(`_ , [, ]`)のみ利用可能です。
6
- - input[type="radio"] は name を適切に設定することでラジオグループが確立され、キーボード操作しやすくなる等のメリットがあります。
7
- - checkTypeオプションに 'allow-spread-attributes' を指定することで spread attributeが設定されている場合はcorrectに出来ます。
3
+ input, textarea, select など入力要素に name 属性を設定することを強制するルールです。
4
+
5
+ ## なぜ入力要素にname属性を設定する必要があるのか
6
+
7
+ 入力要素にname属性を適切に設定することで、キーボード操作や補完機能が適切に動作するようになります。<br />
8
+ 例えば入力要素の一種である `input[type="radio"]` は同一のname属性が設定されたものは一つのグループとして扱われ、矢印キーでグループ内を適切に移動できるようになります。<br />
9
+ 他にはname属性に `address` `zip_code` など住所が連想される単語を含めた場合、ブラウザの補完機能で住所が自動入力される可能性が高まります。
10
+
11
+ ## name属性に設定する文字列のフォーマットについて
12
+
13
+ なるべく同一のフォーマットをもちいることで自動補完される可能性を上げるためname属性が一定のフォーマットになるようチェックを行っています。<br />
14
+ 使える文字は以下のとおりです。
15
+
16
+ - 半角英数の大文字・小文字
17
+ - 一部記号(`_` `[` `]`)
18
+
19
+ ## spread-attributesが設定されているなら許容したい場合
20
+
21
+ 下記の様にspread attributesが設定されていれば、name属性が設定されている扱いにしたい場合、lintのoptionとして `checkType` に `allow-spread-attributes` を設定してください。
22
+
23
+ ```jsx
24
+ // checkType: 'allow-spread-attributes'
25
+ <AnyInput {...args} />
26
+ ```
27
+
28
+ 便利な設定ではありますが、**name属性が実際に設定されているかは判定出来ていないため、チェック漏れが発生する可能性があります。**
29
+ 設定する場合は慎重に検討してください。
8
30
 
9
31
  ## rules
10
32
 
@@ -22,24 +44,18 @@
22
44
  ## ❌ Incorrect
23
45
 
24
46
  ```jsx
47
+ // name属性が存在しないためNG
25
48
  <RadioButton />
26
49
  <Input type="radio" />
27
50
  <input type="text" />
28
51
  <Textarea />
29
52
  <Select />
30
-
31
- // checkType: 'always'
32
- <AnyInput {...args} />
33
- <AnyInput {...args} any="any" />
34
53
  ```
35
54
 
36
-
37
55
  ```jsx
38
- import styled from 'styled-components';
39
-
40
- const StyledHoge = styled.input``;
41
- const StyledFuga = styled(Input)``;
42
- const StyledPiyo = styled(RadioButton)``;
56
+ // 仮にargsにname属性が存在していてもNG
57
+ // checkType: 'always'
58
+ <AnyInput {...args} />
43
59
  ```
44
60
 
45
61
  ## ✅ Correct
@@ -50,16 +66,9 @@ const StyledPiyo = styled(RadioButton)``;
50
66
  <input type="text" name="any" />
51
67
  <Textarea name="some" />
52
68
  <Select name="piyo" />
53
-
54
- // checkType: 'allow-spread-attributes'
55
- <AnyInput {...args} />
56
- <AnyInput {...args} any="any" />
57
69
  ```
58
70
 
59
71
  ```jsx
60
- import styled from 'styled-components';
61
-
62
- const StyledInput = styled.input``;
63
- const StyledInput = styled(Input)``;
64
- const StyledRadioButton = styled(RadioButton)``;
72
+ // checkType: 'allow-spread-attributes'
73
+ <AnyInput {...args} />
65
74
  ```
@@ -1,8 +1,27 @@
1
1
  # smarthr/a11y-input-in-form-control
2
2
 
3
- - 入力要素をsmarthr-ui/FormControl、もしくはsmarthr-ui/Fieldsetで囲むことを促すルールです
4
- - SectioningContent + Heading でのマークアップではなく、FormControl・Fieldsetを使うように促します
5
- - FormControlFieldsetを使うことでlabelと入力要素が適切に紐づいたり、RadioButtonなどが適切にグルーピングされるようになり、アクセシビリティ的メリットが得られます
3
+ 入力要素をsmarthr-ui/FormControl、もしくはsmarthr-ui/Fieldsetで囲むことを促すルールです
4
+
5
+ ## なぜFormControlFieldsetで囲まなければならないのか
6
+
7
+ FormControl、Fieldsetはそれぞれlabel要素、fieldset要素とlegend要素を含み、それらは適切に入力要素と関連付けされます。<br />
8
+ 例えばFormControlでInputをラップした場合、FormControlが出力するlabel要素は適切にInputと紐づくので、label要素をクリックすることで入力要素にfocusされます。<br />
9
+ この動作はブラウザがデフォルトで持つ機能であり、当たり前に期待される動作のため必ず設定する必要性があります。<br />
10
+
11
+ また入力要素は必ずラベルとなるテキストを何らかの形でもつ必要があります。<br />
12
+ 紐づくラベルを持たない入力要素はスクリーンリーダーなど一部のブラウザに置いて **その入力要素がなんであるか?という情報が欠落した状態** になり、ユーザー操作に影響します。<br />
13
+
14
+ 同様にFieldsetは複数の入力要素やFormControlを一つのグループとして扱うことを表します。<br />
15
+ 適切に設定することでformの内容をわかりやすく示すことができます。
16
+
17
+ ### SectioningContent ではなく Fieldset の利用を推奨する理由
18
+
19
+ このルールではform要素以下でSectioninContent(Section,Article,Aside,Nav)を発見した場合、Fieldsetに変更することを促します。<br />
20
+ SectioninContentでもmarkupの観点としては問題ありませんが、一部スクリーンリーダーなどに搭載されている **特定の要素に対するジャンプ機能** などを利用する際にデメリットが生じます。<br />
21
+
22
+ form要素以下で見出しとして利用される事が多いlabel要素、legend要素以外に**Headingが混ざった場合、ジャンプ機能によって意図せずスキップされてしまう可能性**が高まります。
23
+
24
+ fielset要素はmarkupとしては必須でinputを持つ必要性はないこと、form内に存在する見出しはformの別の入力要素に対する説明である場合がほとんどであることなどからこのルールではform要素以下では全ての見出しをlabel, legendに統一するためこのチェックを行っています。
6
25
 
7
26
  ## rules
8
27
 
@@ -25,33 +44,42 @@
25
44
  ```jsx
26
45
  // FormControlで囲まれていないためNG
27
46
  <Input />
47
+ ```
28
48
 
49
+ ```jsx
29
50
  // FormControl・FieldsetではなくSectionでマークアップされているためNG
30
51
  <Section>
31
52
  <Heading />
32
53
  <Select />
33
54
  </Section>
55
+ ```
34
56
 
35
- // RadioButton, CheckboxはFieldsetでグルーピングする必要があるためNG
57
+ ```jsx
58
+ // RadioButton, Checkboxは内部にlabel要素を持つためFormControlで囲むことは不適切
59
+ // 見出しを設定したい場合、Fieldsetでグルーピングする
36
60
  <FormControl title="any heading">
37
61
  <RadioButton>{a.label}</RadioButton>
38
62
  </FormControl>
63
+ ```
39
64
 
65
+ ```jsx
40
66
  // FormControlが複数の入力要素を持ってしまっているのでNG
41
67
  <FormControl title="any heading">
42
68
  <Input />
43
69
  <Combobox />
44
70
  </FormControl>
71
+ ```
45
72
 
46
-
73
+ ```jsx
47
74
  // FormControlがネストしてしまっているのでNG
48
75
  <FormControl>
49
76
  <SubFormControl>
50
77
  <Checkbox />
51
78
  </SubFormControl>
52
79
  </FormControl>
80
+ ```
53
81
 
54
-
82
+ ```jsx
55
83
  // Fieldsetには role="group" がデフォルトで設定されているのでNG
56
84
  <Fieldset role="group" />
57
85
  ```
@@ -62,42 +90,38 @@
62
90
  <FormControl title="any heading">
63
91
  <Input />
64
92
  </FormControl>
93
+ ```
94
+
95
+ ```jsx
96
+ // smarthr-ui/Checkbox はlabelを含むため、なんの入力要素かが単独で伝えられるので
97
+ // FormControl・Fieldsetで囲む必要はない (Fieldsetで囲んでも問題はない)
98
+ <Checkbox />
99
+ ```
65
100
 
101
+ ```jsx
66
102
  <Fieldset title="any heading">
67
103
  {radios.map((a) => (
68
104
  <RadioButton>{a.label}</RadioButton>
69
105
  ))}
70
106
  </Fieldset>
107
+ ```
71
108
 
72
- <FormControl title="any heading" role="group">
73
- <WarekiPicker />
74
- ~
75
- <WarekiPicker />
76
- </FormControl>
77
-
78
- <Fieldset title="any heading">
79
- <FormControl role="group">
109
+ ```jsx
110
+ <Fieldset title="date range">
111
+ <FormControl label={{ text: "start", unrecommendedHide: true }}>
80
112
  <WarekiPicker />
81
- ~
113
+ </FormControl>
114
+ ~
115
+ <FormControl label={{ text: "end", unrecommendedHide: true }}>
82
116
  <WarekiPicker />
83
- </FormControl>>
117
+ </FormControl>
84
118
  </Fieldset>
119
+ ```
85
120
 
121
+ ```jsx
86
122
  // childrenを持たないFieldset、FormControlは入力要素として扱うためOK
87
123
  <Fieldset title="any heading">
88
124
  <HogeFieldset />
89
125
  <FugaFormControl />
90
126
  </Fieldset>
91
-
92
- // Sectionより先にFormControl・Fieldsetで囲んでいるためOK
93
- <Section>
94
- <Heading />
95
- <FormControl title="any heading">
96
- <Input />
97
- </FormControl>
98
- </Section>
99
-
100
- // smarthr-ui/Checkbox はlabelを含むため、なんの入力要素かが単独で伝えられるので
101
- // FormControl・Fieldsetで囲む必要はない (囲んでも問題はない)
102
- <Checkbox />
103
127
  ```
@@ -1,7 +1,29 @@
1
1
  # smarthr/a11y-numbered-text-within-ol
2
2
 
3
- - "1. hoge", "2. fuga" ... のように連番のテキストをもつコンポーネントはol要素でマークアップすることを促すルールです
4
- - ol要素でマークアップすることで連番テキストをもつ要素同士の関係、順番に意味があることを適切に示すことが出来ます
3
+ "1. hoge", "2. fuga" ... のように連番のテキストはol要素でマークアップすることを促すルールです
4
+
5
+ ## 連番のテキストをolでマークアップするメリット
6
+
7
+ ol要素でマークアップすることで連番テキストをもつ要素同士の関係、順番に意味があることを適切に表すことが出来ます。<br />
8
+ 連番のテキストをol要素でマークアップすることは必須ではありませんが、このルールを導入することでマークアップの基準を揃えることができ、ユーザーに対する情報提供の方法が統一されます。
9
+
10
+ ## 連番として扱われるテキスト
11
+
12
+ 以下の条件に当てはまった場合、連番が設定されているテキストとして扱われます。
13
+
14
+ - 数値と数値以外の二文字が先頭にある文字列(Aとします)
15
+ - A以降で、Aに設定された数値+1の値を先頭にあり、かつその直後に数値以外の二文字がある文字列
16
+
17
+ ## olとして扱われる要素・コンポーネント
18
+
19
+ 以下はol要素として扱われます
20
+
21
+ - ol
22
+ - OrderedListが名称のsuffixにつくコンポーネント
23
+ - 例:
24
+ - OrderedList
25
+ - XxxOrderedList
26
+ - XxxOrderedYyyList
5
27
 
6
28
  ## rules
7
29
 
@@ -19,17 +41,23 @@
19
41
  // ol要素で囲まれていないためNG
20
42
  <Any>1. hoge</Any>
21
43
  <Any>2. fuga</Any>
44
+ ```
22
45
 
46
+ ```jsx
23
47
  // 属性でも同様にチェックする
24
48
  <Any title="1. hoge" />
25
49
  <Any title="2. fuga" />
50
+ ```
26
51
 
52
+ ```jsx
27
53
  // ol要素内で連番を設定しているとNG
28
54
  <OrderedList>
29
55
  <li>1. hoge</li>
30
56
  <li>2. fuga</li>
31
57
  </OrderedList>
58
+ ```
32
59
 
60
+ ```jsx
33
61
  // 同一のol要素で囲まれていないためNG
34
62
  <OrderedList>
35
63
  <li>hoge</li>
@@ -37,7 +65,6 @@
37
65
  <OrderedList>
38
66
  <li>fuga</li>
39
67
  </OrderedList>>
40
-
41
68
  ```
42
69
 
43
70
  ## ✅ Correct
@@ -47,12 +74,16 @@
47
74
  <li>hoge</li>
48
75
  <li>fuga</li>
49
76
  </ol>
77
+ ```
50
78
 
79
+ ```jsx
51
80
  <OrderedList>
52
81
  <Any title="hoge" />
53
82
  <Any title="fuga" />
54
83
  </OrderedList>
84
+ ```
55
85
 
86
+ ```jsx
56
87
  // デフォルトの連番からフォーマット、スタイルを変更したい場合
57
88
  // counter-reset + counter-increment で表現する
58
89
  // 参考: [MDN CSS カウンターの使用](https://developer.mozilla.org/ja/docs/Web/CSS/CSS_counter_styles/Using_CSS_counters)
@@ -71,7 +102,7 @@
71
102
 
72
103
  const OrderedList = styled.ol`
73
104
  list-style: none; // デフォルトのstyleを消す
74
- counter-reset: hoge; // カウンターの名称。わかりやすいものなら何でもOK
105
+ counter-reset: hoge; // カウンターの名称。同一html内で被らない名称にする
75
106
  `
76
107
  const NumberedHeading = styled(Heading)`
77
108
  &::before {
@@ -1,6 +1,7 @@
1
1
  const MULTI_CHILDREN_REGEX = /(Cluster|Stack)$/
2
2
  const REGEX_NLSP = /^\s*\n+\s*$/
3
3
  const FLEX_END_REGEX = /^(flex-)?end$/
4
+ const MAP_METHOD_REGEX = /^(map|flatMap)$/
4
5
 
5
6
  const LAYOUT_COMPONENT_REGEX_WITHOUT_STACK = /(Center|Cluster|Container|Reel|Sidebar)$/
6
7
  const LAYOUT_COMPONENT_REGEX = /(Center|Cluster|Container|Reel|Stack|Sidebar)$/
@@ -44,7 +45,7 @@ const searchChildren = (node) => {
44
45
  case 'ChainExpression':
45
46
  return searchChildren(node.expression)
46
47
  case 'CallExpression':
47
- return node.callee.property?.name !== 'map'
48
+ return !MAP_METHOD_REGEX.test(node.callee.property?.name)
48
49
  case 'ConditionalExpression':
49
50
  return searchChildren(node.consequent) && searchChildren(node.alternate)
50
51
  case 'LogicalExpression':
@@ -185,7 +186,7 @@ module.exports = {
185
186
  message: `Fieldsetのlegend属性にアイコンを設定する場合 <Fieldset legend={{ text: 'テキスト', icon: <XxxIcon /> }} /> のようにlegend.icon属性を利用してください${DETAIL_LINK_MESSAGE}`,
186
187
  })
187
188
  },
188
- [`JSXElement:has( > JSXOpeningElement[name.name=/RadioButton(Panel)?$/]) ${LAYOUT_ELEMENT_NOT_SPAN}`]: (node) => {
189
+ [`JSXElement[openingElement.name.name=/RadioButton(Panel)?$/] ${LAYOUT_ELEMENT_NOT_SPAN}`]: (node) => {
189
190
  const component = node.name.name.match(LAYOUT_COMPONENT_REGEX)[1]
190
191
 
191
192
  context.report({
@@ -193,7 +194,7 @@ module.exports = {
193
194
  message: `RadioButton, RadioButtonPanelの子孫に${component}を置く場合、as属性、もしくはforwardedAs属性に \`span\` を指定してください${DETAIL_LINK_MESSAGE}`,
194
195
  })
195
196
  },
196
- [`JSXElement:has( > JSXOpeningElement[name.name=/Checkbox?$/]) ${LAYOUT_ELEMENT_NOT_SPAN}`]: (node) => {
197
+ [`JSXElement[openingElement.name.name=/Checkbox?$/] ${LAYOUT_ELEMENT_NOT_SPAN}`]: (node) => {
197
198
  const component = node.name.name.match(LAYOUT_COMPONENT_REGEX)[1]
198
199
 
199
200
  context.report({
@@ -27,12 +27,97 @@ const anyAction = (a) => {
27
27
  ```
28
28
 
29
29
  - 上記のような条件に修正することで、本質的に実行したい処理の条件がわかりやすくなります
30
- - このルールは以下の条件すべてに合致する場合、NGにします
30
+ - このルールは基本的に以下の条件すべてに合致する場合、NGにします
31
31
  - 早期return以降にifやelse、switch, tryがない
32
32
  - 早期returnの条件ブロック以降に変数宣言がない
33
33
  - 早期returnの条件ブロック以降にreturnがない
34
34
  - 早期returnが値を返していない
35
35
 
36
+ ## 特殊なパターンのチェックについて
37
+
38
+ ### 早期returnが連続する場合
39
+
40
+ 下記のように早期returnが連続している場合、NGになります。
41
+
42
+ ```jsx
43
+ const anyAction = (a, b) => {
44
+ if (a) return
45
+ if (b) { return }
46
+
47
+ const hoge = 'any'
48
+ ...
49
+ }
50
+ ```
51
+
52
+ これらの早期returnは本質的に一つの条件のためまとめてください。
53
+
54
+ ```jsx
55
+ const anyAction = (a, b) => {
56
+ if (a || b) return
57
+
58
+ const hoge = 'any'
59
+ ...
60
+ }
61
+ ```
62
+
63
+ 上記のように一つの条件にまとめることで一連の条件であること、よりよい条件がある場合見つけやすくなることなどのメリットがあります。
64
+
65
+
66
+ ### 早期return直後にifが単独で存在する場合
67
+
68
+ 下記の様に早期return直後にifが単独で存在する場合、NGになります。
69
+
70
+ ```jsx
71
+ const anyAction = (a, b) => {
72
+ if (a) return
73
+
74
+ if (b) {
75
+ ...
76
+ }
77
+ }
78
+ ```
79
+
80
+ これらのifは本質的に一つの条件のためまとめてください。
81
+
82
+ ```jsx
83
+ const anyAction = (a, b) => {
84
+ if (!a && b) {
85
+ ...
86
+ }
87
+ }
88
+ ```
89
+
90
+ 上記のように一つの条件にまとめることで一連の条件であること、よりよい条件がある場合見つけやすくなることなどのメリットがあります。
91
+ このチェックは **早期return直後にifが単独で存在する場合** のみNGとするため **早期returnの直後にelseをもつifが存在する場合** や **早期returnの直後に複数のifが存在する場合** はNGになりません。
92
+
93
+ ```jsx
94
+ // 早期returnの直後にelseをもつifが存在する場合はOK
95
+ const anyAction = (a, b) => {
96
+ if (a) return
97
+
98
+ if (b) {
99
+ ...
100
+ } else {
101
+ // else ifの場合もNGにならない
102
+ ...
103
+ }
104
+ }
105
+ ```
106
+
107
+ ```jsx
108
+ // 早期returnの直後に複数のifが存在する場合はOK
109
+ const anyAction = (a, b, c) => {
110
+ if (a) return
111
+
112
+ if (b) {
113
+ ...
114
+ }
115
+ if (c) {
116
+ ...
117
+ }
118
+ }
119
+ ```
120
+
36
121
  ## rules
37
122
 
38
123
  ```js
@@ -40,8 +40,9 @@ module.exports = {
40
40
  const action = (node) => {
41
41
  const fn = searchFunction(node).body.body
42
42
  // 0: 最初の早期returnを検索中
43
- // 1: 最初の早期returnを発見、その後も連続して早期returnが存在する場合
44
- // 2: 1の後、早期returnの連続が途切れた場合
43
+ // 1: 最初の早期returnを発見直後
44
+ // 2: 1の直後に早期returnではないifが見つかった場合
45
+ // 3: 1の後にif以外が見つかった場合
45
46
  let flg = 0
46
47
 
47
48
  for (let i = 0; i < fn.length; i++) {
@@ -62,25 +63,43 @@ module.exports = {
62
63
  case 'TryStatement':
63
64
  return
64
65
  case 'IfStatement':
65
- if (flg === 1 && node === getEarlyReturn(b)) {
66
- context.report({
67
- node,
68
- message: `早期returnのifが分割されています${DETAIL_LINK}
66
+ if (flg === 1) {
67
+ if (node === getEarlyReturn(b)) {
68
+ context.report({
69
+ node,
70
+ message: `早期returnのifが分割されています${DETAIL_LINK}
69
71
  - 一つのifにまとめるよう、条件を調整してください`,
70
- })
72
+ })
73
+ return
74
+ } else if (!b.alternate) {
75
+ flg = 2
76
+ continue
77
+ }
71
78
  }
79
+
72
80
  return
73
81
  }
74
82
 
75
- flg = 2
83
+ flg = 3
76
84
  }
77
85
 
78
- context.report({
79
- node,
80
- message: `後続の処理の逆の条件の早期returnのため修正してください。${DETAIL_LINK}
86
+ switch (flg) {
87
+ case 2:
88
+ context.report({
89
+ node,
90
+ message: `本質的に一つの条件が複数のifに分割されています${DETAIL_LINK}
91
+ - 直後のifと一つにまとめるよう、条件を調整してください`,
92
+ })
93
+ return
94
+ case 3:
95
+ context.report({
96
+ node,
97
+ message: `後続の処理の逆の条件の早期returnのため修正してください。${DETAIL_LINK}
81
98
  - 本質的に行いたい処理の条件とは逆がifに記述されているため、ロジックを確認する際条件を逆転させて考える余計な手間が発生しています
82
99
  - 条件を逆転させたうえで後続の処理をifの内部に移動してください`,
83
- })
100
+ })
101
+ return
102
+ }
84
103
  }
85
104
 
86
105
  return {
@@ -1,16 +1,5 @@
1
1
  const SCHEMA = []
2
2
 
3
- const searchBubbleUp = (node) => {
4
- switch (node.type) {
5
- case 'Program':
6
- case 'JSXAttribute':
7
- return null
8
- case 'TemplateLiteral':
9
- return node
10
- }
11
-
12
- return searchBubbleUp(node.parent)
13
- }
14
3
 
15
4
  /**
16
5
  * @type {import('@typescript-eslint/utils').TSESLint.RuleModule<''>}
@@ -23,20 +12,20 @@ module.exports = {
23
12
  },
24
13
  create(context) {
25
14
  const checker = (node) => {
26
- // HINT: TemplateLiteralがネストしている場合、親側だけチェックする
27
- if (!searchBubbleUp(node.parent)) {
28
- return context.report({
29
- node,
30
- message: `属性に設定している文字列から先頭、末尾の空白文字を削除してください
15
+ return context.report({
16
+ node,
17
+ message: `属性に設定している文字列から先頭、末尾の空白文字を削除してください
31
18
  - 詳細: https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/trim-props`,
32
- fix: (fixer) => fixer.replaceText(node, context.sourceCode.getText(node).replace(/^('|"|`)\s+/, '$1').replace(/\s+('|"|`)$/, '$1')),
33
- })
34
- }
19
+ fix: (fixer) => fixer.replaceText(node, context.sourceCode.getText(node).replace(/^('|"|`)\s+/, '$1').replace(/\s+('|"|`)$/, '$1')),
20
+ })
35
21
  }
36
22
 
37
23
  return {
38
- 'JSXAttribute Literal[value=/(^ | $)/]': checker,
39
- 'JSXAttribute TemplateLiteral:has( > TemplateElement:matches(:first-child[value.raw=/^ /],:last-child[value.raw=/ $/]))': checker,
24
+ 'JSXAttribute > Literal[value=/(^ | $)/]': checker,
25
+ 'JSXAttribute > JSXExpressionContainer>Literal[value=/(^ | $)/]': checker,
26
+ 'JSXAttribute > JSXExpressionContainer > TemplateLiteral > TemplateElement:matches(:first-child[value.raw=/^ /],:last-child[value.raw=/ $/])': (node) => {
27
+ checker(node.parent)
28
+ },
40
29
  }
41
30
  },
42
31
  }
@@ -20,9 +20,9 @@ const pageMessage = `smarthr-ui/PageHeading が同一ファイル内に複数存
20
20
  - 詳細: https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/a11y-heading-in-sectioning-content`
21
21
  const pageInSectionMessage = `smarthr-ui/PageHeadingはsmarthr-uiのArticle, Aside, Nav, Sectionで囲まないでください。囲んでしまうとページ全体の見出しではなくなってしまいます。
22
22
  - 詳細: https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/a11y-heading-in-sectioning-content`
23
- const noTagAttrMessage = `tag属性を指定せず、smarthr-uiのArticle, Aside, Nav, Sectionのいずれかの自動レベル計算に任せるよう、tag属性を削除してください。
23
+ const noTagAttrMessage = `tag属性、unrecommendedTag属性を指定せず、smarthr-uiのArticle, Aside, Nav, Sectionのいずれかの自動レベル計算に任せるよう、tag属性、unrecommendedTag属性を削除してください。
24
24
  - 詳細: https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/a11y-heading-in-sectioning-content
25
- - tag属性を指定することで意図しないレベルに固定されてしまう可能性があります。`
25
+ - tag属性、unrecommendedTag属性を指定することで意図しないレベルに固定されてしまう可能性があります。`
26
26
  const notHaveHeadingMessage = (elementName, isNav) => `${elementName} はHeading要素を含んでいません。
27
27
  - 詳細: https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/a11y-heading-in-sectioning-content
28
28
  - SectioningContentはHeadingを含むようにマークアップする必要があります
@@ -69,6 +69,7 @@ ruleTester.run('a11y-heading-in-sectioning-content', rule, {
69
69
  { code: '<Section><Heading>hoge</Heading><Heading>fuga</Heading></Section>', errors: [ { message: lowerMessage } ] },
70
70
  { code: '<Section><PageHeading>hoge</PageHeading></Section>', errors: [ { message: pageInSectionMessage } ] },
71
71
  { code: '<Section><Heading tag="h2">hoge</Heading></Section>', errors: [ { message: noTagAttrMessage } ] },
72
+ { code: '<Section><Heading unrecommendedTag="h2">hoge</Heading></Section>', errors: [ { message: noTagAttrMessage } ] },
72
73
  { code: '<Section></Section>', errors: [ { message: notHaveHeadingMessage('Section') } ] },
73
74
  { code: '<Aside><HogeSection></HogeSection></Aside>', errors: [ { message: notHaveHeadingMessage('Aside') }, { message: notHaveHeadingMessage('HogeSection') } ] },
74
75
  { code: '<Aside any="hoge"><HogeSection><Heading /></HogeSection></Aside>', errors: [ { message: notHaveHeadingMessage('Aside') } ] },
@@ -17,7 +17,7 @@ const errorMessage = (type, name) => `${name}には子要素が一つしか無
17
17
  - styleを確認し、div・spanなど、別要素でマークアップし直すか、${name}を削除してください
18
18
  - as, forwardedAsなどでSectioningContent系要素に変更している場合、対応するsmarthr-ui/Section, Aside, Nav, Article のいずれかに差し替えてください`
19
19
 
20
- ruleTester.run('best-practice-for-button-element', rule, {
20
+ ruleTester.run('best-practice-for-layouts', rule, {
21
21
  valid: [
22
22
  { code: `<Center />` },
23
23
  { code: `<Cluster />` },
@@ -30,30 +30,30 @@ ruleTester.run('best-practice-for-button-element', rule, {
30
30
  { code: `<Stack><Hoge /><Hoge /></Stack>` },
31
31
  { code: `<Stack>{a}<Hoge /></Stack>` },
32
32
  { code: `<AnyStack>{a.map(action)}</AnyStack>` },
33
- { code: `<AnyStack>{a?.map(action)}</AnyStack>` },
33
+ { code: `<AnyStack>{a?.flatMap(action)}</AnyStack>` },
34
34
  { code: `<AnyStack>{a.b?.map(action)}</AnyStack>` },
35
- { code: `<AnyStack>{a?.b?.map(action)}</AnyStack>` },
35
+ { code: `<AnyStack>{a?.b?.flatMap(action)}</AnyStack>` },
36
36
  { code: `<AnyStack>{a && <><Hoge /><Hoge /></>}</AnyStack>` },
37
37
  { code: `<AnyStack>{a && a.map(action)}</AnyStack>` },
38
- { code: `<AnyStack>{a && a.b.map(action)}</AnyStack>` },
38
+ { code: `<AnyStack>{a && a.b.flatMap(action)}</AnyStack>` },
39
39
  { code: `<AnyStack>{a || <><Hoge /><Hoge /></>}</AnyStack>` },
40
40
  { code: `<AnyStack>{a || a.map(action)}</AnyStack>` },
41
- { code: `<AnyStack>{a || a.b.map(action)}</AnyStack>` },
41
+ { code: `<AnyStack>{a || a.b.flatMap(action)}</AnyStack>` },
42
42
  { code: `<AnyStack>{a ? a.b.map(action) : <Hoge />}</AnyStack>` },
43
- { code: `<AnyStack>{a ? <Hoge /> : a.b.map(action)}</AnyStack>` },
43
+ { code: `<AnyStack>{a ? <Hoge /> : a.b.flatMap(action)}</AnyStack>` },
44
44
  { code: `<AnyStack>{a ? <Hoge /> : a ? <Hoge /> : a.b.map(action)}</AnyStack>` },
45
45
  { code: `<Cluster><Hoge /><Hoge /></Cluster>` },
46
46
  { code: `<Cluster>{a}<Hoge /></Cluster>` },
47
- { code: `<AnyCluster>{a.map(action)}</AnyCluster>` },
47
+ { code: `<AnyCluster>{a.flatMap(action)}</AnyCluster>` },
48
48
  { code: `<AnyCluster>{a && <><Hoge /><Hoge /></>}</AnyCluster>` },
49
49
  { code: `<AnyCluster>{a && a.map(action)}</AnyCluster>` },
50
- { code: `<AnyCluster>{a && a.b.map(action)}</AnyCluster>` },
50
+ { code: `<AnyCluster>{a && a.b.flatMap(action)}</AnyCluster>` },
51
51
  { code: `<AnyCluster>{a || <><Hoge /><Hoge /></>}</AnyCluster>` },
52
- { code: `<AnyCluster>{a || a.map(action)}</AnyCluster>` },
52
+ { code: `<AnyCluster>{a || a.flatMap(action)}</AnyCluster>` },
53
53
  { code: `<AnyCluster>{a || a.b.map(action)}</AnyCluster>` },
54
- { code: `<AnyCluster>{a ? a.b.map(action) : <Hoge />}</AnyCluster>` },
54
+ { code: `<AnyCluster>{a ? a.b.flatMap(action) : <Hoge />}</AnyCluster>` },
55
55
  { code: `<AnyCluster>{a ? <Hoge /> : a.b.map(action)}</AnyCluster>` },
56
- { code: `<AnyCluster>{a ? <Hoge /> : a ? <Hoge /> : a.b.map(action)}</AnyCluster>` },
56
+ { code: `<AnyCluster>{a ? <Hoge /> : a ? <Hoge /> : a.b.flatMap(action)}</AnyCluster>` },
57
57
  { code: `<Cluster justify="flex-end">{a}</Cluster>` },
58
58
  { code: `<HogeCluster justify="end" gap={0}>{a}</HogeCluster>` },
59
59
  { code: `<Stack align="flex-end">{a}</Stack>` },
@@ -18,6 +18,9 @@ const UNNESESSARY_EARLY_RETURN_ERROR = `後続の処理の逆の条件の早期r
18
18
  const SPLIT_EARLY_RETURN_ERROR = `早期returnのifが分割されています
19
19
  - 詳細: https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/best-practice-for-unnesessary-early-return
20
20
  - 一つのifにまとめるよう、条件を調整してください`
21
+ const SPLIT_CONFIG_ERROR = `本質的に一つの条件が複数のifに分割されています
22
+ - 詳細: https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/best-practice-for-unnesessary-early-return
23
+ - 直後のifと一つにまとめるよう、条件を調整してください`
21
24
 
22
25
  ruleTester.run('best-practice-for-unnesessary-early-return', rule, {
23
26
  valid: [
@@ -109,6 +112,36 @@ ruleTester.run('best-practice-for-unnesessary-early-return', rule, {
109
112
  }
110
113
  }
111
114
  ` },
115
+ { code: `
116
+ const anyAction = (a) => {
117
+ if (!a) {
118
+ return
119
+ }
120
+
121
+ if (b) { /* any 1 */ } else { /* any 2 */ }
122
+ }
123
+ ` },
124
+ { code: `
125
+ const anyAction = (a) => {
126
+ if (!a) {
127
+ return
128
+ }
129
+
130
+ if (b) { /* any 1 */ }
131
+ if (c) { /* any 2 */ }
132
+ }
133
+ ` },
134
+ { code: `
135
+ const anyAction = (a) => {
136
+ if (!a) {
137
+ return
138
+ }
139
+
140
+ if (b) { /* any 1 */ }
141
+ if (c) { /* any 2 */ }
142
+ any()
143
+ }
144
+ ` },
112
145
  ],
113
146
  invalid: [
114
147
  { code: `
@@ -184,6 +217,15 @@ ruleTester.run('best-practice-for-unnesessary-early-return', rule, {
184
217
 
185
218
  otherAction()
186
219
  }
187
- `, errors: [ SPLIT_EARLY_RETURN_ERROR ] },
220
+ `, errors: [ UNNESESSARY_EARLY_RETURN_ERROR, SPLIT_EARLY_RETURN_ERROR ] },
221
+ { code: `
222
+ const anyAction = (a, b) => {
223
+ if (!a) {
224
+ return
225
+ }
226
+
227
+ if (b) { /* any 1 */ }
228
+ }
229
+ `, errors: [ SPLIT_CONFIG_ERROR ] },
188
230
  ]
189
231
  })
@@ -89,12 +89,12 @@ ruleTester.run('trim-props', rule, {
89
89
  {
90
90
  code: '<div data-spec={` a${b} c `}>....</div>',
91
91
  output: '<div data-spec={`a${b} c`}>....</div>',
92
- errors: [{ message: ERROR_MESSAGE }],
92
+ errors: [{ message: ERROR_MESSAGE }, { message: ERROR_MESSAGE }],
93
93
  },
94
94
  {
95
95
  code: '<div data-spec={` a${b ? ` ${c} ` : " "} d `}>....</div>',
96
96
  output: '<div data-spec={`a${b ? ` ${c} ` : " "} d`}>....</div>',
97
- errors: [{ message: ERROR_MESSAGE }],
97
+ errors: [{ message: ERROR_MESSAGE }, { message: ERROR_MESSAGE }],
98
98
  },
99
99
  ],
100
100
  })