eslint-plugin-smarthr 0.3.27 → 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
CHANGED
|
@@ -2,6 +2,22 @@
|
|
|
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
|
+
|
|
5
21
|
### [0.3.27](https://github.com/kufu/eslint-plugin-smarthr/compare/v0.3.26...v0.3.27) (2024-01-28)
|
|
6
22
|
|
|
7
23
|
|
package/README.md
CHANGED
|
@@ -20,7 +20,6 @@
|
|
|
20
20
|
- [prohibit-file-name](https://github.com/kufu/eslint-plugin-smarthr/tree/main/rules/prohibit-file-name)
|
|
21
21
|
- [prohibit-import](https://github.com/kufu/eslint-plugin-smarthr/tree/main/rules/prohibit-import)
|
|
22
22
|
- [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
23
|
- [require-barrel-import](https://github.com/kufu/eslint-plugin-smarthr/tree/main/rules/require-barrel-import)
|
|
25
24
|
- [require-declaration](https://github.com/kufu/eslint-plugin-smarthr/tree/main/rules/require-declaration)
|
|
26
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": {
|
|
@@ -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,6 +57,7 @@ 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
|
|
|
@@ -132,6 +136,8 @@ module.exports = {
|
|
|
132
136
|
}
|
|
133
137
|
}
|
|
134
138
|
}
|
|
139
|
+
|
|
140
|
+
const isPreMultiple = isAdditionalMultiInput || isFormControlInput && nodeName.match(SUFFIX_S_REGEX)
|
|
135
141
|
const isRadio = (isPureInput && isTypeRadio) || nodeName.match(RADIO_BUTTONS_REGEX);
|
|
136
142
|
const isCheckbox = !isRadio && (isPureInput && isTypeCheck || nodeName.match(CHECKBOX_REGEX));
|
|
137
143
|
|
|
@@ -154,7 +160,7 @@ module.exports = {
|
|
|
154
160
|
}
|
|
155
161
|
}
|
|
156
162
|
|
|
157
|
-
const isMultiInput =
|
|
163
|
+
const isMultiInput = isPreMultiple || hit || isInMap
|
|
158
164
|
const matcherFormControl = name.match(FORM_CONTROL_REGEX)
|
|
159
165
|
|
|
160
166
|
if (matcherFormControl) {
|
|
@@ -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,10 @@ 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: '<Fieldset><HogeRadioButtons /></Fieldset>' },
|
|
133
|
+
{ code: '<Fieldset><HogeRadioButtonPanels /></Fieldset>' },
|
|
134
|
+
{ code: '<Fieldset><HogeCheckBoxs /></Fieldset>' },
|
|
135
|
+
{ code: '<Fieldset><HogeCheckBoxes /></Fieldset>' },
|
|
132
136
|
],
|
|
133
137
|
invalid: [
|
|
134
138
|
{ code: `import hoge from 'styled-components'`, errors: [ { message: `styled-components をimportする際は、名称が"styled" となるようにしてください。例: "import styled from 'styled-components'"` } ] },
|
|
@@ -169,5 +173,8 @@ ruleTester.run('a11y-input-in-form-control', rule, {
|
|
|
169
173
|
{ code: '<FormControl><HogeFieldset /></FormControl>', errors: [ { message: invalidChildreninFormControl('HogeFieldset') } ] },
|
|
170
174
|
{ code: '<FormControl><HogeFormControl /></FormControl>', errors: [ { message: invalidChildreninFormControl('HogeFormControl') } ] },
|
|
171
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() } ] },
|
|
172
179
|
]
|
|
173
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
|