eslint-plugin-smarthr 6.9.1 → 6.10.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 +19 -0
- package/package.json +2 -2
- package/rules/a11y-heading-in-sectioning-content/README.md +1 -1
- package/rules/a11y-heading-in-sectioning-content/index.js +1 -1
- package/rules/best-practice-for-text-component/index.js +118 -199
- package/rules/require-barrel-import/index.js +187 -110
- package/test/a11y-heading-in-sectioning-content.js +2 -0
- package/test/best-practice-for-text-component.js +35 -34
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
|
4
4
|
|
|
5
|
+
## [6.10.1](https://github.com/kufu/tamatebako/compare/eslint-plugin-smarthr-v6.10.0...eslint-plugin-smarthr-v6.10.1) (2026-04-08)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* **require-barrel-import:** next.jsの特殊文字パスでのバグ修正とリファクタリング ([#1210](https://github.com/kufu/tamatebako/issues/1210)) ([458cfe9](https://github.com/kufu/tamatebako/commit/458cfe90084cbcee936bcb375216f5807880edd7))
|
|
11
|
+
|
|
12
|
+
## [6.10.0](https://github.com/kufu/tamatebako/compare/eslint-plugin-smarthr-v6.9.1...eslint-plugin-smarthr-v6.10.0) (2026-04-08)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
### Features
|
|
16
|
+
|
|
17
|
+
* **a11y-heading-in-sectioning-content:** Scrollerコンポーネントを許可リストに追加 ([#1204](https://github.com/kufu/tamatebako/issues/1204)) ([7131d5c](https://github.com/kufu/tamatebako/commit/7131d5c2b6feb55ac44f952250f58db962153399))
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
### Bug Fixes
|
|
21
|
+
|
|
22
|
+
* **best-practice-for-text-component:** key属性対応とコードの最適化 ([#1205](https://github.com/kufu/tamatebako/issues/1205)) ([1c9821c](https://github.com/kufu/tamatebako/commit/1c9821c1d2c4f5d8b09f2a1a1f7e18310aecc31e))
|
|
23
|
+
|
|
5
24
|
## [6.9.1](https://github.com/kufu/tamatebako/compare/eslint-plugin-smarthr-v6.9.0...eslint-plugin-smarthr-v6.9.1) (2026-04-07)
|
|
6
25
|
|
|
7
26
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-smarthr",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.10.1",
|
|
4
4
|
"author": "SmartHR",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "A sharable ESLint plugin for SmartHR",
|
|
@@ -37,5 +37,5 @@
|
|
|
37
37
|
"eslintplugin",
|
|
38
38
|
"smarthr"
|
|
39
39
|
],
|
|
40
|
-
"gitHead": "
|
|
40
|
+
"gitHead": "e2e55eea3e72809fab55082ad6ef4da0c6025ae8"
|
|
41
41
|
}
|
|
@@ -14,7 +14,7 @@ HeadingコンポーネントをSectioningContent(Article, Aside, Nav, Section)
|
|
|
14
14
|
|
|
15
15
|
## SectioningContentとして扱うコンポーネントについて
|
|
16
16
|
|
|
17
|
-
このルールではsmarthr-ui/Layout系コンポーネント(Center, Reel, Sidebar, Stack)にas属性・forwardedAs属性で`section` `article` `aside` `nav` のいずれかの要素が指定されている場合、SectioningContentとして扱います。<br />
|
|
17
|
+
このルールではsmarthr-ui/Layout系コンポーネント(Center, Cluster, Reel, Scroller, Sidebar, Stack)にas属性・forwardedAs属性で`section` `article` `aside` `nav` のいずれかの要素が指定されている場合、SectioningContentとして扱います。<br />
|
|
18
18
|
Layout系コンポーネントがSectioningContentとして扱われている場合、smarthr-uiの内部実装レベルでもSectioningContentとして扱われるため、前述のHeadingのレベルの自動計算が有効になります。
|
|
19
19
|
|
|
20
20
|
## section要素などbuildinのSectiongContentに属する要素の利用について
|
|
@@ -4,7 +4,7 @@ const declaratorHeadingRegex = /Heading$/
|
|
|
4
4
|
const sectioningRegex = /((A(rticle|side))|Nav|Section|^SectioningFragment)$/
|
|
5
5
|
const bareTagRegex = /^(article|aside|nav|section)$/
|
|
6
6
|
const modelessDialogRegex = /ModelessDialog$/
|
|
7
|
-
const layoutComponentRegex = /((C(ent|lust)er)|Reel|Sidebar|Stack|Base(Column)?)$/
|
|
7
|
+
const layoutComponentRegex = /((C(ent|lust)er)|Reel|Sidebar|Stack|Scroller|Base(Column)?)$/
|
|
8
8
|
const asRegex = /^(as|forwardedAs)$/
|
|
9
9
|
const ignoreCheckParentTypeRegex = /^(Program|ExportNamedDeclaration)$/
|
|
10
10
|
const noHeadingTagNamesRegex = /^(span|legend)$/
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
const SCHEMA = []
|
|
2
2
|
|
|
3
|
+
// ============================================================
|
|
3
4
|
// smarthr-ui/Textコンポーネントのクラス名とプロパティのマッピング
|
|
5
|
+
// ============================================================
|
|
4
6
|
const CLASS_TO_PROP_MAP = {
|
|
5
7
|
// size
|
|
6
8
|
'shr-text-2xs': { prop: 'size', value: 'XXS' },
|
|
@@ -32,35 +34,49 @@ const CLASS_TO_PROP_MAP = {
|
|
|
32
34
|
|
|
33
35
|
const REGEX_CLASSNAME_SPLIT = /\s+/
|
|
34
36
|
|
|
35
|
-
//
|
|
36
|
-
|
|
37
|
+
// ============================================================
|
|
38
|
+
// ESLintセレクタ構築用の定数
|
|
39
|
+
// ============================================================
|
|
40
|
+
const CONVERTIBLE_SHR_CLASSES = Object.keys(CLASS_TO_PROP_MAP).join('|')
|
|
41
|
+
const CONVERTIBLE_SHR_PATTERN = `(^|\\s)(${CONVERTIBLE_SHR_CLASSES})(\\s|$)`
|
|
42
|
+
|
|
37
43
|
const ATTR_CLASSNAME = 'JSXAttribute[name.name="className"]'
|
|
38
|
-
const ATTR_TEXT_PROPS = 'JSXAttribute[name.name=/^(size|weight|color|leading|italic|whiteSpace|maxLines|styleType|icon|prefixIcon|suffixIcon|iconGap)$/]'
|
|
39
44
|
const LITERAL_TYPE = '[value.type="Literal"]'
|
|
40
|
-
const HAS_SHR_CLASS = '[value.value=/shr-/]'
|
|
41
45
|
|
|
42
|
-
// ESLintセレクタの共通部分
|
|
43
46
|
const TEXT_OPENING = 'JSXOpeningElement[name.name="Text"]'
|
|
44
|
-
const
|
|
45
|
-
const HAS_TEXT_PROPS =
|
|
46
|
-
const NOT_HAS_TEXT_PROPS = `:not(${HAS_TEXT_PROPS})`
|
|
47
|
+
const HAS_ANY_ATTR = ':has(JSXAttribute)'
|
|
48
|
+
const HAS_TEXT_PROPS = ':has(JSXAttribute[name.name=/^(size|weight|color|leading|italic|whiteSpace|maxLines|styleType|icon|prefixIcon|suffixIcon|iconGap)$/])'
|
|
47
49
|
const CHILD_CLASSNAME_LITERAL = `> ${ATTR_CLASSNAME}${LITERAL_TYPE}`
|
|
48
|
-
const
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
50
|
+
const HAS_CONVERTIBLE_SHR_CLASS = `[value.value=/${CONVERTIBLE_SHR_PATTERN}/]`
|
|
51
|
+
const HAS_SPREAD = ':has(JSXSpreadAttribute)'
|
|
52
|
+
const NOT_HAS_SPREAD = `:not(${HAS_SPREAD})`
|
|
53
|
+
const NOT_HAS_AS_VARIABLE = ':not(:has(JSXAttribute[name.name="as"]:not([value.type="Literal"])))'
|
|
54
|
+
|
|
55
|
+
// セレクタ構築用の共通パターン
|
|
56
|
+
const TEXT_WITHOUT_TEXT_PROPS = `${TEXT_OPENING}:not(${HAS_TEXT_PROPS})`
|
|
57
|
+
const CLASSNAME_WITH_CONVERTIBLE_SHR = `${CHILD_CLASSNAME_LITERAL}${HAS_CONVERTIBLE_SHR_CLASS}`
|
|
58
|
+
|
|
59
|
+
// ============================================================
|
|
60
|
+
// ESLintセレクタ
|
|
61
|
+
// ============================================================
|
|
62
|
+
// Stage 1: shr-クラス → Text属性変換
|
|
63
|
+
const SELECTOR_CONVERTIBLE_SHR_TO_PROPS = `${TEXT_WITHOUT_TEXT_PROPS}${NOT_HAS_SPREAD} ${CLASSNAME_WITH_CONVERTIBLE_SHR}`
|
|
64
|
+
const SELECTOR_CONVERTIBLE_SHR_TO_PROPS_WITH_SPREAD = `${TEXT_WITHOUT_TEXT_PROPS}${HAS_SPREAD} ${CLASSNAME_WITH_CONVERTIBLE_SHR}`
|
|
65
|
+
|
|
66
|
+
// Stage 2: Text専用属性なし → HTML要素変換
|
|
67
|
+
const SELECTOR_UNNECESSARY_TEXT_NO_ATTRS = `${TEXT_OPENING}:not(${HAS_ANY_ATTR})${NOT_HAS_SPREAD}`
|
|
68
|
+
const SELECTOR_UNNECESSARY_TEXT_NO_CLASSNAME = `${TEXT_WITHOUT_TEXT_PROPS}:not(:has(${ATTR_CLASSNAME}))${HAS_ANY_ATTR}${NOT_HAS_SPREAD}${NOT_HAS_AS_VARIABLE}`
|
|
69
|
+
const SELECTOR_UNNECESSARY_TEXT_WITH_CLASSNAME = `${TEXT_WITHOUT_TEXT_PROPS}:has(${ATTR_CLASSNAME}${LITERAL_TYPE}:not(${HAS_CONVERTIBLE_SHR_CLASS}))${NOT_HAS_SPREAD}${NOT_HAS_AS_VARIABLE}`
|
|
70
|
+
|
|
71
|
+
// 矛盾検出
|
|
72
|
+
const SELECTOR_CONFLICTING_PROPS_SHR = `${TEXT_OPENING}${HAS_TEXT_PROPS}${NOT_HAS_SPREAD} ${CHILD_CLASSNAME_LITERAL}`
|
|
73
|
+
|
|
74
|
+
// ============================================================
|
|
75
|
+
// ヘルパー関数
|
|
76
|
+
// ============================================================
|
|
59
77
|
|
|
60
78
|
/**
|
|
61
|
-
* className
|
|
62
|
-
* セレクタで [value.type="Literal"] を保証しているため、必ず文字列リテラル
|
|
63
|
-
* trim-props ルールで先頭・末尾の空白は禁止されているため、trim() は不要
|
|
79
|
+
* className属性から変換可能なクラス名のみを抽出(矛盾検出用)
|
|
64
80
|
*/
|
|
65
81
|
function getConvertible(classNameAttrNode) {
|
|
66
82
|
const classNames = classNameAttrNode.value.value.split(REGEX_CLASSNAME_SPLIT)
|
|
@@ -76,9 +92,7 @@ function getConvertible(classNameAttrNode) {
|
|
|
76
92
|
}
|
|
77
93
|
|
|
78
94
|
/**
|
|
79
|
-
* className
|
|
80
|
-
* セレクタで [value.type="Literal"] を保証しているため、必ず文字列リテラル
|
|
81
|
-
* trim-props ルールで先頭・末尾の空白は禁止されているため、trim() は不要
|
|
95
|
+
* className属性を変換可能/不可能に分類し、Text属性ペアを生成
|
|
82
96
|
*/
|
|
83
97
|
function categorizeClassNames(classNameAttrNode) {
|
|
84
98
|
const classNames = classNameAttrNode.value.value.split(REGEX_CLASSNAME_SPLIT)
|
|
@@ -104,8 +118,7 @@ function categorizeClassNames(classNameAttrNode) {
|
|
|
104
118
|
}
|
|
105
119
|
|
|
106
120
|
/**
|
|
107
|
-
*
|
|
108
|
-
* セレクタで [value.type="Literal"] を保証しているため、型チェックは不要
|
|
121
|
+
* 属性の文字列リテラル値を取得
|
|
109
122
|
*/
|
|
110
123
|
function getAttributeLiteralValue(openingElement, attrName) {
|
|
111
124
|
const attr = openingElement.attributes.find(
|
|
@@ -115,7 +128,7 @@ function getAttributeLiteralValue(openingElement, attrName) {
|
|
|
115
128
|
}
|
|
116
129
|
|
|
117
130
|
/**
|
|
118
|
-
*
|
|
131
|
+
* 属性ノードを取得
|
|
119
132
|
*/
|
|
120
133
|
function getAttributeNode(openingElement, attrName) {
|
|
121
134
|
return openingElement.attributes.find(
|
|
@@ -134,72 +147,12 @@ module.exports = {
|
|
|
134
147
|
},
|
|
135
148
|
create(context) {
|
|
136
149
|
return {
|
|
137
|
-
//
|
|
138
|
-
[SELECTOR_UNNECESSARY_TEXT_NO_ATTRS]: (node) => {
|
|
139
|
-
context.report({
|
|
140
|
-
node,
|
|
141
|
-
message: `属性を持たないTextコンポーネントは、<span>に置き換えるか、要素を削除してテキストのみにすることを検討してください。
|
|
142
|
-
- 詳細: https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/best-practice-for-text-component
|
|
143
|
-
- Textコンポーネントに属性がない場合、直接HTML要素を使用するか、不要な要素を削除することでシンプルになります
|
|
144
|
-
- weight、size、color等の属性がある場合は、Textコンポーネントのまま利用してください`,
|
|
145
|
-
})
|
|
146
|
-
},
|
|
147
|
-
|
|
148
|
-
// パターン1-2: as属性のみ(文字列リテラル)
|
|
149
|
-
[SELECTOR_UNNECESSARY_TEXT_ONLY_AS]: (asAttrNode) => {
|
|
150
|
-
const tagName = asAttrNode.value.value
|
|
151
|
-
|
|
152
|
-
context.report({
|
|
153
|
-
node: asAttrNode.parent,
|
|
154
|
-
message: `as属性のみを持つTextコンポーネントは、ネイティブHTML要素(<${tagName}>)に置き換えてください。
|
|
155
|
-
- 詳細: https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/best-practice-for-text-component
|
|
156
|
-
- Textコンポーネントにas以外の属性がない場合、直接HTML要素を使用することでシンプルになります
|
|
157
|
-
- weight、size、color等の属性がある場合は、Textコンポーネントのまま利用してください`,
|
|
158
|
-
fix(fixer) {
|
|
159
|
-
const openingElement = asAttrNode.parent
|
|
160
|
-
const jsxElement = openingElement.parent
|
|
161
|
-
const sourceCode = context.sourceCode || context.getSourceCode()
|
|
162
|
-
|
|
163
|
-
// 属性とその前のスペースを含めて削除
|
|
164
|
-
const tokenBefore = sourceCode.getTokenBefore(asAttrNode)
|
|
165
|
-
const rangeStart = tokenBefore.range[1]
|
|
166
|
-
const rangeEnd = asAttrNode.range[1]
|
|
167
|
-
|
|
168
|
-
return [
|
|
169
|
-
fixer.removeRange([rangeStart, rangeEnd]),
|
|
170
|
-
fixer.replaceText(openingElement.name, tagName),
|
|
171
|
-
fixer.replaceText(jsxElement.closingElement.name, tagName)
|
|
172
|
-
]
|
|
173
|
-
},
|
|
174
|
-
})
|
|
175
|
-
},
|
|
176
|
-
|
|
177
|
-
// パターン2-1: classNameのみ(asなし)、Text属性なし、shr-クラスあり
|
|
150
|
+
// Stage 1: shr-クラス → Text属性変換(spread attributesなし)
|
|
178
151
|
[SELECTOR_CONVERTIBLE_SHR_TO_PROPS]: (classNameAttrNode) => {
|
|
179
152
|
const { nonConvertible, propSuggestions, convertible } = categorizeClassNames(classNameAttrNode)
|
|
180
153
|
const openingElement = classNameAttrNode.parent
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
// 変換可能なクラスが0個の場合、spanに変換(パターン1-3と同じ動作)
|
|
184
|
-
if (!propSuggestions) {
|
|
185
|
-
const classNameText = `className="${classNameAttrNode.value.value}"`
|
|
186
|
-
context.report({
|
|
187
|
-
node: openingElement,
|
|
188
|
-
message: `Textコンポーネントの機能を使用していないため、ネイティブHTML要素(<span>)に置き換えてください。
|
|
189
|
-
- 詳細: https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/best-practice-for-text-component
|
|
190
|
-
- 推奨: <span ${classNameText}>
|
|
191
|
-
- Textコンポーネントの機能(weight、size、color等)を使用しない場合は、直接HTML要素を使用することでシンプルになります`,
|
|
192
|
-
fix(fixer) {
|
|
193
|
-
return [
|
|
194
|
-
fixer.replaceText(openingElement.name, 'span'),
|
|
195
|
-
fixer.replaceText(jsxElement.closingElement.name, 'span')
|
|
196
|
-
]
|
|
197
|
-
},
|
|
198
|
-
})
|
|
199
|
-
return
|
|
200
|
-
}
|
|
154
|
+
const sourceCode = context.sourceCode || context.getSourceCode()
|
|
201
155
|
|
|
202
|
-
// 変換可能なクラスがある場合、属性に変換
|
|
203
156
|
context.report({
|
|
204
157
|
node: openingElement,
|
|
205
158
|
message: `classNameで指定されたshr-プレフィックスのクラスは、Textコンポーネントの属性に置き換えてください。
|
|
@@ -208,157 +161,123 @@ module.exports = {
|
|
|
208
161
|
- 変換可能なクラス: ${convertible}
|
|
209
162
|
- shr-プレフィックスのクラスをTextの属性に置き換えることで、型安全性が向上し、意図がより明確になります`,
|
|
210
163
|
fix(fixer) {
|
|
211
|
-
const sourceCode = context.sourceCode || context.getSourceCode()
|
|
212
164
|
const fixes = []
|
|
213
|
-
|
|
214
165
|
if (nonConvertible) {
|
|
215
|
-
// classNameの値を更新(属性自体は残す)
|
|
216
166
|
fixes.push(fixer.replaceText(classNameAttrNode.value, `"${nonConvertible}"`))
|
|
217
167
|
} else {
|
|
218
|
-
// className属性全体を削除(shr-クラスのみの場合)
|
|
219
168
|
const tokenBefore = sourceCode.getTokenBefore(classNameAttrNode)
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
169
|
+
fixes.push(fixer.removeRange([tokenBefore.range[1], classNameAttrNode.range[1]]))
|
|
170
|
+
}
|
|
171
|
+
const asAttrNode = getAttributeNode(openingElement, 'as')
|
|
172
|
+
if (asAttrNode) {
|
|
173
|
+
fixes.push(fixer.insertTextAfter(asAttrNode, ` ${propSuggestions}`))
|
|
174
|
+
} else {
|
|
175
|
+
fixes.push(fixer.insertTextAfter(openingElement.name, ` ${propSuggestions}`))
|
|
223
176
|
}
|
|
224
|
-
|
|
225
|
-
// 新しいpropsを追加
|
|
226
|
-
fixes.push(fixer.insertTextAfter(openingElement.name, ` ${propSuggestions}`))
|
|
227
|
-
|
|
228
177
|
return fixes
|
|
229
178
|
},
|
|
230
179
|
})
|
|
231
180
|
},
|
|
232
181
|
|
|
233
|
-
//
|
|
234
|
-
[
|
|
235
|
-
const
|
|
182
|
+
// Stage 1: shr-クラス → Text属性変換(spread attributesあり、fixなし)
|
|
183
|
+
[SELECTOR_CONVERTIBLE_SHR_TO_PROPS_WITH_SPREAD]: (classNameAttrNode) => {
|
|
184
|
+
const { convertible } = categorizeClassNames(classNameAttrNode)
|
|
185
|
+
const openingElement = classNameAttrNode.parent
|
|
236
186
|
|
|
237
187
|
context.report({
|
|
238
|
-
node:
|
|
239
|
-
message: `Text
|
|
188
|
+
node: openingElement,
|
|
189
|
+
message: `classNameで指定されたshr-プレフィックスのクラスは、Textコンポーネントの属性に置き換えてください。
|
|
240
190
|
- 詳細: https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/best-practice-for-text-component
|
|
241
|
-
-
|
|
242
|
-
-
|
|
243
|
-
|
|
244
|
-
const openingElement = classNameAttrNode.parent
|
|
245
|
-
const jsxElement = openingElement.parent
|
|
246
|
-
|
|
247
|
-
return [
|
|
248
|
-
fixer.replaceText(openingElement.name, 'span'),
|
|
249
|
-
fixer.replaceText(jsxElement.closingElement.name, 'span')
|
|
250
|
-
]
|
|
251
|
-
},
|
|
191
|
+
- 変換可能なクラス: ${convertible}
|
|
192
|
+
- spread attributes ({...props}) があるため自動修正できません。手動で修正してください
|
|
193
|
+
- shr-プレフィックスのクラスをTextの属性に置き換えることで、型安全性が向上し、意図がより明確になります`,
|
|
252
194
|
})
|
|
253
195
|
},
|
|
254
196
|
|
|
255
|
-
//
|
|
256
|
-
[
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
const asValue = getAttributeLiteralValue(openingElement, 'as')
|
|
261
|
-
|
|
262
|
-
// 変換可能なクラスが0個の場合、as属性で指定されたタグに変換(パターン1-4と同じ動作)
|
|
263
|
-
if (!propSuggestions) {
|
|
264
|
-
const classNameValue = getAttributeLiteralValue(openingElement, 'className')
|
|
265
|
-
const classNameText = `className="${classNameValue}"`
|
|
266
|
-
context.report({
|
|
267
|
-
node: openingElement,
|
|
268
|
-
message: `Textコンポーネントの機能を使用していないため、ネイティブHTML要素に置き換えてください。
|
|
197
|
+
// Stage 2: 属性なし
|
|
198
|
+
[SELECTOR_UNNECESSARY_TEXT_NO_ATTRS]: (node) => {
|
|
199
|
+
context.report({
|
|
200
|
+
node,
|
|
201
|
+
message: `属性を持たないTextコンポーネントは、<span>に置き換えるか、要素を削除してテキストのみにすることを検討してください。
|
|
269
202
|
- 詳細: https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/best-practice-for-text-component
|
|
270
|
-
-
|
|
271
|
-
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
const asAttrNode = getAttributeNode(openingElement, 'as')
|
|
203
|
+
- Textコンポーネントに属性がない場合、直接HTML要素を使用するか、不要な要素を削除することでシンプルになります
|
|
204
|
+
- weight、size、color等の属性がある場合は、Textコンポーネントのまま利用してください`,
|
|
205
|
+
})
|
|
206
|
+
},
|
|
275
207
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
return [
|
|
282
|
-
fixer.removeRange([rangeStart, rangeEnd]),
|
|
283
|
-
fixer.replaceText(openingElement.name, asValue),
|
|
284
|
-
fixer.replaceText(jsxElement.closingElement.name, asValue)
|
|
285
|
-
]
|
|
286
|
-
},
|
|
287
|
-
})
|
|
288
|
-
return
|
|
289
|
-
}
|
|
208
|
+
// Stage 2: classNameなし
|
|
209
|
+
[SELECTOR_UNNECESSARY_TEXT_NO_CLASSNAME]: (node) => {
|
|
210
|
+
const asValue = getAttributeLiteralValue(node, 'as')
|
|
211
|
+
const tagName = asValue || 'span'
|
|
212
|
+
const sourceCode = context.sourceCode || context.getSourceCode()
|
|
290
213
|
|
|
291
|
-
// 変換可能なクラスがある場合、属性に変換
|
|
292
214
|
context.report({
|
|
293
|
-
node
|
|
294
|
-
message:
|
|
215
|
+
node,
|
|
216
|
+
message: asValue
|
|
217
|
+
? `as属性のみを持つTextコンポーネントは、ネイティブHTML要素(<${tagName}>)に置き換えてください。
|
|
295
218
|
- 詳細: https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/best-practice-for-text-component
|
|
296
|
-
-
|
|
297
|
-
-
|
|
298
|
-
|
|
219
|
+
- Textコンポーネントにas以外の属性がない場合、直接HTML要素を使用することでシンプルになります
|
|
220
|
+
- weight、size、color等の属性がある場合は、Textコンポーネントのまま利用してください`
|
|
221
|
+
: `Textコンポーネントの機能を使用していないため、ネイティブHTML要素(<span>)に置き換えてください。
|
|
222
|
+
- 詳細: https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/best-practice-for-text-component
|
|
223
|
+
- 推奨: <span>
|
|
224
|
+
- Textコンポーネントの機能(weight、size、color等)を使用しない場合は、直接HTML要素を使用することでシンプルになります`,
|
|
299
225
|
fix(fixer) {
|
|
300
|
-
const
|
|
226
|
+
const jsxElement = node.parent
|
|
301
227
|
const fixes = []
|
|
302
|
-
|
|
303
|
-
if (
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
// className属性全体を削除(shr-クラスのみの場合)
|
|
308
|
-
const tokenBefore = sourceCode.getTokenBefore(classNameAttrNode)
|
|
309
|
-
const rangeStart = tokenBefore.range[1]
|
|
310
|
-
const rangeEnd = classNameAttrNode.range[1]
|
|
311
|
-
fixes.push(fixer.removeRange([rangeStart, rangeEnd]))
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
// 新しいpropsを追加(as属性がある場合はその後ろに挿入)
|
|
315
|
-
const asAttrNode = getAttributeNode(openingElement, 'as')
|
|
316
|
-
if (asAttrNode) {
|
|
317
|
-
fixes.push(fixer.insertTextAfter(asAttrNode, ` ${propSuggestions}`))
|
|
318
|
-
} else {
|
|
319
|
-
fixes.push(fixer.insertTextAfter(openingElement.name, ` ${propSuggestions}`))
|
|
228
|
+
// as属性があれば削除
|
|
229
|
+
if (asValue) {
|
|
230
|
+
const asAttrNode = getAttributeNode(node, 'as')
|
|
231
|
+
const tokenBefore = sourceCode.getTokenBefore(asAttrNode)
|
|
232
|
+
fixes.push(fixer.removeRange([tokenBefore.range[1], asAttrNode.range[1]]))
|
|
320
233
|
}
|
|
321
|
-
|
|
234
|
+
fixes.push(fixer.replaceText(node.name, tagName))
|
|
235
|
+
fixes.push(fixer.replaceText(jsxElement.closingElement.name, tagName))
|
|
322
236
|
return fixes
|
|
323
237
|
},
|
|
324
238
|
})
|
|
325
239
|
},
|
|
326
240
|
|
|
327
|
-
//
|
|
328
|
-
[
|
|
329
|
-
const
|
|
330
|
-
const
|
|
331
|
-
const
|
|
332
|
-
const
|
|
241
|
+
// Stage 2: classNameあり
|
|
242
|
+
[SELECTOR_UNNECESSARY_TEXT_WITH_CLASSNAME]: (node) => {
|
|
243
|
+
const asValue = getAttributeLiteralValue(node, 'as')
|
|
244
|
+
const classNameValue = getAttributeLiteralValue(node, 'className')
|
|
245
|
+
const tagName = asValue || 'span'
|
|
246
|
+
const sourceCode = context.sourceCode || context.getSourceCode()
|
|
247
|
+
|
|
248
|
+
// classNameは常に表示(fixerが自動で保持するため)
|
|
249
|
+
const classNameText = classNameValue ? ` className="${classNameValue}"` : ''
|
|
333
250
|
|
|
334
251
|
context.report({
|
|
335
|
-
node
|
|
336
|
-
message:
|
|
252
|
+
node,
|
|
253
|
+
message: asValue
|
|
254
|
+
? `Textコンポーネントの機能を使用していないため、ネイティブHTML要素に置き換えてください。
|
|
337
255
|
- 詳細: https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/best-practice-for-text-component
|
|
338
256
|
- <${tagName}>要素にclassNameを移動してください
|
|
257
|
+
- Textコンポーネントの機能(weight、size、color等)を使用しない場合は、直接HTML要素を使用することでシンプルになります`
|
|
258
|
+
: `Textコンポーネントの機能を使用していないため、ネイティブHTML要素(<span>)に置き換えてください。
|
|
259
|
+
- 詳細: https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/best-practice-for-text-component
|
|
260
|
+
- 推奨: <span${classNameText}>
|
|
339
261
|
- Textコンポーネントの機能(weight、size、color等)を使用しない場合は、直接HTML要素を使用することでシンプルになります`,
|
|
340
262
|
fix(fixer) {
|
|
341
|
-
const jsxElement =
|
|
342
|
-
const
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
fixer.replaceText(jsxElement.closingElement.name, tagName)
|
|
353
|
-
]
|
|
263
|
+
const jsxElement = node.parent
|
|
264
|
+
const fixes = []
|
|
265
|
+
// as属性があれば削除
|
|
266
|
+
if (asValue) {
|
|
267
|
+
const asAttrNode = getAttributeNode(node, 'as')
|
|
268
|
+
const tokenBefore = sourceCode.getTokenBefore(asAttrNode)
|
|
269
|
+
fixes.push(fixer.removeRange([tokenBefore.range[1], asAttrNode.range[1]]))
|
|
270
|
+
}
|
|
271
|
+
fixes.push(fixer.replaceText(node.name, tagName))
|
|
272
|
+
fixes.push(fixer.replaceText(jsxElement.closingElement.name, tagName))
|
|
273
|
+
return fixes
|
|
354
274
|
},
|
|
355
275
|
})
|
|
356
276
|
},
|
|
357
277
|
|
|
358
|
-
//
|
|
278
|
+
// 矛盾検出
|
|
359
279
|
[SELECTOR_CONFLICTING_PROPS_SHR]: (classNameAttrNode) => {
|
|
360
280
|
const convertible = getConvertible(classNameAttrNode)
|
|
361
|
-
|
|
362
281
|
if (convertible) {
|
|
363
282
|
context.report({
|
|
364
283
|
node: classNameAttrNode.parent,
|
|
@@ -31,62 +31,169 @@ const SCHEMA = [
|
|
|
31
31
|
}
|
|
32
32
|
]
|
|
33
33
|
|
|
34
|
-
const entriedReplacePaths = Object.entries(replacePaths)
|
|
35
34
|
const CWD = process.cwd()
|
|
36
35
|
const REGEX_UNNECESSARY_SLASH = /(\/)+/g
|
|
37
36
|
const REGEX_ROOT_PATH = new RegExp(`^${rootPath}/index\.`)
|
|
38
37
|
const REGEX_INDEX_FILE = /\/index\.(ts|js)x?$/
|
|
39
38
|
const TARGET_EXTS = ['ts', 'tsx', 'js', 'jsx']
|
|
40
39
|
|
|
41
|
-
//
|
|
42
|
-
const entriedReplacePathsWithRegex =
|
|
40
|
+
// Path aliasの正規表現を事前生成してキャッシュ
|
|
41
|
+
const entriedReplacePathsWithRegex = Object.entries(replacePaths).map(([key, values]) => [
|
|
43
42
|
key,
|
|
44
43
|
values,
|
|
45
44
|
new RegExp(`^${key}(.+)$`),
|
|
46
45
|
values.map(v => new RegExp(`^${path.resolve(`${CWD}/${v}`)}(.+)$`))
|
|
47
46
|
])
|
|
48
47
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
48
|
+
/**
|
|
49
|
+
* Path aliasを絶対パスに変換する
|
|
50
|
+
* @param {string} importPath - import文のパス(例: '@/components/Button')
|
|
51
|
+
* @returns {string} 絶対パス(例: '/path/to/src/components/Button')
|
|
52
|
+
*/
|
|
53
|
+
const resolvePathAlias = (importPath) => {
|
|
54
|
+
if (importPath[0] === '/') {
|
|
55
|
+
return importPath
|
|
52
56
|
}
|
|
53
57
|
|
|
54
|
-
return entriedReplacePathsWithRegex.reduce((
|
|
55
|
-
if (
|
|
56
|
-
return values.reduce((
|
|
57
|
-
if (
|
|
58
|
-
return
|
|
58
|
+
return entriedReplacePathsWithRegex.reduce((result, [key, values, keyRegex]) => {
|
|
59
|
+
if (result === importPath) {
|
|
60
|
+
return values.reduce((resolved, value) => {
|
|
61
|
+
if (resolved === result && keyRegex.test(result)) {
|
|
62
|
+
return resolved.replace(keyRegex, `${path.resolve(`${CWD}/${value}`)}/$1`)
|
|
59
63
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}, prev)
|
|
64
|
+
return resolved
|
|
65
|
+
}, result)
|
|
63
66
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
}, source)
|
|
67
|
+
return result
|
|
68
|
+
}, importPath)
|
|
67
69
|
}
|
|
68
|
-
const calculateReplacedImportPath = (source) => {
|
|
69
|
-
return entriedReplacePathsWithRegex.reduce((prev, [key, values, keyRegex, valueRegexes]) => {
|
|
70
|
-
if (source === prev) {
|
|
71
|
-
return values.reduce((p, v, index) => {
|
|
72
|
-
if (prev === p) {
|
|
73
|
-
const regexp = valueRegexes[index]
|
|
74
70
|
|
|
75
|
-
|
|
76
|
-
|
|
71
|
+
/**
|
|
72
|
+
* 絶対パスをPath aliasに変換する
|
|
73
|
+
* @param {string} absolutePath - 絶対パス(例: '/path/to/src/components/Button')
|
|
74
|
+
* @returns {string} Path alias(例: '@/components/Button')
|
|
75
|
+
*/
|
|
76
|
+
const convertToPathAlias = (absolutePath) => {
|
|
77
|
+
return entriedReplacePathsWithRegex.reduce((result, [key, values, keyRegex, valueRegexes]) => {
|
|
78
|
+
if (result === absolutePath) {
|
|
79
|
+
return values.reduce((converted, value, index) => {
|
|
80
|
+
if (converted === result) {
|
|
81
|
+
const regexp = valueRegexes[index]
|
|
82
|
+
if (regexp.test(converted)) {
|
|
83
|
+
return converted.replace(regexp, `${key}/$1`).replace(REGEX_UNNECESSARY_SLASH, '/')
|
|
77
84
|
}
|
|
78
85
|
}
|
|
86
|
+
return converted
|
|
87
|
+
}, result)
|
|
88
|
+
}
|
|
89
|
+
return result
|
|
90
|
+
}, absolutePath)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* import先がimport元の内部にあるかチェック(同階層・サブディレクトリからのimport)
|
|
95
|
+
* (Next.js App Routerの特殊文字パスにも対応)
|
|
96
|
+
* @param {string} importerDir - import元のディレクトリ
|
|
97
|
+
* @param {string} importedPath - import先のパス
|
|
98
|
+
* @returns {boolean}
|
|
99
|
+
*/
|
|
100
|
+
const isImportedInsideImporter = (importerDir, importedPath) => {
|
|
101
|
+
return importedPath === importerDir || importedPath.startsWith(importerDir + '/')
|
|
102
|
+
}
|
|
79
103
|
|
|
80
|
-
|
|
81
|
-
|
|
104
|
+
/**
|
|
105
|
+
* allowedImportsオプションに基づいて、特定のimportが許可されているかチェックする
|
|
106
|
+
* @param {object} node - ImportDeclaration node
|
|
107
|
+
* @param {string} importerDir - import元のディレクトリ
|
|
108
|
+
* @param {Array} targetAllowedImports - 適用されるallowedImportsのキー配列
|
|
109
|
+
* @param {object} allowedImportsConfig - allowedImportsの設定
|
|
110
|
+
* @returns {{ shouldSkip: boolean, deniedModules: Array }} チェック結果
|
|
111
|
+
*/
|
|
112
|
+
const checkAllowedImports = (node, importerDir, targetAllowedImports, allowedImportsConfig) => {
|
|
113
|
+
let isDenyPath = false
|
|
114
|
+
let deniedModules = []
|
|
115
|
+
|
|
116
|
+
for (const allowedKey of targetAllowedImports) {
|
|
117
|
+
const allowedOption = allowedImportsConfig[allowedKey]
|
|
118
|
+
|
|
119
|
+
for (const targetModule in allowedOption) {
|
|
120
|
+
const actualTarget = targetModule[0] !== '.'
|
|
121
|
+
? targetModule
|
|
122
|
+
: path.resolve(`${CWD}/${targetModule}`)
|
|
123
|
+
|
|
124
|
+
let importSource = node.source.value
|
|
125
|
+
|
|
126
|
+
// 絶対パスの場合は、import元ディレクトリを基準に解決
|
|
127
|
+
if (actualTarget[0] === '/') {
|
|
128
|
+
importSource = path.resolve(`${importerDir}/${importSource}`)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (actualTarget !== importSource) {
|
|
132
|
+
continue
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const allowedModules = allowedOption[targetModule] || true
|
|
136
|
+
|
|
137
|
+
if (!Array.isArray(allowedModules)) {
|
|
138
|
+
isDenyPath = true
|
|
139
|
+
deniedModules.push(true)
|
|
140
|
+
} else {
|
|
141
|
+
const importedNames = node.specifiers.map(s => s.imported?.name)
|
|
142
|
+
const notAllowedModules = importedNames.filter(name => !allowedModules.includes(name))
|
|
143
|
+
deniedModules.push(notAllowedModules)
|
|
144
|
+
}
|
|
82
145
|
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// 完全に許可されている場合はスキップ
|
|
149
|
+
const shouldSkip =
|
|
150
|
+
(isDenyPath && deniedModules[0] === true) ||
|
|
151
|
+
(!isDenyPath && deniedModules.length === 1 && deniedModules[0].length === 0)
|
|
83
152
|
|
|
84
|
-
|
|
85
|
-
}, source)
|
|
153
|
+
return { shouldSkip, deniedModules }
|
|
86
154
|
}
|
|
87
155
|
|
|
88
|
-
|
|
89
|
-
|
|
156
|
+
/**
|
|
157
|
+
* import先のパスから親方向に barrel ファイルを探索する
|
|
158
|
+
* @param {string} importedPath - import先の絶対パス
|
|
159
|
+
* @param {string} importerDir - import元のディレクトリ
|
|
160
|
+
* @returns {string|undefined} 見つかったbarrelファイルのパス
|
|
161
|
+
*/
|
|
162
|
+
const findBarrelFile = (importedPath, importerDir) => {
|
|
163
|
+
const pathSegments = importedPath.split('/')
|
|
164
|
+
let currentPath = importedPath
|
|
165
|
+
let barrel = undefined
|
|
166
|
+
|
|
167
|
+
// ディレクトリ指定の場合、そのindex.tsを指していることは自明なので一階層上から探索
|
|
168
|
+
if (fs.existsSync(currentPath) && fs.statSync(currentPath).isDirectory()) {
|
|
169
|
+
pathSegments.pop()
|
|
170
|
+
currentPath = pathSegments.join('/')
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
while (pathSegments.length > 0) {
|
|
174
|
+
// 以下の場合は探索終了
|
|
175
|
+
// 1. root pathに到達した場合
|
|
176
|
+
// 2. import先がimport元の内部にある場合(同階層・サブディレクトリからのimport)
|
|
177
|
+
if (importerDir === rootPath || isImportedInsideImporter(importerDir, currentPath)) {
|
|
178
|
+
break
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// 現在のパスにbarrelファイルがあるかチェック
|
|
182
|
+
const foundBarrel = TARGET_EXTS
|
|
183
|
+
.map(ext => `${currentPath}/index.${ext}`)
|
|
184
|
+
.find(filePath => fs.existsSync(filePath))
|
|
185
|
+
|
|
186
|
+
if (foundBarrel) {
|
|
187
|
+
barrel = foundBarrel
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// 一階層上に移動
|
|
191
|
+
pathSegments.pop()
|
|
192
|
+
currentPath = pathSegments.join('/')
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return barrel
|
|
196
|
+
}
|
|
90
197
|
|
|
91
198
|
/**
|
|
92
199
|
* @type {import('@typescript-eslint/utils').TSESLint.RuleModule<''>}
|
|
@@ -99,108 +206,78 @@ module.exports = {
|
|
|
99
206
|
create(context) {
|
|
100
207
|
const option = context.options[0] || {}
|
|
101
208
|
|
|
102
|
-
|
|
103
|
-
|
|
209
|
+
// ignoresオプションでスキップ対象のファイルかチェック
|
|
210
|
+
if (option.ignores) {
|
|
211
|
+
const isIgnored = option.ignores.some(pattern =>
|
|
212
|
+
new RegExp(pattern).test(context.filename)
|
|
213
|
+
)
|
|
214
|
+
if (isIgnored) {
|
|
215
|
+
return {}
|
|
216
|
+
}
|
|
104
217
|
}
|
|
105
218
|
|
|
106
|
-
const
|
|
219
|
+
const importerDir = getParentDir(context.filename)
|
|
220
|
+
|
|
221
|
+
// このファイルに適用されるallowedImportsのキーを収集
|
|
107
222
|
const targetAllowedImports = []
|
|
108
223
|
if (option?.allowedImports) {
|
|
109
|
-
for (const
|
|
110
|
-
if (
|
|
111
|
-
targetAllowedImports.push(
|
|
224
|
+
for (const pattern in option.allowedImports) {
|
|
225
|
+
if (new RegExp(pattern).test(context.filename)) {
|
|
226
|
+
targetAllowedImports.push(pattern)
|
|
112
227
|
}
|
|
113
228
|
}
|
|
114
229
|
}
|
|
115
230
|
|
|
116
231
|
return {
|
|
117
232
|
ImportDeclaration: (node) => {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
let sourceValue = node.source.value
|
|
127
|
-
|
|
128
|
-
if (actualTarget[0] === '/') {
|
|
129
|
-
sourceValue = path.resolve(`${dir}/${sourceValue}`)
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
if (actualTarget !== sourceValue) {
|
|
133
|
-
continue
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
let allowedModules = allowedOption[targetModule] || true
|
|
137
|
-
|
|
138
|
-
if (!Array.isArray(allowedModules)) {
|
|
139
|
-
isDenyPath = true
|
|
140
|
-
deniedModules.push(true)
|
|
141
|
-
} else {
|
|
142
|
-
deniedModules.push(node.specifiers.map(pickImportedName).filter(i => allowedModules.indexOf(i) == -1))
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
if (
|
|
148
|
-
isDenyPath && deniedModules[0] === true ||
|
|
149
|
-
!isDenyPath && deniedModules.length === 1 && deniedModules[0].length === 0
|
|
150
|
-
) {
|
|
233
|
+
// allowedImportsチェック
|
|
234
|
+
const { shouldSkip, deniedModules } = checkAllowedImports(
|
|
235
|
+
node,
|
|
236
|
+
importerDir,
|
|
237
|
+
targetAllowedImports,
|
|
238
|
+
option.allowedImports || {}
|
|
239
|
+
)
|
|
240
|
+
if (shouldSkip) {
|
|
151
241
|
return
|
|
152
242
|
}
|
|
153
243
|
|
|
154
|
-
|
|
244
|
+
// import先のパスを絶対パスに変換
|
|
245
|
+
let importedPath = node.source.value
|
|
155
246
|
|
|
156
|
-
|
|
157
|
-
|
|
247
|
+
// 相対パスの場合、絶対パスに変換
|
|
248
|
+
if (importedPath[0] === '.') {
|
|
249
|
+
importedPath = path.resolve(`${importerDir}/${importedPath}`)
|
|
158
250
|
}
|
|
159
251
|
|
|
160
|
-
|
|
252
|
+
// Path alias(@/, ~/など)を絶対パスに変換
|
|
253
|
+
importedPath = resolvePathAlias(importedPath)
|
|
161
254
|
|
|
162
|
-
|
|
255
|
+
// 絶対パスでない場合(node_modulesなど)はスキップ
|
|
256
|
+
if (importedPath[0] !== '/') {
|
|
163
257
|
return
|
|
164
258
|
}
|
|
165
259
|
|
|
166
|
-
|
|
260
|
+
// barrel ファイルを探索
|
|
261
|
+
const barrelPath = findBarrelFile(importedPath, importerDir)
|
|
167
262
|
|
|
168
|
-
//
|
|
169
|
-
if (
|
|
170
|
-
|
|
171
|
-
sourceValue = sources.join('/')
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
let barrel = undefined
|
|
175
|
-
|
|
176
|
-
while (sources.length > 0) {
|
|
177
|
-
// HINT: 以下の場合は即終了
|
|
178
|
-
// - import元以下のimportだった場合
|
|
179
|
-
// - rootまで捜索した場合
|
|
180
|
-
if (
|
|
181
|
-
dir === rootPath ||
|
|
182
|
-
dir.match(new RegExp(`^${sourceValue}`))
|
|
183
|
-
) {
|
|
184
|
-
break
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
barrel = TARGET_EXTS.map((e) => `${sourceValue}/index.${e}`).find(findExistsSync) || barrel
|
|
188
|
-
|
|
189
|
-
sources.pop()
|
|
190
|
-
sourceValue = sources.join('/')
|
|
263
|
+
// barrel が見つからない、またはroot pathのindex.tsの場合はスキップ
|
|
264
|
+
if (!barrelPath || REGEX_ROOT_PATH.test(barrelPath)) {
|
|
265
|
+
return
|
|
191
266
|
}
|
|
192
267
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
268
|
+
// barrel パスをPath aliasに変換
|
|
269
|
+
const barrelWithAlias = convertToPathAlias(barrelPath)
|
|
270
|
+
const barrelDirWithAlias = barrelWithAlias.replace(REGEX_INDEX_FILE, '')
|
|
271
|
+
const uniqueDeniedModules = [...new Set(deniedModules.flat())]
|
|
272
|
+
|
|
273
|
+
// エラーを報告
|
|
274
|
+
context.report({
|
|
275
|
+
node,
|
|
276
|
+
message: uniqueDeniedModules.length
|
|
277
|
+
? `${uniqueDeniedModules.join(', ')} は ${barrelDirWithAlias} からimportしてください`
|
|
278
|
+
: `${barrelDirWithAlias} からimportするか、${barrelWithAlias} のbarrelファイルを削除して直接import可能にしてください`
|
|
279
|
+
+ '\n - 詳細: https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/require-barrel-import',
|
|
280
|
+
})
|
|
204
281
|
},
|
|
205
282
|
}
|
|
206
283
|
},
|
|
@@ -47,11 +47,13 @@ ruleTester.run('a11y-heading-in-sectioning-content', rule, {
|
|
|
47
47
|
{ code: '<Reel as="aside"><div><Heading>hoge</Heading></div></Reel>' },
|
|
48
48
|
{ code: '<Sidebar as="nav"><div><Heading>hoge</Heading></div></Sidebar>' },
|
|
49
49
|
{ code: '<Stack as="section"><div><Heading>hoge</Heading></div></Stack>' },
|
|
50
|
+
{ code: '<Scroller as="section"><div><Heading>hoge</Heading></div></Scroller>' },
|
|
50
51
|
{ code: '<HogeCenter forwardedAs="section"><div><Heading>hoge</Heading></div></HogeCenter>' },
|
|
51
52
|
{ code: '<HogeCluster forwardedAs="section"><div><Heading>hoge</Heading></div></HogeCluster>' },
|
|
52
53
|
{ code: '<HogeReel forwardedAs="aside"><div><Heading>hoge</Heading></div></HogeReel>' },
|
|
53
54
|
{ code: '<HogeSidebar forwardedAs="nav"><div><Heading>hoge</Heading></div></HogeSidebar>' },
|
|
54
55
|
{ code: '<HogeStack forwardedAs="section"><div><Heading>hoge</Heading></div></HogeStack>' },
|
|
56
|
+
{ code: '<HogeScroller forwardedAs="section"><div><Heading>hoge</Heading></div></HogeScroller>' },
|
|
55
57
|
{ code: '<HogeBase as="aside"><Heading>hoge</Heading></HogeBase>' },
|
|
56
58
|
{ code: '<HogeBaseColumn forwardedAs="nav"><Heading>hoge</Heading></HogeBaseColumn>' },
|
|
57
59
|
{ code: '<HogeNav aria-label="any"><Any /></HogeNav>' },
|
|
@@ -23,7 +23,7 @@ const errorAsOnly = (tag) => `as属性のみを持つTextコンポーネント
|
|
|
23
23
|
|
|
24
24
|
const errorUnnecessaryClassName = (className) => `Textコンポーネントの機能を使用していないため、ネイティブHTML要素(<span>)に置き換えてください。
|
|
25
25
|
- 詳細: https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/best-practice-for-text-component
|
|
26
|
-
- 推奨: <span className="${className}">
|
|
26
|
+
- 推奨: <span${className ? ` className="${className}"` : ''}>
|
|
27
27
|
- Textコンポーネントの機能(weight、size、color等)を使用しない場合は、直接HTML要素を使用することでシンプルになります`
|
|
28
28
|
|
|
29
29
|
const errorUnnecessaryAsClassName = (tag) => `Textコンポーネントの機能を使用していないため、ネイティブHTML要素に置き換えてください。
|
|
@@ -31,12 +31,18 @@ const errorUnnecessaryAsClassName = (tag) => `Textコンポーネントの機能
|
|
|
31
31
|
- <${tag}>要素にclassNameを移動してください
|
|
32
32
|
- Textコンポーネントの機能(weight、size、color等)を使用しない場合は、直接HTML要素を使用することでシンプルになります`
|
|
33
33
|
|
|
34
|
-
const errorConvertibleShr = (suggestion, convertible
|
|
34
|
+
const errorConvertibleShr = (suggestion, convertible) => `classNameで指定されたshr-プレフィックスのクラスは、Textコンポーネントの属性に置き換えてください。
|
|
35
35
|
- 詳細: https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/best-practice-for-text-component
|
|
36
|
-
- 推奨: <Text
|
|
36
|
+
- 推奨: <Text ${suggestion}>
|
|
37
37
|
- 変換可能なクラス: ${convertible}
|
|
38
38
|
- shr-プレフィックスのクラスをTextの属性に置き換えることで、型安全性が向上し、意図がより明確になります`
|
|
39
39
|
|
|
40
|
+
const errorConvertibleShrWithSpread = (convertible) => `classNameで指定されたshr-プレフィックスのクラスは、Textコンポーネントの属性に置き換えてください。
|
|
41
|
+
- 詳細: https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/best-practice-for-text-component
|
|
42
|
+
- 変換可能なクラス: ${convertible}
|
|
43
|
+
- spread attributes ({...props}) があるため自動修正できません。手動で修正してください
|
|
44
|
+
- shr-プレフィックスのクラスをTextの属性に置き換えることで、型安全性が向上し、意図がより明確になります`
|
|
45
|
+
|
|
40
46
|
const errorConflictingProps = (convertible) => `Textコンポーネントの属性とclassNameで矛盾する指定があります。
|
|
41
47
|
- 詳細: https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/best-practice-for-text-component
|
|
42
48
|
- 変換可能なクラス: ${convertible}
|
|
@@ -52,12 +58,6 @@ ruleTester.run('best-practice-for-text-component', rule, {
|
|
|
52
58
|
{ code: `<Text weight="bold">content</Text>` },
|
|
53
59
|
{ code: `<Text size="M">text</Text>` },
|
|
54
60
|
{ code: `<Text color="TEXT_GREY">text</Text>` },
|
|
55
|
-
{ code: `<Text leading="TIGHT">text</Text>` },
|
|
56
|
-
{ code: `<Text italic>text</Text>` },
|
|
57
|
-
{ code: `<Text whiteSpace="nowrap">text</Text>` },
|
|
58
|
-
{ code: `<Text maxLines={2}>text</Text>` },
|
|
59
|
-
{ code: `<Text styleType="blockTitle">text</Text>` },
|
|
60
|
-
{ code: `<Text icon={<Icon />}>text</Text>` },
|
|
61
61
|
// as + スタイリング属性
|
|
62
62
|
{ code: `<Text as="p" weight="bold">content</Text>` },
|
|
63
63
|
// Textのスタイリング属性 + className(変換不可能なクラスのみ)
|
|
@@ -65,28 +65,33 @@ ruleTester.run('best-practice-for-text-component', rule, {
|
|
|
65
65
|
// className が変数や式の場合は静的解析できないのでスキップ
|
|
66
66
|
{ code: `<Text className={customClass}>text</Text>` },
|
|
67
67
|
{ code: `<Text className={\`custom-\${type}\`}>text</Text>` },
|
|
68
|
-
{ code: `<Text as="p" className={customClass}>text</Text>` },
|
|
69
|
-
{ code: `<Text size="M" className={customClass}>text</Text>` },
|
|
70
68
|
// as が変数の場合
|
|
71
69
|
{ code: `<Text as={component}>text</Text>` },
|
|
70
|
+
// spread attributes は静的解析できないのでスキップ(変換不可能なclassNameの場合)
|
|
71
|
+
{ code: `<Text {...props}>content</Text>` },
|
|
72
|
+
{ code: `<Text {...props} className="custom">content</Text>` },
|
|
73
|
+
// key + Text専用属性(size等)→ valid
|
|
74
|
+
{ code: `<Text key="item-1" size="M">text</Text>` },
|
|
75
|
+
{ code: `<Text key={itemId} weight="bold">text</Text>` },
|
|
72
76
|
],
|
|
73
77
|
invalid: [
|
|
74
78
|
// パターン1-1: 属性なし
|
|
75
79
|
{ code: `<Text>content</Text>`, errors: [{ message: ERROR_NO_ATTRS }] },
|
|
76
80
|
|
|
81
|
+
// パターン1-1b: key属性のみ(key属性は無視してspanに変換)
|
|
82
|
+
{ code: `<Text key="item-1">content</Text>`, output: `<span key="item-1">content</span>`, errors: [{ message: errorUnnecessaryClassName('') }] },
|
|
83
|
+
|
|
77
84
|
// パターン1-2: as属性のみ
|
|
78
85
|
{ code: `<Text as="p">content</Text>`, output: `<p>content</p>`, errors: [{ message: errorAsOnly('p') }] },
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
</Text>`, output: `<p>
|
|
83
|
-
nested content
|
|
84
|
-
</p>`, errors: [{ message: errorAsOnly('p') }] },
|
|
86
|
+
|
|
87
|
+
// パターン1-2b: as + key属性のみ(key属性は無視してasタグに変換)
|
|
88
|
+
{ code: `<Text as="p" key="item-1">content</Text>`, output: `<p key="item-1">content</p>`, errors: [{ message: errorAsOnly('p') }] },
|
|
85
89
|
|
|
86
90
|
// パターン1-3: classNameのみ(変換不可能なクラスのみ)
|
|
87
91
|
{ code: `<Text className="custom">content</Text>`, output: `<span className="custom">content</span>`, errors: [{ message: errorUnnecessaryClassName('custom') }] },
|
|
88
|
-
|
|
89
|
-
|
|
92
|
+
|
|
93
|
+
// パターン1-3b: className + key属性(key属性は保持される)
|
|
94
|
+
{ code: `<Text className="custom" key="item-1">content</Text>`, output: `<span className="custom" key="item-1">content</span>`, errors: [{ message: errorUnnecessaryClassName('custom') }] },
|
|
90
95
|
|
|
91
96
|
// パターン1-4: className + as(変換不可能なクラスのみ)
|
|
92
97
|
{ code: `<Text as="p" className="custom">content</Text>`, output: `<p className="custom">content</p>`, errors: [{ message: errorUnnecessaryAsClassName('p') }] },
|
|
@@ -97,12 +102,12 @@ ruleTester.run('best-practice-for-text-component', rule, {
|
|
|
97
102
|
{ code: `<Text className="shr-text-lg shr-font-bold shr-text-grey">text</Text>`, output: `<Text size="L" weight="bold" color="TEXT_GREY">text</Text>`, errors: [{ message: errorConvertibleShr('size="L" weight="bold" color="TEXT_GREY"', 'shr-text-lg, shr-font-bold, shr-text-grey') }] },
|
|
98
103
|
|
|
99
104
|
// パターン2-2: className + as(すべて変換可能)
|
|
100
|
-
{ code: `<Text as="p" className="shr-text-sm">text</Text>`, output: `<Text as="p" size="S">text</Text>`, errors: [{ message: errorConvertibleShr('size="S"', 'shr-text-sm'
|
|
101
|
-
{ code: `<Text as="p" className="shr-text-sm shr-font-bold">text</Text>`, output: `<Text as="p" size="S" weight="bold">text</Text>`, errors: [{ message: errorConvertibleShr('size="S" weight="bold"', 'shr-text-sm, shr-font-bold'
|
|
105
|
+
{ code: `<Text as="p" className="shr-text-sm">text</Text>`, output: `<Text as="p" size="S">text</Text>`, errors: [{ message: errorConvertibleShr('size="S"', 'shr-text-sm') }] },
|
|
106
|
+
{ code: `<Text as="p" className="shr-text-sm shr-font-bold">text</Text>`, output: `<Text as="p" size="S" weight="bold">text</Text>`, errors: [{ message: errorConvertibleShr('size="S" weight="bold"', 'shr-text-sm, shr-font-bold') }] },
|
|
102
107
|
|
|
103
108
|
// パターン2-3: 一部のみ変換可能
|
|
104
109
|
{ code: `<Text className="shr-text-sm custom-class">text</Text>`, output: `<Text size="S" className="custom-class">text</Text>`, errors: [{ message: errorConvertibleShr('size="S" className="custom-class"', 'shr-text-sm') }] },
|
|
105
|
-
{ code: `<Text as="p" className="shr-text-sm custom-class">text</Text>`, output: `<Text as="p" size="S" className="custom-class">text</Text>`, errors: [{ message: errorConvertibleShr('size="S" className="custom-class"', 'shr-text-sm'
|
|
110
|
+
{ code: `<Text as="p" className="shr-text-sm custom-class">text</Text>`, output: `<Text as="p" size="S" className="custom-class">text</Text>`, errors: [{ message: errorConvertibleShr('size="S" className="custom-class"', 'shr-text-sm') }] },
|
|
106
111
|
{ code: `<Text className="shr-text-lg shr-font-bold custom-one custom-two">text</Text>`, output: `<Text size="L" weight="bold" className="custom-one custom-two">text</Text>`, errors: [{ message: errorConvertibleShr('size="L" weight="bold" className="custom-one custom-two"', 'shr-text-lg, shr-font-bold') }] },
|
|
107
112
|
|
|
108
113
|
// パターン2-4: shr-プレフィックスがあるが変換不可能なクラスのみ(spanに変換)
|
|
@@ -116,19 +121,15 @@ ruleTester.run('best-practice-for-text-component', rule, {
|
|
|
116
121
|
{ code: `<Text size="L" className="shr-text-sm shr-font-bold">text</Text>`, errors: [{ message: errorConflictingProps('shr-text-sm, shr-font-bold') }] },
|
|
117
122
|
{ code: `<Text size="M" className="shr-text-sm custom-class">text</Text>`, errors: [{ message: errorConflictingProps('shr-text-sm') }] },
|
|
118
123
|
|
|
119
|
-
// 追加テスト: 各クラスの網羅性確認
|
|
120
|
-
{ code: `<Text className="shr-text-2xs">text</Text>`, output: `<Text size="XXS">text</Text>`, errors: [{ message: errorConvertibleShr('size="XXS"', 'shr-text-2xs') }] },
|
|
121
|
-
{ code: `<Text className="shr-text-2xl">text</Text>`, output: `<Text size="XXL">text</Text>`, errors: [{ message: errorConvertibleShr('size="XXL"', 'shr-text-2xl') }] },
|
|
122
|
-
{ code: `<Text className="shr-leading-none">text</Text>`, output: `<Text leading="NONE">text</Text>`, errors: [{ message: errorConvertibleShr('leading="NONE"', 'shr-leading-none') }] },
|
|
123
|
-
{ code: `<Text className="shr-text-white">text</Text>`, output: `<Text color="TEXT_WHITE">text</Text>`, errors: [{ message: errorConvertibleShr('color="TEXT_WHITE"', 'shr-text-white') }] },
|
|
124
|
-
{ code: `<Text className="shr-text-color-inherit">text</Text>`, output: `<Text color="inherit">text</Text>`, errors: [{ message: errorConvertibleShr('color="inherit"', 'shr-text-color-inherit') }] },
|
|
125
|
-
|
|
126
124
|
// 追加テスト: 未知の属性が保持されることを確認
|
|
127
125
|
{ code: `<Text className="custom" id="foo">text</Text>`, output: `<span className="custom" id="foo">text</span>`, errors: [{ message: errorUnnecessaryClassName('custom') }] },
|
|
128
|
-
{ code: `<Text as="p"
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
{ code: `<Text
|
|
132
|
-
|
|
126
|
+
{ code: `<Text as="p" id="foo" className="shr-text-sm">text</Text>`, output: `<Text as="p" size="S" id="foo">text</Text>`, errors: [{ message: errorConvertibleShr('size="S"', 'shr-text-sm') }] },
|
|
127
|
+
|
|
128
|
+
// key属性対応テスト(追加のエッジケース)
|
|
129
|
+
{ code: `<Text key="item-1" className="shr-text-sm">text</Text>`, output: `<Text size="S" key="item-1">text</Text>`, errors: [{ message: errorConvertibleShr('size="S"', 'shr-text-sm') }] },
|
|
130
|
+
|
|
131
|
+
// spread attributes + 変換可能なclassName(fixなし、警告のみ)
|
|
132
|
+
{ code: `<Text {...props} className="shr-text-sm">text</Text>`, errors: [{ message: errorConvertibleShrWithSpread('shr-text-sm') }] },
|
|
133
|
+
{ code: `<Text {...props} className="shr-text-sm shr-font-bold">text</Text>`, errors: [{ message: errorConvertibleShrWithSpread('shr-text-sm, shr-font-bold') }] },
|
|
133
134
|
]
|
|
134
135
|
})
|