eslint-plugin-smarthr 6.8.0 → 6.9.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 CHANGED
@@ -2,6 +2,20 @@
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.9.1](https://github.com/kufu/tamatebako/compare/eslint-plugin-smarthr-v6.9.0...eslint-plugin-smarthr-v6.9.1) (2026-04-07)
6
+
7
+
8
+ ### Bug Fixes
9
+
10
+ * **best-practice-for-text-component:** 変換不可能なshr-クラスのみの場合に不要なスペースが挿入される問題を修正 ([#1199](https://github.com/kufu/tamatebako/issues/1199)) ([ad942fc](https://github.com/kufu/tamatebako/commit/ad942fcfa2484013fe82fd1c9d9b94c097a0e27a))
11
+
12
+ ## [6.9.0](https://github.com/kufu/tamatebako/compare/eslint-plugin-smarthr-v6.8.0...eslint-plugin-smarthr-v6.9.0) (2026-04-05)
13
+
14
+
15
+ ### Features
16
+
17
+ * **autofixer-smarthr-ui-migration:** smarthr-uiのalias対応を追加 ([#1191](https://github.com/kufu/tamatebako/issues/1191)) ([4a23056](https://github.com/kufu/tamatebako/commit/4a23056695a2955885ca8c1134b953f40f4588d4))
18
+
5
19
  ## [6.8.0](https://github.com/kufu/tamatebako/compare/eslint-plugin-smarthr-v6.7.0...eslint-plugin-smarthr-v6.8.0) (2026-04-03)
6
20
 
7
21
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-smarthr",
3
- "version": "6.8.0",
3
+ "version": "6.9.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": "37f60f6de393d28ec95cca0d84df567fb417d424"
40
+ "gitHead": "5f958652e81f7a69a347489108ff88cb1b3e56dd"
41
41
  }
@@ -61,6 +61,7 @@ autofixer-smarthr-ui-migrationルールに新しいバージョン(v[XX]→v[Y
61
61
  - rules/autofixer-smarthr-ui-migration/versions/v90-to-v91/README.md(ユーザー向け移行ガイド)
62
62
  - rules/autofixer-smarthr-ui-migration/versions/v90-to-v91/test.js(テストケース)
63
63
  - test/autofixer-smarthr-ui-migration.js(メインテスト)
64
+ - libs/common.js(rootPathの取得、tsconfig.jsonのpaths設定読み込み)
64
65
 
65
66
  ## 対応する変更
66
67
 
@@ -71,26 +72,54 @@ smarthr-ui v[YY]のリリースノート: [GitHubリリースページのURL]
71
72
  1. [変更内容1の説明]
72
73
  - 例: ComponentA が ComponentB にリネーム(破壊的変更)
73
74
  - 自動修正: [可能/不可能/条件付き]
75
+ - セレクター: `ImportDeclaration`, `JSXOpeningElement[name.name="ComponentA"]`
74
76
 
75
77
  2. [変更内容2の説明]
76
78
  - 例: propsXがpropsYにリネーム(破壊的変更)
77
79
  - 自動修正: [可能/不可能/条件付き]
80
+ - セレクター: `JSXAttribute[name.name="propsX"]`
78
81
 
79
82
  3. [変更内容3の説明]
80
83
  - 例: 非推奨パターンから推奨パターンへの置き換え(非破壊的)
81
84
  - 自動修正: [可能/不可能/条件付き]
85
+ - セレクター: [該当するASTセレクター]
82
86
 
83
87
  4. ...
84
88
 
89
+ **自動修正の判断基準:**
90
+ - ✅ 自動修正可能: 機械的に100%正しく変換できる場合
91
+ - ⚠️ エラーのみ: 手動確認が必要な場合(未知の属性がある、複数の対処方法がある等)
92
+ - ❌ 検出しない: 複雑すぎる、影響範囲が広すぎる場合
93
+
85
94
  ## 実装内容
86
95
 
87
96
  1. versionディレクトリを作成: `versions/v[XX]-to-v[YY]/`
88
97
 
89
98
  2. `versions/v[XX]-to-v[YY]/index.js` を作成
90
99
  - messages定義
91
- - createCheckers関数の実装
100
+ - createCheckers関数の実装(`createCheckers(context, sourceCode, options = {})`)
92
101
  - 必要に応じてヘルパー関数
93
102
 
103
+ **smarthrUiAliasオプションを利用する場合:**
104
+ ```javascript
105
+ const { rootPath } = require('../../../../libs/common')
106
+
107
+ createCheckers(context, sourceCode, options = {}) {
108
+ const customSmarthrUiAlias = options.smarthrUiAlias
109
+ const validSources = ['smarthr-ui']
110
+ if (customSmarthrUiAlias) {
111
+ validSources.push(customSmarthrUiAlias)
112
+ }
113
+
114
+ const isAliasFile = customSmarthrUiAlias && isFileMatchingSmarthrUiAlias(
115
+ context.getFilename(),
116
+ customSmarthrUiAlias
117
+ )
118
+
119
+ // ... チェッカー実装
120
+ }
121
+ ```
122
+
94
123
  3. `versions/v[XX]-to-v[YY]/README.md` を作成(ユーザー向け移行ガイド)
95
124
  - 各変更の説明
96
125
  - Before/Afterのコード例
@@ -104,6 +133,26 @@ smarthr-ui v[YY]のリリースノート: [GitHubリリースページのURL]
104
133
  - valid: v[YY]形式が正常に通ること
105
134
  - invalid: v[XX]形式が検出されて修正されること
106
135
 
136
+ **テストケースの構造:**
137
+ ```javascript
138
+ const v[XX]Tov[YY]Options = [{ from: '[XX]', to: '[YY]' }]
139
+
140
+ module.exports = {
141
+ valid: [
142
+ { code: `import { NewComponent } from 'smarthr-ui'`, options: v[XX]Tov[YY]Options },
143
+ { code: `<NewComponent>...</NewComponent>`, options: v[XX]Tov[YY]Options },
144
+ ],
145
+ invalid: [
146
+ {
147
+ code: `import { OldComponent } from 'smarthr-ui'`,
148
+ output: `import { NewComponent } from 'smarthr-ui'`,
149
+ options: v[XX]Tov[YY]Options,
150
+ errors: [{ messageId: 'renameComponent', data: { old: 'OldComponent', new: 'NewComponent', to: 'v[YY]' } }],
151
+ },
152
+ ],
153
+ }
154
+ ```
155
+
107
156
  6. `index.js`のVERSION_MODULESに登録
108
157
  ```javascript
109
158
  const v[XX]Tov[YY] = require('./versions/v[XX]-to-v[YY]/index')
@@ -144,6 +193,50 @@ smarthr-ui v[YY]のリリースノート: [GitHubリリースページのURL]
144
193
  - JSDocコメントを適切に追加してください
145
194
  - ディレクトリ名は必ず `vXX-to-vYY` 形式にしてください(内部キーと統一)
146
195
 
196
+ ## 共通機能:smarthrUiAlias オプション
197
+
198
+ プロジェクト固有のsmarthr-ui aliasパスに対応するため、`smarthrUiAlias`オプションが利用可能です。
199
+
200
+ ### createCheckers関数でのオプション利用
201
+
202
+ ```javascript
203
+ createCheckers(context, sourceCode, options = {}) {
204
+ const customSmarthrUiAlias = options.smarthrUiAlias
205
+ const validSources = ['smarthr-ui']
206
+ if (customSmarthrUiAlias) {
207
+ validSources.push(customSmarthrUiAlias)
208
+ }
209
+
210
+ // aliasファイルかどうかの判定
211
+ const isAliasFile = customSmarthrUiAlias && isFileMatchingSmarthrUiAlias(
212
+ context.getFilename(),
213
+ customSmarthrUiAlias
214
+ )
215
+
216
+ // ...
217
+ }
218
+ ```
219
+
220
+ ### 主な用途
221
+
222
+ 1. **importのチェック範囲拡張**: `smarthr-ui`に加えて、aliasパス(例: `@/components/parts/smarthr-ui`)からのimportも置換対象
223
+ 2. **aliasファイル内のexport変数名置換**: `smarthrUiAlias`配下のファイルで、smarthr-uiコンポーネント名と同じexport変数名を自動置換
224
+
225
+ 詳細は[README.md](./README.md#smarthr-ui-の-alias-を使用している場合)を参照。
226
+
227
+ ### 🔄 今後の検討事項:共通化
228
+
229
+ **現状:** 各versionディレクトリ(v90-to-v91など)で個別にsmarthrUiAlias関連の処理を実装しています。
230
+
231
+ **検討中:** 以下の処理を共通化できる可能性があります:
232
+ - `validSources`の拡張ロジック
233
+ - `isFileMatchingSmarthrUiAlias`ヘルパー関数
234
+ - export変数名の置換チェッカー追加ロジック
235
+
236
+ **実装時期:** v92移行ルール追加時に、重複を確認して共通化を検討してください。共通化する場合は、以下のような場所が候補です:
237
+ - `libs/common.js`に共通ヘルパー関数を追加
238
+ - 各versionモジュールで共通の基底関数を提供
239
+
147
240
  ## 完了後の作業
148
241
 
149
242
  実装が完了したら、**必ずこのDEVELOPER.mdを更新**してください:
@@ -163,7 +256,11 @@ smarthr-ui v[YY]のリリースノート: [GitHubリリースページのURL]
163
256
  - [ ] `versions/vXX-to-vYY/` ディレクトリを作成
164
257
  - [ ] `versions/vXX-to-vYY/index.js` を作成
165
258
  - [ ] messages定義が含まれている
166
- - [ ] createCheckers関数が実装されている
259
+ - [ ] createCheckers関数が実装されている(`createCheckers(context, sourceCode, options = {})`)
260
+ - [ ] smarthrUiAliasオプションに対応している場合:
261
+ - [ ] `const { rootPath } = require('../../../../libs/common')` をimport
262
+ - [ ] `validSources`に`customSmarthrUiAlias`を追加
263
+ - [ ] `isAliasFile`でファイル判定を実装
167
264
  - [ ] ヘルパー関数にJSDocコメントがある
168
265
  - [ ] ファイル冒頭に変更サマリーコメントがある
169
266
  - [ ] `versions/vXX-to-vYY/README.md` を作成(ユーザー向け移行ガイド)
@@ -191,6 +288,120 @@ smarthr-ui v[YY]のリリースノート: [GitHubリリースページのURL]
191
288
  - [ ] `npm test -- test/autofixer-smarthr-ui-migration.js` が通過
192
289
  - [ ] オプション `{ from: "[XX]", to: "[YY]" }` で動作確認
193
290
  - [ ] 複数バージョンスキップ(例: `{ from: "90", to: "[YY]" }`)でも動作確認
291
+ - [ ] smarthrUiAliasオプション対応がある場合:
292
+ - [ ] `{ from: "[XX]", to: "[YY]", smarthrUiAlias: "@/components/parts/smarthr-ui" }` で動作確認
293
+ - [ ] aliasファイル内のexport変数名が置換されることを確認
294
+
295
+ ### 実プロダクトでの検証
296
+
297
+ 単体テストが通過したら、実際のプロダクトで動作確認を行います。
298
+
299
+ **手順:**
300
+
301
+ 1. **対象プロダクトの選択**
302
+ - ローカルにある実プロダクトを選択(例: `/works/workflow/michi/frontend`)
303
+
304
+ 2. **ブランチ作成**
305
+ - `staging` ブランチから新しいブランチを作成(`staging` がない場合は `master` または `main`)
306
+ ```bash
307
+ cd /path/to/product
308
+ git checkout staging # または master/main
309
+ git pull
310
+ git checkout -b test/migrator-vXX-to-vYY
311
+ ```
312
+
313
+ 3. **migratorの追加と設定**
314
+ - 開発中の `eslint-plugin-smarthr` を対象プロダクトに追加
315
+ ```bash
316
+ # 相対パスで開発中のパッケージを追加
317
+ pnpm add -D ../../../tamatebako/packages/eslint-plugin-smarthr
318
+ # または npm/yarn の場合
319
+ # npm install -D ../../../tamatebako/packages/eslint-plugin-smarthr
320
+ ```
321
+
322
+ - **Legacy Config形式** (`.eslintrc.js`) の場合:
323
+ ```javascript
324
+ module.exports = {
325
+ extends: ['smarthr'],
326
+ rules: {
327
+ 'smarthr/autofixer-smarthr-ui-migration': [
328
+ 'error',
329
+ { from: 'XX', to: 'YY', smarthrUiAlias: '@/components/parts/smarthr-ui' }
330
+ ]
331
+ }
332
+ }
333
+ ```
334
+
335
+ - **Flat Config形式** (`eslint.config.mjs`) の場合:
336
+ ```javascript
337
+ import smarthr from 'eslint-config-smarthr'
338
+ import smarthrPlugin from 'eslint-plugin-smarthr' // 追加
339
+
340
+ export default [
341
+ ...smarthr,
342
+ {
343
+ plugins: {
344
+ 'smarthr-local': smarthrPlugin, // 別名で登録
345
+ },
346
+ rules: {
347
+ 'smarthr-local/autofixer-smarthr-ui-migration': [
348
+ 'error',
349
+ { from: 'XX', to: 'YY', smarthrUiAlias: '@/components/parts/smarthr-ui' }
350
+ ],
351
+ },
352
+ },
353
+ ]
354
+ ```
355
+ **注意:** Flat Configでは、すでに `eslint-config-smarthr` 経由で `smarthr` プラグインが登録されているため、開発中のバージョンを使用するには別名(例: `smarthr-local`)で登録する必要があります。
356
+
357
+ 4. **初回実行**
358
+ ```bash
359
+ npm run lint:fix # または eslint --fix .
360
+ ```
361
+
362
+ 5. **問題の修正と再実行**
363
+ - 実行時に出た問題(エラー、不正な変換など)を確認
364
+ - 問題があれば migrator の実装を修正
365
+ - **重要:** 再実行前に、staging が更新されていない状態(migration前の状態)に戻す
366
+ ```bash
367
+ git reset --hard HEAD # 変更を破棄
368
+ # migratorを修正後、再度実行
369
+ npm run lint:fix
370
+ ```
371
+ - 問題が解決するまで手順5を繰り返す
372
+
373
+ 6. **PR作成**
374
+ - 問題が修正されたことを確認できたら、差分を確認しやすいよう draft で PR 作成
375
+ ```bash
376
+ git add .
377
+ git commit -m "test: vXX to vYY migration test"
378
+ git push -u origin test/migrator-vXX-to-vYY
379
+ gh pr create --draft --title "test: vXX to vYY migration test" --body "migratorの動作確認用PR"
380
+ ```
381
+ - PR の差分をレビューして、期待通りの変換が行われているか確認
382
+
383
+ 7. **クリーンアップ**
384
+ - 検証完了後、対象プロダクトを元の状態に戻す
385
+ ```bash
386
+ # PRはクローズ(マージしない)
387
+ gh pr close test/migrator-vXX-to-vYY
388
+
389
+ # ブランチを削除
390
+ git checkout staging # または master/main
391
+ git branch -D test/migrator-vXX-to-vYY
392
+ git push origin --delete test/migrator-vXX-to-vYY
393
+
394
+ # package.jsonとlockファイルを元に戻す
395
+ git checkout package.json pnpm-lock.yaml # または package-lock.json/yarn.lock
396
+ pnpm install # 依存関係を再インストール
397
+ ```
398
+
399
+ **確認ポイント:**
400
+ - [ ] エラーが出ずに実行完了する
401
+ - [ ] 意図した変換が正しく行われている
402
+ - [ ] 不要な変更が含まれていない
403
+ - [ ] エッジケースでも正しく動作する
404
+ - [ ] aliasファイル内のexport変数名も正しく置換されている(smarthrUiAliasオプション使用時)
194
405
 
195
406
  ## 参考情報
196
407
 
@@ -31,6 +31,160 @@ smarthr-ui のバージョン間の移行を支援する自動修正ルールで
31
31
  }
32
32
  ```
33
33
 
34
+ ### smarthr-ui の alias を使用している場合
35
+
36
+ プロジェクトで smarthr-ui を独自のパスから re-export している場合(例: `@/components/parts/smarthr-ui`)、`smarthrUiAlias` オプションを指定することで、alias ファイル内のコンポーネント定義も自動修正の対象になります。
37
+
38
+ #### 前提条件
39
+
40
+ このオプションを使用するには、`tsconfig.json` で paths 設定が必要です:
41
+
42
+ ```json
43
+ {
44
+ "compilerOptions": {
45
+ "paths": {
46
+ "@/*": ["src/*"]
47
+ }
48
+ }
49
+ }
50
+ ```
51
+
52
+ #### 使用方法
53
+
54
+ ```javascript
55
+ {
56
+ "rules": {
57
+ "smarthr/autofixer-smarthr-ui-migration": [
58
+ "error",
59
+ {
60
+ "from": "90",
61
+ "to": "91",
62
+ "smarthrUiAlias": "@/components/parts/smarthr-ui"
63
+ }
64
+ ]
65
+ }
66
+ }
67
+ ```
68
+
69
+ #### 動作
70
+
71
+ このオプションを指定すると、以下の3つが置換対象になります:
72
+
73
+ 1. **`smarthr-ui` からの直接 import**(`smarthrUiAlias` 指定に関わらず常に置換)
74
+ ```typescript
75
+ // Before
76
+ import { ActionDialog } from 'smarthr-ui'
77
+ // After
78
+ import { ControlledActionDialog } from 'smarthr-ui'
79
+ ```
80
+
81
+ 2. **alias パスからの import**
82
+ ```typescript
83
+ // Before
84
+ import { ActionDialog } from '@/components/parts/smarthr-ui'
85
+ // After
86
+ import { ControlledActionDialog } from '@/components/parts/smarthr-ui'
87
+ ```
88
+
89
+ 3. **alias ファイル内の export 変数名**(smarthr-ui のコンポーネント名と同じ場合のみ)
90
+ ```typescript
91
+ // @/components/parts/smarthr-ui/ActionDialog.tsx(aliasファイル)
92
+
93
+ // Before(v90 使用時)
94
+ import { ActionDialog as ShrActionDialog } from 'smarthr-ui'
95
+ export const ActionDialog = (props) => {
96
+ return <ShrActionDialog {...props} customProp="value" />
97
+ }
98
+
99
+ // After(v91 移行後)
100
+ import { ControlledActionDialog as ShrActionDialog } from 'smarthr-ui'
101
+ export const ControlledActionDialog = (props) => {
102
+ return <ShrActionDialog {...props} customProp="value" />
103
+ }
104
+ ```
105
+
106
+ #### barrel import 構造への対応
107
+
108
+ `smarthrUiAlias` で指定されたパス配下のすべてのファイルが置換対象になります。
109
+
110
+ **ディレクトリ形式:**
111
+ ```
112
+ @/components/parts/smarthr-ui/
113
+ ├── index.tsx # ✅ 置換対象
114
+ ├── ActionDialog.tsx # ✅ 置換対象
115
+ ├── FormDialog.tsx # ✅ 置換対象
116
+ └── dialogs/
117
+ └── MessageDialog.tsx # ✅ 置換対象(サブディレクトリも含む)
118
+ ```
119
+
120
+ ```typescript
121
+ // index.tsx(barrel export)
122
+ export { ActionDialog } from './ActionDialog'
123
+ export { FormDialog } from './FormDialog'
124
+
125
+ // または、smarthr-uiから直接re-export
126
+ export { ActionDialog } from 'smarthr-ui'
127
+ // → export { ControlledActionDialog } from 'smarthr-ui' に自動置換
128
+
129
+ // ActionDialog.tsx
130
+ import { ActionDialog as ShrActionDialog } from 'smarthr-ui'
131
+ export const ActionDialog = (props) => <ShrActionDialog {...props} />
132
+ // → export const ControlledActionDialog に自動置換
133
+ ```
134
+
135
+ **単一ファイル形式:**
136
+ ```
137
+ @/components/parts/smarthr-ui.tsx # ✅ 置換対象
138
+ ```
139
+
140
+ ```typescript
141
+ // smarthr-ui.tsx
142
+ export const ActionDialog = (props) => <div>{props.children}</div>
143
+ // → export const ControlledActionDialog に自動置換
144
+ ```
145
+
146
+ #### 制限事項
147
+
148
+ - **対象ファイルの範囲:** `smarthrUiAlias` で指定されたパス配下のファイルのみ。他のディレクトリにある同名の export は変更されません
149
+ ```typescript
150
+ // src/components/parts/smarthr-ui/index.tsx → 置換される ✅
151
+ // src/features/custom/ActionDialog.tsx → 置換されない ✅
152
+ ```
153
+
154
+ - **変数名の判定:** smarthr-ui が提供するコンポーネント名と完全一致する export 変数名のみ置換
155
+ ```typescript
156
+ export const ActionDialog = ... // ✅ 置換される
157
+ export const MyActionDialog = ... // ❌ 置換されない
158
+ export const CustomDialog = ... // ❌ 置換されない
159
+ ```
160
+
161
+ - **export 形式:** 現在は `export const` 形式のみサポート
162
+ ```typescript
163
+ export const ActionDialog = ... // ✅ サポート
164
+ export function ActionDialog() // ❌ 未サポート
165
+ export class ActionDialog // ❌ 未サポート
166
+ ```
167
+
168
+ - **ファイル名の変更:** ファイル名が変更対象のコンポーネント名と一致する場合、ファイル名の変更を促すエラーが表示されます(自動修正不可)
169
+ ```
170
+ // エラー例
171
+ smarthr-ui v91 では ActionDialog が ControlledActionDialog にリネームされました。
172
+ 以下の手順で対応してください:
173
+ 1. ファイル名を変更(例: git mv ActionDialog.tsx ControlledActionDialog.tsx)
174
+ 2. このファイルをimportしている箇所を更新(例: from '@/path/ActionDialog' → from '@/path/ControlledActionDialog')
175
+ ```
176
+
177
+ **対応手順:**
178
+ 1. ファイル名を変更: `git mv ActionDialog.tsx ControlledActionDialog.tsx`
179
+ 2. このファイルをimportしている箇所を手動で更新:
180
+ ```typescript
181
+ // Before
182
+ import { FormDialog } from '@/components/parts/smarthr-ui/FormDialog'
183
+
184
+ // After
185
+ import { ControlledFormDialog } from '@/components/parts/smarthr-ui/ControlledFormDialog'
186
+ ```
187
+
34
188
  ## サポートされているバージョン
35
189
 
36
190
  各バージョンの破壊的変更の詳細と対応内容については、リンク先の移行ガイドを参照してください。
@@ -38,6 +38,9 @@ module.exports = {
38
38
  type: 'string',
39
39
  pattern: '^[0-9]+$',
40
40
  },
41
+ smarthrUiAlias: {
42
+ type: 'string',
43
+ },
41
44
  },
42
45
  required: ['from', 'to'],
43
46
  additionalProperties: false,
@@ -90,7 +93,7 @@ module.exports = {
90
93
  // 例: v90→v92 なら [v90→v91のチェッカー] を収集
91
94
  const checkersList = path.map((stepKey) => {
92
95
  const module = VERSION_MODULES[stepKey]
93
- return module.createCheckers(context, sourceCode)
96
+ return module.createCheckers(context, sourceCode, options)
94
97
  })
95
98
 
96
99
  const mergedCheckers = mergeCheckers(checkersList)
@@ -77,11 +77,36 @@ module.exports = {
77
77
 
78
78
  ESLintのセレクターとハンドラーを返す関数です。
79
79
 
80
+ **シグネチャ:**
80
81
  ```javascript
81
- createCheckers(context, sourceCode) {
82
- return {
82
+ createCheckers(context, sourceCode, options = {})
83
+ ```
84
+
85
+ - `context`: ESLintのcontext
86
+ - `sourceCode`: ESLintのsourceCode
87
+ - `options`: ユーザーが指定したオプション(`smarthrUiAlias`など)
88
+
89
+ **基本構造:**
90
+ ```javascript
91
+ createCheckers(context, sourceCode, options = {}) {
92
+ // smarthrUiAliasオプションの取得
93
+ const customSmarthrUiAlias = options.smarthrUiAlias
94
+ const validSources = ['smarthr-ui']
95
+ if (customSmarthrUiAlias) {
96
+ validSources.push(customSmarthrUiAlias)
97
+ }
98
+
99
+ // aliasファイルかどうかの判定
100
+ const isAliasFile = customSmarthrUiAlias && isFileMatchingSmarthrUiAlias(
101
+ context.getFilename(),
102
+ customSmarthrUiAlias
103
+ )
104
+
105
+ const checkers = {
83
106
  // インポート文の処理
84
107
  ImportDeclaration(node) {
108
+ // smarthr-ui + smarthrUiAlias の両方をチェック
109
+ if (!validSources.includes(node.source.value)) return
85
110
  // ...
86
111
  },
87
112
 
@@ -95,6 +120,15 @@ createCheckers(context, sourceCode) {
95
120
  // ...
96
121
  },
97
122
  }
123
+
124
+ // aliasファイルの場合のみ、export変数名の置換を追加
125
+ if (isAliasFile) {
126
+ checkers['ExportNamedDeclaration > VariableDeclaration > VariableDeclarator'] = function(node) {
127
+ // export const ActionDialog = ... を置換
128
+ }
129
+ }
130
+
131
+ return checkers
98
132
  }
99
133
  ```
100
134
 
@@ -156,13 +190,18 @@ function findTargetParent(node, sourceCode) {
156
190
 
157
191
  ### ✅ 自動修正可能
158
192
 
159
- 機械的に100%正しく変換できる場合のみ自動修正を実装します。
193
+ 機械的に100%正しく変換できる場合に自動修正を実装します。ただし、実用上は100%の正確性が保証できない場合でも、以下の条件を満たせば自動修正を実装して構いません:
194
+
195
+ - **実際には使用されていない機能や属性**である可能性が高い
196
+ - 置換することで**利便性が明らかに向上する**
197
+ - 誤った変換が行われても**影響が限定的**である
160
198
 
161
199
  **例:**
162
200
  - コンポーネント名のリネーム(ActionDialog → ControlledActionDialog)
163
201
  - 属性名のリネーム(type → status)
164
202
  - 属性の削除(値が変わらない場合)
165
203
  - **未知の属性を保持したまま移行可能な場合**(Text → span など、移行先が明確な置換)
204
+ - **理論的には100%正しくないが、実用上問題ない場合**(使用頻度が極めて低い機能の置換など)
166
205
 
167
206
  ### ⚠️ エラーのみ(自動修正なし)
168
207
 
@@ -437,6 +476,200 @@ function hasUnknownAttributes(node, ...knownAttrs) {
437
476
  - **属性の移行先が明確** → パターンA(未知の属性も保持して自動修正)
438
477
  - **属性の移行先が不明確** → パターンB(エラーのみ、手動対応)
439
478
 
479
+ ### パターン6: smarthrUiAlias オプションへの対応
480
+
481
+ プロジェクト固有のsmarthr-ui aliasパスに対応するため、`smarthrUiAlias`オプションを利用します。
482
+
483
+ #### validSourcesの拡張
484
+
485
+ `smarthr-ui`に加えて、aliasパスからのimportもチェック対象にします。
486
+
487
+ ```javascript
488
+ createCheckers(context, sourceCode, options = {}) {
489
+ const customSmarthrUiAlias = options.smarthrUiAlias
490
+ const validSources = ['smarthr-ui']
491
+ if (customSmarthrUiAlias) {
492
+ validSources.push(customSmarthrUiAlias)
493
+ }
494
+
495
+ return {
496
+ ImportDeclaration(node) {
497
+ // smarthr-ui または @/components/parts/smarthr-ui からのimport
498
+ if (!validSources.includes(node.source.value)) return
499
+
500
+ // ...置換処理
501
+ }
502
+ }
503
+ }
504
+ ```
505
+
506
+ #### aliasファイル内のexport変数名置換
507
+
508
+ aliasディレクトリ配下のファイルで、smarthr-uiコンポーネント名と同じ変数名をexportしている場合に置換します。
509
+
510
+ ```javascript
511
+ // aliasファイルかどうかの判定
512
+ const isAliasFile = customSmarthrUiAlias && isFileMatchingSmarthrUiAlias(
513
+ context.getFilename(),
514
+ customSmarthrUiAlias
515
+ )
516
+
517
+ const checkers = {
518
+ // ... 通常のチェッカー
519
+ }
520
+
521
+ // aliasファイルの場合のみ、export変数名の置換を追加
522
+ if (isAliasFile) {
523
+ checkers['ExportNamedDeclaration > VariableDeclaration > VariableDeclarator'] = function(node) {
524
+ const variableName = node.id.name
525
+ const newName = DIALOG_COMPONENTS[variableName]
526
+
527
+ if (newName) {
528
+ context.report({
529
+ node: node.id,
530
+ messageId: 'renameDialog',
531
+ data: { old: variableName, new: newName, to: TARGET_VERSION },
532
+ fix(fixer) {
533
+ return fixer.replaceText(node.id, newName)
534
+ },
535
+ })
536
+ }
537
+ }
538
+ }
539
+
540
+ return checkers
541
+ ```
542
+
543
+ #### ファイルパスのマッチング(ヘルパー関数)
544
+
545
+ ```javascript
546
+ const { rootPath } = require('../../../../libs/common')
547
+
548
+ function isFileMatchingSmarthrUiAlias(filename, smarthrUiAlias) {
549
+ // rootPathを使って絶対パスで比較を試みる
550
+ const resolved = smarthrUiAlias.replace(/^@\//, `${rootPath}/`)
551
+ if (filename.includes(resolved)) {
552
+ return true
553
+ }
554
+
555
+ // rootPathでマッチしない場合:
556
+ // パスの一部としてマッチング(テスト環境などで使用)
557
+ const pathPart = smarthrUiAlias.replace(/^@\//, '').replace(/^~\//, '')
558
+
559
+ // 以下のパターンにマッチング:
560
+ // 1. ディレクトリ形式: /components/parts/smarthr-ui/index.tsx
561
+ // 2. 個別ファイル: /components/parts/smarthr-ui/ActionDialog.tsx
562
+ // 3. 単一ファイル形式: /components/parts/smarthr-ui.tsx
563
+ return (
564
+ filename.includes(`/${pathPart}/`) ||
565
+ filename.endsWith(`/${pathPart}`) ||
566
+ filename.includes(`/${pathPart}.`)
567
+ )
568
+ }
569
+ ```
570
+
571
+ **このパターンが適用されるケース:**
572
+ - **barrel import構造**: `@/components/parts/smarthr-ui/index.tsx` + 個別ファイル
573
+ - **個別ファイルのみ**: `@/components/parts/smarthr-ui/ActionDialog.tsx` など
574
+ - **単一ファイル形式**: `@/components/parts/smarthr-ui.tsx` (ディレクトリではなく1つのファイル)
575
+
576
+ **ポイント:**
577
+ - importチェックは`validSources`で拡張
578
+ - export変数名の置換は`isAliasFile`条件付きで追加
579
+ - サブディレクトリも含めてマッチング(`filename.includes(resolved)`)
580
+ - 単一ファイル形式にも対応(`filename.includes(\`/\${pathPart}.\`)`)
581
+
582
+ ### パターン7: aliasファイル名の変更チェック
583
+
584
+ aliasファイルのファイル名が変更対象のコンポーネント名と一致する場合、ファイル名の変更を促すエラーを表示します。
585
+
586
+ ```javascript
587
+ checkers.Program = function(node) {
588
+ if (!isAliasFile) return
589
+
590
+ // ファイル名からコンポーネント名を抽出(拡張子を除く)
591
+ const fileBasename = filename.split('/').pop() || ''
592
+ const componentName = fileBasename.replace(/\.(tsx?|jsx?)$/, '')
593
+
594
+ // Dialog系コンポーネント名と一致するかチェック
595
+ const newName = DIALOG_COMPONENTS[componentName]
596
+ if (newName) {
597
+ const oldFile = fileBasename
598
+ const newFile = fileBasename.replace(componentName, newName)
599
+
600
+ context.report({
601
+ node,
602
+ messageId: 'renameAliasFile',
603
+ data: {
604
+ old: componentName,
605
+ new: newName,
606
+ to: TARGET_VERSION,
607
+ oldFile,
608
+ newFile,
609
+ },
610
+ // fixは提供しない(ファイル名の変更はESLintでは不可能)
611
+ })
612
+ }
613
+ }
614
+ ```
615
+
616
+ **このパターンが必要なケース:**
617
+ - aliasファイルのファイル名がコンポーネント名と一致している場合
618
+ - 例: `ActionDialog.tsx` → `ControlledActionDialog.tsx` に変更が必要
619
+
620
+ **ポイント:**
621
+ - `Program` ノードに対してチェック(ファイルごとに1回だけ実行)
622
+ - ファイル名の変更はESLintでは不可能なので、fix関数は提供しない
623
+ - エラーメッセージに新旧のファイル名を含める
624
+ - export変数名の置換とは別のエラーとして表示される
625
+
626
+ **エラーメッセージに含める情報:**
627
+ - ファイル名の変更手順(`git mv` の例)
628
+ - ファイル名変更後、import文も更新が必要であることを明記
629
+
630
+ **注意:**
631
+ - re-export(`export { ActionDialog } from 'smarthr-ui'`)にも対応するため、`ExportNamedDeclaration`チェッカーも実装が必要
632
+ - ファイル名変更後、そのファイルをimportしている箇所も手動で更新する必要がある
633
+ ```typescript
634
+ // Before
635
+ import { FormDialog } from '@/components/parts/smarthr-ui/FormDialog'
636
+
637
+ // After (ファイル名変更後)
638
+ import { ControlledFormDialog } from '@/components/parts/smarthr-ui/ControlledFormDialog'
639
+ ```
640
+
641
+ ```javascript
642
+ ExportNamedDeclaration(node) {
643
+ // sourceがない場合(通常のexport)はスキップ
644
+ if (!node.source) return
645
+ if (!validSources.includes(node.source.value)) return
646
+
647
+ node.specifiers.forEach((specifier) => {
648
+ if (specifier.type !== 'ExportSpecifier') return
649
+
650
+ const exportedName = specifier.exported.name
651
+ const localName = specifier.local.name
652
+ const newName = DIALOG_COMPONENTS[localName]
653
+
654
+ if (newName) {
655
+ context.report({
656
+ node: specifier,
657
+ messageId: 'renameDialog',
658
+ data: { old: localName, new: newName, to: TARGET_VERSION },
659
+ fix(fixer) {
660
+ // export { ActionDialog } のように local === exported の場合
661
+ if (localName === exportedName) {
662
+ return fixer.replaceText(specifier, newName)
663
+ }
664
+ // export { ActionDialog as MyDialog } のような場合
665
+ return fixer.replaceText(specifier.local, newName)
666
+ },
667
+ })
668
+ }
669
+ })
670
+ }
671
+ ```
672
+
440
673
  ## トラブルシューティング
441
674
 
442
675
  ### Fix objects must not be overlapped
@@ -13,6 +13,8 @@
13
13
  * 参考: https://github.com/kufu/smarthr-ui/releases/tag/smarthr-ui-v91.0.0
14
14
  */
15
15
 
16
+ const { rootPath } = require('../../../../libs/common')
17
+
16
18
  // ============================================================
17
19
  // 定数定義
18
20
  // ============================================================
@@ -51,10 +53,56 @@ module.exports = {
51
53
  migrateResponseMessage: '見出し/ラベル内の ResponseMessage は親コンポーネントの icon 属性に移行してください',
52
54
  migrateResponseMessageWithUnknownAttrs: '見出し/ラベル内の ResponseMessage は親コンポーネントの icon 属性に移行してください。status/iconGap 以外の属性(id, onClick など)がある場合は手動で移行してください',
53
55
  removeArbitraryDisplayName: 'AppHeader の arbitraryDisplayName 属性は削除されました。email, empCode, firstName, lastName から自動生成されます',
56
+ renameAliasFile: 'smarthr-ui {{to}} では {{old}} が {{new}} にリネームされました。以下の手順で対応してください: 1. ファイル名を変更(例: git mv {{oldFile}} {{newFile}})2. このファイルをimportしている箇所を更新(例: from \'@/path/{{old}}\' → from \'@/path/{{new}}\')',
54
57
  },
55
58
 
56
- createCheckers(context, sourceCode) {
57
- return {
59
+ createCheckers(context, sourceCode, options = {}) {
60
+ const customSmarthrUiAlias = options.smarthrUiAlias
61
+ const validSources = ['smarthr-ui']
62
+ if (customSmarthrUiAlias) {
63
+ validSources.push(customSmarthrUiAlias)
64
+ }
65
+
66
+ // 現在のファイルがaliasファイルか判定
67
+ const filename = context.getFilename()
68
+ const isAliasFile = customSmarthrUiAlias && isFileMatchingSmarthrUiAlias(
69
+ filename,
70
+ customSmarthrUiAlias
71
+ )
72
+
73
+ const checkers = {
74
+ // ============================================================
75
+ // 0. aliasファイル名の変更チェック
76
+ // ============================================================
77
+
78
+ // aliasファイルの場合、ファイル名も変更を促す
79
+ Program(node) {
80
+ if (!isAliasFile) return
81
+
82
+ // ファイル名からコンポーネント名を抽出(拡張子を除く)
83
+ const fileBasename = filename.split('/').pop() || ''
84
+ const componentName = fileBasename.replace(/\.(tsx?|jsx?)$/, '')
85
+
86
+ // Dialog系コンポーネント名と一致するかチェック
87
+ const newName = DIALOG_COMPONENTS[componentName]
88
+ if (newName) {
89
+ const oldFile = fileBasename
90
+ const newFile = fileBasename.replace(componentName, newName)
91
+
92
+ context.report({
93
+ node,
94
+ messageId: 'renameAliasFile',
95
+ data: {
96
+ old: componentName,
97
+ new: newName,
98
+ to: TARGET_VERSION,
99
+ oldFile,
100
+ newFile,
101
+ },
102
+ })
103
+ }
104
+ },
105
+
58
106
  // ============================================================
59
107
  // 1. Dialogコンポーネントのリネーム
60
108
  // ============================================================
@@ -63,7 +111,7 @@ module.exports = {
63
111
  // 例: import { ActionDialog } from 'smarthr-ui'
64
112
  // → import { ControlledActionDialog } from 'smarthr-ui'
65
113
  ImportDeclaration(node) {
66
- if (node.source.value !== 'smarthr-ui') return
114
+ if (!validSources.includes(node.source.value)) return
67
115
 
68
116
  node.specifiers.forEach((specifier) => {
69
117
  if (specifier.type !== 'ImportSpecifier') return
@@ -84,6 +132,39 @@ module.exports = {
84
132
  })
85
133
  },
86
134
 
135
+ // export文での検出と修正(re-export)
136
+ // 例: export { ActionDialog } from 'smarthr-ui'
137
+ // → export { ControlledActionDialog } from 'smarthr-ui'
138
+ ExportNamedDeclaration(node) {
139
+ // sourceがない場合(通常のexport)はスキップ
140
+ if (!node.source) return
141
+ if (!validSources.includes(node.source.value)) return
142
+
143
+ node.specifiers.forEach((specifier) => {
144
+ if (specifier.type !== 'ExportSpecifier') return
145
+
146
+ const exportedName = specifier.exported.name
147
+ const localName = specifier.local.name
148
+ const newName = DIALOG_COMPONENTS[localName]
149
+
150
+ if (newName) {
151
+ context.report({
152
+ node: specifier,
153
+ messageId: 'renameDialog',
154
+ data: { old: localName, new: newName, to: TARGET_VERSION },
155
+ fix(fixer) {
156
+ // export { ActionDialog } のように local === exported の場合
157
+ if (localName === exportedName) {
158
+ return fixer.replaceText(specifier, newName)
159
+ }
160
+ // export { ActionDialog as MyDialog } のような場合
161
+ return fixer.replaceText(specifier.local, newName)
162
+ },
163
+ })
164
+ }
165
+ })
166
+ },
167
+
87
168
  // JSX要素での検出と修正
88
169
  // 例: <ActionDialog>...</ActionDialog>
89
170
  // → <ControlledActionDialog>...</ControlledActionDialog>
@@ -467,5 +548,60 @@ module.exports = {
467
548
  return fixer.replaceText(attr.value, newValue)
468
549
  }
469
550
  }
551
+
552
+ // ============================================================
553
+ // aliasファイル内のexport変数名の置換
554
+ // ============================================================
555
+
556
+ // aliasファイルの場合のみ、export変数名を置換
557
+ if (isAliasFile) {
558
+ checkers['ExportNamedDeclaration > VariableDeclaration > VariableDeclarator'] = function (node) {
559
+ const variableName = node.id.name
560
+ const newName = DIALOG_COMPONENTS[variableName]
561
+
562
+ if (newName) {
563
+ context.report({
564
+ node: node.id,
565
+ messageId: 'renameDialog',
566
+ data: { old: variableName, new: newName, to: TARGET_VERSION },
567
+ fix(fixer) {
568
+ return fixer.replaceText(node.id, newName)
569
+ },
570
+ })
571
+ }
572
+ }
573
+ }
574
+
575
+ return checkers
470
576
  },
471
577
  }
578
+
579
+ /**
580
+ * smarthrUiAliasで指定されたパスと現在のファイルパスがマッチするか判定
581
+ *
582
+ * @param {string} filename - 現在処理中のファイルパス
583
+ * @param {string} smarthrUiAlias - smarthrUiAliasオプションの値(例: '@/components/parts/smarthr-ui')
584
+ * @returns {boolean} マッチする場合true
585
+ */
586
+ function isFileMatchingSmarthrUiAlias(filename, smarthrUiAlias) {
587
+ // rootPathを使って絶対パスで比較を試みる
588
+ const resolved = smarthrUiAlias.replace(/^@\//, `${rootPath}/`)
589
+ if (filename.includes(resolved)) {
590
+ return true
591
+ }
592
+
593
+ // rootPathでマッチしない場合:
594
+ // パスの一部としてマッチング(テスト環境などで使用)
595
+ // 例: '@/components/parts/smarthr-ui' -> 'components/parts/smarthr-ui'
596
+ const pathPart = smarthrUiAlias.replace(/^@\//, '').replace(/^~\//, '')
597
+
598
+ // 以下のパターンにマッチング:
599
+ // 1. ディレクトリ形式: /components/parts/smarthr-ui/index.tsx
600
+ // 2. 個別ファイル: /components/parts/smarthr-ui/ActionDialog.tsx
601
+ // 3. 単一ファイル形式: /components/parts/smarthr-ui.tsx
602
+ return (
603
+ filename.includes(`/${pathPart}/`) ||
604
+ filename.endsWith(`/${pathPart}`) ||
605
+ filename.includes(`/${pathPart}.`)
606
+ )
607
+ }
@@ -48,6 +48,43 @@ module.exports = {
48
48
  { code: `<ResponseMessage status="success">Xxxx</ResponseMessage>`, options: v90ToV91Options },
49
49
  { code: `<Heading icon={{ prefix: <FaCheckIcon />, gap: 0.5 }}>Xxxx</Heading>`, options: v90ToV91Options },
50
50
  { code: `<AppHeader email="test@example.com" />`, options: v90ToV91Options },
51
+
52
+ // smarthrUiAliasオプション: aliasファイル外では置換しない
53
+ {
54
+ code: `export const ActionDialog = (props) => <div>{props.children}</div>`,
55
+ filename: '/Users/test/src/features/custom/ActionDialog.tsx',
56
+ options: [{ from: '90', to: '91', smarthrUiAlias: '@/components/parts/smarthr-ui' }],
57
+ },
58
+
59
+ // smarthrUiAliasオプション: v91形式は正常
60
+ {
61
+ code: `import { ControlledActionDialog } from '@/components/parts/smarthr-ui'`,
62
+ options: [{ from: '90', to: '91', smarthrUiAlias: '@/components/parts/smarthr-ui' }],
63
+ },
64
+ {
65
+ code: `export const ControlledActionDialog = (props) => <div>{props.children}</div>`,
66
+ filename: '/Users/test/src/components/parts/smarthr-ui/ControlledActionDialog.tsx',
67
+ options: [{ from: '90', to: '91', smarthrUiAlias: '@/components/parts/smarthr-ui' }],
68
+ },
69
+
70
+ // smarthrUiAliasオプション: カスタム名のexportは置換しない
71
+ {
72
+ code: `export const MyActionDialog = (props) => <div>{props.children}</div>`,
73
+ filename: '/Users/test/src/components/parts/smarthr-ui/MyActionDialog.tsx',
74
+ options: [{ from: '90', to: '91', smarthrUiAlias: '@/components/parts/smarthr-ui' }],
75
+ },
76
+ {
77
+ code: `export const CustomDialog = (props) => <div>{props.children}</div>`,
78
+ filename: '/Users/test/src/components/parts/smarthr-ui/CustomDialog.tsx',
79
+ options: [{ from: '90', to: '91', smarthrUiAlias: '@/components/parts/smarthr-ui' }],
80
+ },
81
+
82
+ // smarthrUiAliasオプション: 単一ファイル形式でもv91形式は正常
83
+ {
84
+ code: `export const ControlledFormDialog = (props) => <div>{props.children}</div>`,
85
+ filename: '/Users/test/src/components/parts/smarthr-ui.tsx',
86
+ options: [{ from: '90', to: '91', smarthrUiAlias: '@/components/parts/smarthr-ui' }],
87
+ },
51
88
  ],
52
89
 
53
90
  invalid: [
@@ -217,5 +254,80 @@ module.exports = {
217
254
  options: v90ToV91Options,
218
255
  errors: [{ messageId: 'removeArbitraryDisplayName' }],
219
256
  },
257
+
258
+ // ============================================================
259
+ // 6. smarthrUiAliasオプション
260
+ // ============================================================
261
+ // aliasからのimport
262
+ {
263
+ code: `import { ActionDialog } from '@/components/parts/smarthr-ui'`,
264
+ output: `import { ControlledActionDialog } from '@/components/parts/smarthr-ui'`,
265
+ options: [{ from: '90', to: '91', smarthrUiAlias: '@/components/parts/smarthr-ui' }],
266
+ errors: [{ messageId: 'renameDialog', data: { old: 'ActionDialog', new: 'ControlledActionDialog', to: 'v91' } }],
267
+ },
268
+ // aliasファイル内のexport変数名置換(index.tsx)
269
+ {
270
+ code: `export const FormDialog = (props) => <div>{props.children}</div>`,
271
+ output: `export const ControlledFormDialog = (props) => <div>{props.children}</div>`,
272
+ filename: '/Users/test/src/components/parts/smarthr-ui/index.tsx',
273
+ options: [{ from: '90', to: '91', smarthrUiAlias: '@/components/parts/smarthr-ui' }],
274
+ errors: [{ messageId: 'renameDialog', data: { old: 'FormDialog', new: 'ControlledFormDialog', to: 'v91' } }],
275
+ },
276
+ // barrel import: smarthr-uiからimport + export(個別ファイル)
277
+ {
278
+ code: `import { ActionDialog as ShrActionDialog } from 'smarthr-ui'\nexport const ActionDialog = (props) => <ShrActionDialog {...props} />`,
279
+ output: `import { ControlledActionDialog as ShrActionDialog } from 'smarthr-ui'\nexport const ControlledActionDialog = (props) => <ShrActionDialog {...props} />`,
280
+ filename: '/Users/test/src/components/parts/smarthr-ui/ActionDialog.tsx',
281
+ options: [{ from: '90', to: '91', smarthrUiAlias: '@/components/parts/smarthr-ui' }],
282
+ errors: [
283
+ { messageId: 'renameAliasFile', data: { old: 'ActionDialog', new: 'ControlledActionDialog', to: 'v91', oldFile: 'ActionDialog.tsx', newFile: 'ControlledActionDialog.tsx' } },
284
+ { messageId: 'renameDialog', data: { old: 'ActionDialog', new: 'ControlledActionDialog', to: 'v91' } },
285
+ { messageId: 'renameDialog', data: { old: 'ActionDialog', new: 'ControlledActionDialog', to: 'v91' } },
286
+ ],
287
+ },
288
+ // サブディレクトリ内のファイルも対象
289
+ {
290
+ code: `export const MessageDialog = (props) => <div>{props.children}</div>`,
291
+ output: `export const ControlledMessageDialog = (props) => <div>{props.children}</div>`,
292
+ filename: '/Users/test/src/components/parts/smarthr-ui/dialogs/MessageDialog.tsx',
293
+ options: [{ from: '90', to: '91', smarthrUiAlias: '@/components/parts/smarthr-ui' }],
294
+ errors: [
295
+ { messageId: 'renameAliasFile', data: { old: 'MessageDialog', new: 'ControlledMessageDialog', to: 'v91', oldFile: 'MessageDialog.tsx', newFile: 'ControlledMessageDialog.tsx' } },
296
+ { messageId: 'renameDialog', data: { old: 'MessageDialog', new: 'ControlledMessageDialog', to: 'v91' } },
297
+ ],
298
+ },
299
+ // 単一ファイル形式も対象
300
+ {
301
+ code: `export const FormDialog = (props) => <div>{props.children}</div>`,
302
+ output: `export const ControlledFormDialog = (props) => <div>{props.children}</div>`,
303
+ filename: '/Users/test/src/components/parts/smarthr-ui.tsx',
304
+ options: [{ from: '90', to: '91', smarthrUiAlias: '@/components/parts/smarthr-ui' }],
305
+ errors: [{ messageId: 'renameDialog', data: { old: 'FormDialog', new: 'ControlledFormDialog', to: 'v91' } }],
306
+ },
307
+
308
+ // ============================================================
309
+ // 7. aliasファイル名の変更
310
+ // ============================================================
311
+ // ファイル名がDialog系コンポーネント名の場合、ファイル名変更を促す
312
+ {
313
+ code: `// ActionDialog.tsx\nexport { ActionDialog } from 'smarthr-ui'`,
314
+ output: `// ActionDialog.tsx\nexport { ControlledActionDialog } from 'smarthr-ui'`,
315
+ filename: '/Users/test/src/components/parts/smarthr-ui/ActionDialog.tsx',
316
+ options: [{ from: '90', to: '91', smarthrUiAlias: '@/components/parts/smarthr-ui' }],
317
+ errors: [
318
+ { messageId: 'renameAliasFile', data: { old: 'ActionDialog', new: 'ControlledActionDialog', to: 'v91', oldFile: 'ActionDialog.tsx', newFile: 'ControlledActionDialog.tsx' } },
319
+ { messageId: 'renameDialog', data: { old: 'ActionDialog', new: 'ControlledActionDialog', to: 'v91' } },
320
+ ],
321
+ },
322
+ {
323
+ code: `// FormDialog.tsx\nconst foo = 'bar'`,
324
+ output: null,
325
+ filename: '/Users/test/src/components/parts/smarthr-ui/FormDialog.tsx',
326
+ options: [{ from: '90', to: '91', smarthrUiAlias: '@/components/parts/smarthr-ui' }],
327
+ errors: [
328
+ { messageId: 'renameAliasFile', data: { old: 'FormDialog', new: 'ControlledFormDialog', to: 'v91', oldFile: 'FormDialog.tsx', newFile: 'ControlledFormDialog.tsx' } },
329
+ ],
330
+ },
331
+
220
332
  ],
221
333
  }
@@ -177,16 +177,37 @@ module.exports = {
177
177
  // パターン2-1: classNameのみ(asなし)、Text属性なし、shr-クラスあり
178
178
  [SELECTOR_CONVERTIBLE_SHR_TO_PROPS]: (classNameAttrNode) => {
179
179
  const { nonConvertible, propSuggestions, convertible } = categorizeClassNames(classNameAttrNode)
180
+ 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
+ }
180
201
 
202
+ // 変換可能なクラスがある場合、属性に変換
181
203
  context.report({
182
- node: classNameAttrNode.parent,
204
+ node: openingElement,
183
205
  message: `classNameで指定されたshr-プレフィックスのクラスは、Textコンポーネントの属性に置き換えてください。
184
206
  - 詳細: https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/best-practice-for-text-component
185
207
  - 推奨: <Text ${propSuggestions}${nonConvertible ? ` className="${nonConvertible}"` : ''}>
186
208
  - 変換可能なクラス: ${convertible}
187
209
  - shr-プレフィックスのクラスをTextの属性に置き換えることで、型安全性が向上し、意図がより明確になります`,
188
210
  fix(fixer) {
189
- const openingElement = classNameAttrNode.parent
190
211
  const sourceCode = context.sourceCode || context.getSourceCode()
191
212
  const fixes = []
192
213
 
@@ -235,8 +256,39 @@ module.exports = {
235
256
  [SELECTOR_CONVERTIBLE_SHR_TO_PROPS_WITH_AS]: (classNameAttrNode) => {
236
257
  const { nonConvertible, propSuggestions, convertible } = categorizeClassNames(classNameAttrNode)
237
258
  const openingElement = classNameAttrNode.parent
259
+ const jsxElement = openingElement.parent
238
260
  const asValue = getAttributeLiteralValue(openingElement, 'as')
239
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要素に置き換えてください。
269
+ - 詳細: 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')
275
+
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
+ }
290
+
291
+ // 変換可能なクラスがある場合、属性に変換
240
292
  context.report({
241
293
  node: openingElement,
242
294
  message: `classNameで指定されたshr-プレフィックスのクラスは、Textコンポーネントの属性に置き換えてください。
@@ -105,6 +105,11 @@ ruleTester.run('best-practice-for-text-component', rule, {
105
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') }] },
106
106
  { 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
107
 
108
+ // パターン2-4: shr-プレフィックスがあるが変換不可能なクラスのみ(spanに変換)
109
+ { code: `<Text className="shr-w-[10rem]">text</Text>`, output: `<span className="shr-w-[10rem]">text</span>`, errors: [{ message: errorUnnecessaryClassName('shr-w-[10rem]') }] },
110
+ { 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') }] },
111
+ { 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') }] },
112
+
108
113
  // パターン3: 属性とclassNameの矛盾
109
114
  { code: `<Text size="M" className="shr-text-sm">text</Text>`, errors: [{ message: errorConflictingProps('shr-text-sm') }] },
110
115
  { code: `<Text weight="bold" className="shr-font-normal">text</Text>`, errors: [{ message: errorConflictingProps('shr-font-normal') }] },