mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-05-19 23:33:07 +08:00
docs: add native Japanese translation of ECC documentation (ja-JP)
Translate everything-claude-code repository to Japanese including: - 17 root documentation files - 60 agent documentation files - 80 command documentation files - 99 rule files across 18 language directories (common, angular, arkts, cpp, csharp, dart, fsharp, golang, java, kotlin, perl, php, python, ruby, rust, swift, typescript, web) - 199 skill documentation files Total: 455 files translated to Japanese with: - Consistent terminology glossary applied throughout - YAML field names preserved in English (name, description, etc.) - Code blocks and examples untouched (comments translated) - Markdown structure and relative links preserved - Professional translation maintaining technical accuracy This translation expands ECC accessibility to Japanese-speaking developers and teams. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
159
docs/ja-JP/rules/dart/coding-style.md
Normal file
159
docs/ja-JP/rules/dart/coding-style.md
Normal file
@@ -0,0 +1,159 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.dart"
|
||||
- "**/pubspec.yaml"
|
||||
- "**/analysis_options.yaml"
|
||||
---
|
||||
# Dart/Flutter コーディングスタイル
|
||||
|
||||
> このファイルは [common/coding-style.md](../common/coding-style.md) を Dart および Flutter 固有のコンテンツで拡張します。
|
||||
|
||||
## フォーマット
|
||||
|
||||
- すべての `.dart` ファイルに **dart format** を使用 — CI で強制適用 (`dart format --set-exit-if-changed .`)
|
||||
- 行の長さ: 80文字 (dart format のデフォルト)
|
||||
- 差分とフォーマットを改善するため、複数行の引数/パラメータリストには末尾カンマを付ける
|
||||
|
||||
## イミュータビリティ
|
||||
|
||||
- ローカル変数には `final` を、コンパイル時定数には `const` を優先する
|
||||
- すべてのフィールドが `final` の場合は `const` コンストラクタを使用する
|
||||
- パブリック API からは変更不可コレクションを返す (`List.unmodifiable`、`Map.unmodifiable`)
|
||||
- イミュータブルなステートクラスでのステート変更には `copyWith()` を使用する
|
||||
|
||||
```dart
|
||||
// BAD
|
||||
var count = 0;
|
||||
List<String> items = ['a', 'b'];
|
||||
|
||||
// GOOD
|
||||
final count = 0;
|
||||
const items = ['a', 'b'];
|
||||
```
|
||||
|
||||
## 命名規則
|
||||
|
||||
Dart の規約に従う:
|
||||
- 変数、パラメータ、名前付きコンストラクタには `camelCase`
|
||||
- クラス、列挙型、typedef、拡張機能には `PascalCase`
|
||||
- ファイル名とライブラリ名には `snake_case`
|
||||
- トップレベルで `const` 宣言された定数には `SCREAMING_SNAKE_CASE`
|
||||
- プライベートメンバーには `_` プレフィックスを付ける
|
||||
- 拡張機能名は拡張対象の型を表す: `MyHelpers` ではなく `StringExtensions`
|
||||
|
||||
## Null 安全性
|
||||
|
||||
- `!` (bang演算子) の使用を避ける — `?.`、`??`、`if (x != null)`、またはDart 3のパターンマッチングを優先する。`!` はnullがプログラムエラーを示し、クラッシュが適切な動作である場合にのみ使用する
|
||||
- `late` の使用は初めて使用される前に初期化が保証されている場合のみに限定する(nullableまたはコンストラクタ初期化を優先する)
|
||||
- 常に提供しなければならないコンストラクタパラメータには `required` を使用する
|
||||
|
||||
```dart
|
||||
// BAD — user が null の場合、実行時にクラッシュする
|
||||
final name = user!.name;
|
||||
|
||||
// GOOD — null対応演算子を使用
|
||||
final name = user?.name ?? 'Unknown';
|
||||
|
||||
// GOOD — Dart 3 パターンマッチング (網羅的、コンパイラによるチェック)
|
||||
final name = switch (user) {
|
||||
User(:final name) => name,
|
||||
null => 'Unknown',
|
||||
};
|
||||
|
||||
// GOOD — 早期リターンによる null ガード
|
||||
String getUserName(User? user) {
|
||||
if (user == null) return 'Unknown';
|
||||
return user.name; // ガードの後、非nullに昇格
|
||||
}
|
||||
```
|
||||
|
||||
## sealed 型とパターンマッチング (Dart 3+)
|
||||
|
||||
クローズドな状態階層をモデル化するには sealed クラスを使用する:
|
||||
|
||||
```dart
|
||||
sealed class AsyncState<T> {
|
||||
const AsyncState();
|
||||
}
|
||||
|
||||
final class Loading<T> extends AsyncState<T> {
|
||||
const Loading();
|
||||
}
|
||||
|
||||
final class Success<T> extends AsyncState<T> {
|
||||
const Success(this.data);
|
||||
final T data;
|
||||
}
|
||||
|
||||
final class Failure<T> extends AsyncState<T> {
|
||||
const Failure(this.error);
|
||||
final Object error;
|
||||
}
|
||||
```
|
||||
|
||||
sealed 型には常に網羅的な `switch` を使用する — default/ワイルドカードは使用しない:
|
||||
|
||||
```dart
|
||||
// BAD
|
||||
if (state is Loading) { ... }
|
||||
|
||||
// GOOD
|
||||
return switch (state) {
|
||||
Loading() => const CircularProgressIndicator(),
|
||||
Success(:final data) => DataWidget(data),
|
||||
Failure(:final error) => ErrorWidget(error.toString()),
|
||||
};
|
||||
```
|
||||
|
||||
## エラーハンドリング
|
||||
|
||||
- `on` 節で例外の型を指定する — 裸の `catch (e)` は絶対に使用しない
|
||||
- `Error` サブタイプは絶対にキャッチしない — それらはプログラムのバグを示す
|
||||
- 回復可能なエラーには `Result` スタイルの型またはsealed クラスを使用する
|
||||
- 制御フローに例外を使用しない
|
||||
|
||||
```dart
|
||||
// BAD
|
||||
try {
|
||||
await fetchUser();
|
||||
} catch (e) {
|
||||
log(e.toString());
|
||||
}
|
||||
|
||||
// GOOD
|
||||
try {
|
||||
await fetchUser();
|
||||
} on NetworkException catch (e) {
|
||||
log('Network error: ${e.message}');
|
||||
} on NotFoundException {
|
||||
handleNotFound();
|
||||
}
|
||||
```
|
||||
|
||||
## 非同期 / Future
|
||||
|
||||
- 常に Future を `await` するか、意図的なfire-and-forgetを示すために明示的に `unawaited()` を呼び出す
|
||||
- 何も `await` しない場合は関数を `async` とマークしない
|
||||
- 並行操作には `Future.wait` / `Future.any` を使用する
|
||||
- `await` の後に `BuildContext` を使用する前に `context.mounted` を確認する (Flutter 3.7+)
|
||||
|
||||
```dart
|
||||
// BAD — Future を無視している
|
||||
fetchData(); // 意図を示さずにfire-and-forget
|
||||
|
||||
// GOOD
|
||||
unawaited(fetchData()); // 明示的なfire-and-forget
|
||||
await fetchData(); // または適切に await する
|
||||
```
|
||||
|
||||
## インポート
|
||||
|
||||
- 全体を通じて `package:` インポートを使用する — クロスフィーチャーまたはクロスレイヤーのコードに相対インポート (`../`) を使用しない
|
||||
- 順序: `dart:` → 外部 `package:` → 内部 `package:` (同じパッケージ)
|
||||
- 未使用のインポートは禁止 — `dart analyze` が `unused_import` で強制する
|
||||
|
||||
## コード生成
|
||||
|
||||
- 生成されたファイル (`.g.dart`、`.freezed.dart`、`.gr.dart`) はコミットするかgitignoreで一貫して除外する — プロジェクトごとに1つの戦略を選択する
|
||||
- 生成されたファイルを手動で編集しない
|
||||
- ジェネレータアノテーション (`@JsonSerializable`、`@freezed`、`@riverpod` 等) は正規のソースファイルのみに記述する
|
||||
66
docs/ja-JP/rules/dart/hooks.md
Normal file
66
docs/ja-JP/rules/dart/hooks.md
Normal file
@@ -0,0 +1,66 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.dart"
|
||||
- "**/pubspec.yaml"
|
||||
- "**/analysis_options.yaml"
|
||||
---
|
||||
# Dart/Flutter フック
|
||||
|
||||
> このファイルは [common/hooks.md](../common/hooks.md) を Dart および Flutter 固有のコンテンツで拡張します。
|
||||
|
||||
## PostToolUse フック
|
||||
|
||||
`~/.claude/settings.json` で設定する:
|
||||
|
||||
- **dart format**: 編集後に `.dart` ファイルを自動フォーマット
|
||||
- **dart analyze**: Dart ファイルの編集後に静的解析を実行し、警告を表示
|
||||
- **flutter test**: 大きな変更後に影響を受けるテストをオプションで実行
|
||||
|
||||
## 推奨フック設定
|
||||
|
||||
```json
|
||||
{
|
||||
"hooks": {
|
||||
"PostToolUse": [
|
||||
{
|
||||
"matcher": { "tool_name": "Edit", "file_paths": ["**/*.dart"] },
|
||||
"hooks": [
|
||||
{ "type": "command", "command": "dart format $CLAUDE_FILE_PATHS" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## コミット前チェック
|
||||
|
||||
Dart/Flutter の変更をコミットする前に実行する:
|
||||
|
||||
```bash
|
||||
dart format --set-exit-if-changed .
|
||||
dart analyze --fatal-infos
|
||||
flutter test
|
||||
```
|
||||
|
||||
## 便利なワンライナー
|
||||
|
||||
```bash
|
||||
# すべての Dart ファイルをフォーマット
|
||||
dart format .
|
||||
|
||||
# 解析して問題を報告
|
||||
dart analyze
|
||||
|
||||
# カバレッジ付きですべてのテストを実行
|
||||
flutter test --coverage
|
||||
|
||||
# コード生成ファイルを再生成
|
||||
dart run build_runner build --delete-conflicting-outputs
|
||||
|
||||
# 古くなったパッケージを確認
|
||||
flutter pub outdated
|
||||
|
||||
# 制約の範囲内でパッケージをアップグレード
|
||||
flutter pub upgrade
|
||||
```
|
||||
261
docs/ja-JP/rules/dart/patterns.md
Normal file
261
docs/ja-JP/rules/dart/patterns.md
Normal file
@@ -0,0 +1,261 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.dart"
|
||||
- "**/pubspec.yaml"
|
||||
---
|
||||
# Dart/Flutter パターン
|
||||
|
||||
> このファイルは [common/patterns.md](../common/patterns.md) を Dart、Flutter、および一般的なエコシステム固有のコンテンツで拡張します。
|
||||
|
||||
## リポジトリパターン
|
||||
|
||||
```dart
|
||||
abstract interface class UserRepository {
|
||||
Future<User?> getById(String id);
|
||||
Future<List<User>> getAll();
|
||||
Stream<List<User>> watchAll();
|
||||
Future<void> save(User user);
|
||||
Future<void> delete(String id);
|
||||
}
|
||||
|
||||
class UserRepositoryImpl implements UserRepository {
|
||||
const UserRepositoryImpl(this._remote, this._local);
|
||||
|
||||
final UserRemoteDataSource _remote;
|
||||
final UserLocalDataSource _local;
|
||||
|
||||
@override
|
||||
Future<User?> getById(String id) async {
|
||||
final local = await _local.getById(id);
|
||||
if (local != null) return local;
|
||||
final remote = await _remote.getById(id);
|
||||
if (remote != null) await _local.save(remote);
|
||||
return remote;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<User>> getAll() async {
|
||||
final remote = await _remote.getAll();
|
||||
for (final user in remote) {
|
||||
await _local.save(user);
|
||||
}
|
||||
return remote;
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<List<User>> watchAll() => _local.watchAll();
|
||||
|
||||
@override
|
||||
Future<void> save(User user) => _local.save(user);
|
||||
|
||||
@override
|
||||
Future<void> delete(String id) async {
|
||||
await _remote.delete(id);
|
||||
await _local.delete(id);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## ステート管理: BLoC/Cubit
|
||||
|
||||
```dart
|
||||
// Cubit — シンプルなステート遷移
|
||||
class CounterCubit extends Cubit<int> {
|
||||
CounterCubit() : super(0);
|
||||
|
||||
void increment() => emit(state + 1);
|
||||
void decrement() => emit(state - 1);
|
||||
}
|
||||
|
||||
// BLoC — イベント駆動
|
||||
@immutable
|
||||
sealed class CartEvent {}
|
||||
class CartItemAdded extends CartEvent { CartItemAdded(this.item); final Item item; }
|
||||
class CartItemRemoved extends CartEvent { CartItemRemoved(this.id); final String id; }
|
||||
class CartCleared extends CartEvent {}
|
||||
|
||||
@immutable
|
||||
class CartState {
|
||||
const CartState({this.items = const []});
|
||||
final List<Item> items;
|
||||
CartState copyWith({List<Item>? items}) => CartState(items: items ?? this.items);
|
||||
}
|
||||
|
||||
class CartBloc extends Bloc<CartEvent, CartState> {
|
||||
CartBloc() : super(const CartState()) {
|
||||
on<CartItemAdded>((event, emit) =>
|
||||
emit(state.copyWith(items: [...state.items, event.item])));
|
||||
on<CartItemRemoved>((event, emit) =>
|
||||
emit(state.copyWith(items: state.items.where((i) => i.id != event.id).toList())));
|
||||
on<CartCleared>((_, emit) => emit(const CartState()));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## ステート管理: Riverpod
|
||||
|
||||
```dart
|
||||
// シンプルなプロバイダー
|
||||
@riverpod
|
||||
Future<List<User>> users(Ref ref) async {
|
||||
final repo = ref.watch(userRepositoryProvider);
|
||||
return repo.getAll();
|
||||
}
|
||||
|
||||
// ミュータブルなステート用の Notifier
|
||||
@riverpod
|
||||
class CartNotifier extends _$CartNotifier {
|
||||
@override
|
||||
List<Item> build() => [];
|
||||
|
||||
void add(Item item) => state = [...state, item];
|
||||
void remove(String id) => state = state.where((i) => i.id != id).toList();
|
||||
void clear() => state = [];
|
||||
}
|
||||
|
||||
// ConsumerWidget
|
||||
class CartPage extends ConsumerWidget {
|
||||
const CartPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final items = ref.watch(cartNotifierProvider);
|
||||
return ListView(
|
||||
children: items.map((item) => CartItemTile(item: item)).toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 依存性注入
|
||||
|
||||
コンストラクタ注入が推奨される。コンポジションルートで `get_it` または Riverpod プロバイダーを使用する:
|
||||
|
||||
```dart
|
||||
// get_it の登録 (セットアップファイル内)
|
||||
void setupDependencies() {
|
||||
final di = GetIt.instance;
|
||||
di.registerSingleton<ApiClient>(ApiClient(baseUrl: Env.apiUrl));
|
||||
di.registerSingleton<UserRepository>(
|
||||
UserRepositoryImpl(di<ApiClient>(), di<LocalDatabase>()),
|
||||
);
|
||||
di.registerFactory(() => UserListViewModel(di<UserRepository>()));
|
||||
}
|
||||
```
|
||||
|
||||
## ViewModel パターン (BLoC/Riverpod なし)
|
||||
|
||||
```dart
|
||||
class UserListViewModel extends ChangeNotifier {
|
||||
UserListViewModel(this._repository);
|
||||
|
||||
final UserRepository _repository;
|
||||
|
||||
AsyncState<List<User>> _state = const Loading();
|
||||
AsyncState<List<User>> get state => _state;
|
||||
|
||||
Future<void> load() async {
|
||||
_state = const Loading();
|
||||
notifyListeners();
|
||||
try {
|
||||
final users = await _repository.getAll();
|
||||
_state = Success(users);
|
||||
} on Exception catch (e) {
|
||||
_state = Failure(e);
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## UseCase パターン
|
||||
|
||||
```dart
|
||||
class GetUserUseCase {
|
||||
const GetUserUseCase(this._repository);
|
||||
final UserRepository _repository;
|
||||
|
||||
Future<User?> call(String id) => _repository.getById(id);
|
||||
}
|
||||
|
||||
class CreateUserUseCase {
|
||||
const CreateUserUseCase(this._repository, this._idGenerator);
|
||||
final UserRepository _repository;
|
||||
final IdGenerator _idGenerator; // 注入 — ドメイン層は uuid パッケージに直接依存してはならない
|
||||
|
||||
Future<void> call(CreateUserInput input) async {
|
||||
// バリデーション、ビジネスルールの適用、その後永続化
|
||||
final user = User(id: _idGenerator.generate(), name: input.name, email: input.email);
|
||||
await _repository.save(user);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## freezed を使ったイミュータブルなステート
|
||||
|
||||
```dart
|
||||
@freezed
|
||||
class UserState with _$UserState {
|
||||
const factory UserState({
|
||||
@Default([]) List<User> users,
|
||||
@Default(false) bool isLoading,
|
||||
String? errorMessage,
|
||||
}) = _UserState;
|
||||
}
|
||||
```
|
||||
|
||||
## クリーンアーキテクチャのレイヤー境界
|
||||
|
||||
```
|
||||
lib/
|
||||
├── domain/ # 純粋な Dart — Flutter なし、外部パッケージなし
|
||||
│ ├── entities/
|
||||
│ ├── repositories/ # 抽象インターフェース
|
||||
│ └── usecases/
|
||||
├── data/ # ドメインインターフェースの実装
|
||||
│ ├── datasources/
|
||||
│ ├── models/ # fromJson/toJson を持つ DTO
|
||||
│ └── repositories/
|
||||
└── presentation/ # Flutter ウィジェット + ステート管理
|
||||
├── pages/
|
||||
├── widgets/
|
||||
└── providers/ (or blocs/ or viewmodels/)
|
||||
```
|
||||
|
||||
- ドメイン層は `package:flutter` やデータ層のパッケージをインポートしてはならない
|
||||
- データ層はリポジトリ境界で DTO をドメインエンティティにマッピングする
|
||||
- プレゼンテーション層はリポジトリを直接使用せず、ユースケースを呼び出す
|
||||
|
||||
## ナビゲーション (GoRouter)
|
||||
|
||||
```dart
|
||||
final router = GoRouter(
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/',
|
||||
builder: (context, state) => const HomePage(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/users/:id',
|
||||
builder: (context, state) {
|
||||
final id = state.pathParameters['id']!;
|
||||
return UserDetailPage(userId: id);
|
||||
},
|
||||
),
|
||||
],
|
||||
// refreshListenable は認証ステートが変わるたびに redirect を再評価する
|
||||
refreshListenable: GoRouterRefreshStream(authCubit.stream),
|
||||
redirect: (context, state) {
|
||||
final isLoggedIn = context.read<AuthCubit>().state is AuthAuthenticated;
|
||||
if (!isLoggedIn && !state.matchedLocation.startsWith('/login')) {
|
||||
return '/login';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
## 参考資料
|
||||
|
||||
スキル `flutter-dart-code-review` で包括的なレビューチェックリストを参照。
|
||||
スキル `compose-multiplatform-patterns` で Kotlin Multiplatform/Flutter 相互運用パターンを参照。
|
||||
135
docs/ja-JP/rules/dart/security.md
Normal file
135
docs/ja-JP/rules/dart/security.md
Normal file
@@ -0,0 +1,135 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.dart"
|
||||
- "**/pubspec.yaml"
|
||||
- "**/AndroidManifest.xml"
|
||||
- "**/Info.plist"
|
||||
---
|
||||
# Dart/Flutter セキュリティ
|
||||
|
||||
> このファイルは [common/security.md](../common/security.md) を Dart、Flutter、およびモバイル固有のコンテンツで拡張します。
|
||||
|
||||
## シークレット管理
|
||||
|
||||
- Dart ソースコードに API キー、トークン、認証情報をハードコードしない
|
||||
- コンパイル時設定には `--dart-define` または `--dart-define-from-file` を使用する (値は真のシークレットではない — サーバーサイドのシークレットにはバックエンドプロキシを使用する)
|
||||
- `flutter_dotenv` または同等のものを使用し、`.env` ファイルを `.gitignore` に記載する
|
||||
- ランタイムシークレットはプラットフォームのセキュアなストレージに保存する: `flutter_secure_storage` (iOS の Keychain、Android の EncryptedSharedPreferences)
|
||||
|
||||
```dart
|
||||
// BAD
|
||||
const apiKey = 'sk-abc123...';
|
||||
|
||||
// GOOD — コンパイル時設定 (シークレットではなく、設定可能な値)
|
||||
const apiKey = String.fromEnvironment('API_KEY');
|
||||
|
||||
// GOOD — セキュアなストレージからのランタイムシークレット
|
||||
final token = await secureStorage.read(key: 'auth_token');
|
||||
```
|
||||
|
||||
## ネットワークセキュリティ
|
||||
|
||||
- HTTPS を強制する — 本番環境で `http://` の呼び出しは禁止
|
||||
- Android の `network_security_config.xml` を設定してクリアテキストトラフィックをブロックする
|
||||
- `Info.plist` の `NSAppTransportSecurity` を設定して任意のロードを禁止する
|
||||
- すべての HTTP クライアントにリクエストタイムアウトを設定する — デフォルトのままにしない
|
||||
- セキュリティが重要なエンドポイントには証明書ピンニングを検討する
|
||||
|
||||
```dart
|
||||
// タイムアウトと HTTPS 強制を設定した Dio
|
||||
final dio = Dio(BaseOptions(
|
||||
baseUrl: 'https://api.example.com',
|
||||
connectTimeout: const Duration(seconds: 10),
|
||||
receiveTimeout: const Duration(seconds: 30),
|
||||
));
|
||||
```
|
||||
|
||||
## 入力バリデーション
|
||||
|
||||
- API またはストレージに送信する前にすべてのユーザー入力をバリデートおよびサニタイズする
|
||||
- SQLクエリに未サニタイズの入力を渡さない — パラメータ化クエリを使用する (sqflite、drift)
|
||||
- ナビゲーション前にディープリンク URL をサニタイズする — スキーム、ホスト、パスパラメータを検証する
|
||||
- ナビゲーション前に `Uri.tryParse` を使用して検証する
|
||||
|
||||
```dart
|
||||
// BAD — SQL インジェクション
|
||||
await db.rawQuery("SELECT * FROM users WHERE email = '$userInput'");
|
||||
|
||||
// GOOD — パラメータ化クエリ
|
||||
await db.query('users', where: 'email = ?', whereArgs: [userInput]);
|
||||
|
||||
// BAD — 未検証のディープリンク
|
||||
final uri = Uri.parse(incomingLink);
|
||||
context.go(uri.path); // 任意のルートにナビゲートできてしまう
|
||||
|
||||
// GOOD — 検証済みのディープリンク
|
||||
final uri = Uri.tryParse(incomingLink);
|
||||
if (uri != null && uri.host == 'myapp.com' && _allowedPaths.contains(uri.path)) {
|
||||
context.go(uri.path);
|
||||
}
|
||||
```
|
||||
|
||||
## データ保護
|
||||
|
||||
- トークン、PII、認証情報は `flutter_secure_storage` にのみ保存する
|
||||
- 機密データを `SharedPreferences` やローカルファイルに平文で書き込まない
|
||||
- ログアウト時に認証ステートをクリアする: トークン、キャッシュされたユーザーデータ、Cookie
|
||||
- 機密操作には生体認証 (`local_auth`) を使用する
|
||||
- 機密データをログに記録しない — `print(token)` や `debugPrint(password)` は禁止
|
||||
|
||||
## Android 固有
|
||||
|
||||
- `AndroidManifest.xml` で必要なパーミッションのみを宣言する
|
||||
- Android コンポーネント (`Activity`、`Service`、`BroadcastReceiver`) は必要な場合のみ export する。不要な場合は `android:exported="false"` を追加する
|
||||
- インテントフィルターを確認する — 暗黙的インテントフィルターを持つ export されたコンポーネントはどのアプリからもアクセス可能
|
||||
- 機密データを表示する画面では `FLAG_SECURE` を使用する (スクリーンショットを防止)
|
||||
|
||||
```xml
|
||||
<!-- AndroidManifest.xml — エクスポートされるコンポーネントを制限 -->
|
||||
<activity android:name=".MainActivity" android:exported="true">
|
||||
<!-- ランチャーアクティビティのみ exported=true が必要 -->
|
||||
</activity>
|
||||
<activity android:name=".SensitiveActivity" android:exported="false" />
|
||||
```
|
||||
|
||||
## iOS 固有
|
||||
|
||||
- `Info.plist` で必要な使用説明のみを宣言する (`NSCameraUsageDescription` など)
|
||||
- シークレットは Keychain に保存する — `flutter_secure_storage` は iOS で Keychain を使用する
|
||||
- App Transport Security (ATS) を使用する — 任意のロードを禁止する
|
||||
- 機密ファイルのデータ保護エンタイトルメントを有効にする
|
||||
|
||||
## WebView セキュリティ
|
||||
|
||||
- `webview_flutter` v4+ (`WebViewController` / `WebViewWidget`) を使用する — レガシーの `WebView` ウィジェットは削除済み
|
||||
- 明示的に必要でない限り JavaScript を無効にする (`JavaScriptMode.disabled`)
|
||||
- URL をロードする前に検証する — ディープリンクから任意の URL をロードしない
|
||||
- 必要不可欠で注意深くサンドボックス化されている場合を除き、Dart コールバックを JavaScript に公開しない
|
||||
- `NavigationDelegate.onNavigationRequest` を使用してナビゲーションリクエストをインターセプトして検証する
|
||||
|
||||
```dart
|
||||
// webview_flutter v4+ API (WebViewController + WebViewWidget)
|
||||
final controller = WebViewController()
|
||||
..setJavaScriptMode(JavaScriptMode.disabled) // 必要でない限り無効
|
||||
..setNavigationDelegate(
|
||||
NavigationDelegate(
|
||||
onNavigationRequest: (request) {
|
||||
final uri = Uri.tryParse(request.url);
|
||||
if (uri == null || uri.host != 'trusted.example.com') {
|
||||
return NavigationDecision.prevent;
|
||||
}
|
||||
return NavigationDecision.navigate;
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
// ウィジェットツリー内:
|
||||
WebViewWidget(controller: controller)
|
||||
```
|
||||
|
||||
## 難読化とビルドセキュリティ
|
||||
|
||||
- リリースビルドで難読化を有効にする: `flutter build apk --obfuscate --split-debug-info=./debug-info/`
|
||||
- `--split-debug-info` の出力はバージョン管理から除外する (クラッシュシンボル化のみに使用)
|
||||
- ProGuard/R8 のルールがシリアライズされたクラスを意図せず公開しないことを確認する
|
||||
- リリース前に `flutter analyze` を実行してすべての警告に対応する
|
||||
215
docs/ja-JP/rules/dart/testing.md
Normal file
215
docs/ja-JP/rules/dart/testing.md
Normal file
@@ -0,0 +1,215 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.dart"
|
||||
- "**/pubspec.yaml"
|
||||
- "**/analysis_options.yaml"
|
||||
---
|
||||
# Dart/Flutter テスト
|
||||
|
||||
> このファイルは [common/testing.md](../common/testing.md) を Dart および Flutter 固有のコンテンツで拡張します。
|
||||
|
||||
## テストフレームワーク
|
||||
|
||||
- **flutter_test** / **dart:test** — 組み込みテストランナー
|
||||
- **mockito** (`@GenerateMocks` 付き) または **mocktail** (コード生成なし) でモック
|
||||
- **bloc_test** — BLoC/Cubit のユニットテスト
|
||||
- **fake_async** — ユニットテストでの時間制御
|
||||
- **integration_test** — エンドツーエンドのデバイステスト
|
||||
|
||||
## テストの種類
|
||||
|
||||
| 種類 | ツール | 場所 | 書くタイミング |
|
||||
|------|------|----------|---------------|
|
||||
| ユニット | `dart:test` | `test/unit/` | すべてのドメインロジック、ステートマネージャー、リポジトリ |
|
||||
| ウィジェット | `flutter_test` | `test/widget/` | 意味のある動作を持つすべてのウィジェット |
|
||||
| ゴールデン | `flutter_test` | `test/golden/` | デザインが重要な UI コンポーネント |
|
||||
| インテグレーション | `integration_test` | `integration_test/` | 実機/エミュレーターでの重要なユーザーフロー |
|
||||
|
||||
## ユニットテスト: ステートマネージャー
|
||||
|
||||
### `bloc_test` を使った BLoC
|
||||
|
||||
```dart
|
||||
group('CartBloc', () {
|
||||
late CartBloc bloc;
|
||||
late MockCartRepository repository;
|
||||
|
||||
setUp(() {
|
||||
repository = MockCartRepository();
|
||||
bloc = CartBloc(repository);
|
||||
});
|
||||
|
||||
tearDown(() => bloc.close());
|
||||
|
||||
blocTest<CartBloc, CartState>(
|
||||
'CartItemAdded 時に更新されたアイテムを emit する',
|
||||
build: () => bloc,
|
||||
act: (b) => b.add(CartItemAdded(testItem)),
|
||||
expect: () => [CartState(items: [testItem])],
|
||||
);
|
||||
|
||||
blocTest<CartBloc, CartState>(
|
||||
'CartCleared 時に空のカートを emit する',
|
||||
seed: () => CartState(items: [testItem]),
|
||||
build: () => bloc,
|
||||
act: (b) => b.add(CartCleared()),
|
||||
expect: () => [const CartState()],
|
||||
);
|
||||
});
|
||||
```
|
||||
|
||||
### `ProviderContainer` を使った Riverpod
|
||||
|
||||
```dart
|
||||
test('usersProvider がリポジトリからユーザーをロードする', () async {
|
||||
final container = ProviderContainer(
|
||||
overrides: [userRepositoryProvider.overrideWithValue(FakeUserRepository())],
|
||||
);
|
||||
addTearDown(container.dispose);
|
||||
|
||||
final result = await container.read(usersProvider.future);
|
||||
expect(result, isNotEmpty);
|
||||
});
|
||||
```
|
||||
|
||||
## ウィジェットテスト
|
||||
|
||||
```dart
|
||||
testWidgets('CartPage がアイテム数バッジを表示する', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
ProviderScope(
|
||||
overrides: [
|
||||
cartNotifierProvider.overrideWith(() => FakeCartNotifier([testItem])),
|
||||
],
|
||||
child: const MaterialApp(home: CartPage()),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pump();
|
||||
expect(find.text('1'), findsOneWidget);
|
||||
expect(find.byType(CartItemTile), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('カートが空のときに空の状態を表示する', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
ProviderScope(
|
||||
overrides: [cartNotifierProvider.overrideWith(() => FakeCartNotifier([]))],
|
||||
child: const MaterialApp(home: CartPage()),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pump();
|
||||
expect(find.text('Your cart is empty'), findsOneWidget);
|
||||
});
|
||||
```
|
||||
|
||||
## モックよりもフェイクを優先
|
||||
|
||||
複雑な依存関係には手書きのフェイクを優先する:
|
||||
|
||||
```dart
|
||||
class FakeUserRepository implements UserRepository {
|
||||
final _users = <String, User>{};
|
||||
Object? fetchError;
|
||||
|
||||
@override
|
||||
Future<User?> getById(String id) async {
|
||||
if (fetchError != null) throw fetchError!;
|
||||
return _users[id];
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<User>> getAll() async {
|
||||
if (fetchError != null) throw fetchError!;
|
||||
return _users.values.toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<List<User>> watchAll() => Stream.value(_users.values.toList());
|
||||
|
||||
@override
|
||||
Future<void> save(User user) async {
|
||||
_users[user.id] = user;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> delete(String id) async {
|
||||
_users.remove(id);
|
||||
}
|
||||
|
||||
void addUser(User user) => _users[user.id] = user;
|
||||
}
|
||||
```
|
||||
|
||||
## 非同期テスト
|
||||
|
||||
```dart
|
||||
// タイマーと Future を制御するために fake_async を使用
|
||||
test('300ms 後にデバウンスが発火する', () {
|
||||
fakeAsync((async) {
|
||||
final debouncer = Debouncer(delay: const Duration(milliseconds: 300));
|
||||
var callCount = 0;
|
||||
debouncer.run(() => callCount++);
|
||||
expect(callCount, 0);
|
||||
async.elapse(const Duration(milliseconds: 200));
|
||||
expect(callCount, 0);
|
||||
async.elapse(const Duration(milliseconds: 200));
|
||||
expect(callCount, 1);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## ゴールデンテスト
|
||||
|
||||
```dart
|
||||
testWidgets('UserCard ゴールデンテスト', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(home: UserCard(user: testUser)),
|
||||
);
|
||||
|
||||
await expectLater(
|
||||
find.byType(UserCard),
|
||||
matchesGoldenFile('goldens/user_card.png'),
|
||||
);
|
||||
});
|
||||
```
|
||||
|
||||
意図的な視覚的変更があった場合は `flutter test --update-goldens` を実行する。
|
||||
|
||||
## テストの命名
|
||||
|
||||
説明的で振る舞いに焦点を当てた名前を使用する:
|
||||
|
||||
```dart
|
||||
test('ユーザーが存在しない場合に null を返す', () { ... });
|
||||
test('id が空文字列の場合に NotFoundException をスローする', () { ... });
|
||||
testWidgets('フォームが無効な間は送信ボタンを無効にする', (tester) async { ... });
|
||||
```
|
||||
|
||||
## テストの構成
|
||||
|
||||
```
|
||||
test/
|
||||
├── unit/
|
||||
│ ├── domain/
|
||||
│ │ └── usecases/
|
||||
│ └── data/
|
||||
│ └── repositories/
|
||||
├── widget/
|
||||
│ └── presentation/
|
||||
│ └── pages/
|
||||
└── golden/
|
||||
└── widgets/
|
||||
|
||||
integration_test/
|
||||
└── flows/
|
||||
├── login_flow_test.dart
|
||||
└── checkout_flow_test.dart
|
||||
```
|
||||
|
||||
## カバレッジ
|
||||
|
||||
- ビジネスロジック (ドメイン + ステートマネージャー) で 80%以上の行カバレッジを目標とする
|
||||
- すべてのステート遷移にテストが必要: ローディング → 成功、ローディング → エラー、リトライ
|
||||
- `flutter test --coverage` を実行し、カバレッジレポーターで `lcov.info` を確認する
|
||||
- カバレッジが閾値を下回った場合は CI でブロックする
|
||||
Reference in New Issue
Block a user