eslint-plugin-smarthr 6.9.0 → 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,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.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
+
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)
18
+
19
+
20
+ ### Bug Fixes
21
+
22
+ * **best-practice-for-text-component:** 変換不可能なshr-クラスのみの場合に不要なスペースが挿入される問題を修正 ([#1199](https://github.com/kufu/tamatebako/issues/1199)) ([ad942fc](https://github.com/kufu/tamatebako/commit/ad942fcfa2484013fe82fd1c9d9b94c097a0e27a))
23
+
5
24
  ## [6.9.0](https://github.com/kufu/tamatebako/compare/eslint-plugin-smarthr-v6.8.0...eslint-plugin-smarthr-v6.9.0) (2026-04-05)
6
25
 
7
26
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-smarthr",
3
- "version": "6.9.0",
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": "41bc1d6941b48121d18250aaecb83812f87955e1"
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,179 +147,137 @@ 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)
153
+ const openingElement = classNameAttrNode.parent
154
+ const sourceCode = context.sourceCode || context.getSourceCode()
180
155
 
181
156
  context.report({
182
- node: classNameAttrNode.parent,
157
+ node: openingElement,
183
158
  message: `classNameで指定されたshr-プレフィックスのクラスは、Textコンポーネントの属性に置き換えてください。
184
159
  - 詳細: https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/best-practice-for-text-component
185
160
  - 推奨: <Text ${propSuggestions}${nonConvertible ? ` className="${nonConvertible}"` : ''}>
186
161
  - 変換可能なクラス: ${convertible}
187
162
  - shr-プレフィックスのクラスをTextの属性に置き換えることで、型安全性が向上し、意図がより明確になります`,
188
163
  fix(fixer) {
189
- const openingElement = classNameAttrNode.parent
190
- const sourceCode = context.sourceCode || context.getSourceCode()
191
164
  const fixes = []
192
-
193
165
  if (nonConvertible) {
194
- // classNameの値を更新(属性自体は残す)
195
166
  fixes.push(fixer.replaceText(classNameAttrNode.value, `"${nonConvertible}"`))
196
167
  } else {
197
- // className属性全体を削除(shr-クラスのみの場合)
198
168
  const tokenBefore = sourceCode.getTokenBefore(classNameAttrNode)
199
- const rangeStart = tokenBefore.range[1]
200
- const rangeEnd = classNameAttrNode.range[1]
201
- 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}`))
202
176
  }
203
-
204
- // 新しいpropsを追加
205
- fixes.push(fixer.insertTextAfter(openingElement.name, ` ${propSuggestions}`))
206
-
207
177
  return fixes
208
178
  },
209
179
  })
210
180
  },
211
181
 
212
- // パターン1-3: classNameのみ(asなし)、Text属性なし、shr-クラスなし
213
- [SELECTOR_UNNECESSARY_TEXT_CLASSNAME]: (classNameAttrNode) => {
214
- 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
215
186
 
216
187
  context.report({
217
- node: classNameAttrNode.parent,
218
- message: `Textコンポーネントの機能を使用していないため、ネイティブHTML要素(<span>)に置き換えてください。
188
+ node: openingElement,
189
+ message: `classNameで指定されたshr-プレフィックスのクラスは、Textコンポーネントの属性に置き換えてください。
219
190
  - 詳細: https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/best-practice-for-text-component
220
- - 推奨: <span ${classNameText}>
221
- - Textコンポーネントの機能(weight、size、color等)を使用しない場合は、直接HTML要素を使用することでシンプルになります`,
222
- fix(fixer) {
223
- const openingElement = classNameAttrNode.parent
224
- const jsxElement = openingElement.parent
191
+ - 変換可能なクラス: ${convertible}
192
+ - spread attributes ({...props}) があるため自動修正できません。手動で修正してください
193
+ - shr-プレフィックスのクラスをTextの属性に置き換えることで、型安全性が向上し、意図がより明確になります`,
194
+ })
195
+ },
225
196
 
226
- return [
227
- fixer.replaceText(openingElement.name, 'span'),
228
- fixer.replaceText(jsxElement.closingElement.name, 'span')
229
- ]
230
- },
197
+ // Stage 2: 属性なし
198
+ [SELECTOR_UNNECESSARY_TEXT_NO_ATTRS]: (node) => {
199
+ context.report({
200
+ node,
201
+ message: `属性を持たないTextコンポーネントは、<span>に置き換えるか、要素を削除してテキストのみにすることを検討してください。
202
+ - 詳細: https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/best-practice-for-text-component
203
+ - Textコンポーネントに属性がない場合、直接HTML要素を使用するか、不要な要素を削除することでシンプルになります
204
+ - weight、size、color等の属性がある場合は、Textコンポーネントのまま利用してください`,
231
205
  })
232
206
  },
233
207
 
234
- // パターン2-2, 2-3: className + as(文字列リテラル)、Text属性なし、shr-クラスあり
235
- [SELECTOR_CONVERTIBLE_SHR_TO_PROPS_WITH_AS]: (classNameAttrNode) => {
236
- const { nonConvertible, propSuggestions, convertible } = categorizeClassNames(classNameAttrNode)
237
- const openingElement = classNameAttrNode.parent
238
- const asValue = getAttributeLiteralValue(openingElement, 'as')
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()
239
213
 
240
214
  context.report({
241
- node: openingElement,
242
- message: `classNameで指定されたshr-プレフィックスのクラスは、Textコンポーネントの属性に置き換えてください。
215
+ node,
216
+ message: asValue
217
+ ? `as属性のみを持つTextコンポーネントは、ネイティブHTML要素(<${tagName}>)に置き換えてください。
243
218
  - 詳細: https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/best-practice-for-text-component
244
- - 推奨: <Text${asValue ? ` as="${asValue}"` : ''} ${propSuggestions}${nonConvertible ? ` className="${nonConvertible}"` : ''}>
245
- - 変換可能なクラス: ${convertible}
246
- - 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要素を使用することでシンプルになります`,
247
225
  fix(fixer) {
248
- const sourceCode = context.sourceCode || context.getSourceCode()
226
+ const jsxElement = node.parent
249
227
  const fixes = []
250
-
251
- if (nonConvertible) {
252
- // classNameの値を更新(属性自体は残す)
253
- fixes.push(fixer.replaceText(classNameAttrNode.value, `"${nonConvertible}"`))
254
- } else {
255
- // className属性全体を削除(shr-クラスのみの場合)
256
- const tokenBefore = sourceCode.getTokenBefore(classNameAttrNode)
257
- const rangeStart = tokenBefore.range[1]
258
- const rangeEnd = classNameAttrNode.range[1]
259
- fixes.push(fixer.removeRange([rangeStart, rangeEnd]))
260
- }
261
-
262
- // 新しいpropsを追加(as属性がある場合はその後ろに挿入)
263
- const asAttrNode = getAttributeNode(openingElement, 'as')
264
- if (asAttrNode) {
265
- fixes.push(fixer.insertTextAfter(asAttrNode, ` ${propSuggestions}`))
266
- } else {
267
- 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]]))
268
233
  }
269
-
234
+ fixes.push(fixer.replaceText(node.name, tagName))
235
+ fixes.push(fixer.replaceText(jsxElement.closingElement.name, tagName))
270
236
  return fixes
271
237
  },
272
238
  })
273
239
  },
274
240
 
275
- // パターン1-4: className + as(文字列リテラル)、Text属性なし、shr-クラスなし
276
- [SELECTOR_UNNECESSARY_TEXT_AS_CLASSNAME]: (asAttrNode) => {
277
- const tagName = asAttrNode.value.value
278
- const openingElement = asAttrNode.parent
279
- const classNameValue = getAttributeLiteralValue(openingElement, 'className')
280
- 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}"` : ''
281
250
 
282
251
  context.report({
283
- node: openingElement,
284
- message: `Textコンポーネントの機能を使用していないため、ネイティブHTML要素に置き換えてください。
252
+ node,
253
+ message: asValue
254
+ ? `Textコンポーネントの機能を使用していないため、ネイティブHTML要素に置き換えてください。
285
255
  - 詳細: https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/best-practice-for-text-component
286
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}>
287
261
  - Textコンポーネントの機能(weight、size、color等)を使用しない場合は、直接HTML要素を使用することでシンプルになります`,
288
262
  fix(fixer) {
289
- const jsxElement = openingElement.parent
290
- const sourceCode = context.sourceCode || context.getSourceCode()
291
-
292
- // 属性とその前のスペースを含めて削除
293
- const tokenBefore = sourceCode.getTokenBefore(asAttrNode)
294
- const rangeStart = tokenBefore.range[1]
295
- const rangeEnd = asAttrNode.range[1]
296
-
297
- return [
298
- fixer.removeRange([rangeStart, rangeEnd]),
299
- fixer.replaceText(openingElement.name, tagName),
300
- fixer.replaceText(jsxElement.closingElement.name, tagName)
301
- ]
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
302
274
  },
303
275
  })
304
276
  },
305
277
 
306
- // パターン3: Text属性あり、classNameにshr-クラスあり(矛盾)
278
+ // 矛盾検出
307
279
  [SELECTOR_CONFLICTING_PROPS_SHR]: (classNameAttrNode) => {
308
280
  const convertible = getConvertible(classNameAttrNode)
309
-
310
281
  if (convertible) {
311
282
  context.report({
312
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,33 +102,34 @@ 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
 
113
+ // パターン2-4: shr-プレフィックスがあるが変換不可能なクラスのみ(spanに変換)
114
+ { code: `<Text className="shr-w-[10rem]">text</Text>`, output: `<span className="shr-w-[10rem]">text</span>`, errors: [{ message: errorUnnecessaryClassName('shr-w-[10rem]') }] },
115
+ { code: `<Text className="shr-inline-block shr-mr-0.5">text</Text>`, output: `<span className="shr-inline-block shr-mr-0.5">text</span>`, errors: [{ message: errorUnnecessaryClassName('shr-inline-block shr-mr-0.5') }] },
116
+ { code: `<Text as="p" className="shr-bg-background shr-block">text</Text>`, output: `<p className="shr-bg-background shr-block">text</p>`, errors: [{ message: errorUnnecessaryAsClassName('p') }] },
117
+
108
118
  // パターン3: 属性とclassNameの矛盾
109
119
  { code: `<Text size="M" className="shr-text-sm">text</Text>`, errors: [{ message: errorConflictingProps('shr-text-sm') }] },
110
120
  { code: `<Text weight="bold" className="shr-font-normal">text</Text>`, errors: [{ message: errorConflictingProps('shr-font-normal') }] },
111
121
  { code: `<Text size="L" className="shr-text-sm shr-font-bold">text</Text>`, errors: [{ message: errorConflictingProps('shr-text-sm, shr-font-bold') }] },
112
122
  { code: `<Text size="M" className="shr-text-sm custom-class">text</Text>`, errors: [{ message: errorConflictingProps('shr-text-sm') }] },
113
123
 
114
- // 追加テスト: 各クラスの網羅性確認
115
- { code: `<Text className="shr-text-2xs">text</Text>`, output: `<Text size="XXS">text</Text>`, errors: [{ message: errorConvertibleShr('size="XXS"', 'shr-text-2xs') }] },
116
- { code: `<Text className="shr-text-2xl">text</Text>`, output: `<Text size="XXL">text</Text>`, errors: [{ message: errorConvertibleShr('size="XXL"', 'shr-text-2xl') }] },
117
- { code: `<Text className="shr-leading-none">text</Text>`, output: `<Text leading="NONE">text</Text>`, errors: [{ message: errorConvertibleShr('leading="NONE"', 'shr-leading-none') }] },
118
- { code: `<Text className="shr-text-white">text</Text>`, output: `<Text color="TEXT_WHITE">text</Text>`, errors: [{ message: errorConvertibleShr('color="TEXT_WHITE"', 'shr-text-white') }] },
119
- { code: `<Text className="shr-text-color-inherit">text</Text>`, output: `<Text color="inherit">text</Text>`, errors: [{ message: errorConvertibleShr('color="inherit"', 'shr-text-color-inherit') }] },
120
-
121
124
  // 追加テスト: 未知の属性が保持されることを確認
122
125
  { code: `<Text className="custom" id="foo">text</Text>`, output: `<span className="custom" id="foo">text</span>`, errors: [{ message: errorUnnecessaryClassName('custom') }] },
123
- { code: `<Text as="p" className="custom" id="foo">text</Text>`, output: `<p className="custom" id="foo">text</p>`, errors: [{ message: errorUnnecessaryAsClassName('p') }] },
124
- { 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') }] },
125
- { 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') }] },
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', 'p') }] },
127
- { 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') }] },
128
134
  ]
129
135
  })