eslint-plugin-smarthr 6.9.1 → 6.10.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,18 @@
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.0](https://github.com/kufu/tamatebako/compare/eslint-plugin-smarthr-v6.9.1...eslint-plugin-smarthr-v6.10.0) (2026-04-08)
6
+
7
+
8
+ ### Features
9
+
10
+ * **a11y-heading-in-sectioning-content:** Scrollerコンポーネントを許可リストに追加 ([#1204](https://github.com/kufu/tamatebako/issues/1204)) ([7131d5c](https://github.com/kufu/tamatebako/commit/7131d5c2b6feb55ac44f952250f58db962153399))
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * **best-practice-for-text-component:** key属性対応とコードの最適化 ([#1205](https://github.com/kufu/tamatebako/issues/1205)) ([1c9821c](https://github.com/kufu/tamatebako/commit/1c9821c1d2c4f5d8b09f2a1a1f7e18310aecc31e))
16
+
5
17
  ## [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
18
 
7
19
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-smarthr",
3
- "version": "6.9.1",
3
+ "version": "6.10.0",
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": "5f958652e81f7a69a347489108ff88cb1b3e56dd"
40
+ "gitHead": "9f682d10a15702c061e23d7c0922e0d3f69149d5"
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
- // ESLintセレクタの基本要素
36
- const ATTR_AS = 'JSXAttribute[name.name="as"]'
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 NOT_HAS_AS = `:not(:has(${ATTR_AS}))`
45
- const HAS_TEXT_PROPS = `:has(${ATTR_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 NOT_HAS_SHR_CLASS = `:not(${HAS_SHR_CLASS})`
49
- const CHILD_AS_LITERAL = `> ${ATTR_AS}${LITERAL_TYPE}`
50
-
51
- // 完全なESLintセレクタ(事前計算)
52
- const SELECTOR_UNNECESSARY_TEXT_NO_ATTRS = `${TEXT_OPENING}:not(:has(JSXAttribute))`
53
- const SELECTOR_UNNECESSARY_TEXT_ONLY_AS = `${TEXT_OPENING}[attributes.length=1] ${CHILD_AS_LITERAL}`
54
- const SELECTOR_CONVERTIBLE_SHR_TO_PROPS = `${TEXT_OPENING}${NOT_HAS_AS}${NOT_HAS_TEXT_PROPS} ${CHILD_CLASSNAME_LITERAL}${HAS_SHR_CLASS}`
55
- const SELECTOR_UNNECESSARY_TEXT_CLASSNAME = `${TEXT_OPENING}${NOT_HAS_AS}${NOT_HAS_TEXT_PROPS} ${CHILD_CLASSNAME_LITERAL}${NOT_HAS_SHR_CLASS}`
56
- const SELECTOR_CONVERTIBLE_SHR_TO_PROPS_WITH_AS = `${TEXT_OPENING}:has(${ATTR_AS}${LITERAL_TYPE})${NOT_HAS_TEXT_PROPS} ${CHILD_CLASSNAME_LITERAL}${HAS_SHR_CLASS}`
57
- const SELECTOR_UNNECESSARY_TEXT_AS_CLASSNAME = `${TEXT_OPENING}:has(${ATTR_CLASSNAME}${LITERAL_TYPE}${NOT_HAS_SHR_CLASS})${NOT_HAS_TEXT_PROPS} ${CHILD_AS_LITERAL}`
58
- const SELECTOR_CONFLICTING_PROPS_SHR = `${TEXT_OPENING}${HAS_TEXT_PROPS} ${CHILD_CLASSNAME_LITERAL}`
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属性の変換可能なクラス名のみを取得(パターン3専用: 矛盾チェック)
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属性のクラス名を解析し、1回のループで全ての情報を生成(パターン2-1, 2-2, 2-3専用)
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
- // パターン1-1: 属性なし
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 jsxElement = openingElement.parent
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
- const rangeStart = tokenBefore.range[1]
221
- const rangeEnd = classNameAttrNode.range[1]
222
- fixes.push(fixer.removeRange([rangeStart, rangeEnd]))
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
- // パターン1-3: classNameのみ(asなし)、Text属性なし、shr-クラスなし
234
- [SELECTOR_UNNECESSARY_TEXT_CLASSNAME]: (classNameAttrNode) => {
235
- const classNameText = `className="${classNameAttrNode.value.value}"`
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: classNameAttrNode.parent,
239
- message: `Textコンポーネントの機能を使用していないため、ネイティブHTML要素(<span>)に置き換えてください。
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
- - 推奨: <span ${classNameText}>
242
- - Textコンポーネントの機能(weight、size、color等)を使用しない場合は、直接HTML要素を使用することでシンプルになります`,
243
- fix(fixer) {
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
- // パターン2-2, 2-3: className + as(文字列リテラル)、Text属性なし、shr-クラスあり
256
- [SELECTOR_CONVERTIBLE_SHR_TO_PROPS_WITH_AS]: (classNameAttrNode) => {
257
- const { nonConvertible, propSuggestions, convertible } = categorizeClassNames(classNameAttrNode)
258
- const openingElement = classNameAttrNode.parent
259
- const jsxElement = openingElement.parent
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
- - <${asValue}>要素にclassNameを移動してください
271
- - Textコンポーネントの機能(weight、size、color等)を使用しない場合は、直接HTML要素を使用することでシンプルになります`,
272
- fix(fixer) {
273
- const sourceCode = context.sourceCode || context.getSourceCode()
274
- const asAttrNode = getAttributeNode(openingElement, 'as')
203
+ - Textコンポーネントに属性がない場合、直接HTML要素を使用するか、不要な要素を削除することでシンプルになります
204
+ - weight、size、color等の属性がある場合は、Textコンポーネントのまま利用してください`,
205
+ })
206
+ },
275
207
 
276
- // 属性とその前のスペースを含めて削除
277
- const tokenBefore = sourceCode.getTokenBefore(asAttrNode)
278
- const rangeStart = tokenBefore.range[1]
279
- const rangeEnd = asAttrNode.range[1]
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: openingElement,
294
- message: `classNameで指定されたshr-プレフィックスのクラスは、Textコンポーネントの属性に置き換えてください。
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
- - 推奨: <Text${asValue ? ` as="${asValue}"` : ''} ${propSuggestions}${nonConvertible ? ` className="${nonConvertible}"` : ''}>
297
- - 変換可能なクラス: ${convertible}
298
- - shr-プレフィックスのクラスをTextの属性に置き換えることで、型安全性が向上し、意図がより明確になります`,
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 sourceCode = context.sourceCode || context.getSourceCode()
226
+ const jsxElement = node.parent
301
227
  const fixes = []
302
-
303
- if (nonConvertible) {
304
- // classNameの値を更新(属性自体は残す)
305
- fixes.push(fixer.replaceText(classNameAttrNode.value, `"${nonConvertible}"`))
306
- } else {
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
- // パターン1-4: className + as(文字列リテラル)、Text属性なし、shr-クラスなし
328
- [SELECTOR_UNNECESSARY_TEXT_AS_CLASSNAME]: (asAttrNode) => {
329
- const tagName = asAttrNode.value.value
330
- const openingElement = asAttrNode.parent
331
- const classNameValue = getAttributeLiteralValue(openingElement, 'className')
332
- const classNameText = `className="${classNameValue}"`
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: openingElement,
336
- message: `Textコンポーネントの機能を使用していないため、ネイティブHTML要素に置き換えてください。
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 = openingElement.parent
342
- const sourceCode = context.sourceCode || context.getSourceCode()
343
-
344
- // 属性とその前のスペースを含めて削除
345
- const tokenBefore = sourceCode.getTokenBefore(asAttrNode)
346
- const rangeStart = tokenBefore.range[1]
347
- const rangeEnd = asAttrNode.range[1]
348
-
349
- return [
350
- fixer.removeRange([rangeStart, rangeEnd]),
351
- fixer.replaceText(openingElement.name, tagName),
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
- // パターン3: Text属性あり、classNameにshr-クラスあり(矛盾)
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,
@@ -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, as = null) => `classNameで指定されたshr-プレフィックスのクラスは、Textコンポーネントの属性に置き換えてください。
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${as ? ` as="${as}"` : ''} ${suggestion}>
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
- { code: `<Text as="div">content</Text>`, output: `<div>content</div>`, errors: [{ message: errorAsOnly('div') }] },
80
- { code: `<Text as="p">
81
- nested content
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
- { code: `<Text className="custom-class another-class">text</Text>`, output: `<span className="custom-class another-class">text</span>`, errors: [{ message: errorUnnecessaryClassName('custom-class another-class') }] },
89
- { code: `<Text className="custom"><span>nested</span></Text>`, output: `<span className="custom"><span>nested</span></span>`, errors: [{ message: errorUnnecessaryClassName('custom') }] },
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', 'p') }] },
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', 'p') }] },
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', 'p') }] },
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" className="custom" id="foo">text</Text>`, output: `<p className="custom" id="foo">text</p>`, errors: [{ message: errorUnnecessaryAsClassName('p') }] },
129
- { code: `<Text id="foo" className="shr-text-sm">text</Text>`, output: `<Text size="S" id="foo">text</Text>`, errors: [{ message: errorConvertibleShr('size="S"', 'shr-text-sm') }] },
130
- { code: `<Text id="foo" className="shr-text-sm custom">text</Text>`, output: `<Text size="S" id="foo" className="custom">text</Text>`, errors: [{ message: errorConvertibleShr('size="S" className="custom"', 'shr-text-sm') }] },
131
- { 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', 'p') }] },
132
- { code: `<Text as="p" id="foo" onClick={handler} className="shr-text-sm custom">text</Text>`, output: `<Text as="p" size="S" id="foo" onClick={handler} className="custom">text</Text>`, errors: [{ message: errorConvertibleShr('size="S" className="custom"', 'shr-text-sm', 'p') }] },
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
  })