diff --git a/packages/eslint-plugin-smarthr/rules/autofixer-smarthr-ui-migration/DEVELOPER.md b/packages/eslint-plugin-smarthr/rules/autofixer-smarthr-ui-migration/DEVELOPER.md
index e8fac6dd..2e22fb97 100644
--- a/packages/eslint-plugin-smarthr/rules/autofixer-smarthr-ui-migration/DEVELOPER.md
+++ b/packages/eslint-plugin-smarthr/rules/autofixer-smarthr-ui-migration/DEVELOPER.md
@@ -66,10 +66,10 @@ autofixer-smarthr-ui-migrationルールに新しいバージョン(v[XX]→v[Y
## 参考にするファイル
必ず以下のファイルを読んで、実装パターンを踏襲してください(最新のversionディレクトリを参照):
-- rules/autofixer-smarthr-ui-migration/versions/v93-to-v94/REFERENCE.md(実装パターンの詳細説明)
-- rules/autofixer-smarthr-ui-migration/versions/v93-to-v94/index.js(実装例)
-- rules/autofixer-smarthr-ui-migration/versions/v93-to-v94/README.md(ユーザー向け移行ガイド)
-- rules/autofixer-smarthr-ui-migration/versions/v93-to-v94/test.js(テストケース)
+- rules/autofixer-smarthr-ui-migration/versions/v94-to-v95/REFERENCE.md(実装パターンの詳細説明)
+- rules/autofixer-smarthr-ui-migration/versions/v94-to-v95/index.js(実装例)
+- rules/autofixer-smarthr-ui-migration/versions/v94-to-v95/README.md(ユーザー向け移行ガイド)
+- rules/autofixer-smarthr-ui-migration/versions/v94-to-v95/test.js(テストケース)
- test/autofixer-smarthr-ui-migration.js(メインテスト)
- libs/common.js(rootPathの取得、tsconfig.jsonのpaths設定読み込み)
@@ -588,7 +588,7 @@ https://github.com/kufu/smarthr-ui/releases
各versionディレクトリに`REFERENCE.md`があり、実装パターンや注意点が記載されています。
-**最新version:** [v93-to-v94/REFERENCE.md](./versions/v93-to-v94/REFERENCE.md)
+**最新version:** [v94-to-v95/REFERENCE.md](./versions/v94-to-v95/REFERENCE.md)
このドキュメントには以下が含まれます:
- ファイル構造と各セクションの説明
diff --git a/packages/eslint-plugin-smarthr/rules/autofixer-smarthr-ui-migration/README.md b/packages/eslint-plugin-smarthr/rules/autofixer-smarthr-ui-migration/README.md
index bc9cae54..5658d54e 100644
--- a/packages/eslint-plugin-smarthr/rules/autofixer-smarthr-ui-migration/README.md
+++ b/packages/eslint-plugin-smarthr/rules/autofixer-smarthr-ui-migration/README.md
@@ -195,6 +195,7 @@ export const ActionDialog = (props) =>
{props.children}
| `91` → `92` | [移行ガイド](./versions/v91-to-v92/README.md) |
| `92` → `93` | [移行ガイド](./versions/v92-to-v93/README.md) |
| `93` → `94` | [移行ガイド](./versions/v93-to-v94/README.md) |
+| `94` → `95` | [移行ガイド](./versions/v94-to-v95/README.md) |
## 使用方法
diff --git a/packages/eslint-plugin-smarthr/rules/autofixer-smarthr-ui-migration/index.js b/packages/eslint-plugin-smarthr/rules/autofixer-smarthr-ui-migration/index.js
index 0dc4d47d..4777f538 100644
--- a/packages/eslint-plugin-smarthr/rules/autofixer-smarthr-ui-migration/index.js
+++ b/packages/eslint-plugin-smarthr/rules/autofixer-smarthr-ui-migration/index.js
@@ -19,6 +19,7 @@ const v90ToV91 = require('./versions/v90-to-v91/index')
const v91ToV92 = require('./versions/v91-to-v92/index')
const v92ToV93 = require('./versions/v92-to-v93/index')
const v93ToV94 = require('./versions/v93-to-v94/index')
+const v94ToV95 = require('./versions/v94-to-v95/index')
// サポートしているバージョン間の移行モジュール
const VERSION_MODULES = {
@@ -26,6 +27,7 @@ const VERSION_MODULES = {
'v91-v92': v91ToV92,
'v92-v93': v92ToV93,
'v93-v94': v93ToV94,
+ 'v94-v95': v94ToV95,
}
module.exports = {
@@ -60,6 +62,8 @@ module.exports = {
...v90ToV91.messages,
...v91ToV92.messages,
...v92ToV93.messages,
+ ...v93ToV94.messages,
+ ...v94ToV95.messages,
},
},
create(context) {
diff --git a/packages/eslint-plugin-smarthr/rules/autofixer-smarthr-ui-migration/versions/v94-to-v95/README.md b/packages/eslint-plugin-smarthr/rules/autofixer-smarthr-ui-migration/versions/v94-to-v95/README.md
new file mode 100644
index 00000000..a9023b14
--- /dev/null
+++ b/packages/eslint-plugin-smarthr/rules/autofixer-smarthr-ui-migration/versions/v94-to-v95/README.md
@@ -0,0 +1,309 @@
+# smarthr-ui v94 → v95 移行ガイド
+
+このドキュメントは、smarthr-ui v94からv95への移行に必要な変更をまとめたものです。
+
+## 対応する破壊的変更
+
+### 1. LanguageSwitcher: decorators属性の削除
+
+v95では、`LanguageSwitcher`コンポーネントから`decorators`属性が削除されました。翻訳はsmarthr-ui内で自動的に行われます。
+
+#### 変更内容
+
+- `decorators` 属性削除
+- トリガーラベルは常に`'Language'`で固定
+- チェックアイコンのaltはsmarthr-uiの翻訳が自動適用(全9言語対応済み)
+
+#### 移行方法
+
+**Before (v94):**
+```tsx
+ 'Language' }} />
+```
+
+**After (v95):**
+```tsx
+
+```
+
+#### 自動修正可能なパターン
+
+以下のパターンは、ESLintの`--fix`オプションで自動的に修正されます:
+
+```tsx
+// decorators属性を削除
+ '言語' }} />
+→
+```
+
+### 1-2. AppLauncher: decorators.triggerLabelをtriggerLabel属性に移行
+
+v95では、`AppLauncher`コンポーネントから`decorators`属性が削除され、`triggerLabel`属性が追加されました。
+
+#### 変更内容
+
+- `decorators.triggerLabel` → `triggerLabel` 属性に移行
+- `triggerLabel`が指定されていない場合はsmarthr-uiの翻訳が自動適用(全9言語対応済み)
+- 動的な値(例: featureName)を渡す必要がある場合のみ、`triggerLabel`属性を使用
+
+#### 移行方法
+
+**Before (v94):**
+```tsx
+// 固定値の場合
+ 'Apps' }} />
+
+// 動的な値の場合
+ featureName }} />
+```
+
+**After (v95):**
+```tsx
+// 固定値の場合 → decoratorsを削除してIntlProviderに任せる
+
+
+// 動的な値の場合 → triggerLabel属性に移行
+
+```
+
+#### 自動修正可能なパターン
+
+以下のパターンは、ESLintの`--fix`オプションで自動的に修正されます:
+
+```tsx
+// 固定値の場合 → decoratorsを削除
+ "Apps" }} />
+→
+
+ 'アプリ' }} />
+→
+
+// 動的な値の場合 → triggerLabel属性に移行
+ featureName }} />
+→
+
+ getLabel() }} />
+→
+
+ labels.app }} />
+→
+```
+
+**注意:** 引数ありの関数(例: `(lang) => ...`)やBlockStatement形式(例: `() => { return "Apps" }`)は自動修正できません。手動で対応してください。
+
+### 2. InputFile: decorators属性の削除
+
+v95では、`InputFile`コンポーネントから`decorators`属性が削除されました。削除ボタンのラベルはsmarthr-uiが提供する翻訳が自動的に適用されます(IntlProvider経由)。
+
+#### 移行方法
+
+**Before (v94):**
+```tsx
+ '削除' }} />
+```
+
+**After (v95):**
+```tsx
+
+```
+
+### 3. FormDialog: ボタン属性をObject形式に統合
+
+v95では、`FormDialog`のボタン関連の属性がObject形式に統合されました。
+
+#### 変更内容
+
+以下のpropsが削除されました:
+- `actionText` → `actionButton`で指定
+- `actionTheme` → `actionButton={{ text: "...", theme: "..." }}`で指定
+- `actionDisabled` → `actionButton={{ text: "...", disabled: true }}`で指定
+- `closeDisabled` → `closeButton={{ text: "...", disabled: true }}`で指定
+- `decorators.closeButtonLabel` → `closeButton="キャンセル"`で指定
+
+#### 移行方法
+
+**シンプルな使い方(文字列のみ指定):**
+```tsx
+// Before (v94)
+ 'キャンセル' }}
+>
+
+// After (v95)
+
+```
+
+**詳細な設定が必要な場合:**
+```tsx
+// Before (v94)
+ '閉じる' }}
+>
+
+// After (v95)
+
+```
+
+#### 自動修正可能なパターン
+
+以下のパターンは、ESLintの`--fix`オプションで自動的に修正されます:
+
+```tsx
+// actionText のみの場合は自動でリネーム
+
+→
+
+// 複数の属性がある場合は自動でObject形式に変換
+
+→
+
+
+→
+
+// decorators.closeButtonLabel(引数なしの関数の場合)
+ "キャンセル" }}>
+→
+
+ buttonLabel }}>
+→
+
+ getLabel() }}>
+→
+
+ labels.close }}>
+→
+```
+
+**注意:** 引数ありの関数(例: `(lang) => ...`)やBlockStatement形式(例: `() => { return "OK" }`)は自動修正できません。手動で対応してください。
+
+### 4. ActionDialog: ボタン属性をObject形式に統合
+
+`ActionDialog`も`FormDialog`と同様に、ボタン関連の属性がObject形式に統合されました。
+
+#### 変更内容と移行方法
+
+FormDialogと同じです。詳細は「3. FormDialog」セクションを参照してください。
+
+### 5. MessageDialog: decorators削除とcloseButton属性への統一
+
+v95では、`MessageDialog`の`decorators.closeButtonLabel`が`closeButton`属性に統一されました。
+
+#### 変更内容
+
+以下のpropsが削除されました:
+- `decorators.closeButtonLabel` → `closeButton`で指定
+
+#### 移行方法
+
+**デフォルトラベル使用の場合(省略可能):**
+```tsx
+// Before (v94)
+
+ メッセージ本文
+
+
+// After (v95) - 変更なし
+
+ メッセージ本文
+
+```
+
+**カスタムラベルが必要な場合:**
+```tsx
+// Before (v94)
+ 'OK' }}
+>
+ メッセージ本文
+
+
+// After (v95)
+
+ メッセージ本文
+
+```
+
+#### 自動修正可能なパターン
+
+以下のパターンは、ESLintの`--fix`オプションで自動的に修正されます:
+
+```tsx
+// 引数なしの関数から値を抽出
+ "OK" }} />
+→
+
+ buttonLabel }} />
+→
+
+ getLabel() }} />
+→
+
+ labels.close }} />
+→
+```
+
+**注意:** 引数ありの関数(例: `(lang) => ...`)やBlockStatement形式(例: `() => { return "OK" }`)は自動修正できません。手動で対応してください。
+
+## ESLintルールの使用方法
+
+### .eslintrc.js の設定
+
+```javascript
+module.exports = {
+ rules: {
+ 'smarthr/autofixer-smarthr-ui-migration': ['error', { from: '94', to: '95' }],
+ },
+}
+```
+
+### 自動修正の実行
+
+```bash
+# エラーを確認
+pnpm run lint
+
+# 自動修正を実行
+pnpm run lint --fix
+```
+
+### 注意事項
+
+- 以下のパターンは自動修正できません。エラーメッセージを確認して、手動で修正してください:
+ - `closeDisabled`属性の移行(`closeButton`のObject形式への変換が必要)
+ - 引数ありの関数: `decorators={{ closeButtonLabel: (lang) => ... }}`、`decorators={{ triggerLabel: (lang) => ... }}`
+ - BlockStatement形式の関数: `decorators={{ closeButtonLabel: () => { return "OK" } }}`、`decorators={{ triggerLabel: () => { return "Apps" } }}`
+
+### smarthrUiAlias オプション
+
+プロジェクト固有のsmarthr-ui aliasパスを使用している場合は、`smarthrUiAlias`オプションを指定してください。
+
+```javascript
+module.exports = {
+ rules: {
+ 'smarthr/autofixer-smarthr-ui-migration': [
+ 'error',
+ {
+ from: '94',
+ to: '95',
+ smarthrUiAlias: '@/components/parts/smarthr-ui', // プロジェクト固有のalias
+ },
+ ],
+ },
+}
+```
+
+## 参考リンク
+
+- [smarthr-ui v95.0.0 リリースノート](https://github.com/kufu/smarthr-ui/releases/tag/smarthr-ui-v95.0.0)
diff --git a/packages/eslint-plugin-smarthr/rules/autofixer-smarthr-ui-migration/versions/v94-to-v95/REFERENCE.md b/packages/eslint-plugin-smarthr/rules/autofixer-smarthr-ui-migration/versions/v94-to-v95/REFERENCE.md
new file mode 100644
index 00000000..8d791438
--- /dev/null
+++ b/packages/eslint-plugin-smarthr/rules/autofixer-smarthr-ui-migration/versions/v94-to-v95/REFERENCE.md
@@ -0,0 +1,656 @@
+# v94-to-v95 実装の参考ポイント
+
+このドキュメントは、v94→v95の移行ルール実装の構造と、新しいversionを追加する際の参考ポイントを説明します。
+
+## v94→v95 特有の実装パターン
+
+### 1. LanguageSwitcher, InputFile の decorators 属性削除
+
+v95では LanguageSwitcher, InputFile コンポーネントから `decorators` 属性が削除されました。v93-to-v94のThCheckboxと同じく、**新しい属性への移行はなく、完全に削除する**だけのシンプルなパターンです。
+
+#### 1-1. decorators属性のチェッカー(シンプルな削除)
+
+```javascript
+'JSXAttribute[name.name="decorators"]'(node) {
+ const componentName = node.parent.name.name
+
+ // 対象コンポーネントのみ(LanguageSwitcher, InputFile)
+ if (!COMPONENTS_REMOVE_DECORATORS.includes(componentName)) return
+
+ context.report({
+ node,
+ messageId: 'removeDecorators',
+ data: { component: componentName, to: TARGET_VERSION },
+ fix(fixer) {
+ // decorators属性を削除
+ const tokenBefore = sourceCode.getTokenBefore(node)
+ if (tokenBefore && tokenBefore.range[1] < node.range[0]) {
+ return fixer.removeRange([tokenBefore.range[1], node.range[1]])
+ }
+ return fixer.remove(node)
+ },
+ })
+}
+```
+
+**ポイント:**
+- v93-to-v94のThCheckboxと同じパターン
+- **値の解析不要**: decoratorsの内容に関わらず削除するため、複雑な解析関数は不要
+- **条件分岐なし**: 常に削除するだけ
+- **自動修正: 常に可能**: 手動対応が必要なケースなし
+
+### 1-2. AppLauncher の decorators.triggerLabel を triggerLabel 属性に移行
+
+v95では AppLauncher コンポーネントの `decorators.triggerLabel` が `triggerLabel` 属性に移行されました。これは**値の抽出が必要な複雑なパターン**です。
+
+#### 変更内容
+
+**削除される属性:**
+- `decorators.triggerLabel` → `triggerLabel` 属性(動的な値の場合のみ)
+
+**移行後の形式:**
+```tsx
+// 固定値の場合 → decoratorsを削除してIntlProviderに任せる
+
+
+// 動的な値の場合 → triggerLabel属性に移行
+
+```
+
+#### 実装の制約と自動修正可能なパターン
+
+**自動修正可能:**
+- 既に`triggerLabel`属性がある場合 → `decorators`を削除
+- 固定値(リテラル)の場合 → `decorators`を削除(IntlProviderに任せる)
+- 動的な値(変数、関数呼び出しなど)の場合 → `triggerLabel`属性に移行
+
+**自動修正不可(エラーのみ表示):**
+- 引数ありの関数: `(lang) => ...`
+- BlockStatement形式: `() => { return "Apps" }`
+
+#### 実装パターン
+
+```javascript
+'JSXOpeningElement[name.name="AppLauncher"] > JSXAttribute[name.name="decorators"]'(node) {
+ // decorators属性の値を解析してtriggerLabelがあるかチェック
+ const decoratorsValue = sourceCode.getText(node.value)
+ if (decoratorsValue.includes('triggerLabel')) {
+ // triggerLabel属性が既にあるかチェック
+ const triggerLabelAttr = node.parent.attributes.find(
+ (attr) => attr.type === 'JSXAttribute' && attr.name.name === 'triggerLabel'
+ )
+
+ context.report({
+ node,
+ messageId: 'migrateAppLauncherDecorators',
+ data: { to: TARGET_VERSION },
+ fix(fixer) {
+ // triggerLabel属性が既にある場合は削除のみ
+ if (triggerLabelAttr) {
+ const tokenBefore = sourceCode.getTokenBefore(node)
+ if (tokenBefore && tokenBefore.range[1] < node.range[0]) {
+ return fixer.removeRange([tokenBefore.range[1], node.range[1]])
+ }
+ return fixer.remove(node)
+ }
+
+ // decorators={{ triggerLabel: () => "Apps" }}から値を抽出
+ const extractedValue = extractDecoratorValue(node, 'triggerLabel')
+ if (extractedValue) {
+ // 固定値(リテラル)の場合 → decoratorsを削除するだけ
+ // 動的な値(変数など)の場合 → triggerLabel属性に移行
+ if (extractedValue.startsWith('"') || extractedValue.startsWith("'")) {
+ // 固定値: decoratorsを削除
+ const tokenBefore = sourceCode.getTokenBefore(node)
+ if (tokenBefore && tokenBefore.range[1] < node.range[0]) {
+ return fixer.removeRange([tokenBefore.range[1], node.range[1]])
+ }
+ return fixer.remove(node)
+ } else {
+ // 動的な値: triggerLabel属性に移行
+ return fixer.replaceText(node, `triggerLabel=${extractedValue}`)
+ }
+ }
+
+ // 複雑なため、エラーのみ表示(手動対応)
+ return null
+ },
+ })
+ }
+}
+```
+
+**ポイント:**
+- `extractDecoratorValue(node, 'triggerLabel')`で値を抽出
+- 抽出した値がリテラル(`"`または`'`で始まる)の場合 → `decorators`を削除
+- 抽出した値がJSX式(`{...}`形式)の場合 → `triggerLabel={...}`に置換
+- 固定値の場合はIntlProviderに任せるため、属性自体を削除
+- 動的な値の場合のみ`triggerLabel`属性を追加
+
+### 2. FormDialog/ActionDialog のボタン属性統合
+
+v95では FormDialog と ActionDialog のボタン関連属性が Object 形式に統合されました。これは**複雑な移行パターン**です。
+
+#### 2-1. 移行が必要な属性
+
+**削除される属性:**
+- `actionText` → `actionButton` (文字列 or Object)
+- `actionTheme` → `actionButton={{ theme: "..." }}` (Object)
+- `actionDisabled` → `actionButton={{ disabled: true }}` (Object)
+- `closeDisabled` → `closeButton={{ disabled: true }}` (Object)
+- `decorators.closeButtonLabel` → `closeButton` (文字列 or Object)
+
+**統合後の形式:**
+```tsx
+// シンプルな場合(文字列のみ)
+actionButton="保存"
+closeButton="キャンセル"
+
+// 詳細設定(Object形式)
+actionButton={{ text: "削除", theme: "danger", disabled: false }}
+closeButton={{ text: "閉じる", disabled: true }}
+```
+
+#### 2-2. 実装の制約と自動修正可能なパターン
+
+**自動修正可能:**
+- `actionText`のみの場合 → `actionButton`にリネーム
+- `actionText` + `actionTheme` → Object形式へ自動変換
+- `actionText` + `actionDisabled` → Object形式へ自動変換
+- `actionText` + `actionTheme` + `actionDisabled` → Object形式へ自動変換
+- 既に`actionButton`/`closeButton`がある場合 → 古い属性を削除
+- `decorators.closeButtonLabel`(引数なしの関数) → 値を抽出して`closeButton`に変換
+- `decorators.triggerLabel`(引数なしの関数) → 固定値は削除、動的な値は`triggerLabel`に変換
+
+**自動修正不可(エラーのみ表示):**
+- `closeDisabled` → Object形式への変換が複雑
+- `decorators`(引数あり、またはBlockStatement)
+
+#### 2-3. 実装パターン(段階的な対応)
+
+```javascript
+'JSXOpeningElement'(node) {
+ const componentName = node.name.name
+
+ // 対象コンポーネントのみ
+ if (!DIALOG_COMPONENTS_WITH_BUTTONS.includes(componentName)) return
+
+ // 各属性を収集
+ let actionTextAttr = null
+ let actionThemeAttr = null
+ let actionDisabledAttr = null
+ let closeDisabledAttr = null
+ let decoratorsAttr = null
+ let actionButtonAttr = null
+ let closeButtonAttr = null
+
+ node.attributes.forEach((attr) => {
+ if (attr.type !== 'JSXAttribute') return
+ const attrName = attr.name.name
+
+ if (attrName === 'actionText') actionTextAttr = attr
+ if (attrName === 'actionTheme') actionThemeAttr = attr
+ // ... 他の属性も同様
+ })
+
+ const hasActionButton = !!actionButtonAttr
+ const hasCloseButton = !!closeButtonAttr
+
+ // actionText を actionButton に移行
+ if (actionTextAttr) {
+ context.report({
+ node: actionTextAttr,
+ messageId: 'migrateActionText',
+ fix(fixer) {
+ if (hasActionButton) {
+ // actionButton属性が既にある場合は削除のみ
+ const tokenBefore = sourceCode.getTokenBefore(actionTextAttr)
+ if (tokenBefore && tokenBefore.range[1] < actionTextAttr.range[0]) {
+ return fixer.removeRange([tokenBefore.range[1], actionTextAttr.range[1]])
+ }
+ return fixer.remove(actionTextAttr)
+ }
+
+ // actionText のみの場合、actionButton にリネーム
+ if (!actionThemeAttr && !actionDisabledAttr) {
+ return fixer.replaceText(actionTextAttr.name, 'actionButton')
+ }
+
+ // 複雑な場合はエラーのみ(手動対応)
+ return null
+ },
+ })
+ }
+
+ // actionTheme, actionDisabled, closeDisabled も同様
+ // ...
+}
+```
+
+**ポイント:**
+- **属性の収集**: すべての関連属性を先に収集
+- **既存の新属性チェック**: `actionButton`/`closeButton`が既にあるかチェック
+- **段階的な修正**:
+ 1. 新属性が既にある → 古い属性を削除
+ 2. 単一属性のみ → 単純なリネーム
+ 3. 複数属性 → Object形式へ自動変換(v94-to-v95で追加)
+
+**Object形式への自動変換(actionText + actionTheme/actionDisabled):**
+
+```javascript
+if (actionTextAttr) {
+ context.report({
+ node: actionTextAttr,
+ messageId: 'migrateActionText',
+ fix(fixer) {
+ if (hasActionButton) {
+ // actionButton属性が既にある場合は削除のみ
+ return removeAttribute(fixer, actionTextAttr)
+ }
+
+ // actionButton属性がない場合
+ if (!actionThemeAttr && !actionDisabledAttr) {
+ // actionTextのみ → 単純にリネーム
+ return fixer.replaceText(actionTextAttr.name, 'actionButton')
+ }
+
+ // 複数の属性がある場合、Object形式に変換
+ const fixes = []
+ const textValue = getAttributeValue(actionTextAttr)
+ const themeValue = actionThemeAttr ? getAttributeValue(actionThemeAttr) : null
+ const disabledValue = actionDisabledAttr ? getAttributeValue(actionDisabledAttr) : null
+
+ const objectParts = [`text: ${textValue}`]
+ if (themeValue) objectParts.push(`theme: ${themeValue}`)
+ if (disabledValue !== null) objectParts.push(`disabled: ${disabledValue}`)
+
+ const newValue = `actionButton={{ ${objectParts.join(', ')} }}`
+
+ // actionText属性をactionButton={{ ... }}に置換
+ fixes.push(fixer.replaceText(actionTextAttr, newValue))
+
+ // actionTheme/actionDisabled属性を削除
+ if (actionThemeAttr) {
+ fixes.push(removeAttribute(fixer, actionThemeAttr))
+ }
+ if (actionDisabledAttr) {
+ fixes.push(removeAttribute(fixer, actionDisabledAttr))
+ }
+
+ return fixes
+ },
+ })
+}
+
+// actionTheme/actionDisabledのfix関数では、actionTextが存在する場合はnullを返す
+// (actionTextのfixでまとめて処理されるため)
+```
+
+**getAttributeValue ヘルパー関数:**
+
+```javascript
+function getAttributeValue(attr) {
+ if (!attr || !attr.value) return '""'
+
+ // 文字列リテラル: actionText="保存"
+ if (attr.value.type === 'Literal') {
+ return JSON.stringify(attr.value.value)
+ }
+
+ // JSX式: actionTheme={"danger"} または actionDisabled={false}
+ if (attr.value.type === 'JSXExpressionContainer') {
+ const expr = attr.value.expression
+ if (expr.type === 'Literal') {
+ return JSON.stringify(expr.value)
+ }
+ // 変数や式の場合はソースコードをそのまま取得
+ return sourceCode.getText(expr)
+ }
+
+ return '""'
+}
+```
+
+#### 2-4. decorators.closeButtonLabel の処理(値の自動抽出)
+
+v94→v95では、引数なしの関数(`() => "OK"`形式)から値を抽出する実装を追加しました。
+
+```javascript
+if (decoratorsAttr) {
+ // decorators属性の値を解析してcloseButtonLabelがあるかチェック
+ const decoratorsValue = sourceCode.getText(decoratorsAttr.value)
+ if (decoratorsValue.includes('closeButtonLabel')) {
+ context.report({
+ node: decoratorsAttr,
+ messageId: 'migrateDecoratorsCloseButtonLabel',
+ fix(fixer) {
+ // closeButton属性が既にある場合は削除のみ
+ if (hasCloseButton) {
+ const tokenBefore = sourceCode.getTokenBefore(decoratorsAttr)
+ if (tokenBefore && tokenBefore.range[1] < decoratorsAttr.range[0]) {
+ return fixer.removeRange([tokenBefore.range[1], decoratorsAttr.range[1]])
+ }
+ return fixer.remove(decoratorsAttr)
+ }
+
+ // decorators={{ closeButtonLabel: () => "OK" }}から値を抽出
+ const extractedValue = extractDecoratorValue(decoratorsAttr, 'closeButtonLabel')
+ if (extractedValue) {
+ // 自動修正可能: decoratorsを削除してcloseButtonを追加
+ return fixer.replaceText(decoratorsAttr, `closeButton=${extractedValue}`)
+ }
+
+ // 複雑なため、エラーのみ(手動対応)
+ return null
+ },
+ })
+ }
+}
+```
+
+**extractDecoratorValue 関数:**
+
+```javascript
+/**
+ * decorators属性から指定されたプロパティの値を抽出
+ *
+ * decorators={{ closeButtonLabel: () => "OK" }} から "OK" を抽出
+ * decorators={{ closeButtonLabel: () => variable }} から {variable} を抽出
+ *
+ * @param {Object} decoratorsAttr - decorators属性のASTノード
+ * @param {string} propertyName - 抽出するプロパティ名
+ * @returns {string|null} 抽出された値、または抽出不可の場合null
+ */
+function extractDecoratorValue(decoratorsAttr, propertyName) {
+ if (!decoratorsAttr || !decoratorsAttr.value) return null
+ if (decoratorsAttr.value.type !== 'JSXExpressionContainer') return null
+
+ const expr = decoratorsAttr.value.expression
+ if (expr.type !== 'ObjectExpression') return null
+
+ // プロパティを探す
+ const property = expr.properties.find((prop) => {
+ return (
+ prop.type === 'Property' &&
+ prop.key &&
+ ((prop.key.type === 'Identifier' && prop.key.name === propertyName) ||
+ (prop.key.type === 'Literal' && prop.key.value === propertyName))
+ )
+ })
+
+ if (!property || !property.value) return null
+
+ // ArrowFunctionExpression: () => "OK"
+ if (property.value.type !== 'ArrowFunctionExpression') return null
+
+ // 引数なしの場合のみ処理
+ if (property.value.params.length !== 0) return null
+
+ const body = property.value.body
+
+ // BlockStatement(ブロック形式)は対応しない: () => { return "OK" }
+ if (body.type === 'BlockStatement') return null
+
+ // Literal: () => "OK" → "OK"
+ if (body.type === 'Literal') {
+ return JSON.stringify(body.value)
+ }
+
+ // その他のExpression: () => variable → {variable}
+ // () => a() → {a()}
+ // () => obj.prop → {obj.prop}
+ return `{${sourceCode.getText(body)}}`
+}
+```
+
+**ポイント:**
+- `ArrowFunctionExpression`の`params.length === 0`(引数なし)の場合のみ処理
+- `BlockStatement`(`() => { return "OK" }`)は対応しない
+- `body.type === 'Literal'` → 文字列として返す(`"OK"`)
+- その他のExpression → JSX式として返す
+ - `() => variable` → `{variable}`
+ - `() => a()` → `{a()}`
+ - `() => obj.prop` → `{obj.prop}`
+ - `sourceCode.getText(body)`でbody部分のソースコードをそのまま抽出
+
+### 3. MessageDialog の decorators 削除
+
+MessageDialogは FormDialog/ActionDialog と似ていますが、`closeButtonLabel`のみの対応です。FormDialogと同じく`extractDecoratorValue`を使って値を自動抽出します。
+
+```javascript
+'JSXOpeningElement[name.name="MessageDialog"] > JSXAttribute[name.name="decorators"]'(node) {
+ const decoratorsValue = sourceCode.getText(node.value)
+ if (decoratorsValue.includes('closeButtonLabel')) {
+ const closeButtonAttr = node.parent.attributes.find(
+ (attr) => attr.type === 'JSXAttribute' && attr.name.name === 'closeButton'
+ )
+
+ context.report({
+ node,
+ messageId: 'migrateMessageDialogDecorators',
+ fix(fixer) {
+ // closeButton属性が既にある場合は削除のみ
+ if (closeButtonAttr) {
+ const tokenBefore = sourceCode.getTokenBefore(node)
+ if (tokenBefore && tokenBefore.range[1] < node.range[0]) {
+ return fixer.removeRange([tokenBefore.range[1], node.range[1]])
+ }
+ return fixer.remove(node)
+ }
+
+ // decorators={{ closeButtonLabel: () => "OK" }}から値を抽出
+ const extractedValue = extractDecoratorValue(node, 'closeButtonLabel')
+ if (extractedValue) {
+ // 自動修正可能
+ return fixer.replaceText(node, `closeButton=${extractedValue}`)
+ }
+
+ // 複雑なため、エラーのみ(手動対応)
+ return null
+ },
+ })
+ }
+}
+```
+
+## v93-to-v94との実装比較
+
+### ThCheckbox (v93-to-v94) - シンプルな削除
+
+```javascript
+// 値の解析不要、条件分岐なし
+context.report({
+ node,
+ messageId: 'removeDecorators',
+ fix(fixer) {
+ // decorators削除のみ
+ const tokenBefore = sourceCode.getTokenBefore(node)
+ if (tokenBefore && tokenBefore.range[1] < node.range[0]) {
+ return fixer.removeRange([tokenBefore.range[1], node.range[1]])
+ }
+ return fixer.remove(node)
+ }
+})
+```
+
+### FormDialog (v94-to-v95) - 複雑な属性統合
+
+```javascript
+// 複数の属性を収集
+let actionTextAttr = null
+let actionThemeAttr = null
+// ...
+
+// 条件分岐で段階的に対応
+if (actionTextAttr) {
+ context.report({
+ fix(fixer) {
+ if (hasActionButton) {
+ // 削除のみ
+ return fixer.remove(actionTextAttr)
+ }
+ if (!actionThemeAttr && !actionDisabledAttr) {
+ // 単純なリネーム
+ return fixer.replaceText(actionTextAttr.name, 'actionButton')
+ }
+ // 複雑な場合はエラーのみ
+ return null
+ }
+ })
+}
+```
+
+## テストケースのパターン
+
+### valid(エラーにならないケース)
+
+```javascript
+// decoratorsなし
+'',
+'',
+'',
+
+// AppLauncher with triggerLabel
+'',
+'',
+
+// 既に新しい属性を使用
+'',
+'',
+'',
+```
+
+### invalid(エラーになるケース)
+
+```javascript
+// decorators削除(自動修正可能)
+{
+ code: ' "Language" }} />',
+ output: '',
+ errors: [{ messageId: 'removeDecorators' }]
+},
+
+// AppLauncher decorators.triggerLabel(エラーのみ、自動修正なし)
+{
+ code: ' featureName }} />',
+ output: null, // 自動修正なし
+ errors: [{ messageId: 'migrateAppLauncherDecorators' }]
+},
+
+// AppLauncher(triggerLabel属性が既にある場合、decorators削除)
+{
+ code: ' "Apps" }} triggerLabel={featureName} />',
+ output: '',
+ errors: [{ messageId: 'migrateAppLauncherDecorators' }]
+},
+
+// actionText リネーム(自動修正可能)
+{
+ code: '',
+ output: '',
+ errors: [{ messageId: 'migrateActionText' }]
+},
+
+// 複数属性(エラーのみ、自動修正なし)
+{
+ code: '',
+ output: null, // 自動修正なし
+ errors: [
+ { messageId: 'migrateActionText' },
+ { messageId: 'migrateActionTheme' }
+ ]
+},
+
+// decorators.closeButtonLabel(エラーのみ、自動修正なし)
+{
+ code: ' "キャンセル" }} />',
+ output: null, // 自動修正なし
+ errors: [{ messageId: 'migrateDecoratorsCloseButtonLabel' }]
+},
+```
+
+## 新しいversionを追加する場合
+
+### シンプルな削除パターン(LanguageSwitcherタイプ)の場合
+
+1. **セレクター**: `JSXAttribute[name.name="属性名"]`
+2. **コンポーネント判定**: `if (!TARGET_COMPONENTS.includes(componentName)) return`
+3. **fix**: decorators属性を削除(前の空白も含む)
+4. **テスト**: valid(属性なし、他コンポーネント)、invalid(属性あり → 削除)
+
+### 複雑な属性統合パターン(FormDialogタイプ)の場合
+
+1. **属性収集**: すべての関連属性を先に収集
+2. **既存の新属性チェック**: 新しい属性が既にあるかチェック
+3. **段階的な fix**: 単純なケースのみ自動修正、複雑なケースはエラーのみ
+4. **テスト**: valid(新属性)、invalid(各パターン、自動修正可/不可)
+
+## 実装の参考ポイント
+
+**最新version:** [v94-to-v95/REFERENCE.md](./versions/v94-to-v95/REFERENCE.md)
+
+### ESLint fixer API
+
+- `fixer.remove(node)`: ノードを削除
+- `fixer.removeRange([start, end])`: 範囲を削除
+- `fixer.replaceText(node, text)`: ノードのテキストを置換
+- `fixer.insertTextAfter(node, text)`: ノードの後にテキスト挿入
+- `sourceCode.getTokenBefore(node)`: 前のトークンを取得
+- `sourceCode.getText(node)`: ノードのテキストを取得
+
+### 段階的な自動修正のアプローチ
+
+v94-to-v95のように複雑な変換が必要な場合、完全な自動修正を目指すと実装が複雑になりすぎます。以下のアプローチを推奨します:
+
+**1. 単純なケースのみ自動修正:**
+- `actionText` のみ → `actionButton` にリネーム
+- 既に新属性がある → 古い属性を削除
+
+**2. 複雑なケースはエラーのみ:**
+- 複数属性の統合 → エラーメッセージで手動対応を促す
+- 値の抽出が必要 → エラーメッセージで手動対応を促す
+
+**3. エラーメッセージで移行方法を明示:**
+```javascript
+messages: {
+ migrateActionText: 'smarthr-ui {{to}} では {{component}} の actionText 属性は actionButton に統合されました',
+ // READMEで詳しい移行方法を説明
+}
+```
+
+このアプローチにより:
+- 実装がシンプルに保たれる
+- 多くの一般的なケースは自動修正可能
+- 複雑なケースは手動で正確に対応できる
+
+## トラブルシューティング
+
+### 前の空白が残る
+
+**原因**: `fixer.remove(node)`のみだと、前の空白が残る
+
+**解決策**:
+```javascript
+const tokenBefore = sourceCode.getTokenBefore(node)
+if (tokenBefore && tokenBefore.range[1] < node.range[0]) {
+ return fixer.removeRange([tokenBefore.range[1], node.range[1]])
+}
+```
+
+### 複数の属性を統合する自動修正が難しい
+
+**原因**: Object形式への変換は複雑
+
+**解決策**: 段階的なアプローチ
+- 単純なケース(単一属性)のみ自動修正
+- 複雑なケース(複数属性)はエラーのみ表示
+- READMEで手動での移行方法を詳しく説明
+
+### decoratorsの値を抽出できない
+
+**原因**: `() => '文字列'` や `() => getLabel()` など、様々なパターンがある
+
+**解決策**:
+- 完全な解析は避ける
+- `sourceCode.getText(node.value).includes('closeButtonLabel')`で簡易チェック
+- 値の抽出は手動対応とする
diff --git a/packages/eslint-plugin-smarthr/rules/autofixer-smarthr-ui-migration/versions/v94-to-v95/index.js b/packages/eslint-plugin-smarthr/rules/autofixer-smarthr-ui-migration/versions/v94-to-v95/index.js
new file mode 100644
index 00000000..7933afa4
--- /dev/null
+++ b/packages/eslint-plugin-smarthr/rules/autofixer-smarthr-ui-migration/versions/v94-to-v95/index.js
@@ -0,0 +1,472 @@
+/**
+ * smarthr-ui v94 → v95 移行ルール
+ *
+ * v95での破壊的変更に対応する自動修正を提供します。
+ *
+ * 対応する破壊的変更:
+ * 1. LanguageSwitcher, AppLauncher の decorators 属性削除
+ * 2. InputFile の decorators 属性削除
+ * 3. FormDialog のボタン属性をObject形式に統合
+ * 4. ActionDialog のボタン属性をObject形式に統合
+ * 5. MessageDialog のdecorators削除とcloseButton属性への統一
+ *
+ * 参考: https://github.com/kufu/smarthr-ui/releases/tag/smarthr-ui-v95.0.0
+ */
+
+const { setupSmarthrUiAliasOptions } = require('../../helpers')
+
+// ============================================================
+// 定数定義
+// ============================================================
+
+// v95を示す定数(メッセージで使用)
+const TARGET_VERSION = 'v95'
+
+// decoratorsを削除するコンポーネント(単純削除のみ)
+const COMPONENTS_REMOVE_DECORATORS = ['LanguageSwitcher', 'InputFile']
+
+// ボタン属性を統合するコンポーネント(FormDialog, ActionDialog)
+const DIALOG_COMPONENTS_WITH_BUTTONS = ['FormDialog', 'ActionDialog']
+
+// ============================================================
+// モジュールエクスポート
+// ============================================================
+
+module.exports = {
+ messages: {
+ removeDecorators: 'smarthr-ui {{to}} では {{component}} の decorators 属性は削除されました。翻訳はsmarthr-ui内で自動的に行われます',
+ migrateAppLauncherDecorators: 'smarthr-ui {{to}} では AppLauncher の decorators.triggerLabel は triggerLabel 属性に移行されました。動的な値を渡す場合のみ triggerLabel 属性を使用してください',
+ migrateActionText: 'smarthr-ui {{to}} では {{component}} の actionText 属性は actionButton に統合されました',
+ migrateActionTheme: 'smarthr-ui {{to}} では {{component}} の actionTheme 属性は actionButton に統合されました',
+ migrateActionDisabled: 'smarthr-ui {{to}} では {{component}} の actionDisabled 属性は actionButton に統合されました',
+ migrateCloseDisabled: 'smarthr-ui {{to}} では {{component}} の closeDisabled 属性は closeButton に統合されました',
+ migrateDecoratorsCloseButtonLabel: 'smarthr-ui {{to}} では {{component}} の decorators.closeButtonLabel 属性は closeButton に統合されました',
+ migrateMessageDialogDecorators: 'smarthr-ui {{to}} では MessageDialog の decorators.closeButtonLabel 属性は closeButton に統合されました',
+ },
+
+ createCheckers(context, sourceCode, options = {}) {
+ const { validSources, isAliasFile, filename } = setupSmarthrUiAliasOptions(context, options)
+
+ const checkers = {
+ // ============================================================
+ // 1, 2. LanguageSwitcher, AppLauncher, InputFile の decorators 属性削除
+ // ============================================================
+
+ 'JSXAttribute[name.name="decorators"]'(node) {
+ const componentName = node.parent.name.name
+
+ // 対象コンポーネントのみ
+ if (!COMPONENTS_REMOVE_DECORATORS.includes(componentName)) return
+
+ context.report({
+ node,
+ messageId: 'removeDecorators',
+ data: { component: componentName, to: TARGET_VERSION },
+ fix(fixer) {
+ // decorators属性を削除
+ const tokenBefore = sourceCode.getTokenBefore(node)
+ if (tokenBefore && tokenBefore.range[1] < node.range[0]) {
+ return fixer.removeRange([tokenBefore.range[1], node.range[1]])
+ }
+ return fixer.remove(node)
+ },
+ })
+ },
+
+ // ============================================================
+ // 3, 4. FormDialog/ActionDialog のボタン属性統合
+ // ============================================================
+
+ // FormDialog/ActionDialog要素を検出
+ 'JSXOpeningElement'(node) {
+ const componentName = node.name.name
+
+ // 対象コンポーネントのみ
+ if (!DIALOG_COMPONENTS_WITH_BUTTONS.includes(componentName)) return
+
+ // 各属性を収集
+ let actionTextAttr = null
+ let actionThemeAttr = null
+ let actionDisabledAttr = null
+ let closeDisabledAttr = null
+ let decoratorsAttr = null
+ let actionButtonAttr = null
+ let closeButtonAttr = null
+
+ node.attributes.forEach((attr) => {
+ if (attr.type !== 'JSXAttribute') return
+ const attrName = attr.name.name
+
+ if (attrName === 'actionText') actionTextAttr = attr
+ if (attrName === 'actionTheme') actionThemeAttr = attr
+ if (attrName === 'actionDisabled') actionDisabledAttr = attr
+ if (attrName === 'closeDisabled') closeDisabledAttr = attr
+ if (attrName === 'decorators') decoratorsAttr = attr
+ if (attrName === 'actionButton') actionButtonAttr = attr
+ if (attrName === 'closeButton') closeButtonAttr = attr
+ })
+
+ // actionButton属性が既にある場合、古い属性は削除のみ
+ // closeButton属性が既にある場合、古い属性は削除のみ
+ const hasActionButton = !!actionButtonAttr
+ const hasCloseButton = !!closeButtonAttr
+
+ // actionText を actionButton に移行
+ if (actionTextAttr) {
+ context.report({
+ node: actionTextAttr,
+ messageId: 'migrateActionText',
+ data: { component: componentName, to: TARGET_VERSION },
+ fix(fixer) {
+ if (hasActionButton) {
+ // actionButton属性が既にある場合は削除のみ
+ const tokenBefore = sourceCode.getTokenBefore(actionTextAttr)
+ if (tokenBefore && tokenBefore.range[1] < actionTextAttr.range[0]) {
+ return fixer.removeRange([tokenBefore.range[1], actionTextAttr.range[1]])
+ }
+ return fixer.remove(actionTextAttr)
+ }
+
+ // actionButton属性がない場合、actionTextをactionButtonにリネーム
+ // 他の属性(actionTheme, actionDisabled)がある場合はObject形式に変換
+ if (!actionThemeAttr && !actionDisabledAttr) {
+ // actionTextのみの場合は単純にリネーム
+ return fixer.replaceText(actionTextAttr.name, 'actionButton')
+ }
+
+ // 複数の属性がある場合、Object形式に変換
+ const fixes = []
+
+ // actionButtonの値を構築
+ const textValue = getAttributeValue(actionTextAttr)
+ const themeValue = actionThemeAttr ? getAttributeValue(actionThemeAttr) : null
+ const disabledValue = actionDisabledAttr ? getAttributeValue(actionDisabledAttr) : null
+
+ const objectParts = [`text: ${textValue}`]
+ if (themeValue) objectParts.push(`theme: ${themeValue}`)
+ if (disabledValue !== null) objectParts.push(`disabled: ${disabledValue}`)
+
+ const newValue = `actionButton={{ ${objectParts.join(', ')} }}`
+
+ // actionText属性をactionButton={{ ... }}に置換
+ fixes.push(fixer.replaceText(actionTextAttr, newValue))
+
+ // actionTheme属性を削除
+ if (actionThemeAttr) {
+ const tokenBefore = sourceCode.getTokenBefore(actionThemeAttr)
+ if (tokenBefore && tokenBefore.range[1] < actionThemeAttr.range[0]) {
+ fixes.push(fixer.removeRange([tokenBefore.range[1], actionThemeAttr.range[1]]))
+ } else {
+ fixes.push(fixer.remove(actionThemeAttr))
+ }
+ }
+
+ // actionDisabled属性を削除
+ if (actionDisabledAttr) {
+ const tokenBefore = sourceCode.getTokenBefore(actionDisabledAttr)
+ if (tokenBefore && tokenBefore.range[1] < actionDisabledAttr.range[0]) {
+ fixes.push(fixer.removeRange([tokenBefore.range[1], actionDisabledAttr.range[1]]))
+ } else {
+ fixes.push(fixer.remove(actionDisabledAttr))
+ }
+ }
+
+ return fixes
+ },
+ })
+ }
+
+ // actionTheme を actionButton に移行
+ if (actionThemeAttr) {
+ context.report({
+ node: actionThemeAttr,
+ messageId: 'migrateActionTheme',
+ data: { component: componentName, to: TARGET_VERSION },
+ fix(fixer) {
+ // actionButton属性が既にある場合は削除のみ
+ if (hasActionButton) {
+ const tokenBefore = sourceCode.getTokenBefore(actionThemeAttr)
+ if (tokenBefore && tokenBefore.range[1] < actionThemeAttr.range[0]) {
+ return fixer.removeRange([tokenBefore.range[1], actionThemeAttr.range[1]])
+ }
+ return fixer.remove(actionThemeAttr)
+ }
+
+ // actionTextが存在する場合、actionTextのfixでまとめて処理されるためnull
+ if (actionTextAttr) {
+ return null
+ }
+
+ // actionTextがなくactionThemeのみの場合はエラーのみ(想定外のパターン)
+ return null
+ },
+ })
+ }
+
+ // actionDisabled を actionButton に移行
+ if (actionDisabledAttr) {
+ context.report({
+ node: actionDisabledAttr,
+ messageId: 'migrateActionDisabled',
+ data: { component: componentName, to: TARGET_VERSION },
+ fix(fixer) {
+ // actionButton属性が既にある場合は削除のみ
+ if (hasActionButton) {
+ const tokenBefore = sourceCode.getTokenBefore(actionDisabledAttr)
+ if (tokenBefore && tokenBefore.range[1] < actionDisabledAttr.range[0]) {
+ return fixer.removeRange([tokenBefore.range[1], actionDisabledAttr.range[1]])
+ }
+ return fixer.remove(actionDisabledAttr)
+ }
+
+ // actionTextが存在する場合、actionTextのfixでまとめて処理されるためnull
+ if (actionTextAttr) {
+ return null
+ }
+
+ // actionTextがなくactionDisabledのみの場合はエラーのみ(想定外のパターン)
+ return null
+ },
+ })
+ }
+
+ // closeDisabled を closeButton に移行
+ if (closeDisabledAttr) {
+ context.report({
+ node: closeDisabledAttr,
+ messageId: 'migrateCloseDisabled',
+ data: { component: componentName, to: TARGET_VERSION },
+ fix(fixer) {
+ // closeButton属性が既にある場合は削除のみ
+ if (hasCloseButton) {
+ const tokenBefore = sourceCode.getTokenBefore(closeDisabledAttr)
+ if (tokenBefore && tokenBefore.range[1] < closeDisabledAttr.range[0]) {
+ return fixer.removeRange([tokenBefore.range[1], closeDisabledAttr.range[1]])
+ }
+ return fixer.remove(closeDisabledAttr)
+ }
+
+ // 複雑なため、エラーのみ表示(手動対応)
+ return null
+ },
+ })
+ }
+
+ // decorators.closeButtonLabel を closeButton に移行
+ if (decoratorsAttr) {
+ // decorators属性の値を解析してcloseButtonLabelがあるかチェック
+ const decoratorsValue = sourceCode.getText(decoratorsAttr.value)
+ if (decoratorsValue.includes('closeButtonLabel')) {
+ context.report({
+ node: decoratorsAttr,
+ messageId: 'migrateDecoratorsCloseButtonLabel',
+ data: { component: componentName, to: TARGET_VERSION },
+ fix(fixer) {
+ // closeButton属性が既にある場合は削除のみ
+ if (hasCloseButton) {
+ const tokenBefore = sourceCode.getTokenBefore(decoratorsAttr)
+ if (tokenBefore && tokenBefore.range[1] < decoratorsAttr.range[0]) {
+ return fixer.removeRange([tokenBefore.range[1], decoratorsAttr.range[1]])
+ }
+ return fixer.remove(decoratorsAttr)
+ }
+
+ // decorators={{ closeButtonLabel: () => "OK" }}から値を抽出
+ const extractedValue = extractDecoratorValue(decoratorsAttr, 'closeButtonLabel')
+ if (extractedValue) {
+ // 自動修正可能: decoratorsを削除してcloseButtonを追加
+ return [
+ fixer.replaceText(decoratorsAttr, `closeButton=${extractedValue}`),
+ ]
+ }
+
+ // 複雑なため、エラーのみ表示(手動対応)
+ return null
+ },
+ })
+ }
+ }
+ },
+
+ // ============================================================
+ // AppLauncher の decorators.triggerLabel を triggerLabel に移行
+ // ============================================================
+
+ 'JSXOpeningElement[name.name="AppLauncher"] > JSXAttribute[name.name="decorators"]'(node) {
+ // decorators属性の値を解析してtriggerLabelがあるかチェック
+ const decoratorsValue = sourceCode.getText(node.value)
+ if (decoratorsValue.includes('triggerLabel')) {
+ // triggerLabel属性が既にあるかチェック
+ const triggerLabelAttr = node.parent.attributes.find(
+ (attr) => attr.type === 'JSXAttribute' && attr.name.name === 'triggerLabel'
+ )
+
+ context.report({
+ node,
+ messageId: 'migrateAppLauncherDecorators',
+ data: { to: TARGET_VERSION },
+ fix(fixer) {
+ // triggerLabel属性が既にある場合は削除のみ
+ if (triggerLabelAttr) {
+ const tokenBefore = sourceCode.getTokenBefore(node)
+ if (tokenBefore && tokenBefore.range[1] < node.range[0]) {
+ return fixer.removeRange([tokenBefore.range[1], node.range[1]])
+ }
+ return fixer.remove(node)
+ }
+
+ // decorators={{ triggerLabel: () => "Apps" }}から値を抽出
+ const extractedValue = extractDecoratorValue(node, 'triggerLabel')
+ if (extractedValue) {
+ // 固定値(リテラル)の場合 → decoratorsを削除するだけ
+ // 動的な値(変数など)の場合 → triggerLabel属性に移行
+ if (extractedValue.startsWith('"') || extractedValue.startsWith("'")) {
+ // 固定値: decoratorsを削除
+ const tokenBefore = sourceCode.getTokenBefore(node)
+ if (tokenBefore && tokenBefore.range[1] < node.range[0]) {
+ return fixer.removeRange([tokenBefore.range[1], node.range[1]])
+ }
+ return fixer.remove(node)
+ } else {
+ // 動的な値: triggerLabel属性に移行
+ return fixer.replaceText(node, `triggerLabel=${extractedValue}`)
+ }
+ }
+
+ // 複雑なため、エラーのみ表示(手動対応)
+ return null
+ },
+ })
+ }
+ },
+
+ // ============================================================
+ // 5. MessageDialog の decorators 削除と closeButton への統一
+ // ============================================================
+
+ 'JSXOpeningElement[name.name="MessageDialog"] > JSXAttribute[name.name="decorators"]'(node) {
+ // decorators属性の値を解析してcloseButtonLabelがあるかチェック
+ const decoratorsValue = sourceCode.getText(node.value)
+ if (decoratorsValue.includes('closeButtonLabel')) {
+ // closeButton属性が既にあるかチェック
+ const closeButtonAttr = node.parent.attributes.find(
+ (attr) => attr.type === 'JSXAttribute' && attr.name.name === 'closeButton'
+ )
+
+ context.report({
+ node,
+ messageId: 'migrateMessageDialogDecorators',
+ data: { to: TARGET_VERSION },
+ fix(fixer) {
+ // closeButton属性が既にある場合は削除のみ
+ if (closeButtonAttr) {
+ const tokenBefore = sourceCode.getTokenBefore(node)
+ if (tokenBefore && tokenBefore.range[1] < node.range[0]) {
+ return fixer.removeRange([tokenBefore.range[1], node.range[1]])
+ }
+ return fixer.remove(node)
+ }
+
+ // decorators={{ closeButtonLabel: () => "OK" }}から値を抽出
+ const extractedValue = extractDecoratorValue(node, 'closeButtonLabel')
+ if (extractedValue) {
+ // 自動修正可能: decoratorsを削除してcloseButtonを追加
+ return fixer.replaceText(node, `closeButton=${extractedValue}`)
+ }
+
+ // 複雑なため、エラーのみ表示(手動対応)
+ return null
+ },
+ })
+ }
+ },
+ }
+
+ // ============================================================
+ // ヘルパー関数
+ // ============================================================
+
+ /**
+ * JSX属性の値を取得
+ *
+ * @param {Object} attr - 属性のASTノード
+ * @returns {string} 属性値の文字列表現
+ */
+ function getAttributeValue(attr) {
+ if (!attr || !attr.value) return '""'
+
+ // 文字列リテラル: actionText="保存"
+ if (attr.value.type === 'Literal') {
+ return JSON.stringify(attr.value.value)
+ }
+
+ // JSX式: actionTheme={"danger"} または actionDisabled={false}
+ if (attr.value.type === 'JSXExpressionContainer') {
+ const expr = attr.value.expression
+ if (expr.type === 'Literal') {
+ return JSON.stringify(expr.value)
+ }
+ // 変数や式の場合はソースコードをそのまま取得
+ return sourceCode.getText(expr)
+ }
+
+ return '""'
+ }
+
+ /**
+ * decorators属性から指定されたプロパティの値を抽出
+ *
+ * decorators={{ closeButtonLabel: () => "OK" }} から "OK" を抽出
+ * decorators={{ closeButtonLabel: () => variable }} から {variable} を抽出
+ *
+ * @param {Object} decoratorsAttr - decorators属性のASTノード
+ * @param {string} propertyName - 抽出するプロパティ名(例: 'closeButtonLabel')
+ * @returns {string|null} 抽出された値の文字列表現、または抽出不可の場合null
+ */
+ function extractDecoratorValue(decoratorsAttr, propertyName) {
+ if (!decoratorsAttr || !decoratorsAttr.value) return null
+
+ // decorators={...}のJSXExpressionContainerを取得
+ if (decoratorsAttr.value.type !== 'JSXExpressionContainer') return null
+
+ const expr = decoratorsAttr.value.expression
+ // ObjectExpression: { closeButtonLabel: ... }
+ if (expr.type !== 'ObjectExpression') return null
+
+ // closeButtonLabelプロパティを探す
+ const property = expr.properties.find((prop) => {
+ return (
+ prop.type === 'Property' &&
+ prop.key &&
+ ((prop.key.type === 'Identifier' && prop.key.name === propertyName) ||
+ (prop.key.type === 'Literal' && prop.key.value === propertyName))
+ )
+ })
+
+ if (!property || !property.value) return null
+
+ // ArrowFunctionExpression: () => "OK"
+ if (property.value.type !== 'ArrowFunctionExpression') return null
+
+ // 引数なしの場合のみ処理
+ if (property.value.params.length !== 0) return null
+
+ const body = property.value.body
+
+ // BlockStatement(ブロック形式)は対応しない: () => { return "OK" }
+ if (body.type === 'BlockStatement') return null
+
+ // Literal: () => "OK" → "OK"
+ if (body.type === 'Literal') {
+ return JSON.stringify(body.value)
+ }
+
+ // その他のExpression: () => variable → {variable}
+ // () => a() → {a()}
+ // () => obj.prop → {obj.prop}
+ return `{${sourceCode.getText(body)}}`
+ }
+
+ return checkers
+ },
+}
diff --git a/packages/eslint-plugin-smarthr/rules/autofixer-smarthr-ui-migration/versions/v94-to-v95/test.js b/packages/eslint-plugin-smarthr/rules/autofixer-smarthr-ui-migration/versions/v94-to-v95/test.js
new file mode 100644
index 00000000..b8ee5024
--- /dev/null
+++ b/packages/eslint-plugin-smarthr/rules/autofixer-smarthr-ui-migration/versions/v94-to-v95/test.js
@@ -0,0 +1,482 @@
+/**
+ * smarthr-ui v94 → v95 移行ルール テストケース
+ */
+
+const v94ToV95Options = [{ from: '94', to: '95' }]
+
+// ============================================================
+// validテストケース(エラーにならないコード)
+// ============================================================
+
+const valid = [
+ // decoratorsなし
+ { code: '', options: v94ToV95Options },
+ { code: '', options: v94ToV95Options },
+ { code: '', options: v94ToV95Options },
+
+ // AppLauncher with triggerLabel
+ { code: '', options: v94ToV95Options },
+ { code: '', options: v94ToV95Options },
+
+ // 既に新しい属性を使用
+ { code: '', options: v94ToV95Options },
+ { code: '', options: v94ToV95Options },
+ { code: '', options: v94ToV95Options },
+ { code: '', options: v94ToV95Options },
+
+ // 対象外のコンポーネント
+ { code: ' "全選択" }} />', options: v94ToV95Options },
+ { code: '', options: v94ToV95Options },
+]
+
+// ============================================================
+// invalidテストケース(エラーになり、自動修正されるコード)
+// ============================================================
+
+const invalid = [
+ // ============================================================
+ // LanguageSwitcher, AppLauncher の decorators 属性削除
+ // ============================================================
+
+ // LanguageSwitcher
+ {
+ code: ' "Language" }} />',
+ output: '',
+ options: v94ToV95Options,
+ errors: [
+ {
+ messageId: 'removeDecorators',
+ data: { component: 'LanguageSwitcher', to: 'v95' },
+ },
+ ],
+ },
+
+ // AppLauncher(固定値の場合、decoratorsを削除)
+ {
+ code: ' "Apps" }} />',
+ output: '',
+ options: v94ToV95Options,
+ errors: [
+ {
+ messageId: 'migrateAppLauncherDecorators',
+ data: { to: 'v95' },
+ },
+ ],
+ },
+
+ // AppLauncher(シングルクォートの固定値、decoratorsを削除)
+ {
+ code: " 'Apps' }} />",
+ output: '',
+ options: v94ToV95Options,
+ errors: [
+ {
+ messageId: 'migrateAppLauncherDecorators',
+ data: { to: 'v95' },
+ },
+ ],
+ },
+
+ // AppLauncher(動的な値、triggerLabel属性に移行)
+ {
+ code: ' featureName }} />',
+ output: '',
+ options: v94ToV95Options,
+ errors: [
+ {
+ messageId: 'migrateAppLauncherDecorators',
+ data: { to: 'v95' },
+ },
+ ],
+ },
+
+ // AppLauncher(関数呼び出し、triggerLabel属性に移行)
+ {
+ code: ' getLabel() }} />',
+ output: '',
+ options: v94ToV95Options,
+ errors: [
+ {
+ messageId: 'migrateAppLauncherDecorators',
+ data: { to: 'v95' },
+ },
+ ],
+ },
+
+ // AppLauncher(オブジェクトプロパティ、triggerLabel属性に移行)
+ {
+ code: ' labels.app }} />',
+ output: '',
+ options: v94ToV95Options,
+ errors: [
+ {
+ messageId: 'migrateAppLauncherDecorators',
+ data: { to: 'v95' },
+ },
+ ],
+ },
+
+ // AppLauncher(引数あり、自動修正不可)
+ {
+ code: ' lang === "ja" ? "アプリ" : "Apps" }} />',
+ output: null,
+ options: v94ToV95Options,
+ errors: [
+ {
+ messageId: 'migrateAppLauncherDecorators',
+ data: { to: 'v95' },
+ },
+ ],
+ },
+
+ // AppLauncher(BlockStatement、自動修正不可)
+ {
+ code: ' { return "Apps" } }} />',
+ output: null,
+ options: v94ToV95Options,
+ errors: [
+ {
+ messageId: 'migrateAppLauncherDecorators',
+ data: { to: 'v95' },
+ },
+ ],
+ },
+
+ // AppLauncher(triggerLabel属性が既にある場合、decoratorsを削除)
+ {
+ code: ' "Apps" }} triggerLabel={featureName} />',
+ output: '',
+ options: v94ToV95Options,
+ errors: [
+ {
+ messageId: 'migrateAppLauncherDecorators',
+ data: { to: 'v95' },
+ },
+ ],
+ },
+
+ // 改行あり
+ {
+ code: ` "言語" }}
+/>`,
+ output: ``,
+ options: v94ToV95Options,
+ errors: [
+ {
+ messageId: 'removeDecorators',
+ data: { component: 'LanguageSwitcher', to: 'v95' },
+ },
+ ],
+ },
+
+ // ============================================================
+ // InputFile の decorators 属性削除
+ // ============================================================
+
+ {
+ code: ' "削除" }} />',
+ output: '',
+ options: v94ToV95Options,
+ errors: [
+ {
+ messageId: 'removeDecorators',
+ data: { component: 'InputFile', to: 'v95' },
+ },
+ ],
+ },
+
+ // ============================================================
+ // FormDialog のボタン属性統合
+ // ============================================================
+
+ // actionText のみ(自動修正可能)
+ {
+ code: '',
+ output: '',
+ options: v94ToV95Options,
+ errors: [
+ {
+ messageId: 'migrateActionText',
+ data: { component: 'FormDialog', to: 'v95' },
+ },
+ ],
+ },
+
+ // actionText + actionTheme(自動修正可能)
+ {
+ code: '',
+ output: '',
+ options: v94ToV95Options,
+ errors: [
+ {
+ messageId: 'migrateActionText',
+ data: { component: 'FormDialog', to: 'v95' },
+ },
+ {
+ messageId: 'migrateActionTheme',
+ data: { component: 'FormDialog', to: 'v95' },
+ },
+ ],
+ },
+
+ // actionText + actionDisabled(自動修正可能)
+ {
+ code: '',
+ output: '',
+ options: v94ToV95Options,
+ errors: [
+ {
+ messageId: 'migrateActionText',
+ data: { component: 'FormDialog', to: 'v95' },
+ },
+ {
+ messageId: 'migrateActionDisabled',
+ data: { component: 'FormDialog', to: 'v95' },
+ },
+ ],
+ },
+
+ // actionText + actionTheme + actionDisabled(自動修正可能)
+ {
+ code: '',
+ output: '',
+ options: v94ToV95Options,
+ errors: [
+ {
+ messageId: 'migrateActionText',
+ data: { component: 'FormDialog', to: 'v95' },
+ },
+ {
+ messageId: 'migrateActionTheme',
+ data: { component: 'FormDialog', to: 'v95' },
+ },
+ {
+ messageId: 'migrateActionDisabled',
+ data: { component: 'FormDialog', to: 'v95' },
+ },
+ ],
+ },
+
+ // closeDisabled(エラーのみ、自動修正なし)
+ {
+ code: '',
+ output: '',
+ options: v94ToV95Options,
+ errors: [
+ {
+ messageId: 'migrateActionText',
+ data: { component: 'FormDialog', to: 'v95' },
+ },
+ {
+ messageId: 'migrateCloseDisabled',
+ data: { component: 'FormDialog', to: 'v95' },
+ },
+ ],
+ },
+
+ // decorators.closeButtonLabel(自動修正可能)
+ {
+ code: ' "キャンセル" }} />',
+ output: '',
+ options: v94ToV95Options,
+ errors: [
+ {
+ messageId: 'migrateDecoratorsCloseButtonLabel',
+ data: { component: 'FormDialog', to: 'v95' },
+ },
+ ],
+ },
+
+ // actionButton が既にある場合、actionText を削除
+ {
+ code: '',
+ output: '',
+ options: v94ToV95Options,
+ errors: [
+ {
+ messageId: 'migrateActionText',
+ data: { component: 'FormDialog', to: 'v95' },
+ },
+ ],
+ },
+
+ // closeButton が既にある場合、decorators を削除
+ {
+ code: ' "キャンセル" }} closeButton="閉じる" />',
+ output: '',
+ options: v94ToV95Options,
+ errors: [
+ {
+ messageId: 'migrateDecoratorsCloseButtonLabel',
+ data: { component: 'FormDialog', to: 'v95' },
+ },
+ ],
+ },
+
+ // ============================================================
+ // ActionDialog のボタン属性統合
+ // ============================================================
+
+ // actionText のみ(自動修正可能)
+ {
+ code: '',
+ output: '',
+ options: v94ToV95Options,
+ errors: [
+ {
+ messageId: 'migrateActionText',
+ data: { component: 'ActionDialog', to: 'v95' },
+ },
+ ],
+ },
+
+ // actionText + actionTheme(自動修正可能)
+ {
+ code: '',
+ output: '',
+ options: v94ToV95Options,
+ errors: [
+ {
+ messageId: 'migrateActionText',
+ data: { component: 'ActionDialog', to: 'v95' },
+ },
+ {
+ messageId: 'migrateActionTheme',
+ data: { component: 'ActionDialog', to: 'v95' },
+ },
+ ],
+ },
+
+ // ============================================================
+ // MessageDialog の decorators 削除
+ // ============================================================
+
+ // decorators.closeButtonLabel(自動修正可能)
+ {
+ code: ' "OK" }} />',
+ output: '',
+ options: v94ToV95Options,
+ errors: [
+ {
+ messageId: 'migrateMessageDialogDecorators',
+ data: { to: 'v95' },
+ },
+ ],
+ },
+
+ // closeButton が既にある場合、decorators を削除
+ {
+ code: ' "OK" }} closeButton="閉じる" />',
+ output: '',
+ options: v94ToV95Options,
+ errors: [
+ {
+ messageId: 'migrateMessageDialogDecorators',
+ data: { to: 'v95' },
+ },
+ ],
+ },
+
+ // decorators.closeButtonLabel(シングルクォート、自動修正可能)
+ {
+ code: " 'OK' }} />",
+ output: '',
+ options: v94ToV95Options,
+ errors: [
+ {
+ messageId: 'migrateMessageDialogDecorators',
+ data: { to: 'v95' },
+ },
+ ],
+ },
+
+ // decorators.closeButtonLabel(変数、自動修正可能)
+ {
+ code: ' buttonLabel }} />',
+ output: '',
+ options: v94ToV95Options,
+ errors: [
+ {
+ messageId: 'migrateMessageDialogDecorators',
+ data: { to: 'v95' },
+ },
+ ],
+ },
+
+ // decorators.closeButtonLabel(BlockStatement、自動修正不可)
+ {
+ code: ' { return "OK" } }} />',
+ output: null,
+ options: v94ToV95Options,
+ errors: [
+ {
+ messageId: 'migrateMessageDialogDecorators',
+ data: { to: 'v95' },
+ },
+ ],
+ },
+
+ // decorators.closeButtonLabel(関数呼び出し、自動修正可能)
+ {
+ code: ' getLabel() }} />',
+ output: '',
+ options: v94ToV95Options,
+ errors: [
+ {
+ messageId: 'migrateMessageDialogDecorators',
+ data: { to: 'v95' },
+ },
+ ],
+ },
+
+ // decorators.closeButtonLabel(オブジェクトプロパティ、自動修正可能)
+ {
+ code: ' labels.close }} />',
+ output: '',
+ options: v94ToV95Options,
+ errors: [
+ {
+ messageId: 'migrateMessageDialogDecorators',
+ data: { to: 'v95' },
+ },
+ ],
+ },
+
+ // decorators.closeButtonLabel(引数あり、自動修正不可)
+ {
+ code: ' lang === "ja" ? "OK" : "Close" }} />',
+ output: null,
+ options: v94ToV95Options,
+ errors: [
+ {
+ messageId: 'migrateMessageDialogDecorators',
+ data: { to: 'v95' },
+ },
+ ],
+ },
+
+ // 改行あり
+ {
+ code: `
+ 内容
+`,
+ output: `
+ 内容
+`,
+ options: v94ToV95Options,
+ errors: [
+ {
+ messageId: 'migrateActionText',
+ data: { component: 'FormDialog', to: 'v95' },
+ },
+ ],
+ },
+]
+
+module.exports = { valid, invalid }
diff --git a/packages/eslint-plugin-smarthr/test/autofixer-smarthr-ui-migration.js b/packages/eslint-plugin-smarthr/test/autofixer-smarthr-ui-migration.js
index a14cb36f..7cb31bce 100644
--- a/packages/eslint-plugin-smarthr/test/autofixer-smarthr-ui-migration.js
+++ b/packages/eslint-plugin-smarthr/test/autofixer-smarthr-ui-migration.js
@@ -4,6 +4,7 @@ const v90ToV91Tests = require('../rules/autofixer-smarthr-ui-migration/versions/
const v91ToV92Tests = require('../rules/autofixer-smarthr-ui-migration/versions/v91-to-v92/test')
const v92ToV93Tests = require('../rules/autofixer-smarthr-ui-migration/versions/v92-to-v93/test')
const v93ToV94Tests = require('../rules/autofixer-smarthr-ui-migration/versions/v93-to-v94/test')
+const v94ToV95Tests = require('../rules/autofixer-smarthr-ui-migration/versions/v94-to-v95/test')
const ruleTester = new RuleTester({
languageOptions: {
@@ -21,6 +22,7 @@ ruleTester.run('autofixer-smarthr-ui-migration', rule, {
...v91ToV92Tests.valid,
...v92ToV93Tests.valid,
...v93ToV94Tests.valid,
+ ...v94ToV95Tests.valid,
],
invalid: [
@@ -33,7 +35,7 @@ ruleTester.run('autofixer-smarthr-ui-migration', rule, {
},
{
code: `import { ActionDialog } from 'smarthr-ui'`,
- options: [{ from: '94', to: '95' }],
+ options: [{ from: '95', to: '96' }],
errors: [{ messageId: 'unsupportedVersion' }],
},
@@ -42,9 +44,9 @@ ruleTester.run('autofixer-smarthr-ui-migration', rule, {
// ============================================================
{
code: `import { ThCheckbox } from 'smarthr-ui'`,
- options: [{ from: '93', to: '95' }],
+ options: [{ from: '94', to: '96' }],
errors: [
- { messageId: 'skippedVersion', data: { version: 'v95' } },
+ { messageId: 'skippedVersion', data: { version: 'v96' } },
],
},
{
@@ -69,5 +71,6 @@ ruleTester.run('autofixer-smarthr-ui-migration', rule, {
...v91ToV92Tests.invalid,
...v92ToV93Tests.invalid,
...v93ToV94Tests.invalid,
+ ...v94ToV95Tests.invalid,
],
})