eslint-plugin-smarthr 0.3.26 → 0.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 +23 -0
- package/README.md +1 -1
- package/package.json +1 -2
- package/rules/a11y-heading-in-sectioning-content/index.js +1 -0
- package/rules/a11y-input-in-form-control/index.js +32 -9
- package/test/a11y-heading-in-sectioning-content.js +2 -0
- package/test/a11y-input-in-form-control.js +35 -10
- package/rules/redundant-name/README.md +0 -97
- package/rules/redundant-name/index.js +0 -505
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,29 @@
|
|
|
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.0](https://github.com/kufu/eslint-plugin-smarthr/compare/v0.3.27...v0.4.0) (2024-02-05)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### ⚠ BREAKING CHANGES
|
|
9
|
+
|
|
10
|
+
* 利用者がいなくなったsmarthr/redundant-nameを削除する (#111)
|
|
11
|
+
|
|
12
|
+
### Features
|
|
13
|
+
|
|
14
|
+
* 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))
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### Bug Fixes
|
|
18
|
+
|
|
19
|
+
* 利用者がいなくなったsmarthr/redundant-nameを削除する ([#111](https://github.com/kufu/eslint-plugin-smarthr/issues/111)) ([2bc1011](https://github.com/kufu/eslint-plugin-smarthr/commit/2bc10118cc0a18366300c3816f091060d2a0677d))
|
|
20
|
+
|
|
21
|
+
### [0.3.27](https://github.com/kufu/eslint-plugin-smarthr/compare/v0.3.26...v0.3.27) (2024-01-28)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
### Features
|
|
25
|
+
|
|
26
|
+
* a11y系コンポーネントで入力要素の拡張コンポーネントの判定をfunctionを使っている場合にも正しく判定できるように修正 ([#108](https://github.com/kufu/eslint-plugin-smarthr/issues/108)) ([c929760](https://github.com/kufu/eslint-plugin-smarthr/commit/c929760b3d8e166e7e3f7befcf048fa28cc48042))
|
|
27
|
+
|
|
5
28
|
### [0.3.26](https://github.com/kufu/eslint-plugin-smarthr/compare/v0.3.25...v0.3.26) (2024-01-24)
|
|
6
29
|
|
|
7
30
|
|
package/README.md
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
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
10
|
- [a11y-prohibit-input-placeholder](https://github.com/kufu/eslint-plugin-smarthr/tree/main/rules/a11y-prohibit-input-placeholder)
|
|
11
|
+
- [a11y-prohibit-useless-sectioning-fragment](https://github.com/kufu/eslint-plugin-smarthr/tree/main/rules/a11y-prohibit-useless-sectioning-fragment)
|
|
11
12
|
- [a11y-trigger-has-button](https://github.com/kufu/eslint-plugin-smarthr/tree/main/rules/a11y-trigger-has-button)
|
|
12
13
|
- [best-practice-for-date](https://github.com/kufu/eslint-plugin-smarthr/tree/main/rules/best-practice-for-date)
|
|
13
14
|
- [best-practice-for-remote-trigger-dialog](https://github.com/kufu/eslint-plugin-smarthr/tree/main/rules/best-practice-for-remote-trigger-dialog)
|
|
@@ -19,7 +20,6 @@
|
|
|
19
20
|
- [prohibit-file-name](https://github.com/kufu/eslint-plugin-smarthr/tree/main/rules/prohibit-file-name)
|
|
20
21
|
- [prohibit-import](https://github.com/kufu/eslint-plugin-smarthr/tree/main/rules/prohibit-import)
|
|
21
22
|
- [prohibit-path-within-template-literal](https://github.com/kufu/eslint-plugin-smarthr/tree/main/rules/prohibit-path-within-template-literal)
|
|
22
|
-
- [redundant-name](https://github.com/kufu/eslint-plugin-smarthr/tree/main/rules/redundant-name)
|
|
23
23
|
- [require-barrel-import](https://github.com/kufu/eslint-plugin-smarthr/tree/main/rules/require-barrel-import)
|
|
24
24
|
- [require-declaration](https://github.com/kufu/eslint-plugin-smarthr/tree/main/rules/require-declaration)
|
|
25
25
|
- [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
|
+
"version": "0.4.0",
|
|
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": {
|
|
@@ -112,6 +112,7 @@ const searchBubbleUp = (node) => {
|
|
|
112
112
|
if (
|
|
113
113
|
// Headingコンポーネントの拡張なので対象外
|
|
114
114
|
node.type === 'VariableDeclarator' && ignoreHeadingCheckParentType.includes(node.parent.parent?.type) && node.id.name.match(declaratorHeadingRegex) ||
|
|
115
|
+
node.type === 'FunctionDeclaration' && ignoreHeadingCheckParentType.includes(node.parent.type) && node.id.name.match(declaratorHeadingRegex) ||
|
|
115
116
|
// ModelessDialogのheaderにHeadingを設定している場合も対象外
|
|
116
117
|
node.type === 'JSXAttribute' && node.name.name === 'header' && node.parent.name.name.match(modelessDialogRegex)
|
|
117
118
|
) {
|
|
@@ -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)$',
|
|
@@ -33,6 +36,7 @@ const EXPECTED_NAMES = {
|
|
|
33
36
|
'Reel$': '(Reel)$',
|
|
34
37
|
'Sidebar$': '(Sidebar)$',
|
|
35
38
|
'Stack$': '(Stack)$',
|
|
39
|
+
'(L|^l)abel$': '(Label)$',
|
|
36
40
|
|
|
37
41
|
}
|
|
38
42
|
|
|
@@ -42,9 +46,9 @@ const FORM_CONTROL_INPUTS_REGEX = new RegExp(`(${Object.keys(EXPECTED_INPUT_NAME
|
|
|
42
46
|
const LABELED_INPUTS_REGEX = new RegExp(`(${Object.keys(EXPECTED_LABELED_INPUT_NAMES).join('|')})`)
|
|
43
47
|
const SEARCH_INPUT_REGEX = /SearchInput$/
|
|
44
48
|
const INPUT_REGEX = /(i|I)nput$/
|
|
45
|
-
const RADIO_BUTTONS_REGEX = /RadioButton(Panel)?$/
|
|
46
|
-
const CHECKBOX_REGEX = /Check(B|b)ox?$/
|
|
47
|
-
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)?$/
|
|
48
52
|
const FROM_CONTROLS_REGEX = new RegExp(`(${Object.keys(EXPECTED_FORM_CONTROL_NAMES).join('|')})`)
|
|
49
53
|
const FORM_CONTROL_REGEX = /(Form(Control|Group))$/
|
|
50
54
|
const FIELDSET_REGEX = /Fieldset$/
|
|
@@ -53,6 +57,7 @@ const SECTIONING_REGEX = /(((A|^a)(rticle|side))|(N|^n)av|(S|^s)ection|^Sectioni
|
|
|
53
57
|
const BARE_SECTIONING_TAG_REGEX = /^(article|aside|nav|section)$/
|
|
54
58
|
const LAYOUT_COMPONENT_REGEX = /((C(ent|lust)er)|Reel|Sidebar|Stack)$/
|
|
55
59
|
const AS_REGEX = /^(as|forwardedAs)$/
|
|
60
|
+
const SUFFIX_S_REGEX = /s$/
|
|
56
61
|
|
|
57
62
|
const IGNORE_INPUT_CHECK_PARENT_TYPE = /^(Program|ExportNamedDeclaration)$/
|
|
58
63
|
|
|
@@ -131,6 +136,8 @@ module.exports = {
|
|
|
131
136
|
}
|
|
132
137
|
}
|
|
133
138
|
}
|
|
139
|
+
|
|
140
|
+
const isPreMultiple = isAdditionalMultiInput || isFormControlInput && nodeName.match(SUFFIX_S_REGEX)
|
|
134
141
|
const isRadio = (isPureInput && isTypeRadio) || nodeName.match(RADIO_BUTTONS_REGEX);
|
|
135
142
|
const isCheckbox = !isRadio && (isPureInput && isTypeCheck || nodeName.match(CHECKBOX_REGEX));
|
|
136
143
|
|
|
@@ -153,7 +160,7 @@ module.exports = {
|
|
|
153
160
|
}
|
|
154
161
|
}
|
|
155
162
|
|
|
156
|
-
const isMultiInput =
|
|
163
|
+
const isMultiInput = isPreMultiple || hit || isInMap
|
|
157
164
|
const matcherFormControl = name.match(FORM_CONTROL_REGEX)
|
|
158
165
|
|
|
159
166
|
if (matcherFormControl) {
|
|
@@ -179,15 +186,20 @@ module.exports = {
|
|
|
179
186
|
}
|
|
180
187
|
// HINT: 擬似的にラベルが設定されている場合、無視する
|
|
181
188
|
} else if (!isRadio && !isCheckbox && !isPseudoLabel) {
|
|
189
|
+
const isSelect = nodeName.match(SELECT_REGEX)
|
|
190
|
+
|
|
182
191
|
context.report({
|
|
183
192
|
node: n,
|
|
184
193
|
message: `${name} が ラベルを持たない入力要素(${nodeName})を含んでいます。入力要素が何であるかを正しく伝えるため、以下の方法のいずれかで修正してください。
|
|
185
194
|
- 方法1: ${name} を smarthr-ui/FormControl、もしくはそれを拡張したコンポーネントに変更してください
|
|
186
195
|
- 方法2: ${nodeName} がlabel要素を含むコンポーネントである場合、名称を${FORM_CONTROL_REGEX}にマッチするものに変更してください
|
|
187
|
-
|
|
196
|
+
- smarthr-ui/FormControl、smarthr-ui/FormGroup はlabel要素を内包しています
|
|
188
197
|
- 方法3: ${nodeName} がRadioButton、もしくはCheckboxを表すコンポーネントの場合、名称を${LABELED_INPUTS_REGEX}にマッチするものに変更してください
|
|
189
|
-
|
|
190
|
-
- 方法4: ${name} が smarthr-ui/Fieldset、もしくはそれを拡張しているコンポーネントではない場合、名称を ${FIELDSET_REGEX}
|
|
198
|
+
- smarthr-ui/RadioButton、smarthr-ui/RadioButtonPanel、smarthr-ui/Checkbox はlabel要素を内包しています
|
|
199
|
+
- 方法4: ${name} が smarthr-ui/Fieldset、もしくはそれを拡張しているコンポーネントではない場合、名称を ${FIELDSET_REGEX} にマッチしないものに変更してください
|
|
200
|
+
- 方法5: 別途label要素が存在し、それらと紐づけたい場合はlabel要素のhtmlFor属性、${nodeName}のid属性に同じ文字列を指定してください。この文字列はhtml内で一意である必要があります
|
|
201
|
+
- 方法6: 上記のいずれの方法も適切ではない場合、${nodeName}のtitle属性に "どんな値を${isSelect ? '選択' : '入力'}すれば良いのか" の説明を設定してください
|
|
202
|
+
- 例: <${nodeName} title="${isSelect ? '検索対象を選択してください' : '姓を全角カタカナのみで入力してください'}" />`,
|
|
191
203
|
});
|
|
192
204
|
}
|
|
193
205
|
|
|
@@ -209,7 +221,8 @@ module.exports = {
|
|
|
209
221
|
- 方法1: ${actualName} を${wrapComponentName}、もしくはそれを拡張したコンポーネントに変更してください
|
|
210
222
|
- ${actualName} 内のHeading要素は${wrapComponentName}のtitle属性に変更してください
|
|
211
223
|
- 方法2: ${actualName} と ${nodeName} の間に ${wrapComponentName} が存在するようにマークアップを変更してください${isRadio ? '' : `
|
|
212
|
-
- 方法3:
|
|
224
|
+
- 方法3: 別途label要素が存在し、それらと紐づけたい場合はlabel要素のhtmlFor属性、${nodeName}のid属性に同じ文字列を指定してください。この文字列はhtml内で一意である必要があります
|
|
225
|
+
- 方法4: 上記のいずれの方法も適切ではない場合、${nodeName}のtitle属性に "どんな値を${isSelect ? '選択' : '入力'}すれば良いのか" の説明を設定してください
|
|
213
226
|
- 例: <${nodeName} title="${isSelect ? '検索対象を選択してください' : '姓を全角カタカナのみで入力してください'}" />`}`,
|
|
214
227
|
});
|
|
215
228
|
}
|
|
@@ -233,6 +246,16 @@ module.exports = {
|
|
|
233
246
|
|
|
234
247
|
break
|
|
235
248
|
}
|
|
249
|
+
case 'FunctionDeclaration': {
|
|
250
|
+
if (n.parent.type.match(IGNORE_INPUT_CHECK_PARENT_TYPE)) {
|
|
251
|
+
const name = n.id.name
|
|
252
|
+
|
|
253
|
+
// 入力要素系コンポーネントの拡張なので対象外
|
|
254
|
+
if (name.match(FORM_CONTROL_INPUTS_REGEX) || checkAdditionalMultiInputComponents(name) || checkAdditionalInputComponents(name)) {
|
|
255
|
+
return
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
236
259
|
case 'Program': {
|
|
237
260
|
// HINT: smarthr-ui/CheckBoxはlabelを単独で持つため、FormControl系でラップをする必要はない
|
|
238
261
|
// HINT: 擬似的にラベルが設定されている場合、無視する
|
|
@@ -246,7 +269,7 @@ module.exports = {
|
|
|
246
269
|
- FieldsetでRadioButtonを囲むことでグループ化された入力要素に対して適切なタイトル・説明を追加出来ます` : ``}
|
|
247
270
|
- ${nodeName}が入力要素とラベル・タイトル・説明など含む概念を表示するコンポーネントの場合、コンポーネント名を${FROM_CONTROLS_REGEX}とマッチするように修正してください
|
|
248
271
|
- ${nodeName}が入力要素自体を表現するコンポーネントの一部である場合、ルートとなるコンポーネントの名称を${FORM_CONTROL_INPUTS_REGEX}とマッチするように修正してください${isRadio ? '' : `
|
|
249
|
-
-
|
|
272
|
+
- 上記のいずれの方法も適切ではない場合、${nodeName}のtitle属性に "どんな値を${isSelect ? '選択' : '入力'}すれば良いのか" の説明を設定してください
|
|
250
273
|
- 例: <${nodeName} title="${isSelect ? '検索対象を選択してください' : '姓を全角カタカナのみで入力してください'}" />`}`,
|
|
251
274
|
});
|
|
252
275
|
}
|
|
@@ -55,6 +55,8 @@ ruleTester.run('a11y-heading-in-sectioning-content', rule, {
|
|
|
55
55
|
{ code: '<><Section><Heading>hoge</Heading></Section><Section><Heading>fuga</Heading></Section></>' },
|
|
56
56
|
{ code: 'const HogeHeading = () => <FugaHeading anyArg={abc}>hoge</FugaHeading>' },
|
|
57
57
|
{ code: 'export const HogeHeading = () => <FugaHeading anyArg={abc}>hoge</FugaHeading>' },
|
|
58
|
+
{ code: 'function FugaHeading() { return <PiyoHeading anyArg={abc}>hoge</PiyoHeading> }' },
|
|
59
|
+
{ code: 'export function FugaHeading() { return <PiyoHeading anyArg={abc}>hoge</PiyoHeading> }' },
|
|
58
60
|
{ code: '<Center as="section"><div><Heading>hoge</Heading></div></Center>' },
|
|
59
61
|
{ code: '<Cluster as="section"><div><Heading>hoge</Heading></div></Cluster>' },
|
|
60
62
|
{ code: '<Reel as="aside"><div><Heading>hoge</Heading></div></Reel>' },
|
|
@@ -15,14 +15,14 @@ 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$)/とマッチするように修正してください
|
|
19
|
-
-
|
|
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
|
+
- 上記のいずれの方法も適切ではない場合、${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$)/とマッチするように修正してください
|
|
25
|
-
-
|
|
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
|
+
- 上記のいずれの方法も適切ではない場合、${name}のtitle属性に "どんな値を選択すれば良いのか" の説明を設定してください
|
|
26
26
|
- 例: <${name} title="検索対象を選択してください" />`
|
|
27
27
|
const invalidPureCheckboxInFormControl = (name) => `HogeFormControl が ${name} を含んでいます。smarthr-ui/FormControl を smarthr-ui/Fieldset に変更し、正しくグルーピングされるように修正してください。
|
|
28
28
|
- 可能なら${name}はsmarthr-ui/Checkboxへの変更を検討してください。難しい場合は ${name} と結びつくlabel要素が必ず存在するよう、マークアップする必要があることに注意してください。`
|
|
@@ -41,15 +41,29 @@ const invalidMultiInputsInFormControl = () => `HogeFormControl が複数の入
|
|
|
41
41
|
const noLabeledInputInFieldset = (name) => `HogeFieldset が ラベルを持たない入力要素(${name})を含んでいます。入力要素が何であるかを正しく伝えるため、以下の方法のいずれかで修正してください。
|
|
42
42
|
- 方法1: HogeFieldset を smarthr-ui/FormControl、もしくはそれを拡張したコンポーネントに変更してください
|
|
43
43
|
- 方法2: ${name} がlabel要素を含むコンポーネントである場合、名称を/(Form(Control|Group))$/にマッチするものに変更してください
|
|
44
|
-
|
|
45
|
-
- 方法3: ${name} がRadioButton、もしくはCheckboxを表すコンポーネントの場合、名称を/(RadioButton$|RadioButtonPanel$|Check(B|b)ox$)/にマッチするものに変更してください
|
|
46
|
-
|
|
47
|
-
- 方法4: HogeFieldset が smarthr-ui/Fieldset、もしくはそれを拡張しているコンポーネントではない場合、名称を /Fieldset$/
|
|
44
|
+
- smarthr-ui/FormControl、smarthr-ui/FormGroup はlabel要素を内包しています
|
|
45
|
+
- 方法3: ${name} がRadioButton、もしくはCheckboxを表すコンポーネントの場合、名称を/(RadioButton$|RadioButtons$|RadioButtonPanel$|RadioButtonPanels$|Check(B|b)ox$|Check(B|b)ox(e)?s$)/にマッチするものに変更してください
|
|
46
|
+
- smarthr-ui/RadioButton、smarthr-ui/RadioButtonPanel、smarthr-ui/Checkbox はlabel要素を内包しています
|
|
47
|
+
- 方法4: HogeFieldset が smarthr-ui/Fieldset、もしくはそれを拡張しているコンポーネントではない場合、名称を /Fieldset$/ にマッチしないものに変更してください
|
|
48
|
+
- 方法5: 別途label要素が存在し、それらと紐づけたい場合はlabel要素のhtmlFor属性、${name}のid属性に同じ文字列を指定してください。この文字列はhtml内で一意である必要があります
|
|
49
|
+
- 方法6: 上記のいずれの方法も適切ではない場合、${name}のtitle属性に "どんな値を入力すれば良いのか" の説明を設定してください
|
|
50
|
+
- 例: <${name} title="姓を全角カタカナのみで入力してください" />`
|
|
51
|
+
const noLabeledInputInFieldsetWithSelect = (name) => `HogeFieldset が ラベルを持たない入力要素(${name})を含んでいます。入力要素が何であるかを正しく伝えるため、以下の方法のいずれかで修正してください。
|
|
52
|
+
- 方法1: HogeFieldset を smarthr-ui/FormControl、もしくはそれを拡張したコンポーネントに変更してください
|
|
53
|
+
- 方法2: ${name} がlabel要素を含むコンポーネントである場合、名称を/(Form(Control|Group))$/にマッチするものに変更してください
|
|
54
|
+
- smarthr-ui/FormControl、smarthr-ui/FormGroup はlabel要素を内包しています
|
|
55
|
+
- 方法3: ${name} がRadioButton、もしくはCheckboxを表すコンポーネントの場合、名称を/(RadioButton$|RadioButtons$|RadioButtonPanel$|RadioButtonPanels$|Check(B|b)ox$|Check(B|b)ox(e)?s$)/にマッチするものに変更してください
|
|
56
|
+
- smarthr-ui/RadioButton、smarthr-ui/RadioButtonPanel、smarthr-ui/Checkbox はlabel要素を内包しています
|
|
57
|
+
- 方法4: HogeFieldset が smarthr-ui/Fieldset、もしくはそれを拡張しているコンポーネントではない場合、名称を /Fieldset$/ にマッチしないものに変更してください
|
|
58
|
+
- 方法5: 別途label要素が存在し、それらと紐づけたい場合はlabel要素のhtmlFor属性、${name}のid属性に同じ文字列を指定してください。この文字列はhtml内で一意である必要があります
|
|
59
|
+
- 方法6: 上記のいずれの方法も適切ではない場合、${name}のtitle属性に "どんな値を選択すれば良いのか" の説明を設定してください
|
|
60
|
+
- 例: <${name} title="検索対象を選択してください" />`
|
|
48
61
|
const useFormControlInsteadOfSection = (name, section) => `${name}は${section}より先に、smarthr-ui/FormControlが入力要素を囲むようマークアップを以下のいずれかの方法で変更してください。
|
|
49
62
|
- 方法1: ${section} をFormControl、もしくはそれを拡張したコンポーネントに変更してください
|
|
50
63
|
- ${section} 内のHeading要素はFormControlのtitle属性に変更してください
|
|
51
64
|
- 方法2: ${section} と ${name} の間に FormControl が存在するようにマークアップを変更してください
|
|
52
|
-
- 方法3:
|
|
65
|
+
- 方法3: 別途label要素が存在し、それらと紐づけたい場合はlabel要素のhtmlFor属性、${name}のid属性に同じ文字列を指定してください。この文字列はhtml内で一意である必要があります
|
|
66
|
+
- 方法4: 上記のいずれの方法も適切ではない場合、${name}のtitle属性に "どんな値を入力すれば良いのか" の説明を設定してください
|
|
53
67
|
- 例: <${name} title="姓を全角カタカナのみで入力してください" />`
|
|
54
68
|
const useFormControlInsteadOfSectionInRadio = (name, section) => `${name}は${section}より先に、smarthr-ui/Fieldsetが入力要素を囲むようマークアップを以下のいずれかの方法で変更してください。
|
|
55
69
|
- 方法1: ${section} をFieldset、もしくはそれを拡張したコンポーネントに変更してください
|
|
@@ -62,7 +76,7 @@ const invalidChildreninFormControl = (children) => `FormControl が、${children
|
|
|
62
76
|
- FormControlではなく、smarthr-ui/Fieldset、もしくはsmarthr-ui/Section + smarthr-ui/Heading などでのマークアップを検討してください
|
|
63
77
|
- 方法2: 親要素であるFormControlがsmarthr-ui/FormControlを拡張したコンポーネントではない場合、コンポーネント名を/(Form(Control|Group))$/と一致しない名称に変更してください`
|
|
64
78
|
const requireMultiInputInFormControlWithRoleGroup = () => `HogeFormControl内に入力要素が2個以上存在しないため、'role=\"group\"'を削除してください。'role=\"group\"'は複数の入力要素を一つのグループとして扱うための属性です。
|
|
65
|
-
- 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)$)/にマッチする名称に変更してください`
|
|
66
80
|
|
|
67
81
|
ruleTester.run('a11y-input-in-form-control', rule, {
|
|
68
82
|
valid: [
|
|
@@ -105,6 +119,9 @@ ruleTester.run('a11y-input-in-form-control', rule, {
|
|
|
105
119
|
{ code: '<FugaSection><HogeFormControl><HogeInput /></HogeFormControl></FugaSection>' },
|
|
106
120
|
{ code: '<Stack as="section"><HogeFormControl><HogeInput /></HogeFormControl></Stack>' },
|
|
107
121
|
{ code: `const AnyComboBox = () => <input />` },
|
|
122
|
+
{ code: `export const AnyComboBox = () => <input />` },
|
|
123
|
+
{ code: `function AnySingleCombobox() { return <SingleCombobox /> }` },
|
|
124
|
+
{ code: `export function AnySingleCombobox() { return <SingleCombobox /> }` },
|
|
108
125
|
{ code: `<Fieldset><HogeFieldset /><HogeFormControl /></Fieldset>` },
|
|
109
126
|
{ code: '<HogeFieldset><HogeCheckBox /><HogeInput id="any" /></HogeFieldset>' },
|
|
110
127
|
{ code: '<FugaSection><HogeInput id="any" /></FugaSection>' },
|
|
@@ -112,6 +129,10 @@ ruleTester.run('a11y-input-in-form-control', rule, {
|
|
|
112
129
|
{ code: '<HogeFieldset><HogeCheckBox /><HogeInput title="any" /></HogeFieldset>' },
|
|
113
130
|
{ code: '<FugaSection><HogeInput title="any" /></FugaSection>' },
|
|
114
131
|
{ code: '<HogeTextarea title="any" />' },
|
|
132
|
+
{ code: '<Fieldset><HogeRadioButtons /></Fieldset>' },
|
|
133
|
+
{ code: '<Fieldset><HogeRadioButtonPanels /></Fieldset>' },
|
|
134
|
+
{ code: '<Fieldset><HogeCheckBoxs /></Fieldset>' },
|
|
135
|
+
{ code: '<Fieldset><HogeCheckBoxes /></Fieldset>' },
|
|
115
136
|
],
|
|
116
137
|
invalid: [
|
|
117
138
|
{ code: `import hoge from 'styled-components'`, errors: [ { message: `styled-components をimportする際は、名称が"styled" となるようにしてください。例: "import styled from 'styled-components'"` } ] },
|
|
@@ -141,6 +162,7 @@ ruleTester.run('a11y-input-in-form-control', rule, {
|
|
|
141
162
|
{ code: '<HogeFormControl><RadioButton /></HogeFormControl>', errors: [ { message: invalidRadioInFormControl('RadioButton') } ] },
|
|
142
163
|
{ code: '<HogeFormControl><HogeRadioButtonPanel /></HogeFormControl>', errors: [ { message: invalidRadioInFormControl('HogeRadioButtonPanel') } ] },
|
|
143
164
|
{ code: '<HogeFieldset><HogeCheckBox /><HogeInput /></HogeFieldset>', errors: [ { message: noLabeledInputInFieldset('HogeInput') } ] },
|
|
165
|
+
{ code: '<HogeFieldset><HogeCheckBox /><HogeSelect /></HogeFieldset>', errors: [ { message: noLabeledInputInFieldsetWithSelect('HogeSelect') } ] },
|
|
144
166
|
{ code: '<FugaSection><HogeInput /></FugaSection>', errors: [ { message: useFormControlInsteadOfSection('HogeInput', 'FugaSection') } ] },
|
|
145
167
|
{ code: '<Stack as="section"><HogeInput /></Stack>', errors: [ { message: useFormControlInsteadOfSection('HogeInput', '<Stack as="section">') } ] },
|
|
146
168
|
{ code: '<Center forwardedAs="aside"><HogeInput /></Center>', errors: [ { message: useFormControlInsteadOfSection('HogeInput', '<Center forwardedAs="aside">') } ] },
|
|
@@ -151,5 +173,8 @@ ruleTester.run('a11y-input-in-form-control', rule, {
|
|
|
151
173
|
{ code: '<FormControl><HogeFieldset /></FormControl>', errors: [ { message: invalidChildreninFormControl('HogeFieldset') } ] },
|
|
152
174
|
{ code: '<FormControl><HogeFormControl /></FormControl>', errors: [ { message: invalidChildreninFormControl('HogeFormControl') } ] },
|
|
153
175
|
{ code: '<HogeFormControl role="group"><HogeInput /></HogeFormControl>', errors: [ { message: requireMultiInputInFormControlWithRoleGroup() } ] },
|
|
176
|
+
{ code: '<HogeFormControl><HogeRadioButtons /></HogeFormControl>', errors: [ { message: invalidRadioInFormControl('HogeRadioButtons') } ] },
|
|
177
|
+
{ code: '<HogeFormControl><HogeCheckBoxs /></HogeFormControl>', errors: [ { message: invalidMultiInputsInFormControl() } ] },
|
|
178
|
+
{ code: '<HogeFormControl><HogeCheckBoxes /></HogeFormControl>', errors: [ { message: invalidMultiInputsInFormControl() } ] },
|
|
154
179
|
]
|
|
155
180
|
})
|
|
@@ -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
|