eslint-plugin-smarthr 0.3.27 → 0.4.1

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
+ ### [0.4.1](https://github.com/kufu/eslint-plugin-smarthr/compare/v0.4.0...v0.4.1) (2024-02-21)
6
+
7
+
8
+ ### Features
9
+
10
+ * a11y-numbered-text-within-olを追加する ([#105](https://github.com/kufu/eslint-plugin-smarthr/issues/105)) ([167b92f](https://github.com/kufu/eslint-plugin-smarthr/commit/167b92f0f29db8ee9a446d25e09e04a7b11ce340))
11
+ * ComboBoxなどのinputAttributesでtitle属性が指定された場合、擬似的にラベルが付いていると判定するように修正 ([#113](https://github.com/kufu/eslint-plugin-smarthr/issues/113)) ([5f3b594](https://github.com/kufu/eslint-plugin-smarthr/commit/5f3b5943ec64a18d094a9c66627ad1db2bbabe08))
12
+
13
+ ## [0.4.0](https://github.com/kufu/eslint-plugin-smarthr/compare/v0.3.27...v0.4.0) (2024-02-05)
14
+
15
+
16
+ ### ⚠ BREAKING CHANGES
17
+
18
+ * 利用者がいなくなったsmarthr/redundant-nameを削除する (#111)
19
+
20
+ ### Features
21
+
22
+ * a11y-input-in-form-controlでlabelが設定されている可能性が高いRadio, Checkboxの複数形コンポーネントを正しく判定できるようにする ([#112](https://github.com/kufu/eslint-plugin-smarthr/issues/112)) ([77ee8f4](https://github.com/kufu/eslint-plugin-smarthr/commit/77ee8f4cac883eb0a198875305a416f0172a584e))
23
+
24
+
25
+ ### Bug Fixes
26
+
27
+ * 利用者がいなくなったsmarthr/redundant-nameを削除する ([#111](https://github.com/kufu/eslint-plugin-smarthr/issues/111)) ([2bc1011](https://github.com/kufu/eslint-plugin-smarthr/commit/2bc10118cc0a18366300c3816f091060d2a0677d))
28
+
5
29
  ### [0.3.27](https://github.com/kufu/eslint-plugin-smarthr/compare/v0.3.26...v0.3.27) (2024-01-28)
6
30
 
7
31
 
package/README.md CHANGED
@@ -7,6 +7,7 @@
7
7
  - [a11y-image-has-alt-attribute](https://github.com/kufu/eslint-plugin-smarthr/tree/main/rules/a11y-image-has-alt-attribute)
8
8
  - [a11y-input-has-name-attribute](https://github.com/kufu/eslint-plugin-smarthr/tree/main/rules/a11y-input-has-name-attribute)
9
9
  - [a11y-input-in-form-control](https://github.com/kufu/eslint-plugin-smarthr/tree/main/rules/a11y-input-in-form-control)
10
+ - [a11y-numbered-text-within-ol](https://github.com/kufu/eslint-plugin-smarthr/tree/main/rules/a11y-numbered-text-within-ol)
10
11
  - [a11y-prohibit-input-placeholder](https://github.com/kufu/eslint-plugin-smarthr/tree/main/rules/a11y-prohibit-input-placeholder)
11
12
  - [a11y-prohibit-useless-sectioning-fragment](https://github.com/kufu/eslint-plugin-smarthr/tree/main/rules/a11y-prohibit-useless-sectioning-fragment)
12
13
  - [a11y-trigger-has-button](https://github.com/kufu/eslint-plugin-smarthr/tree/main/rules/a11y-trigger-has-button)
@@ -20,7 +21,6 @@
20
21
  - [prohibit-file-name](https://github.com/kufu/eslint-plugin-smarthr/tree/main/rules/prohibit-file-name)
21
22
  - [prohibit-import](https://github.com/kufu/eslint-plugin-smarthr/tree/main/rules/prohibit-import)
22
23
  - [prohibit-path-within-template-literal](https://github.com/kufu/eslint-plugin-smarthr/tree/main/rules/prohibit-path-within-template-literal)
23
- - [redundant-name](https://github.com/kufu/eslint-plugin-smarthr/tree/main/rules/redundant-name)
24
24
  - [require-barrel-import](https://github.com/kufu/eslint-plugin-smarthr/tree/main/rules/require-barrel-import)
25
25
  - [require-declaration](https://github.com/kufu/eslint-plugin-smarthr/tree/main/rules/require-declaration)
26
26
  - [require-export](https://github.com/kufu/eslint-plugin-smarthr/tree/main/rules/require-export)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-smarthr",
3
- "version": "0.3.27",
3
+ "version": "0.4.1",
4
4
  "author": "SmartHR",
5
5
  "license": "MIT",
6
6
  "description": "A sharable ESLint plugin for SmartHR",
@@ -22,7 +22,6 @@
22
22
  "url": "https://github.com/kufu/eslint-plugin-smarthr/issues"
23
23
  },
24
24
  "dependencies": {
25
- "inflected": "^2.1.0",
26
25
  "json5": "^2.2.0"
27
26
  },
28
27
  "devDependencies": {
@@ -6,7 +6,9 @@
6
6
  - URL遷移を行う場合、hrefが設定されていないとキーボード操作やコンテキストメニューからの遷移ができなくなります
7
7
  - これらの操作は href属性を参照します
8
8
  - 無効化されたリンクであることを表したい場合 `href={undefined}` を設定してください
9
- - checkTypeオプションに 'smart' を指定することで spread attributeが設定されている場合はcorrectに出来ます。
9
+ - checkTypeオプションに 'smart' を指定することで spread attributeが設定されている場合はcorrectに出来ます
10
+ - react-router-dom packageを利用している場合、a要素にto属性が指定されている場合、href属性が指定されているものとして許容します
11
+ - next/link コンポーネント直下のa要素にhref属性が指定されていないことを許容します
10
12
 
11
13
  ## rules
12
14
 
@@ -2,8 +2,11 @@ const { generateTagFormatter } = require('../../libs/format_styled_components')
2
2
 
3
3
  const EXPECTED_LABELED_INPUT_NAMES = {
4
4
  'RadioButton$': '(RadioButton)$',
5
+ 'RadioButtons$': '(RadioButtons)$',
5
6
  'RadioButtonPanel$': '(RadioButtonPanel)$',
7
+ 'RadioButtonPanels$': '(RadioButtonPanels)$',
6
8
  'Check(B|b)ox$': '(CheckBox)$',
9
+ 'Check(B|b)ox(e)?s$': '(CheckBoxes)$',
7
10
  }
8
11
  const EXPECTED_INPUT_NAMES = {
9
12
  '(I|^i)nput$': '(Input)$',
@@ -43,9 +46,9 @@ const FORM_CONTROL_INPUTS_REGEX = new RegExp(`(${Object.keys(EXPECTED_INPUT_NAME
43
46
  const LABELED_INPUTS_REGEX = new RegExp(`(${Object.keys(EXPECTED_LABELED_INPUT_NAMES).join('|')})`)
44
47
  const SEARCH_INPUT_REGEX = /SearchInput$/
45
48
  const INPUT_REGEX = /(i|I)nput$/
46
- const RADIO_BUTTONS_REGEX = /RadioButton(Panel)?$/
47
- const CHECKBOX_REGEX = /Check(B|b)ox?$/
48
- const SELECT_REGEX = /(S|s)elect?$/
49
+ const RADIO_BUTTONS_REGEX = /RadioButton(Panel)?(s)?$/
50
+ const CHECKBOX_REGEX = /Check(B|b)ox(s|es)?$/
51
+ const SELECT_REGEX = /(S|s)elect(s)?$/
49
52
  const FROM_CONTROLS_REGEX = new RegExp(`(${Object.keys(EXPECTED_FORM_CONTROL_NAMES).join('|')})`)
50
53
  const FORM_CONTROL_REGEX = /(Form(Control|Group))$/
51
54
  const FIELDSET_REGEX = /Fieldset$/
@@ -54,11 +57,13 @@ const SECTIONING_REGEX = /(((A|^a)(rticle|side))|(N|^n)av|(S|^s)ection|^Sectioni
54
57
  const BARE_SECTIONING_TAG_REGEX = /^(article|aside|nav|section)$/
55
58
  const LAYOUT_COMPONENT_REGEX = /((C(ent|lust)er)|Reel|Sidebar|Stack)$/
56
59
  const AS_REGEX = /^(as|forwardedAs)$/
60
+ const SUFFIX_S_REGEX = /s$/
57
61
 
58
62
  const IGNORE_INPUT_CHECK_PARENT_TYPE = /^(Program|ExportNamedDeclaration)$/
59
63
 
60
64
  const findRoleGroup = (a) => a.name?.name === 'role' && a.value.value === 'group'
61
65
  const findAsSectioning = (a) => a.name?.name.match(AS_REGEX) && a.value.value.match(BARE_SECTIONING_TAG_REGEX)
66
+ const findTitle = (i) => i.key.name === 'title'
62
67
 
63
68
  const SCHEMA = [
64
69
  {
@@ -117,6 +122,12 @@ module.exports = {
117
122
  case 'title':
118
123
  isPseudoLabel = true
119
124
  break
125
+ case 'inputAttributes': {
126
+ if (!isPseudoLabel && i.value.expression.type === 'ObjectExpression' && i.value.expression.properties.some(findTitle)) {
127
+ isPseudoLabel = true
128
+ }
129
+ break
130
+ }
120
131
  case 'type':
121
132
  switch (i.value.value) {
122
133
  case 'radio':
@@ -132,6 +143,8 @@ module.exports = {
132
143
  }
133
144
  }
134
145
  }
146
+
147
+ const isPreMultiple = isAdditionalMultiInput || isFormControlInput && nodeName.match(SUFFIX_S_REGEX)
135
148
  const isRadio = (isPureInput && isTypeRadio) || nodeName.match(RADIO_BUTTONS_REGEX);
136
149
  const isCheckbox = !isRadio && (isPureInput && isTypeCheck || nodeName.match(CHECKBOX_REGEX));
137
150
 
@@ -154,7 +167,7 @@ module.exports = {
154
167
  }
155
168
  }
156
169
 
157
- const isMultiInput = isAdditionalMultiInput || hit || isInMap
170
+ const isMultiInput = isPreMultiple || hit || isInMap
158
171
  const matcherFormControl = name.match(FORM_CONTROL_REGEX)
159
172
 
160
173
  if (matcherFormControl) {
@@ -0,0 +1,82 @@
1
+ # smarthr/a11y-numbered-text-within-ol
2
+
3
+ - "1. hoge", "2. fuga" ... のように連番のテキストをもつコンポーネントはol要素でマークアップすることを促すルールです
4
+ - ol要素でマークアップすることで連番テキストをもつ要素同士の関係、順番に意味があることを適切に示すことが出来ます
5
+
6
+ ## rules
7
+
8
+ ```js
9
+ {
10
+ rules: {
11
+ 'smarthr/a11y-numbered-text-within-ol': 'error', // 'warn', 'off',
12
+ },
13
+ }
14
+ ```
15
+
16
+ ## ❌ Incorrect
17
+
18
+ ```jsx
19
+ // ol要素で囲まれていないためNG
20
+ <Any>1. hoge</Any>
21
+ <Any>2. fuga</Any>
22
+
23
+ // 属性でも同様にチェックする
24
+ <Any title="1. hoge" />
25
+ <Any title="2. fuga" />
26
+
27
+ // ol要素内で連番を設定しているとNG
28
+ <OrderedList>
29
+ <li>1. hoge</li>
30
+ <li>2. fuga</li>
31
+ </OrderedList>
32
+
33
+ // 同一のol要素で囲まれていないためNG
34
+ <OrderedList>
35
+ <li>hoge</li>
36
+ </OrderedList>
37
+ <OrderedList>
38
+ <li>fuga</li>
39
+ </OrderedList>>
40
+
41
+ ```
42
+
43
+ ## ✅ Correct
44
+
45
+ ```jsx
46
+ <ol>
47
+ <li>hoge</li>
48
+ <li>fuga</li>
49
+ </ol>
50
+
51
+ <OrderedList>
52
+ <Any title="hoge" />
53
+ <Any title="fuga" />
54
+ </OrderedList>
55
+
56
+ // デフォルトの連番からフォーマット、スタイルを変更したい場合
57
+ // counter-reset + counter-increment で表現する
58
+ // 参考: [MDN CSS カウンターの使用](https://developer.mozilla.org/ja/docs/Web/CSS/CSS_counter_styles/Using_CSS_counters)
59
+ <OrderedList>
60
+ <li>
61
+ <NumberedHeading>hoge</NumberedHeading>
62
+ <Any />
63
+ </li>
64
+ <li>
65
+ <NumberedHeading>fuga</NumberedHeading>
66
+ <Any />
67
+ </li>
68
+ </OrderedList>
69
+
70
+ ...
71
+
72
+ const OrderedList = styled.ol`
73
+ list-style: none; // デフォルトのstyleを消す
74
+ counter-reset: hoge; // カウンターの名称。わかりやすいものなら何でもOK
75
+ `
76
+ const NumberedHeading = styled(Heading)`
77
+ &::before {
78
+ counter-increment: hoge; // hogeカウンターを+1する
79
+ content: "No " counter(hoge) ": "; // 表示される連番のフォーマット
80
+ }
81
+ `
82
+ ```
@@ -0,0 +1,134 @@
1
+ const { generateTagFormatter } = require('../../libs/format_styled_components')
2
+
3
+ const EXPECTED_NAMES = {
4
+ '(Ordered(.*)List|^ol)$': '(Ordered(.*)List)$',
5
+ '(S|s)elect$': '(Select)$',
6
+ }
7
+ const UNEXPECTED_NAMES = EXPECTED_NAMES
8
+
9
+ const NUMBERED_TEXT_REGEX = /^[\s\n]*(([1-9])([^1-9]{2})[^\s\n]*)/
10
+ const ORDERED_LIST_REGEX = /(Ordered(.*)List|^ol)$/
11
+ const SELECT_REGEX = /(S|s)elect$/
12
+
13
+ const searchOrderedList = (node) => {
14
+ if (node.type === 'JSXElement' && node.openingElement.name?.name) {
15
+ const name = node.openingElement.name.name
16
+
17
+ if (name.match(ORDERED_LIST_REGEX)) {
18
+ return node.openingElement
19
+ } else if (name.match(SELECT_REGEX)) {
20
+ // HINT: select要素の場合、optionのラベルに連番がついている場合がありえるのでignoreする
21
+ // 通常と処理を分けるためnullではなく0を返す
22
+ return 0
23
+ }
24
+ }
25
+
26
+ if (node.type === 'Program') {
27
+ return null
28
+ }
29
+
30
+ return searchOrderedList(node.parent)
31
+ }
32
+
33
+ const checkNumberedTextInOl = (result, node, context) => {
34
+ if (result) {
35
+ context.report({
36
+ node,
37
+ message: `${result.name.name} 内で連番がテキストとして記述されています。連番はol要素で表現できるため、削除してください。
38
+ - ol要素のデフォルトで表示される連番のフォーマット、スタイルから変更したい場合、counter-reset と counter-increment を利用してください
39
+ - 参考: [MDN CSS カウンターの使用](https://developer.mozilla.org/ja/docs/Web/CSS/CSS_counter_styles/Using_CSS_counters)`,
40
+ })
41
+ }
42
+ }
43
+
44
+ const renderTag = (node) => `\`${node.name.name}="${node.value.value}"\``
45
+ const renderNode = (node, matcher) => node.type === 'JSXText' ? `\`${matcher[1]}\`` : renderTag(node)
46
+
47
+ const SCHEMA = []
48
+
49
+ module.exports = {
50
+ meta: {
51
+ type: 'problem',
52
+ schema: SCHEMA,
53
+ },
54
+ create(context) {
55
+ let firstNumber = 0
56
+ let firstNumberedNode = null
57
+ let firstNumberedMatcher = null
58
+
59
+ function checker(node, matcher) {
60
+ if (matcher) {
61
+ const result = searchOrderedList(node)
62
+
63
+ if (result !== 0) {
64
+ checkNumberedTextInOl(result, node, context)
65
+
66
+ const nowNumber = matcher[2] * 1
67
+
68
+ if (firstNumberedNode && nowNumber !== firstNumber) {
69
+ if (nowNumber === firstNumber + 1) {
70
+ const resultFirst = searchOrderedList(firstNumberedNode)
71
+
72
+ if (!resultFirst) {
73
+ if (!result) {
74
+ context.report({
75
+ node: firstNumberedNode,
76
+ message: `連番を含むテキストがol要素でマークアップされていません。ol要素でマークアップすることで関連する順番に意味のある要素を適切にマークアップできるため以下の方法で修正してください。
77
+ - ${renderNode(firstNumberedNode, firstNumberedMatcher)} と ${renderNode(node, matcher)} が同じol要素内に存在するように修正してください
78
+ - 上記以外にも関連する連番をふくむ要素が存在する場合、それらも同じol内に存在する必要があります`,
79
+ })
80
+ } else {
81
+ context.report({
82
+ node: firstNumberedNode,
83
+ message: `連番を含むテキストがol要素でマークアップされていません。ol要素でマークアップすることで関連する順番に意味のある要素を適切にマークアップできるため以下の方法で修正してください。
84
+ - ${renderNode(firstNumberedNode, firstNumberedMatcher)} が ${renderNode(node, matcher)} を囲んでいるol要素内(<${result.name.name}>)に存在するように修正してください
85
+ - 上記以外にも関連する連番をふくむ要素が存在する場合、それらも同じol要素内(<${result.name.name}>)に存在する必要があります`,
86
+ })
87
+ }
88
+ } else {
89
+ if (!result) {
90
+ context.report({
91
+ node,
92
+ message: `連番を含むテキストがol要素でマークアップされていません。ol要素でマークアップすることで関連する順番に意味のある要素を適切にマークアップできるため以下の方法で修正してください。
93
+ - ${renderNode(node, matcher)} が ${renderNode(firstNumberedNode, firstNumberedMatcher)} を囲んでいるol要素内(<${resultFirst.name.name}>)に存在するように修正してください
94
+ - 上記以外にも関連する連番をふくむ要素が存在する場合、それらも同じol要素内(<${resultFirst.name.name}>)に存在する必要があります`,
95
+ })
96
+
97
+ firstNumberedNode = null
98
+ } else if (resultFirst !== result) {
99
+ context.report({
100
+ node,
101
+ message: `連番を含むテキストが同一のol要素でマークアップされていません。同一のol要素でマークアップすることでリスト内の要素関連性を正しく表せるためマークアップの修正を行ってください。
102
+ - ${renderNode(firstNumberedNode, firstNumberedMatcher)} と ${renderNode(node, matcher)} が同じol要素内に存在するように修正してください
103
+ - 上記以外にも関連する連番をふくむ要素が存在する場合、それらも同じol内に存在する必要があります`,
104
+ })
105
+ }
106
+ }
107
+ }
108
+
109
+ firstNumber = nowNumber
110
+ firstNumberedNode = node
111
+ firstNumberedMatcher = matcher
112
+ } else if (!firstNumberedNode || nowNumber === firstNumber) {
113
+ firstNumber = nowNumber
114
+ firstNumberedNode = node
115
+ firstNumberedMatcher = matcher
116
+ }
117
+ }
118
+ }
119
+ }
120
+
121
+ return {
122
+ ...generateTagFormatter({ context, EXPECTED_NAMES, UNEXPECTED_NAMES }),
123
+ JSXAttribute: (node) => {
124
+ if (node.value?.value) {
125
+ checker(node, node.value.value.match(NUMBERED_TEXT_REGEX))
126
+ }
127
+ },
128
+ JSXText: (node) => {
129
+ checker(node, node.value.match(NUMBERED_TEXT_REGEX))
130
+ },
131
+ }
132
+ },
133
+ }
134
+ module.exports.schema = SCHEMA
@@ -15,13 +15,13 @@ const ruleTester = new RuleTester({
15
15
  const noLabeledInput = (name) => `${name} を、smarthr-ui/FormControl もしくはそれを拡張したコンポーネントが囲むようマークアップを変更してください。
16
16
  - FormControlで入力要素を囲むことでラベルと入力要素が適切に紐づき、操作性が高まります
17
17
  - ${name}が入力要素とラベル・タイトル・説明など含む概念を表示するコンポーネントの場合、コンポーネント名を/((FormGroup)$|(FormControl)$|((F|^f)ieldset)$)/とマッチするように修正してください
18
- - ${name}が入力要素自体を表現するコンポーネントの一部である場合、ルートとなるコンポーネントの名称を/((I|^i)nput$|SearchInput$|(T|^t)extarea$|(S|^s)elect$|InputFile$|Combo(b|B)ox$|DatePicker$|RadioButton$|RadioButtonPanel$|Check(B|b)ox$)/とマッチするように修正してください
18
+ - ${name}が入力要素自体を表現するコンポーネントの一部である場合、ルートとなるコンポーネントの名称を/((I|^i)nput$|SearchInput$|(T|^t)extarea$|(S|^s)elect$|InputFile$|Combo(b|B)ox$|DatePicker$|RadioButton$|RadioButtons$|RadioButtonPanel$|RadioButtonPanels$|Check(B|b)ox$|Check(B|b)ox(e)?s$)/とマッチするように修正してください
19
19
  - 上記のいずれの方法も適切ではない場合、${name}のtitle属性に "どんな値を入力すれば良いのか" の説明を設定してください
20
20
  - 例: <${name} title="姓を全角カタカナのみで入力してください" />`
21
21
  const noLabeledSelect = (name) => `${name} を、smarthr-ui/FormControl もしくはそれを拡張したコンポーネントが囲むようマークアップを変更してください。
22
22
  - FormControlで入力要素を囲むことでラベルと入力要素が適切に紐づき、操作性が高まります
23
23
  - ${name}が入力要素とラベル・タイトル・説明など含む概念を表示するコンポーネントの場合、コンポーネント名を/((FormGroup)$|(FormControl)$|((F|^f)ieldset)$)/とマッチするように修正してください
24
- - ${name}が入力要素自体を表現するコンポーネントの一部である場合、ルートとなるコンポーネントの名称を/((I|^i)nput$|SearchInput$|(T|^t)extarea$|(S|^s)elect$|InputFile$|Combo(b|B)ox$|DatePicker$|RadioButton$|RadioButtonPanel$|Check(B|b)ox$)/とマッチするように修正してください
24
+ - ${name}が入力要素自体を表現するコンポーネントの一部である場合、ルートとなるコンポーネントの名称を/((I|^i)nput$|SearchInput$|(T|^t)extarea$|(S|^s)elect$|InputFile$|Combo(b|B)ox$|DatePicker$|RadioButton$|RadioButtons$|RadioButtonPanel$|RadioButtonPanels$|Check(B|b)ox$|Check(B|b)ox(e)?s$)/とマッチするように修正してください
25
25
  - 上記のいずれの方法も適切ではない場合、${name}のtitle属性に "どんな値を選択すれば良いのか" の説明を設定してください
26
26
  - 例: <${name} title="検索対象を選択してください" />`
27
27
  const invalidPureCheckboxInFormControl = (name) => `HogeFormControl が ${name} を含んでいます。smarthr-ui/FormControl を smarthr-ui/Fieldset に変更し、正しくグルーピングされるように修正してください。
@@ -42,7 +42,7 @@ const noLabeledInputInFieldset = (name) => `HogeFieldset が ラベルを持た
42
42
  - 方法1: HogeFieldset を smarthr-ui/FormControl、もしくはそれを拡張したコンポーネントに変更してください
43
43
  - 方法2: ${name} がlabel要素を含むコンポーネントである場合、名称を/(Form(Control|Group))$/にマッチするものに変更してください
44
44
  - smarthr-ui/FormControl、smarthr-ui/FormGroup はlabel要素を内包しています
45
- - 方法3: ${name} がRadioButton、もしくはCheckboxを表すコンポーネントの場合、名称を/(RadioButton$|RadioButtonPanel$|Check(B|b)ox$)/にマッチするものに変更してください
45
+ - 方法3: ${name} がRadioButton、もしくはCheckboxを表すコンポーネントの場合、名称を/(RadioButton$|RadioButtons$|RadioButtonPanel$|RadioButtonPanels$|Check(B|b)ox$|Check(B|b)ox(e)?s$)/にマッチするものに変更してください
46
46
  - smarthr-ui/RadioButton、smarthr-ui/RadioButtonPanel、smarthr-ui/Checkbox はlabel要素を内包しています
47
47
  - 方法4: HogeFieldset が smarthr-ui/Fieldset、もしくはそれを拡張しているコンポーネントではない場合、名称を /Fieldset$/ にマッチしないものに変更してください
48
48
  - 方法5: 別途label要素が存在し、それらと紐づけたい場合はlabel要素のhtmlFor属性、${name}のid属性に同じ文字列を指定してください。この文字列はhtml内で一意である必要があります
@@ -52,7 +52,7 @@ const noLabeledInputInFieldsetWithSelect = (name) => `HogeFieldset が ラベル
52
52
  - 方法1: HogeFieldset を smarthr-ui/FormControl、もしくはそれを拡張したコンポーネントに変更してください
53
53
  - 方法2: ${name} がlabel要素を含むコンポーネントである場合、名称を/(Form(Control|Group))$/にマッチするものに変更してください
54
54
  - smarthr-ui/FormControl、smarthr-ui/FormGroup はlabel要素を内包しています
55
- - 方法3: ${name} がRadioButton、もしくはCheckboxを表すコンポーネントの場合、名称を/(RadioButton$|RadioButtonPanel$|Check(B|b)ox$)/にマッチするものに変更してください
55
+ - 方法3: ${name} がRadioButton、もしくはCheckboxを表すコンポーネントの場合、名称を/(RadioButton$|RadioButtons$|RadioButtonPanel$|RadioButtonPanels$|Check(B|b)ox$|Check(B|b)ox(e)?s$)/にマッチするものに変更してください
56
56
  - smarthr-ui/RadioButton、smarthr-ui/RadioButtonPanel、smarthr-ui/Checkbox はlabel要素を内包しています
57
57
  - 方法4: HogeFieldset が smarthr-ui/Fieldset、もしくはそれを拡張しているコンポーネントではない場合、名称を /Fieldset$/ にマッチしないものに変更してください
58
58
  - 方法5: 別途label要素が存在し、それらと紐づけたい場合はlabel要素のhtmlFor属性、${name}のid属性に同じ文字列を指定してください。この文字列はhtml内で一意である必要があります
@@ -76,7 +76,7 @@ const invalidChildreninFormControl = (children) => `FormControl が、${children
76
76
  - FormControlではなく、smarthr-ui/Fieldset、もしくはsmarthr-ui/Section + smarthr-ui/Heading などでのマークアップを検討してください
77
77
  - 方法2: 親要素であるFormControlがsmarthr-ui/FormControlを拡張したコンポーネントではない場合、コンポーネント名を/(Form(Control|Group))$/と一致しない名称に変更してください`
78
78
  const requireMultiInputInFormControlWithRoleGroup = () => `HogeFormControl内に入力要素が2個以上存在しないため、'role=\"group\"'を削除してください。'role=\"group\"'は複数の入力要素を一つのグループとして扱うための属性です。
79
- - HogeFormControl内に2つ以上の入力要素が存在する場合、入力要素を含むコンポーネント名全てを/((I|^i)nput$|SearchInput$|(T|^t)extarea$|(S|^s)elect$|InputFile$|Combo(b|B)ox$|DatePicker$|RadioButton$|RadioButtonPanel$|Check(B|b)ox$)/、もしくは/((FormGroup)$|(FormControl)$|((F|^f)ieldset)$)/にマッチする名称に変更してください`
79
+ - HogeFormControl内に2つ以上の入力要素が存在する場合、入力要素を含むコンポーネント名全てを/((I|^i)nput$|SearchInput$|(T|^t)extarea$|(S|^s)elect$|InputFile$|Combo(b|B)ox$|DatePicker$|RadioButton$|RadioButtons$|RadioButtonPanel$|RadioButtonPanels$|Check(B|b)ox$|Check(B|b)ox(e)?s$)/、もしくは/((FormGroup)$|(FormControl)$|((F|^f)ieldset)$)/にマッチする名称に変更してください`
80
80
 
81
81
  ruleTester.run('a11y-input-in-form-control', rule, {
82
82
  valid: [
@@ -129,6 +129,12 @@ ruleTester.run('a11y-input-in-form-control', rule, {
129
129
  { code: '<HogeFieldset><HogeCheckBox /><HogeInput title="any" /></HogeFieldset>' },
130
130
  { code: '<FugaSection><HogeInput title="any" /></FugaSection>' },
131
131
  { code: '<HogeTextarea title="any" />' },
132
+ { code: '<HogeComboBox inputAttributes={{ title: "any" }} />' },
133
+ { code: '<HogeComboBox inputAttributes={{ title }} />' },
134
+ { code: '<Fieldset><HogeRadioButtons /></Fieldset>' },
135
+ { code: '<Fieldset><HogeRadioButtonPanels /></Fieldset>' },
136
+ { code: '<Fieldset><HogeCheckBoxs /></Fieldset>' },
137
+ { code: '<Fieldset><HogeCheckBoxes /></Fieldset>' },
132
138
  ],
133
139
  invalid: [
134
140
  { code: `import hoge from 'styled-components'`, errors: [ { message: `styled-components をimportする際は、名称が"styled" となるようにしてください。例: "import styled from 'styled-components'"` } ] },
@@ -150,6 +156,7 @@ ruleTester.run('a11y-input-in-form-control', rule, {
150
156
  { code: '<HogeSelect />', errors: [ { message: noLabeledSelect('HogeSelect') } ] },
151
157
  { code: '<HogeInputFile />', errors: [ { message: noLabeledInput('HogeInputFile') } ] },
152
158
  { code: '<HogeComboBox />', errors: [ { message: noLabeledInput('HogeComboBox') } ] },
159
+ { code: '<HogeComboBox inputAttributes={{ any }} />', errors: [ { message: noLabeledInput('HogeComboBox') } ] },
153
160
  { code: '<HogeDatePicker />', errors: [ { message: noLabeledInput('HogeDatePicker') } ] },
154
161
  { code: '<HogeFormControl><Input type="checkbox" /><Input type="checkbox" /></HogeFormControl>', errors: [ { message: invalidPureCheckboxInFormControl('Input') } ] },
155
162
  { code: '<HogeFormControl><HogeCheckBox /><Input /></HogeFormControl>', errors: [ { message: invalidMultiInputsInFormControl() } ] },
@@ -169,5 +176,8 @@ ruleTester.run('a11y-input-in-form-control', rule, {
169
176
  { code: '<FormControl><HogeFieldset /></FormControl>', errors: [ { message: invalidChildreninFormControl('HogeFieldset') } ] },
170
177
  { code: '<FormControl><HogeFormControl /></FormControl>', errors: [ { message: invalidChildreninFormControl('HogeFormControl') } ] },
171
178
  { code: '<HogeFormControl role="group"><HogeInput /></HogeFormControl>', errors: [ { message: requireMultiInputInFormControlWithRoleGroup() } ] },
179
+ { code: '<HogeFormControl><HogeRadioButtons /></HogeFormControl>', errors: [ { message: invalidRadioInFormControl('HogeRadioButtons') } ] },
180
+ { code: '<HogeFormControl><HogeCheckBoxs /></HogeFormControl>', errors: [ { message: invalidMultiInputsInFormControl() } ] },
181
+ { code: '<HogeFormControl><HogeCheckBoxes /></HogeFormControl>', errors: [ { message: invalidMultiInputsInFormControl() } ] },
172
182
  ]
173
183
  })
@@ -0,0 +1,111 @@
1
+ const rule = require('../rules/a11y-numbered-text-within-ol')
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-numbered-text-within-ol', rule, {
16
+ valid: [
17
+ { code: `const HogeOrderedFugaList = styled.ol` },
18
+ { code: `const HogeOrderedFugaList = styled(HogeOrderedList)` },
19
+ { code: `<p>1: hoge</p>` },
20
+ { code: `<p title="2. hoge" />` },
21
+ { code: `<><p>2: hoge</p><p>1: hoge</p></>` },
22
+ { code: `<ol><li>abc</li><li>def</li></ol>` },
23
+ { code: `<OrderedList><li>abc</li><li>def</li></OrderedList>` },
24
+ { code: `<Select><optgroup><option value="hoge">1. hoge</option><option value="fuga">2. fuga</option></optgroup></Select>` },
25
+ ],
26
+ invalid: [
27
+ { code: `<><p>1: hoge</p><p>2: hoge</p></>`, errors: [ { message: `連番を含むテキストがol要素でマークアップされていません。ol要素でマークアップすることで関連する順番に意味のある要素を適切にマークアップできるため以下の方法で修正してください。
28
+ - \`1: hoge\` と \`2: hoge\` が同じol要素内に存在するように修正してください
29
+ - 上記以外にも関連する連番をふくむ要素が存在する場合、それらも同じol内に存在する必要があります` } ] },
30
+ { code: `
31
+ <>
32
+ <div>
33
+ 1. abc
34
+ </div>
35
+ <p>
36
+ 2. def
37
+ </p>
38
+ </>`, errors: [ { message: `連番を含むテキストがol要素でマークアップされていません。ol要素でマークアップすることで関連する順番に意味のある要素を適切にマークアップできるため以下の方法で修正してください。
39
+ - \`1. abc\` と \`2. def\` が同じol要素内に存在するように修正してください
40
+ - 上記以外にも関連する連番をふくむ要素が存在する場合、それらも同じol内に存在する必要があります` } ] },
41
+ { code: `<><p>2: hoge</p><p>1: hoge</p><p>2: hoge</p></>`, errors: [ { message: `連番を含むテキストがol要素でマークアップされていません。ol要素でマークアップすることで関連する順番に意味のある要素を適切にマークアップできるため以下の方法で修正してください。
42
+ - \`1: hoge\` と \`2: hoge\` が同じol要素内に存在するように修正してください
43
+ - 上記以外にも関連する連番をふくむ要素が存在する場合、それらも同じol内に存在する必要があります` } ] },
44
+ { code: `<><p>1: hoge</p><ol><li>2: hoge</li></ol></>`, errors: [ { message: `連番を含むテキストがol要素でマークアップされていません。ol要素でマークアップすることで関連する順番に意味のある要素を適切にマークアップできるため以下の方法で修正してください。
45
+ - \`1: hoge\` が \`2: hoge\` を囲んでいるol要素内(<ol>)に存在するように修正してください
46
+ - 上記以外にも関連する連番をふくむ要素が存在する場合、それらも同じol要素内(<ol>)に存在する必要があります` }, { message: `ol 内で連番がテキストとして記述されています。連番はol要素で表現できるため、削除してください。
47
+ - ol要素のデフォルトで表示される連番のフォーマット、スタイルから変更したい場合、counter-reset と counter-increment を利用してください
48
+ - 参考: [MDN CSS カウンターの使用](https://developer.mozilla.org/ja/docs/Web/CSS/CSS_counter_styles/Using_CSS_counters)` } ] },
49
+ { code: `<><p>1: hoge</p><p title="2. hoge" /></>`, errors: [ { message: `連番を含むテキストがol要素でマークアップされていません。ol要素でマークアップすることで関連する順番に意味のある要素を適切にマークアップできるため以下の方法で修正してください。
50
+ - \`1: hoge\` と \`title="2. hoge"\` が同じol要素内に存在するように修正してください
51
+ - 上記以外にも関連する連番をふくむ要素が存在する場合、それらも同じol内に存在する必要があります` } ] },
52
+ { code: `<><ol><li>1: hoge</li></ol><p>2: hoge</p></>`, errors: [ { message: `ol 内で連番がテキストとして記述されています。連番はol要素で表現できるため、削除してください。
53
+ - ol要素のデフォルトで表示される連番のフォーマット、スタイルから変更したい場合、counter-reset と counter-increment を利用してください
54
+ - 参考: [MDN CSS カウンターの使用](https://developer.mozilla.org/ja/docs/Web/CSS/CSS_counter_styles/Using_CSS_counters)` }, { message: `連番を含むテキストがol要素でマークアップされていません。ol要素でマークアップすることで関連する順番に意味のある要素を適切にマークアップできるため以下の方法で修正してください。
55
+ - \`2: hoge\` が \`1: hoge\` を囲んでいるol要素内(<ol>)に存在するように修正してください
56
+ - 上記以外にも関連する連番をふくむ要素が存在する場合、それらも同じol要素内(<ol>)に存在する必要があります` } ] },
57
+ { code: `<><ol><li>1: hoge</li></ol><ol><li>2: hoge</li></ol></>`, errors: [ { message: `ol 内で連番がテキストとして記述されています。連番はol要素で表現できるため、削除してください。
58
+ - ol要素のデフォルトで表示される連番のフォーマット、スタイルから変更したい場合、counter-reset と counter-increment を利用してください
59
+ - 参考: [MDN CSS カウンターの使用](https://developer.mozilla.org/ja/docs/Web/CSS/CSS_counter_styles/Using_CSS_counters)` }, { message: `ol 内で連番がテキストとして記述されています。連番はol要素で表現できるため、削除してください。
60
+ - ol要素のデフォルトで表示される連番のフォーマット、スタイルから変更したい場合、counter-reset と counter-increment を利用してください
61
+ - 参考: [MDN CSS カウンターの使用](https://developer.mozilla.org/ja/docs/Web/CSS/CSS_counter_styles/Using_CSS_counters)` }, { message: `連番を含むテキストが同一のol要素でマークアップされていません。同一のol要素でマークアップすることでリスト内の要素関連性を正しく表せるためマークアップの修正を行ってください。
62
+ - \`1: hoge\` と \`2: hoge\` が同じol要素内に存在するように修正してください
63
+ - 上記以外にも関連する連番をふくむ要素が存在する場合、それらも同じol内に存在する必要があります` } ] },
64
+ { code: `<><Hoge any="1: hoge" /><Hoge any="2: hoge" /></>`, errors: [ { message: `連番を含むテキストがol要素でマークアップされていません。ol要素でマークアップすることで関連する順番に意味のある要素を適切にマークアップできるため以下の方法で修正してください。
65
+ - \`any="1: hoge"\` と \`any="2: hoge"\` が同じol要素内に存在するように修正してください
66
+ - 上記以外にも関連する連番をふくむ要素が存在する場合、それらも同じol内に存在する必要があります` } ] },
67
+ { code: `<><Hoge any="2: hoge" /><Hoge any="1: hoge" /><Hoge any="2: hoge" /></>`, errors: [ { message: `連番を含むテキストがol要素でマークアップされていません。ol要素でマークアップすることで関連する順番に意味のある要素を適切にマークアップできるため以下の方法で修正してください。
68
+ - \`any="1: hoge"\` と \`any="2: hoge"\` が同じol要素内に存在するように修正してください
69
+ - 上記以外にも関連する連番をふくむ要素が存在する場合、それらも同じol内に存在する必要があります` } ] },
70
+ { code: `<><Hoge any="1: hoge" /><ol><li><Hoge any="2: hoge" /></li></ol></>`, errors: [ { message: `連番を含むテキストがol要素でマークアップされていません。ol要素でマークアップすることで関連する順番に意味のある要素を適切にマークアップできるため以下の方法で修正してください。
71
+ - \`any="1: hoge"\` が \`any="2: hoge"\` を囲んでいるol要素内(<ol>)に存在するように修正してください
72
+ - 上記以外にも関連する連番をふくむ要素が存在する場合、それらも同じol要素内(<ol>)に存在する必要があります` }, { message: `ol 内で連番がテキストとして記述されています。連番はol要素で表現できるため、削除してください。
73
+ - ol要素のデフォルトで表示される連番のフォーマット、スタイルから変更したい場合、counter-reset と counter-increment を利用してください
74
+ - 参考: [MDN CSS カウンターの使用](https://developer.mozilla.org/ja/docs/Web/CSS/CSS_counter_styles/Using_CSS_counters)` } ] },
75
+ { code: `<><ol><li><Hoge any="1: hoge" /></li></ol><Hoge any="2: hoge" /></>`, errors: [ { message: `ol 内で連番がテキストとして記述されています。連番はol要素で表現できるため、削除してください。
76
+ - ol要素のデフォルトで表示される連番のフォーマット、スタイルから変更したい場合、counter-reset と counter-increment を利用してください
77
+ - 参考: [MDN CSS カウンターの使用](https://developer.mozilla.org/ja/docs/Web/CSS/CSS_counter_styles/Using_CSS_counters)` }, { message: `連番を含むテキストがol要素でマークアップされていません。ol要素でマークアップすることで関連する順番に意味のある要素を適切にマークアップできるため以下の方法で修正してください。
78
+ - \`any="2: hoge"\` が \`any="1: hoge"\` を囲んでいるol要素内(<ol>)に存在するように修正してください
79
+ - 上記以外にも関連する連番をふくむ要素が存在する場合、それらも同じol要素内(<ol>)に存在する必要があります` } ] },
80
+ { code: `<><ol><li><Hoge any="1: hoge" /></li></ol><ol><li><Hoge any="2: hoge" /></li></ol></>`, errors: [ { message: `ol 内で連番がテキストとして記述されています。連番はol要素で表現できるため、削除してください。
81
+ - ol要素のデフォルトで表示される連番のフォーマット、スタイルから変更したい場合、counter-reset と counter-increment を利用してください
82
+ - 参考: [MDN CSS カウンターの使用](https://developer.mozilla.org/ja/docs/Web/CSS/CSS_counter_styles/Using_CSS_counters)` }, { message: `ol 内で連番がテキストとして記述されています。連番はol要素で表現できるため、削除してください。
83
+ - ol要素のデフォルトで表示される連番のフォーマット、スタイルから変更したい場合、counter-reset と counter-increment を利用してください
84
+ - 参考: [MDN CSS カウンターの使用](https://developer.mozilla.org/ja/docs/Web/CSS/CSS_counter_styles/Using_CSS_counters)` }, { message: `連番を含むテキストが同一のol要素でマークアップされていません。同一のol要素でマークアップすることでリスト内の要素関連性を正しく表せるためマークアップの修正を行ってください。
85
+ - \`any="1: hoge"\` と \`any="2: hoge"\` が同じol要素内に存在するように修正してください
86
+ - 上記以外にも関連する連番をふくむ要素が存在する場合、それらも同じol内に存在する必要があります` } ] },
87
+ { code: `<><Hoge any="1: hoge" /><Hoge any="2: hoge" /><Hoge any="3: hoge" /><Hoge any="4: hoge" /></>`, errors: [ { message: `連番を含むテキストがol要素でマークアップされていません。ol要素でマークアップすることで関連する順番に意味のある要素を適切にマークアップできるため以下の方法で修正してください。
88
+ - \`any="1: hoge"\` と \`any="2: hoge"\` が同じol要素内に存在するように修正してください
89
+ - 上記以外にも関連する連番をふくむ要素が存在する場合、それらも同じol内に存在する必要があります` }, { message: `連番を含むテキストがol要素でマークアップされていません。ol要素でマークアップすることで関連する順番に意味のある要素を適切にマークアップできるため以下の方法で修正してください。
90
+ - \`any="2: hoge"\` と \`any="3: hoge"\` が同じol要素内に存在するように修正してください
91
+ - 上記以外にも関連する連番をふくむ要素が存在する場合、それらも同じol内に存在する必要があります` }, { message: `連番を含むテキストがol要素でマークアップされていません。ol要素でマークアップすることで関連する順番に意味のある要素を適切にマークアップできるため以下の方法で修正してください。
92
+ - \`any="3: hoge"\` と \`any="4: hoge"\` が同じol要素内に存在するように修正してください
93
+ - 上記以外にも関連する連番をふくむ要素が存在する場合、それらも同じol内に存在する必要があります` } ] },
94
+ { code: `<><Hoge any="1: hoge" /><Hoge any="3: hoge" /><Hoge any="4: hoge" /><Hoge any="6: hoge" /></>`, errors: [ { message: `連番を含むテキストがol要素でマークアップされていません。ol要素でマークアップすることで関連する順番に意味のある要素を適切にマークアップできるため以下の方法で修正してください。
95
+ - \`any="3: hoge"\` と \`any="4: hoge"\` が同じol要素内に存在するように修正してください
96
+ - 上記以外にも関連する連番をふくむ要素が存在する場合、それらも同じol内に存在する必要があります` } ] },
97
+ { code: `<><p>1: hoge</p><p>2: hoge</p><p>3: hoge</p><p>4: hoge</p></>`, errors: [ { message: `連番を含むテキストがol要素でマークアップされていません。ol要素でマークアップすることで関連する順番に意味のある要素を適切にマークアップできるため以下の方法で修正してください。
98
+ - \`1: hoge\` と \`2: hoge\` が同じol要素内に存在するように修正してください
99
+ - 上記以外にも関連する連番をふくむ要素が存在する場合、それらも同じol内に存在する必要があります` }, { message: `連番を含むテキストがol要素でマークアップされていません。ol要素でマークアップすることで関連する順番に意味のある要素を適切にマークアップできるため以下の方法で修正してください。
100
+ - \`2: hoge\` と \`3: hoge\` が同じol要素内に存在するように修正してください
101
+ - 上記以外にも関連する連番をふくむ要素が存在する場合、それらも同じol内に存在する必要があります` }, { message: `連番を含むテキストがol要素でマークアップされていません。ol要素でマークアップすることで関連する順番に意味のある要素を適切にマークアップできるため以下の方法で修正してください。
102
+ - \`3: hoge\` と \`4: hoge\` が同じol要素内に存在するように修正してください
103
+ - 上記以外にも関連する連番をふくむ要素が存在する場合、それらも同じol内に存在する必要があります` } ] },
104
+ { code: `<><p>1: hoge</p><p>3: hoge</p><p>4: hoge</p><p>6: hoge</p></>`, errors: [ { message: `連番を含むテキストがol要素でマークアップされていません。ol要素でマークアップすることで関連する順番に意味のある要素を適切にマークアップできるため以下の方法で修正してください。
105
+ - \`3: hoge\` と \`4: hoge\` が同じol要素内に存在するように修正してください
106
+ - 上記以外にも関連する連番をふくむ要素が存在する場合、それらも同じol内に存在する必要があります` } ] },
107
+ { code: `<><p>3: hoge</p><select><option value="hoge">1: hoge</option></select><p>4: hoge</p></>`, errors: [ { message: `連番を含むテキストがol要素でマークアップされていません。ol要素でマークアップすることで関連する順番に意味のある要素を適切にマークアップできるため以下の方法で修正してください。
108
+ - \`3: hoge\` と \`4: hoge\` が同じol要素内に存在するように修正してください
109
+ - 上記以外にも関連する連番をふくむ要素が存在する場合、それらも同じol内に存在する必要があります` } ] },
110
+ ]
111
+ })
@@ -1,97 +0,0 @@
1
- # smarthr/redundant-name
2
-
3
- - ファイル、コードの冗長な部分を取り除くことを提案するruleです
4
- - ファイルが設置されているディレクトリ構造からキーワードを生成し、取り除く文字列を生成します
5
-
6
- ## config
7
-
8
- - tsconfig.json の compilerOptions.pathsに '@/*', もしくは '~/*' としてroot path を指定する必要があります
9
- - tsconfig.json はデフォルトではコマンド実行をしたディレクトリから読み込みます
10
- - tsconfig.json の設置ディレクトリを変更したい場合、 `.eslintrc` などのeslint設定ファイルに `parserOptions.project` を設定してください
11
- - 以下の設定を行えます。全て省略可能です。
12
- - ignoreKeywords
13
- - ディレクトリ名から生成されるキーワードに含めたくない文字列を指定します
14
- - betterNames
15
- - 対象の名前を修正する候補を指定します
16
- - allowedNames
17
- - 許可する名前を指定します
18
- - suffix:
19
- - type のみ指定出来ます
20
- - type のsuffixを指定します
21
-
22
- ### ファイル例
23
- - `@/crews/index/views/page.tsx` の場合
24
- - 生成されるキーワードは `['crews', 'crew', 'index', 'page']`
25
- - `@/crews/index/views/parts/Abc.tsx` の場合
26
- - 生成されるキーワードは `['crews', 'crew', 'index', 'Abc']`
27
- - `@/crews/index/repositories/index.ts` の場合
28
- - 生成されるキーワードは `['crews', 'crew', 'index', 'repositories', 'repository']`
29
-
30
-
31
- ## rules
32
-
33
- ```js
34
- const ignorekeywords = ['views', 'parts']
35
- const betterNames = {
36
- '\/repositories\/': {
37
- operator: '-',
38
- names: ['repository', 'Repository'],
39
- },
40
- '\/entities\/': {
41
- operator: '+',
42
- names: ['entity'],
43
- },
44
- '\/slices\/': {
45
- operator: '=',
46
- names: ['index'],
47
- },
48
- }
49
- // const allowedNames = {
50
- // '\/views\/crews\/histories\/': ['crewId'],
51
- // }
52
-
53
- {
54
- rules: {
55
- 'smarthr/redundant-name': [
56
- 'error', // 'warn', 'off'
57
- {
58
- ignores: [ '\.stories\.' ], // ファイルパスに対して正規表現として一致する場合はチェック対象外にする
59
- type: { ignorekeywords, suffix: ['Props', 'Type'] },
60
- file: { ignorekeywords, betternames },
61
- // property: { ignorekeywords, allowedNames },
62
- // function: { ignorekeywords },
63
- // variable: { ignorekeywords },
64
- // class: { ignorekeywords },
65
- }
66
- ]
67
- },
68
- }
69
- ```
70
-
71
- ## ❌ Incorrect
72
-
73
- ```js
74
- // @/crews/index/views/page.tsx
75
-
76
- type CrewIndexPage = { hoge: string }
77
- type CrewsView = { hoge: string }
78
- ```
79
- ```js
80
- // @/crews/show/repositories/index.tsx
81
-
82
- type CrewIndexRepository = { hoge: () => any }
83
- ```
84
-
85
- ## ✅ Correct
86
-
87
- ```js
88
- // @/crews/index/views/page.tsx
89
-
90
- type ItemProps = { hoge: string }
91
- ```
92
- ```js
93
- // @/crews/show/repositories/index.tsx
94
-
95
- type IndexProps = { hoge: () => any }
96
- type ResponseType = { hoge: () => any }
97
- ```
@@ -1,505 +0,0 @@
1
- const path = require('path')
2
- const Inflector = require('inflected')
3
-
4
- const { rootPath } = require('../../libs/common')
5
-
6
- const uniq = (array) => array.filter((elem, index, self) => self.indexOf(elem) === index)
7
-
8
- const COMMON_DEFAULT_CONFIG = {
9
- IGNORE_KEYWORDS: ['redux', 'views', 'pages', 'parts'],
10
- }
11
- const DEFAULT_CONFIG = {
12
- ignores: [],
13
- type: {
14
- IGNORE_KEYWORDS: [
15
- 'redux', 'views', 'pages', 'parts',
16
- 'props', 'type', 'action', 'actions',
17
- ],
18
- SUFFIX: ['Props', 'Type'],
19
- },
20
- file: COMMON_DEFAULT_CONFIG,
21
- property: COMMON_DEFAULT_CONFIG,
22
- function: COMMON_DEFAULT_CONFIG,
23
- functionParams: COMMON_DEFAULT_CONFIG,
24
- variable: COMMON_DEFAULT_CONFIG,
25
- class: COMMON_DEFAULT_CONFIG,
26
- method: COMMON_DEFAULT_CONFIG,
27
- }
28
-
29
- const BETTER_NAMES_CALCULATER_PROPERTY = {
30
- type: 'object',
31
- properties: {
32
- operator: ['-', '+', '='],
33
- names: {
34
- type: 'array',
35
- items: 'string',
36
- },
37
- },
38
- }
39
- const DEFAULT_SCHEMA_PROPERTY = {
40
- ignoreKeywords: { type: 'array', items: { type: 'string' } },
41
- betterNames: {
42
- type: 'object',
43
- properties: {
44
- operator: ['-', '+', '='],
45
- names: {
46
- type: 'array',
47
- items: 'string',
48
- },
49
- },
50
- },
51
- allowedNames: {
52
- type: 'array',
53
- items: 'string',
54
- },
55
- }
56
-
57
- const SCHEMA = [
58
- {
59
- type: 'object',
60
- properties: {
61
- ignores: { type: 'array', items: { type: 'string' }, default: [] },
62
- type: {
63
- ...DEFAULT_SCHEMA_PROPERTY,
64
- suffix: { type: 'array', items: { type: 'string' } },
65
- },
66
- file: DEFAULT_SCHEMA_PROPERTY,
67
- property: DEFAULT_SCHEMA_PROPERTY,
68
- function: DEFAULT_SCHEMA_PROPERTY,
69
- functionParams: DEFAULT_SCHEMA_PROPERTY,
70
- variable: DEFAULT_SCHEMA_PROPERTY,
71
- class: DEFAULT_SCHEMA_PROPERTY,
72
- method: DEFAULT_SCHEMA_PROPERTY,
73
- },
74
- additionalProperties: false,
75
- }
76
- ]
77
-
78
- const fetchTerminalImportName = (filename) => {
79
- const names = filename.split('/')
80
- let name = names.pop()
81
-
82
- if (name === 'index') {
83
- name = names.pop()
84
- }
85
-
86
- return name
87
- }
88
- const generateRedundantKeywords = ({ args, key, terminalImportName }) => {
89
- const option = args.option[key] || {}
90
- const ignoreKeywords = option.ignoreKeywords || DEFAULT_CONFIG[key].IGNORE_KEYWORDS
91
- const terminalImportKeyword = terminalImportName ? terminalImportName.toLowerCase() : ''
92
-
93
- return args.keywords.reduce((prev, keyword) => {
94
- if (keyword === terminalImportKeyword || ignoreKeywords.includes(keyword)) {
95
- return prev
96
- }
97
-
98
- return [...prev, ...uniq([
99
- Inflector.pluralize(keyword),
100
- keyword,
101
- Inflector.singularize(keyword),
102
- ])]
103
- }, [])
104
- }
105
- const handleReportBetterName = ({
106
- key,
107
- context,
108
- option,
109
- filename,
110
- redundantKeywords,
111
- defaultBetterName,
112
- fetchName,
113
- generateMessage,
114
- }) => {
115
- if (!generateMessage) {
116
- generateMessage = (({ name, betterName }) => `${name} からパスで推測できる箇所を取り除いてしてください (例: ${betterName})`)
117
- }
118
-
119
- return (node) => {
120
- const name = fetchName(node)
121
-
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
- ) {
127
- return
128
- }
129
-
130
- let candidates = []
131
- let conciseName = redundantKeywords.reduce((prev, keyword) => {
132
- const regex = new RegExp(`(${keyword})`, 'i')
133
- const matcher = prev.match(regex)
134
-
135
- if (matcher) {
136
- candidates.push(matcher[1])
137
-
138
- return prev.replace(regex, '')
139
- }
140
-
141
- return prev
142
- }, name)
143
-
144
- if (name !== conciseName) {
145
- conciseName = conciseName
146
- .replace(/^_+/, '')
147
- .replace(/_+$/, '')
148
- .replace(/_+/, '_')
149
- let fullRedundant = false
150
-
151
- if (!conciseName) {
152
- fullRedundant = true
153
- // HINT: 1keywordで構成されている名称はそのままにする
154
- conciseName = candidates.length === 1 ? name : defaultBetterName
155
- }
156
-
157
- // HINT: camelCase、lower_snake_case の場合、keywordが取り除かれた結果違うケースになってしまう場合があるので対応する
158
- if (name.match(/^[a-z]/) && conciseName.match(/^[A-Z]/)) {
159
- conciseName = `${conciseName[0].toLowerCase()}${conciseName.slice(1)}`
160
- }
161
-
162
- if (fullRedundant) {
163
- if (name.match(/^[A-Z]/)) {
164
- candidates = candidates.map((k) => `${k[0].toUpperCase()}${k.slice(1)}`)
165
- }
166
- } else {
167
- candidates = []
168
- }
169
-
170
- candidates = uniq([conciseName, ...candidates].filter((k) => !!k))
171
-
172
- if (option.betterNames) {
173
- Object.entries(option.betterNames).forEach(([regex, calc]) => {
174
- if (calc && filename.match(new RegExp(regex))) {
175
- switch(calc.operator) {
176
- case '=':
177
- candidates = calc.names
178
- break
179
- case '-':
180
- candidates = candidates.filter((c) => !calc.names.includes(c))
181
- break
182
- case '+':
183
- candidates = uniq([...candidates, ...calc.names])
184
- break
185
- }
186
- }
187
- })
188
- }
189
-
190
- candidates = candidates.filter((c) => c !== name)
191
- }
192
-
193
- if (candidates.length > 0) {
194
- context.report({
195
- node,
196
- message: generateMessage({ name, betterName: candidates.join(', ') }),
197
- });
198
- }
199
- }
200
- }
201
-
202
- const generateTypeRedundant = (args) => {
203
- const { context, filename } = args
204
- const key = 'type'
205
- const redundantKeywords = generateRedundantKeywords({ args, key })
206
- const option = args.option[key]
207
- const defaultConfig = DEFAULT_CONFIG[key]
208
-
209
- return (node) => {
210
- const typeName = node.id.name
211
-
212
- if (
213
- option.allowedNames &&
214
- Object.entries(option.allowedNames).find(([regex, calcs]) => filename.match(new RegExp(regex)) && calcs.find((c) => c === typeName))
215
- ) {
216
- return
217
- }
218
-
219
- const suffix = option.suffix || defaultConfig.SUFFIX
220
-
221
- let SuffixedName = typeName
222
- let report = null
223
-
224
- if (!typeName.match(new RegExp(`(${suffix.join('|')})$`))) {
225
- SuffixedName = `${typeName}${suffix[0]}`
226
- report = {
227
- node,
228
- message: `type ${typeName} の名称の末尾に ${suffix.join(', ')} ${suffix.length > 1 ? 'のいずれか' : ''}を追加してください`,
229
- }
230
- }
231
-
232
- let betterName = redundantKeywords.reduce((prev, keyword) => {
233
- const result = prev.replace(new RegExp(keyword, 'i'), '')
234
-
235
- return result === 's' || result.match(/^s[A-Z]/) ? `Multiple${result.slice(1)}` : result
236
- }, SuffixedName) || suffix[0]
237
-
238
- if (betterName === 'Multiple') {
239
- betterName = `${betterName}${suffix[0]}`
240
- }
241
-
242
- if (SuffixedName !== betterName) {
243
- report = {
244
- node,
245
- message: `type ${typeName} の名称からパスで推測できる箇所を取り除いてしてください (例: ${betterName})`,
246
- }
247
- }
248
-
249
- if (report) {
250
- context.report(report)
251
- }
252
- }
253
- }
254
-
255
- const generateTypePropertyRedundant = (args) => {
256
- const key = 'property'
257
- return handleReportBetterName({
258
- ...args,
259
- key,
260
- option: args.option[key],
261
- redundantKeywords: generateRedundantKeywords({ args, key }),
262
- defaultBetterName: '',
263
- fetchName: (node) => node.key.name,
264
- })
265
- }
266
- const generateTypePropertyFunctionParamsRedundant = (args) => {
267
- const key = 'property'
268
- const redundant = handleReportBetterName({
269
- ...args,
270
- key,
271
- option: args.option[key],
272
- redundantKeywords: generateRedundantKeywords({ args, key }),
273
- defaultBetterName: '',
274
- fetchName: (node) => node.name,
275
- })
276
-
277
- return (node) => {
278
- node.params.forEach((param) => redundant(param))
279
- }
280
- }
281
-
282
- const generatePropertyRedundant = (args) => {
283
- const key = 'property'
284
-
285
- return handleReportBetterName({
286
- ...args,
287
- key,
288
- option: args.option[key],
289
- redundantKeywords: generateRedundantKeywords({ args, key }),
290
- defaultBetterName: 'item',
291
- fetchName: (node) => {
292
- // argumentsとしてわたされたobjectの展開などの場合は許可する
293
- // このファイル内で修正すべき場合などは冗長な名前を修正するべき場合はtype propertyなどで判断出来る
294
- if (node.parent.type === 'ObjectPattern') {
295
- return null
296
- }
297
-
298
- return node.key.name
299
- },
300
- })
301
- }
302
-
303
- const generateFileRedundant = (args) => {
304
- const key = 'file'
305
- const terminalImportName = fetchTerminalImportName(args.filename)
306
-
307
- return handleReportBetterName({
308
- ...args,
309
- key,
310
- option: args.option[key],
311
- redundantKeywords: generateRedundantKeywords({ args, key, terminalImportName }),
312
- defaultBetterName: 'index',
313
- fetchName: () => terminalImportName,
314
- generateMessage: ({ name, betterName }) => `${name} のファイル名からパスで推測できる箇所を取り除いてしてください (例: ${betterName})`
315
- })
316
- }
317
-
318
- const generateFunctionRedundant = (args) => {
319
- const key = 'function'
320
-
321
- return handleReportBetterName({
322
- ...args,
323
- key,
324
- option: args.option[key],
325
- redundantKeywords: generateRedundantKeywords({ args, key, terminalImportName: fetchTerminalImportName(args.filename) }),
326
- defaultBetterName: '',
327
- fetchName: (node) => node.id.name,
328
- })
329
- }
330
- const generateFunctionParamsRedundant = (args) => {
331
- const key = 'functionParams'
332
- const redundant = handleReportBetterName({
333
- ...args,
334
- key,
335
- option: args.option[key],
336
- redundantKeywords: generateRedundantKeywords({ args, key }),
337
- defaultBetterName: '',
338
- fetchName: (node) => node.name,
339
- })
340
-
341
- return (node) => {
342
- node.params.forEach((param) => redundant(param))
343
- }
344
- }
345
-
346
- const generateVariableRedundant = (args) => {
347
- const key = 'variable'
348
-
349
- return handleReportBetterName({
350
- ...args,
351
- key,
352
- option: args.option[key],
353
- redundantKeywords: generateRedundantKeywords({ args, key, terminalImportName: fetchTerminalImportName(args.filename) }),
354
- defaultBetterName: '',
355
- fetchName: (node) => node.id.name,
356
- })
357
- }
358
-
359
- const generateClassRedundant = (args) => {
360
- const key = 'class'
361
-
362
- return handleReportBetterName({
363
- ...args,
364
- key,
365
- option: args.option[key],
366
- redundantKeywords: generateRedundantKeywords({ args, key, terminalImportName: fetchTerminalImportName(args.filename) }),
367
- defaultBetterName: '',
368
- fetchName: (node) => node.id.name,
369
- })
370
- }
371
-
372
- const generateMethodRedundant = (args) => {
373
- const key = 'method'
374
-
375
- return handleReportBetterName({
376
- ...args,
377
- key,
378
- option: args.option[key],
379
- redundantKeywords: generateRedundantKeywords({ args, key }),
380
- defaultBetterName: 'item',
381
- fetchName: (node) => node.key.name,
382
- })
383
- }
384
-
385
- module.exports = {
386
- meta: {
387
- type: 'suggestion',
388
- schema: SCHEMA,
389
- },
390
- create(context) {
391
- if (!rootPath) {
392
- throw new Error('tsconfig.json の compilerOptions.paths に `@/*`、もしくは `~/*` 形式でフロントエンドのroot dir を指定してください(例: `"@/*": ["./any_path/*"]`)')
393
- }
394
-
395
- let rules = {}
396
-
397
- const option = context.options[0]
398
- let filename = context.getFilename()
399
-
400
- if ((option.ignores || []).some((i) => !!filename.match(new RegExp(i)))) {
401
- return {}
402
- }
403
-
404
- const keywords = uniq((() => {
405
- const keywordMatcher = filename.match(new RegExp(`${rootPath}/(.+?)$`))
406
-
407
- if (keywordMatcher) {
408
- const keywords = keywordMatcher[1].split('/')
409
- keywords[keywords.length - 1] = keywords[keywords.length - 1].split('.')[0]
410
-
411
- filename = `${rootPath}/${keywords.join('/')}`
412
-
413
- if (keywords[keywords.length - 1] === 'index') {
414
- keywords.pop()
415
- }
416
-
417
- // HINT: ファイル名 > ディレクトリ名 > 親ディレクトリ名 ...
418
- // の順でキーワードとしての重要度が上がる。reverseして重要度順に並べる
419
- return keywords.reverse().reduce((prev, dir, index) => {
420
- prev.push(dir.replace(/_/g, '').toLowerCase())
421
-
422
- return prev
423
- }, [])
424
- }
425
-
426
- return []
427
- })())
428
-
429
- const args = {
430
- context,
431
- option,
432
- filename,
433
- keywords,
434
- }
435
-
436
- const addRule = (key, redundant) => {
437
- const addedRules = rules[key] || []
438
-
439
- rules[key] = [...addedRules, redundant]
440
- }
441
-
442
- if (option.type) {
443
- addRule('TSTypeAliasDeclaration', generateTypeRedundant(args))
444
- // addRule('TSInterfaceDeclaration', generateTypeRedundant(args)) // 必要になったら実装する
445
- }
446
- if (option.property) {
447
- const typePropRedundant = generateTypePropertyRedundant(args)
448
- const typeFuncParamRedundant = generateTypePropertyFunctionParamsRedundant(args)
449
- const redundant = generatePropertyRedundant(args)
450
-
451
- addRule('TSPropertySignature', (node) => {
452
- typePropRedundant(node)
453
-
454
- if (node.typeAnnotation.typeAnnotation.type === 'TSFunctionType') {
455
- typeFuncParamRedundant(node.typeAnnotation.typeAnnotation)
456
- }
457
- })
458
- addRule('Property', redundant)
459
- addRule('PropertyDefinition', redundant)
460
- }
461
- if (option.file) {
462
- addRule('Program', generateFileRedundant(args))
463
- }
464
- if (option.function) {
465
- addRule('FunctionDeclaration', generateFunctionRedundant(args))
466
- }
467
- if (option.functionParams) {
468
- const redundant = generateFunctionParamsRedundant(args)
469
-
470
- addRule('FunctionDeclaration', redundant)
471
- addRule('ArrowFunctionExpression', redundant)
472
- addRule('MethodDefinition', (node) => {
473
- if (node.value.type === 'FunctionExpression') {
474
- redundant(node.value)
475
- }
476
- })
477
- }
478
- if (option.variable) {
479
- const redundant = generateVariableRedundant(args)
480
-
481
- addRule('VariableDeclarator', redundant)
482
- addRule('TSEnumDeclaration', redundant)
483
- }
484
- if (option.class) {
485
- addRule('ClassDeclaration', generateClassRedundant(args))
486
- }
487
- if (option.method) {
488
- addRule('MethodDefinition', generateMethodRedundant(args))
489
- }
490
-
491
- Object.keys(rules).forEach((key) => {
492
- const redundants = rules[key]
493
- rules[key] = (node) => {
494
- redundants.forEach((redundant) => {
495
- redundant(node)
496
- })
497
- }
498
- })
499
-
500
- return rules
501
- },
502
- }
503
-
504
- module.exports.schema = SCHEMA
505
- module.exports.default_config = DEFAULT_CONFIG