mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-05-18 06:43:05 +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:
182
docs/ja-JP/rules/angular/coding-style.md
Normal file
182
docs/ja-JP/rules/angular/coding-style.md
Normal file
@@ -0,0 +1,182 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.component.ts"
|
||||
- "**/*.component.html"
|
||||
- "**/*.service.ts"
|
||||
- "**/*.directive.ts"
|
||||
- "**/*.pipe.ts"
|
||||
- "**/*.guard.ts"
|
||||
- "**/*.resolver.ts"
|
||||
- "**/*.module.ts"
|
||||
---
|
||||
# Angular コーディングスタイル
|
||||
|
||||
> このファイルは [common/coding-style.md](../common/coding-style.md) を Angular 固有のコンテンツで拡張します。
|
||||
|
||||
## バージョンの確認
|
||||
|
||||
コードを書く前に、必ずプロジェクトの Angular バージョンを確認してください — バージョン間で機能が大きく異なります。`ng version` を実行するか、`package.json` を確認してください。新しいプロジェクトを作成する場合、ユーザーが指定しない限りバージョンを固定しないでください。
|
||||
|
||||
Angular コードを生成または変更した後は、完了前に必ず `ng build` を実行してエラーを検出してください。
|
||||
|
||||
## ファイル命名
|
||||
|
||||
Angular CLI の規約に従い、1ファイルにつき1つの成果物を配置します:
|
||||
|
||||
- `user-profile.component.ts` + `user-profile.component.html` + `user-profile.component.spec.ts`
|
||||
- `user.service.ts`、`auth.guard.ts`、`date-format.pipe.ts`
|
||||
- 機能フォルダ: `features/users/`、`features/auth/`
|
||||
- CLI で生成: `ng generate component features/users/user-card`
|
||||
|
||||
## コンポーネント
|
||||
|
||||
スタンドアロンコンポーネント(v17+ デフォルト)を優先します。すべての新しいコンポーネントで `OnPush` 変更検知を使用してください。
|
||||
|
||||
```typescript
|
||||
@Component({
|
||||
selector: 'app-user-card',
|
||||
standalone: true,
|
||||
imports: [RouterModule],
|
||||
templateUrl: './user-card.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class UserCardComponent {
|
||||
user = input.required<User>();
|
||||
select = output<string>();
|
||||
}
|
||||
```
|
||||
|
||||
## 依存性注入
|
||||
|
||||
コンストラクタ注入よりも `inject()` を使用してください。コンストラクタは空にするか、完全に削除してください。
|
||||
|
||||
```typescript
|
||||
// 正しい
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class UserService {
|
||||
private http = inject(HttpClient);
|
||||
private router = inject(Router);
|
||||
}
|
||||
|
||||
// 誤り: コンストラクタ注入は冗長で、ツリーシェイキングが困難
|
||||
constructor(private http: HttpClient, private router: Router) {}
|
||||
```
|
||||
|
||||
非クラス依存関係には `InjectionToken` を使用してください:
|
||||
|
||||
```typescript
|
||||
const API_URL = new InjectionToken<string>('API_URL');
|
||||
|
||||
// 提供:
|
||||
{ provide: API_URL, useValue: 'https://api.example.com' }
|
||||
|
||||
// 使用:
|
||||
private apiUrl = inject(API_URL);
|
||||
```
|
||||
|
||||
## シグナル
|
||||
|
||||
### 基本プリミティブ
|
||||
|
||||
```typescript
|
||||
count = signal(0);
|
||||
doubled = computed(() => this.count() * 2);
|
||||
|
||||
increment() {
|
||||
this.count.update(n => n + 1);
|
||||
}
|
||||
```
|
||||
|
||||
### `linkedSignal` — 書き込み可能な派生状態
|
||||
|
||||
ソースが変更されたときにリセットまたは適応する必要があるが、独立して書き込み可能なシグナルには `linkedSignal` を使用してください:
|
||||
|
||||
```typescript
|
||||
selectedOption = linkedSignal(() => this.options()[0]);
|
||||
// options が変更されると最初のオプションにリセットされるが、ユーザーはオーバーライド可能
|
||||
```
|
||||
|
||||
### `resource` — 非同期データをシグナルに変換
|
||||
|
||||
手動サブスクリプションなしで非同期データをリアクティブに取得するには `resource()` を使用してください:
|
||||
|
||||
```typescript
|
||||
userResource = resource({
|
||||
request: () => ({ id: this.userId() }),
|
||||
loader: ({ request }) => fetch(`/api/users/${request.id}`).then(r => r.json()),
|
||||
});
|
||||
|
||||
// アクセス: userResource.value(), userResource.isLoading(), userResource.error()
|
||||
```
|
||||
|
||||
### `effect` の使用法
|
||||
|
||||
`effect()` はシグナルの変更に反応する必要がある副作用(ログ記録、サードパーティの DOM 操作)にのみ使用してください。シグナルの同期にエフェクトを使用しないでください — 代わりに `computed` または `linkedSignal` を使用してください。レンダリング後の DOM 作業には `afterRenderEffect` を使用してください。
|
||||
|
||||
```typescript
|
||||
// 正しい: 副作用
|
||||
effect(() => console.log('User changed:', this.user()));
|
||||
|
||||
// 誤り: 代わりに computed を使用
|
||||
effect(() => { this.fullName.set(`${this.first()} ${this.last()}`); });
|
||||
```
|
||||
|
||||
## テンプレート
|
||||
|
||||
v17+ のブロック構文を使用してください。`@for` では必ず `track` を指定してください:
|
||||
|
||||
```html
|
||||
@for (item of items(); track item.id) {
|
||||
<app-item [item]="item" />
|
||||
}
|
||||
|
||||
@if (isLoading()) {
|
||||
<app-spinner />
|
||||
} @else if (error()) {
|
||||
<app-error [message]="error()" />
|
||||
} @else {
|
||||
<app-content [data]="data()" />
|
||||
}
|
||||
```
|
||||
|
||||
テンプレート内のロジックは単純な条件式に留め、コンポーネントメソッドまたはパイプに移動してください。
|
||||
|
||||
## フォーム
|
||||
|
||||
プロジェクトの既存アプローチに合ったフォーム戦略を選択してください:
|
||||
|
||||
- **Signal Forms**(v21+): v21+ の新規プロジェクトで推奨。シグナルベースのフォーム状態。
|
||||
- **Reactive Forms**: `FormBuilder` + `FormGroup` + `FormControl`。動的バリデーションを持つ複雑なフォームに最適。
|
||||
- **Template-Driven Forms**: `ngModel`。シンプルなフォームにのみ適しています。
|
||||
|
||||
```typescript
|
||||
// Reactive Forms — ほとんどのアプリの標準的なアプローチ
|
||||
export class LoginComponent {
|
||||
private fb = inject(FormBuilder);
|
||||
|
||||
form = this.fb.group({
|
||||
email: ['', [Validators.required, Validators.email]],
|
||||
password: ['', [Validators.required, Validators.minLength(8)]],
|
||||
});
|
||||
|
||||
submit() {
|
||||
if (this.form.valid) {
|
||||
// this.form.value を使用
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## コンポーネントスタイル
|
||||
|
||||
`ViewEncapsulation.Emulated`(デフォルト)でコンポーネントレベルのスタイルを使用してください。意図的にスタイルを漏洩させるデザインシステムを構築する場合を除き、`ViewEncapsulation.None` を避けてください。
|
||||
|
||||
- スタイルをコンポーネントにスコープし、コンポーネントスタイルシート内でグローバルクラス名を使用しない
|
||||
- ホスト要素のスタイリングには `:host` を使用
|
||||
- テーマ設定可能な値には CSS カスタムプロパティを優先
|
||||
|
||||
## 変更検知
|
||||
|
||||
- すべての新しいコンポーネントでデフォルトとして `ChangeDetectionStrategy.OnPush` を使用
|
||||
- シグナルと `async` パイプが検知を自動的に処理 — `markForCheck()` と `detectChanges()` を避ける
|
||||
- OnPush 使用時に `@Input()` オブジェクトをインプレースで変更しない
|
||||
25
docs/ja-JP/rules/angular/hooks.md
Normal file
25
docs/ja-JP/rules/angular/hooks.md
Normal file
@@ -0,0 +1,25 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.component.ts"
|
||||
- "**/*.component.html"
|
||||
- "**/*.service.ts"
|
||||
- "**/*.directive.ts"
|
||||
- "**/*.pipe.ts"
|
||||
- "**/*.spec.ts"
|
||||
---
|
||||
# Angular フック
|
||||
|
||||
> このファイルは [common/hooks.md](../common/hooks.md) を Angular 固有のコンテンツで拡張します。
|
||||
|
||||
## PostToolUse フック
|
||||
|
||||
`~/.claude/settings.json` で設定してください:
|
||||
|
||||
- **Prettier**: 編集後に `.ts` と `.html` ファイルを自動フォーマット
|
||||
- **ESLint / ng lint**: Angular ソースファイルの編集後に `ng lint` を実行し、デコレータの誤用、テンプレートエラー、スタイル違反を検出
|
||||
- **TypeScript チェック**: `.ts` ファイルの編集後に `tsc --noEmit` を実行
|
||||
- **ビルドチェック**: Angular コードの生成または大幅な変更後に `ng build` を実行し、テンプレートと型エラーを早期に検出
|
||||
|
||||
## Stop フック
|
||||
|
||||
- **Lint 監査**: セッション終了前に変更されたファイル全体で `ng lint` を実行し、未解決の違反を検出
|
||||
249
docs/ja-JP/rules/angular/patterns.md
Normal file
249
docs/ja-JP/rules/angular/patterns.md
Normal file
@@ -0,0 +1,249 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.component.ts"
|
||||
- "**/*.component.html"
|
||||
- "**/*.service.ts"
|
||||
- "**/*.store.ts"
|
||||
- "**/*.routes.ts"
|
||||
---
|
||||
# Angular パターン
|
||||
|
||||
> このファイルは [common/patterns.md](../common/patterns.md) を Angular 固有のコンテンツで拡張します。
|
||||
|
||||
## Smart / Dumb コンポーネント分離
|
||||
|
||||
Smart(コンテナ)コンポーネントはデータ取得と状態を所有します。Dumb(プレゼンテーション)コンポーネントは入力の受け取りと出力の発行のみを行い、サービスの注入は行いません。
|
||||
|
||||
```typescript
|
||||
// Smart — データを所有
|
||||
@Component({ standalone: true, changeDetection: ChangeDetectionStrategy.OnPush })
|
||||
export class UserPageComponent {
|
||||
private userService = inject(UserService);
|
||||
user = toSignal(this.userService.getUser(this.userId));
|
||||
}
|
||||
```
|
||||
|
||||
```html
|
||||
<!-- Dumb — 純粋なプレゼンテーション -->
|
||||
<app-user-card [user]="user()" (select)="onSelect($event)" />
|
||||
```
|
||||
|
||||
## サービスレイヤー
|
||||
|
||||
サービスがすべてのデータアクセスとビジネスロジックを所有します。コンポーネントは委譲のみ — コンポーネント内に `HttpClient` を配置しないでください。
|
||||
|
||||
```typescript
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class UserService {
|
||||
private http = inject(HttpClient);
|
||||
|
||||
getUsers(): Observable<User[]> {
|
||||
return this.http.get<User[]>('/api/users');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## `resource` を使用した非同期データ
|
||||
|
||||
リアクティブな非同期フェッチには `resource()` を使用してください。単純なデータ読み込みには手動の RxJS パイプラインよりも優先してください:
|
||||
|
||||
```typescript
|
||||
export class UserDetailComponent {
|
||||
userId = input.required<string>();
|
||||
|
||||
userResource = resource({
|
||||
request: () => ({ id: this.userId() }),
|
||||
loader: ({ request }) =>
|
||||
firstValueFrom(inject(UserService).getUser(request.id)),
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
状態へのアクセス: `userResource.value()`、`userResource.isLoading()`、`userResource.error()`、`userResource.reload()`。
|
||||
|
||||
## シグナル状態パターン
|
||||
|
||||
```typescript
|
||||
// ローカルの可変状態
|
||||
count = signal(0);
|
||||
|
||||
// 派生(複製しない)
|
||||
doubled = computed(() => this.count() * 2);
|
||||
|
||||
// ソースとともにリセットされる書き込み可能な派生状態
|
||||
selectedItem = linkedSignal(() => this.items()[0]);
|
||||
|
||||
// Observable からシグナルへのブリッジ
|
||||
users = toSignal(this.userService.getUsers(), { initialValue: [] });
|
||||
```
|
||||
|
||||
派生値を別のシグナルに格納しないでください — `computed` を使用してください。シグナルの同期に `effect` を使用しないでください — `computed` または `linkedSignal` を使用してください。
|
||||
|
||||
## サブスクリプションのクリーンアップ
|
||||
|
||||
すべての手動サブスクリプションには `takeUntilDestroyed()` を使用してください。新しいコードでは手動の `ngOnDestroy` + `Subject` + `takeUntil` を使用しないでください。
|
||||
|
||||
```typescript
|
||||
export class UserComponent {
|
||||
private destroyRef = inject(DestroyRef);
|
||||
|
||||
ngOnInit() {
|
||||
this.userService.updates$
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe(update => this.handleUpdate(update));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## ルーティング
|
||||
|
||||
### ルート定義
|
||||
|
||||
```typescript
|
||||
// app.routes.ts
|
||||
export const routes: Routes = [
|
||||
{ path: '', component: HomeComponent },
|
||||
{
|
||||
path: 'admin',
|
||||
canMatch: [authGuard], // CanMatch はチャンクの読み込み自体を防止
|
||||
loadChildren: () => import('./admin/admin.routes').then(m => m.ADMIN_ROUTES),
|
||||
},
|
||||
{
|
||||
path: 'users/:id',
|
||||
resolve: { user: userResolver },
|
||||
component: UserDetailComponent,
|
||||
},
|
||||
];
|
||||
```
|
||||
|
||||
- 未認証ユーザーに対してルートモジュールを読み込まないようにする場合は `canMatch` を `canActivate` より優先
|
||||
- すべての機能モジュールを `loadChildren` で遅延読み込み
|
||||
- コンポーネント内のローディング状態を回避するため `resolve` でデータをプリフェッチ
|
||||
|
||||
### 関数型ガード
|
||||
|
||||
```typescript
|
||||
export const authGuard: CanActivateFn = () => {
|
||||
const auth = inject(AuthService);
|
||||
return auth.isAuthenticated()
|
||||
? true
|
||||
: inject(Router).createUrlTree(['/login']);
|
||||
};
|
||||
```
|
||||
|
||||
### データリゾルバ
|
||||
|
||||
```typescript
|
||||
export const userResolver: ResolveFn<User> = (route) => {
|
||||
return inject(UserService).getUser(route.paramMap.get('id')!);
|
||||
};
|
||||
```
|
||||
|
||||
### ビュートランジション
|
||||
|
||||
View Transitions API でスムーズなルート遷移を有効化:
|
||||
|
||||
```typescript
|
||||
// app.config.ts
|
||||
provideRouter(routes, withViewTransitions())
|
||||
```
|
||||
|
||||
## 依存性注入パターン
|
||||
|
||||
### スコープ付きプロバイダ
|
||||
|
||||
サービスがシングルトンであるべきでない場合、コンポーネントまたはルートレベルで提供してください:
|
||||
|
||||
```typescript
|
||||
@Component({
|
||||
providers: [UserEditService], // このコンポーネントサブツリーにスコープ
|
||||
})
|
||||
export class UserEditComponent {}
|
||||
```
|
||||
|
||||
### `InjectionToken`
|
||||
|
||||
```typescript
|
||||
export const CONFIG = new InjectionToken<AppConfig>('APP_CONFIG');
|
||||
|
||||
// プロバイダ内:
|
||||
{ provide: CONFIG, useValue: appConfig }
|
||||
{ provide: CONFIG, useFactory: () => loadConfig(), deps: [] }
|
||||
|
||||
// 使用:
|
||||
private config = inject(CONFIG);
|
||||
```
|
||||
|
||||
### `viewProviders` と `providers`
|
||||
|
||||
- `providers`: コンポーネントとそのすべてのコンテンツ子要素で利用可能
|
||||
- `viewProviders`: コンポーネント自身のビューでのみ利用可能(投影されたコンテンツでは不可)
|
||||
|
||||
## HTTP インターセプター
|
||||
|
||||
認証、エラーハンドリング、リトライには関数型インターセプター(v15+)を使用してください:
|
||||
|
||||
```typescript
|
||||
export const authInterceptor: HttpInterceptorFn = (req, next) => {
|
||||
const token = inject(AuthService).token();
|
||||
if (!token) return next(req);
|
||||
return next(req.clone({ setHeaders: { Authorization: `Bearer ${token}` } }));
|
||||
};
|
||||
```
|
||||
|
||||
`app.config.ts` で登録:
|
||||
|
||||
```typescript
|
||||
provideHttpClient(withInterceptors([authInterceptor, errorInterceptor]))
|
||||
```
|
||||
|
||||
## RxJS オペレータ
|
||||
|
||||
- `switchMap` — 検索、ナビゲーション(前のリクエストをキャンセル)
|
||||
- `mergeMap` — 独立した並列リクエスト
|
||||
- `exhaustMap` — フォーム送信(完了するまで無視)
|
||||
- 常に `catchError` でエラーを処理 — ストリームを暗黙的に死なせない
|
||||
|
||||
```typescript
|
||||
search$ = this.query$.pipe(
|
||||
debounceTime(300),
|
||||
distinctUntilChanged(),
|
||||
switchMap(q => this.service.search(q).pipe(catchError(() => of([])))),
|
||||
);
|
||||
```
|
||||
|
||||
## フォーム
|
||||
|
||||
プロジェクトの既存のフォーム戦略に合わせてください。v21+ の新規アプリにはシグナルフォームを優先してください。
|
||||
|
||||
```typescript
|
||||
// Reactive Forms — 複雑なフォームの標準
|
||||
export class UserFormComponent {
|
||||
private fb = inject(FormBuilder);
|
||||
|
||||
form = this.fb.group({
|
||||
name: ['', Validators.required],
|
||||
email: ['', [Validators.required, Validators.email]],
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## レンダリング戦略
|
||||
|
||||
- **CSR**(デフォルト): 標準 SPA
|
||||
- **SSR + ハイドレーション**: `ng add @angular/ssr` — FCP と SEO を改善
|
||||
- **SSG(プリレンダリング)**: コンテンツの多いルート向けにビルド時に静的ページを生成
|
||||
|
||||
SSR を使用する場合、`window`、`document`、`localStorage` を直接使用しないでください — `isPlatformBrowser` または `DOCUMENT` トークンを使用してください。
|
||||
|
||||
## アクセシビリティ
|
||||
|
||||
ヘッドレスでアクセシブルなコンポーネント(Accordion、Listbox、Combobox、Menu、Tabs、Toolbar、Tree、Grid)には Angular CDK を使用してください。ARIA 属性を手動で管理するのではなく、スタイルを適用してください:
|
||||
|
||||
```css
|
||||
[aria-selected="true"] { background: var(--color-selected); }
|
||||
```
|
||||
|
||||
## スキルリファレンス
|
||||
|
||||
シグナル、フォーム、ルーティング、DI、SSR、アクセシビリティパターンの詳細なガイダンスについては、スキル: `angular-developer` を参照してください。
|
||||
87
docs/ja-JP/rules/angular/security.md
Normal file
87
docs/ja-JP/rules/angular/security.md
Normal file
@@ -0,0 +1,87 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.component.ts"
|
||||
- "**/*.component.html"
|
||||
- "**/*.service.ts"
|
||||
- "**/*.interceptor.ts"
|
||||
---
|
||||
# Angular セキュリティ
|
||||
|
||||
> このファイルは [common/security.md](../common/security.md) を Angular 固有のコンテンツで拡張します。
|
||||
|
||||
## XSS 防止
|
||||
|
||||
Angular はバインドされた値を自動的にサニタイズします。ユーザー制御の入力に対してサニタイザをバイパスしないでください。
|
||||
|
||||
```typescript
|
||||
// 誤り: サニタイゼーションをバイパス — XSS リスク
|
||||
this.safeHtml = this.sanitizer.bypassSecurityTrustHtml(userInput);
|
||||
|
||||
// 正しい: 信頼する前に明示的にサニタイズ
|
||||
this.safeHtml = this.sanitizer.sanitize(SecurityContext.HTML, userInput);
|
||||
```
|
||||
|
||||
- 文書化されレビューされた理由なしに `bypassSecurityTrust*` メソッドを使用しない
|
||||
- 信頼できないコンテンツに `[innerHTML]` を使用しない — `innerText` またはサニタイズパイプを使用
|
||||
- ユーザー入力に `[href]` をバインドしない — Angular はすべてのコンテキストで `javascript:` URL をブロックするわけではない
|
||||
- ユーザーデータからテンプレート文字列を構築しない
|
||||
|
||||
## HTTP セキュリティ
|
||||
|
||||
`HttpClient` のみを使用してください — 代替手段がない場合を除き、生の `fetch()` や `XHR` を使用しないでください。
|
||||
|
||||
```typescript
|
||||
// 誤り: インターセプターをバイパス(認証ヘッダー、エラーハンドリング、ログ記録)
|
||||
const res = await fetch('/api/users');
|
||||
|
||||
// 正しい
|
||||
users$ = this.http.get<User[]>('/api/users');
|
||||
```
|
||||
|
||||
- インターセプター経由で認証トークンを添付 — 個々のサービス呼び出しにハードコードしない
|
||||
- API レスポンスを型付けしてバリデーション — 境界では外部データを `unknown` として扱う
|
||||
- トークン、PII、または認証情報を含む可能性のある HTTP レスポンスをログに記録しない
|
||||
|
||||
## シークレット管理
|
||||
|
||||
```typescript
|
||||
// 誤り: ソースにハードコードされたシークレット
|
||||
const apiKey = 'sk-live-xxxx';
|
||||
|
||||
// 正しい: 環境経由で注入
|
||||
import { environment } from '../environments/environment';
|
||||
const apiKey = environment.apiKey;
|
||||
```
|
||||
|
||||
- `environment.ts` を設定の形として扱う — ソース管理されている環境ファイルに実際のシークレットを格納しない
|
||||
- CI/CD 経由で本番シークレットを注入(環境変数、シークレットマネージャー)
|
||||
|
||||
## ルートガード
|
||||
|
||||
すべての認証済みまたはロール制限されたルートにはガードが必要です。UI 要素の非表示だけに頼らないでください。
|
||||
|
||||
```typescript
|
||||
{
|
||||
path: 'admin',
|
||||
canMatch: [authGuard, roleGuard('admin')],
|
||||
loadChildren: () => import('./admin/admin.routes'),
|
||||
}
|
||||
```
|
||||
|
||||
機密性の高いルートには `canMatch` を使用してください — 未認証ユーザーに対してルートモジュールの読み込み自体を防止します。
|
||||
|
||||
## SSR セキュリティ
|
||||
|
||||
Angular SSR を使用する場合:
|
||||
|
||||
- 意図的に公開する場合を除き、`TransferState` 経由でサーバーサイドの環境変数をクライアントに公開しない
|
||||
- サーバーサイドレンダリング前にすべての入力をサニタイズ — DOM ベースの XSS はサーバーサイドでも発生する可能性がある
|
||||
- サーバー上で `window`、`document`、`localStorage` を使用しない — `isPlatformBrowser` でゲートするか、`DOCUMENT` トークン経由で注入
|
||||
|
||||
## コンテンツセキュリティポリシー
|
||||
|
||||
サーバーサイドで CSP ヘッダーを設定してください。`script-src` で `unsafe-inline` を避けてください。インラインスクリプトを使用する SSR では、Angular の CSP サポートを通じてナンスを使用してください。
|
||||
|
||||
## エージェントサポート
|
||||
|
||||
- 包括的なセキュリティ監査には **security-reviewer** スキルを使用
|
||||
164
docs/ja-JP/rules/angular/testing.md
Normal file
164
docs/ja-JP/rules/angular/testing.md
Normal file
@@ -0,0 +1,164 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.spec.ts"
|
||||
- "**/*.test.ts"
|
||||
---
|
||||
# Angular テスト
|
||||
|
||||
> このファイルは [common/testing.md](../common/testing.md) を Angular 固有のコンテンツで拡張します。
|
||||
|
||||
## テストランナー
|
||||
|
||||
プロジェクトで設定されているテストランナーを使用してください。`angular.json` と `package.json` を確認してください。Angular プロジェクトでは一般的に Vitest、Jest、または Jasmine + Karma が使用されます。
|
||||
|
||||
```bash
|
||||
ng test # ウォッチモード
|
||||
ng test --no-watch # CI モード
|
||||
```
|
||||
|
||||
## TestBed セットアップ
|
||||
|
||||
スタンドアロンコンポーネントの場合、コンポーネントを直接インポートします。外部テンプレートを持つコンポーネントには `compileComponents()` を呼び出してください。
|
||||
|
||||
```typescript
|
||||
describe('UserCardComponent', () => {
|
||||
let fixture: ComponentFixture<UserCardComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [UserCardComponent],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(UserCardComponent);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## シグナル入力
|
||||
|
||||
シグナルベースの入力は `fixture.componentRef.setInput()` で設定します:
|
||||
|
||||
```typescript
|
||||
fixture.componentRef.setInput('user', mockUser);
|
||||
fixture.detectChanges();
|
||||
```
|
||||
|
||||
## コンポーネントハーネス
|
||||
|
||||
UI インタラクションには直接の DOM クエリよりも Angular CDK コンポーネントハーネスを優先してください。ハーネスはマークアップの変更に対してより耐性があります。
|
||||
|
||||
```typescript
|
||||
import { HarnessLoader } from '@angular/cdk/testing';
|
||||
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
|
||||
import { MatButtonHarness } from '@angular/material/button/testing';
|
||||
|
||||
let loader: HarnessLoader;
|
||||
|
||||
beforeEach(() => {
|
||||
loader = TestbedHarnessEnvironment.loader(fixture);
|
||||
});
|
||||
|
||||
it('triggers save on button click', async () => {
|
||||
const button = await loader.getHarness(MatButtonHarness.with({ text: 'Save' }));
|
||||
await button.click();
|
||||
expect(saveSpy).toHaveBeenCalled();
|
||||
});
|
||||
```
|
||||
|
||||
## ルーターテスト
|
||||
|
||||
ルーターに依存するコンポーネントには `RouterTestingHarness` を使用してください:
|
||||
|
||||
```typescript
|
||||
import { RouterTestingHarness } from '@angular/router/testing';
|
||||
|
||||
it('renders user on navigation', async () => {
|
||||
const harness = await RouterTestingHarness.create();
|
||||
const component = await harness.navigateByUrl('/users/1', UserDetailComponent);
|
||||
expect(component.userId()).toBe('1');
|
||||
});
|
||||
```
|
||||
|
||||
## 非同期テスト
|
||||
|
||||
制御された非同期には `fakeAsync` + `tick` を使用してください。実際の非同期には `waitForAsync` と `fixture.whenStable()` を使用してください。
|
||||
|
||||
```typescript
|
||||
it('loads user after delay', fakeAsync(() => {
|
||||
const service = TestBed.inject(UserService);
|
||||
vi.spyOn(service, 'getUser').mockReturnValue(of(mockUser));
|
||||
|
||||
fixture.detectChanges();
|
||||
tick();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.nativeElement.querySelector('.name').textContent).toBe(mockUser.name);
|
||||
}));
|
||||
```
|
||||
|
||||
## HTTP テスト
|
||||
|
||||
```typescript
|
||||
import { provideHttpClientTesting } from '@angular/common/http/testing';
|
||||
import { HttpTestingController } from '@angular/common/http/testing';
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [provideHttpClient(), provideHttpClientTesting()],
|
||||
});
|
||||
httpMock = TestBed.inject(HttpTestingController);
|
||||
});
|
||||
|
||||
afterEach(() => httpMock.verify());
|
||||
```
|
||||
|
||||
## サービステスト
|
||||
|
||||
コンポーネントフィクスチャなしでサービスを直接注入します:
|
||||
|
||||
```typescript
|
||||
describe('UserService', () => {
|
||||
let service: UserService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [provideHttpClient(), provideHttpClientTesting()],
|
||||
});
|
||||
service = TestBed.inject(UserService);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## テスト対象
|
||||
|
||||
- **サービス**: すべてのパブリックメソッド、エラーパス、HTTP インタラクション
|
||||
- **コンポーネント**: 入力/出力バインディング、主要な状態のレンダリング結果、ハーネスを使用したユーザーインタラクション
|
||||
- **パイプ**: 純粋な変換 — TestBed 不要のプレーンなユニットテスト
|
||||
- **ガード/リゾルバ**: `RouterTestingHarness` を使用した許可および拒否状態の戻り値
|
||||
|
||||
## E2E テスト
|
||||
|
||||
重要なユーザーフローには、Cypress や Playwright などプロジェクトで設定されている E2E フレームワークを使用してください。
|
||||
|
||||
```typescript
|
||||
describe('Login flow', () => {
|
||||
it('redirects to dashboard on valid credentials', () => {
|
||||
cy.visit('/login');
|
||||
cy.get('[data-cy=email]').type('user@example.com');
|
||||
cy.get('[data-cy=password]').type('password123');
|
||||
cy.get('[data-cy=submit]').click();
|
||||
cy.url().should('include', '/dashboard');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
- 安定したセレクタのためにインタラクティブ要素に `data-cy` 属性を追加
|
||||
- E2E テストでセレクタに CSS クラスやテキストコンテンツに依存しない
|
||||
|
||||
## カバレッジ
|
||||
|
||||
サービスとパイプのカバレッジは80%以上を目標にしてください。コンポーネント: 実装の詳細ではなく、振る舞いをテストしてください。
|
||||
|
||||
## スキルリファレンス
|
||||
|
||||
包括的なテストパターン、ハーネスの使用法、非同期のベストプラクティスについては、スキル: `angular-developer` を参照してください。
|
||||
153
docs/ja-JP/rules/arkts/coding-style.md
Normal file
153
docs/ja-JP/rules/arkts/coding-style.md
Normal file
@@ -0,0 +1,153 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.ets"
|
||||
- "**/*.ts"
|
||||
- "**/module.json5"
|
||||
- "**/oh-package.json5"
|
||||
- "**/build-profile.json5"
|
||||
---
|
||||
# HarmonyOS / ArkTS コーディングスタイル
|
||||
|
||||
> このファイルは [common/coding-style.md](../common/coding-style.md) を HarmonyOS および ArkTS 固有のコンテンツで拡張します。
|
||||
|
||||
## ArkTS 言語の制約
|
||||
|
||||
ArkTS は TypeScript の厳格な静的型付きサブセットです。これらの制約に違反すると**コンパイルエラー**が発生します。
|
||||
|
||||
### 型システム
|
||||
|
||||
- `any` や `unknown` 型は使用不可 — 常に明示的な型を使用する
|
||||
- インデックスアクセス型は使用不可 — 型名を直接使用する
|
||||
- 条件付き型エイリアスや `infer` キーワードは使用不可
|
||||
- 交差型は使用不可 — 継承を使用する
|
||||
- マップ型は使用不可 — クラスと通常のイディオムを使用する
|
||||
- 型注釈に `typeof` は使用不可 — 明示的な型宣言を使用する
|
||||
- `as const` アサーションは使用不可 — 明示的な型注釈を使用する
|
||||
- 構造的型付けは使用不可 — 継承、インターフェース、型エイリアスを使用する
|
||||
- `Partial`、`Required`、`Readonly`、`Record` 以外の TypeScript ユーティリティ型は使用不可
|
||||
- `Record<K, V>` のインデックス式の型は `V | undefined`
|
||||
- `catch` 句では型注釈を省略する(ArkTS は `any`/`unknown` をサポートしない)
|
||||
|
||||
### 関数とクラス
|
||||
|
||||
- 関数式は使用不可 — アロー関数を使用する
|
||||
- ネストした関数は使用不可 — ラムダを使用する
|
||||
- ジェネレーター関数は使用不可 — マルチタスクには `async`/`await` を使用する
|
||||
- `Function.apply`、`Function.call`、`Function.bind` は使用不可 — `this` には従来の OOP を使用する
|
||||
- コンストラクタ型式は使用不可 — ラムダを使用する
|
||||
- インターフェースやオブジェクト型のコンストラクタシグネチャは使用不可 — メソッドかクラスを使用する
|
||||
- コンストラクタ内でのクラスフィールド宣言は不可 — クラス本体で宣言する
|
||||
- スタンドアロン関数や静的メソッドで `this` は使用不可 — インスタンスメソッド内のみ
|
||||
- `new.target` は使用不可
|
||||
- 確実な代入アサーション(`let v!: T`)は使用不可 — 初期化済み宣言を使用する
|
||||
- クラスリテラルは使用不可 — 名前付きクラス型を導入する
|
||||
- クラスをオブジェクトとして使用(変数への代入)は不可 — クラス宣言は値ではなく型を導入する
|
||||
- クラスごとに静的ブロックは1つのみ — すべての静的ステートメントをまとめる
|
||||
|
||||
### オブジェクトとプロパティアクセス
|
||||
|
||||
- 動的フィールド宣言や `obj["field"]` アクセスは使用不可 — `obj.field` 構文を使用する
|
||||
- `delete` 演算子は使用不可 — 不在を示すには `null` を持つ nullable 型を使用する
|
||||
- プロトタイプへの代入は使用不可 — クラスとインターフェースを使用する
|
||||
- `in` 演算子は使用不可 — `instanceof` を使用する
|
||||
- オブジェクトメソッドの再代入は不可 — ラッパー関数や継承を使用する
|
||||
- `Symbol()` API は使用不可(`Symbol.iterator` を除く)
|
||||
- `globalThis` やグローバルスコープは使用不可 — 明示的なモジュールのエクスポート/インポートを使用する
|
||||
- 名前空間をオブジェクトとして使用は不可 — クラスかモジュールを使用する
|
||||
- 名前空間内のステートメントは不可 — 関数を使用する
|
||||
|
||||
### 分割代入とスプレッド
|
||||
|
||||
- 分割代入や変数宣言は使用不可 — 中間オブジェクトとフィールドごとのアクセスを使用する
|
||||
- 分割代入のパラメータ宣言は使用不可 — パラメータを直接渡し、ローカル名を手動で割り当てる
|
||||
- スプレッド演算子は配列(または配列派生クラス)をレストパラメータや配列リテラルに展開する場合のみ使用可
|
||||
|
||||
### モジュールとインポート
|
||||
|
||||
- `require()` は使用不可 — 通常の `import` 構文を使用する
|
||||
- `export = ...` は使用不可 — 通常のエクスポート/インポートを使用する
|
||||
- インポートアサーションは使用不可 — ArkTS ではインポートはコンパイル時に解決される
|
||||
- UMD モジュールは使用不可
|
||||
- モジュール名にワイルドカードは使用不可
|
||||
- すべての `import` ステートメントは他のすべてのステートメントより前に記述する
|
||||
- TypeScript のコードベースは import 経由で ArkTS のコードベースに依存してはならない(逆はサポート)
|
||||
|
||||
### その他の制限
|
||||
|
||||
- `var` は使用不可 — `let` を使用する
|
||||
- `for...in` ループは使用不可 — 配列には通常の `for` ループを使用する
|
||||
- `with` ステートメントは使用不可
|
||||
- JSX 式は使用不可
|
||||
- `#` プライベート識別子は使用不可 — `private` キーワードを使用する
|
||||
- 宣言のマージ(クラス、インターフェース、列挙型)は不可 — 定義をコンパクトに保つ
|
||||
- インデックスシグネチャは使用不可 — 配列を使用する
|
||||
- カンマ演算子は `for` ループ内のみ
|
||||
- 単項演算子 `+`、`-`、`~` は数値型のみ(暗黙の文字列変換なし)
|
||||
- 列挙型のメンバー: 明示的な初期化子には同じ型のコンパイル時式のみ
|
||||
- 関数の戻り値型推論は制限あり — 戻り値型を省略した関数呼び出し時は明示的に指定する
|
||||
|
||||
### オブジェクトリテラル
|
||||
|
||||
- コンパイラが対応するクラスやインターフェースを推論できる場合のみサポート
|
||||
- 次の場合はサポートされない: `any`/`Object`/`object` 型、メソッドを持つクラス/インターフェース、パラメータ付きコンストラクタを持つクラス、`readonly` フィールドを持つクラス
|
||||
|
||||
## 命名規則
|
||||
|
||||
- 変数 / 関数: `camelCase`(例: `getUserInfo`、`goodsList`)
|
||||
- クラス / インターフェース: `PascalCase`(例: `UserViewModel`、`IGoodsModel`)
|
||||
- 定数: `UPPER_SNAKE_CASE`(例: `MAX_PAGE_SIZE`、`COLOR_PRIMARY`)
|
||||
- ファイル名: コンポーネントは `PascalCase`(例: `HomePage.ets`)、ユーティリティは `camelCase`
|
||||
|
||||
## フォーマット
|
||||
|
||||
- 文字列にはダブルクォートを優先する
|
||||
- ステートメント末尾にセミコロンを付ける
|
||||
- `var` は絶対に使用しない — `const` を優先し、次に `let`
|
||||
- すべてのメソッド、パラメータ、戻り値には完全な型注釈を付ける
|
||||
|
||||
## ファイル構成
|
||||
|
||||
- コンポーネントファイル(`.ets`): ファイルごとに1つの `@ComponentV2`
|
||||
- ViewModel ファイル: ファイルごとに1つの ViewModel クラス
|
||||
- モデルファイル: 関連するデータモデルは同じファイルに共存可能
|
||||
- ファイルは400行以内に収める。800行に近づく場合はヘルパーを抽出する
|
||||
|
||||
## コメント
|
||||
|
||||
- ファイルヘッダー: `@file`(ファイルの目的)+ `@author`(開発者)— プロジェクトがすでにファイルヘッダーを使用している場合
|
||||
- パブリックメソッド: JSDoc に `@param`、`@returns` を付ける。複雑なメソッドには `@example` を追加する
|
||||
- プロジェクトの既存のドキュメント言語に合わせる。リポジトリが中国語コメントを標準化していない限り英語を使用する
|
||||
|
||||
## エラーハンドリング
|
||||
|
||||
```typescript
|
||||
// 適切なエラーハンドリングで try/catch を使用する
|
||||
try {
|
||||
const result = await riskyOperation()
|
||||
return result
|
||||
} catch (error) {
|
||||
hilog.error(0x0000, 'TAG', 'Operation failed: %{public}s', error)
|
||||
throw new Error('User-friendly error message')
|
||||
}
|
||||
```
|
||||
|
||||
## イミュータビリティ
|
||||
|
||||
共通のイミュータビリティ原則に従う — ミューテートするのではなく新しいオブジェクトを作成する:
|
||||
|
||||
```typescript
|
||||
// BAD: ミューテーション
|
||||
function updateUser(user: UserModel, name: string): UserModel {
|
||||
user.name = name // 直接変更
|
||||
return user
|
||||
}
|
||||
|
||||
// GOOD: イミュータブル — 新しいインスタンスを作成
|
||||
function updateUser(user: UserModel, name: string): UserModel {
|
||||
const updated = new UserModel()
|
||||
updated.id = user.id
|
||||
updated.name = name
|
||||
updated.email = user.email
|
||||
return updated
|
||||
}
|
||||
```
|
||||
135
docs/ja-JP/rules/arkts/hooks.md
Normal file
135
docs/ja-JP/rules/arkts/hooks.md
Normal file
@@ -0,0 +1,135 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.ets"
|
||||
- "**/*.ts"
|
||||
- "**/module.json5"
|
||||
- "**/oh-package.json5"
|
||||
---
|
||||
# HarmonyOS / ArkTS フック
|
||||
|
||||
> このファイルは [common/hooks.md](../common/hooks.md) を HarmonyOS 固有のビルドおよび検証フックで拡張します。
|
||||
|
||||
## ビルドコマンド
|
||||
|
||||
### HAP パッケージのビルド
|
||||
|
||||
```bash
|
||||
# HAP パッケージをビルドする(グローバル hvigor 環境)
|
||||
hvigorw assembleHap -p product=default
|
||||
|
||||
# 特定のモジュールでビルドする
|
||||
hvigorw assembleHap -p module=entry -p product=default
|
||||
|
||||
# クリーンビルド
|
||||
hvigorw clean
|
||||
```
|
||||
|
||||
### DevEco Studio CLI
|
||||
|
||||
```bash
|
||||
# プロジェクト構造を確認する
|
||||
hvigorw --version
|
||||
|
||||
# 依存関係をインストールする
|
||||
ohpm install
|
||||
|
||||
# 依存関係を更新する
|
||||
ohpm update
|
||||
```
|
||||
|
||||
## 推奨 PostToolUse フック
|
||||
|
||||
### .ets/.ts ファイル編集後
|
||||
|
||||
ArkTS のコンパイルエラーを確認するために hvigor ビルドを実行する:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "PostToolUse",
|
||||
"matcher": {
|
||||
"tool": ["Edit", "Write"],
|
||||
"filePath": ["**/*.ets", "**/*.ts"]
|
||||
},
|
||||
"hooks": [
|
||||
{
|
||||
"command": "hvigorw assembleHap -p product=default 2>&1 | tail -20",
|
||||
"async": true,
|
||||
"timeout": 60000
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### module.json5 編集後
|
||||
|
||||
パーミッションとアビリティの宣言を検証する:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "PostToolUse",
|
||||
"matcher": {
|
||||
"tool": "Edit",
|
||||
"filePath": "**/module.json5"
|
||||
},
|
||||
"hooks": [
|
||||
{
|
||||
"command": "echo '[HarmonyOS] module.json5 modified - verify permissions and abilities'",
|
||||
"async": false
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### oh-package.json5 編集後
|
||||
|
||||
依存関係を再インストールする:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "PostToolUse",
|
||||
"matcher": {
|
||||
"tool": "Edit",
|
||||
"filePath": "**/oh-package.json5"
|
||||
},
|
||||
"hooks": [
|
||||
{
|
||||
"command": "ohpm install 2>&1 | tail -10",
|
||||
"async": true,
|
||||
"timeout": 30000
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## PreToolUse フック
|
||||
|
||||
### V1 デコレーターガード
|
||||
|
||||
コードに V1 状態管理デコレーターが含まれている場合に警告する:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "PreToolUse",
|
||||
"matcher": {
|
||||
"tool": ["Write", "Edit"],
|
||||
"filePath": "**/*.ets"
|
||||
},
|
||||
"hooks": [
|
||||
{
|
||||
"command": "echo '[HarmonyOS] Reminder: Use @ComponentV2 / @Local / @Param - V1 decorators (@State, @Prop, @Link) are prohibited'"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 検証チェックリスト
|
||||
|
||||
各実装サイクルの後、以下を確認する:
|
||||
|
||||
- [ ] `hvigorw assembleHap` がエラーなしで完了する
|
||||
- [ ] 新規または変更した `.ets` ファイルに V1 デコレーターがない
|
||||
- [ ] 新規または変更したファイルに `@ohos.router` のインポートがない
|
||||
- [ ] すべての API パーミッションが `module.json5` に宣言されている
|
||||
- [ ] すべての依存関係が `oh-package.json5` に記載されている
|
||||
- [ ] リソース文字列がすべての i18n ディレクトリに追加されている
|
||||
- [ ] 新しいカラーリソースにダークテーマのカラーが提供されている
|
||||
236
docs/ja-JP/rules/arkts/patterns.md
Normal file
236
docs/ja-JP/rules/arkts/patterns.md
Normal file
@@ -0,0 +1,236 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.ets"
|
||||
- "**/*.ts"
|
||||
---
|
||||
# HarmonyOS / ArkTS パターン
|
||||
|
||||
> このファイルは [common/patterns.md](../common/patterns.md) を HarmonyOS および ArkTS 固有のパターンで拡張します。
|
||||
|
||||
## 状態管理: V2 のみ
|
||||
|
||||
ArkUI 状態管理 V2 を**必ず使用**すること。V1 デコレーターは非推奨であり、使用してはならない。
|
||||
|
||||
### V2 デコレーター
|
||||
|
||||
| デコレーター | 用途 |
|
||||
|------------|------|
|
||||
| `@ComponentV2` | 構造体を V2 コンポーネントとしてマークする |
|
||||
| `@Local` | コンポーネント内のローカル状態 |
|
||||
| `@Param` | 親から受け取るプロパティ(読み取り専用) |
|
||||
| `@Event` | 子から親へのコールバックイベント |
|
||||
| `@Provider` | 子孫コンポーネントへ状態を提供する |
|
||||
| `@Consumer` | 祖先の `@Provider` から状態を取得する |
|
||||
| `@Monitor` | 状態変化を監視する(V1 の `@Watch` を置き換え) |
|
||||
| `@Computed` | 派生/計算された値 |
|
||||
| `@ObservedV2` | V2 状態管理のためにクラスをオブザーバブルにする |
|
||||
| `@Trace` | `@ObservedV2` クラスのオブザーバブルプロパティをマークする |
|
||||
|
||||
### 禁止されている V1 デコレーター
|
||||
|
||||
絶対に使用しないこと: `@State`、`@Prop`、`@Link`、`@ObjectLink`、`@Observed`、`@Provide`、`@Consume`、`@Watch`、`@Component`(代わりに `@ComponentV2` を使用)。
|
||||
|
||||
### V2 コンポーネントの例
|
||||
|
||||
```typescript
|
||||
@ObservedV2
|
||||
class UserModel {
|
||||
@Trace name: string = ''
|
||||
@Trace age: number = 0
|
||||
}
|
||||
|
||||
@ComponentV2
|
||||
struct UserCard {
|
||||
@Param user: UserModel = new UserModel()
|
||||
@Event onDelete: () => void = () => {}
|
||||
|
||||
build() {
|
||||
Column() {
|
||||
Text(this.user.name)
|
||||
.fontSize($r('app.float.font_size_title'))
|
||||
Text(`${this.user.age}`)
|
||||
.fontSize($r('app.float.font_size_body'))
|
||||
Button($r('app.string.delete'))
|
||||
.onClick(() => this.onDelete())
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 状態の同期
|
||||
|
||||
```typescript
|
||||
@ComponentV2
|
||||
struct ParentPage {
|
||||
@Provider('userState') userModel: UserModel = new UserModel()
|
||||
|
||||
build() {
|
||||
Column() {
|
||||
ChildComponent() // @Consumer('userState') を自動的に受け取る
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ComponentV2
|
||||
struct ChildComponent {
|
||||
@Consumer('userState') userModel: UserModel = new UserModel()
|
||||
|
||||
build() {
|
||||
Text(this.userModel.name)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## ルーティング: Navigation のみ
|
||||
|
||||
`NavPathStack` を使用した `Navigation` コンポーネントを**必ず使用**すること。`@ohos.router` は絶対に使用しないこと。
|
||||
|
||||
### Navigation のセットアップ
|
||||
|
||||
```typescript
|
||||
@ComponentV2
|
||||
struct MainPage {
|
||||
@Local navPathStack: NavPathStack = new NavPathStack()
|
||||
|
||||
build() {
|
||||
Navigation(this.navPathStack) {
|
||||
// ホームコンテンツ
|
||||
}
|
||||
.navDestination(this.routerMap)
|
||||
}
|
||||
|
||||
@Builder
|
||||
routerMap(name: string, param: ESObject) {
|
||||
if (name === 'detail') {
|
||||
DetailPage()
|
||||
} else if (name === 'settings') {
|
||||
SettingsPage()
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### ページナビゲーション
|
||||
|
||||
```typescript
|
||||
// 新しいページをプッシュする
|
||||
this.navPathStack.pushPath({ name: 'detail', param: { id: '123' } })
|
||||
|
||||
// 現在のページを置き換える
|
||||
this.navPathStack.replacePath({ name: 'settings' })
|
||||
|
||||
// 戻る
|
||||
this.navPathStack.pop()
|
||||
|
||||
// ルートに戻る
|
||||
this.navPathStack.clear()
|
||||
```
|
||||
|
||||
### NavDestination サブページ
|
||||
|
||||
```typescript
|
||||
@ComponentV2
|
||||
struct DetailPage {
|
||||
build() {
|
||||
NavDestination() {
|
||||
Column() {
|
||||
Text($r('app.string.detail_title'))
|
||||
}
|
||||
}
|
||||
.title($r('app.string.detail_nav_title'))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## アーキテクチャパターン: MVVM
|
||||
|
||||
HarmonyOS アプリケーションに推奨されるアーキテクチャ:
|
||||
|
||||
```
|
||||
feature/
|
||||
|-- model/ # データモデル(@ObservedV2 クラス)
|
||||
|-- viewmodel/ # ビジネスロジック(ViewModel クラス)
|
||||
|-- view/ # UI コンポーネント(@ComponentV2 構造体)
|
||||
|-- service/ # API 呼び出し、データアクセス
|
||||
```
|
||||
|
||||
- **View**: レンダリングロジックのみ、`build()` 内にビジネスロジックを含めない
|
||||
- **ViewModel**: すべてのビジネスロジックをここにカプセル化する
|
||||
- **Model**: `@ObservedV2` と `@Trace` を持つ純粋なデータクラス
|
||||
- **Service**: ネットワークリクエスト、データベース操作、ファイル I/O
|
||||
|
||||
## ArkUI アニメーションパターン
|
||||
|
||||
### 状態駆動アニメーション
|
||||
|
||||
```typescript
|
||||
@ComponentV2
|
||||
struct AnimatedCard {
|
||||
@Local isExpanded: boolean = false
|
||||
@Local cardScale: number = 0.8
|
||||
|
||||
build() {
|
||||
Column() {
|
||||
// コンテンツ
|
||||
}
|
||||
.scale({ x: this.cardScale, y: this.cardScale })
|
||||
.animation({ duration: 300, curve: Curve.EaseInOut })
|
||||
.onClick(() => {
|
||||
this.isExpanded = !this.isExpanded
|
||||
this.cardScale = this.isExpanded ? 1.0 : 0.8
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### アニメーションのルール
|
||||
|
||||
- ネイティブ HarmonyOS アニメーション API と高度なテンプレートを優先する
|
||||
- 状態変数の変更でアニメーションをトリガーする状態駆動アニメーションを持つ宣言的 UI を使用する
|
||||
- 複雑なサブコンポーネントアニメーションのレンダリングバッチを削減するために `renderGroup(true)` を設定する
|
||||
- アニメーション中に `width`、`height`、`padding`、`margin` を頻繁に変更しないこと — パフォーマンスに深刻な影響
|
||||
- 明示的なアニメーション制御には `animateTo` を使用する
|
||||
- パフォーマンスの高いアニメーションには `transform`(translate、scale、rotate)と `opacity` を優先する
|
||||
|
||||
## パフォーマンスパターン
|
||||
|
||||
### 大きなリストへの LazyForEach
|
||||
|
||||
```typescript
|
||||
@ComponentV2
|
||||
struct LargeList {
|
||||
@Local dataSource: MyDataSource = new MyDataSource()
|
||||
|
||||
build() {
|
||||
List() {
|
||||
LazyForEach(this.dataSource, (item: ItemModel) => {
|
||||
ListItem() {
|
||||
ItemComponent({ item: item })
|
||||
}
|
||||
}, (item: ItemModel) => item.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### コンポーネントの再利用
|
||||
|
||||
- 再利用可能なコンポーネントを別のファイルに抽出する
|
||||
- コンポーネント内の軽量な UI フラグメントには `@Builder` を使用する
|
||||
- 設定可能なコンポーネントには `@Param` を使用する
|
||||
|
||||
## リソース参照
|
||||
|
||||
UI 定数は常にリソースとして定義し、`$r()` 経由で参照する:
|
||||
|
||||
```typescript
|
||||
// BAD: ハードコードされた値
|
||||
Text('Hello')
|
||||
.fontSize(16)
|
||||
.fontColor('#333333')
|
||||
|
||||
// GOOD: リソース参照
|
||||
Text($r('app.string.greeting'))
|
||||
.fontSize($r('app.float.font_size_body'))
|
||||
.fontColor($r('app.color.text_primary'))
|
||||
```
|
||||
141
docs/ja-JP/rules/arkts/security.md
Normal file
141
docs/ja-JP/rules/arkts/security.md
Normal file
@@ -0,0 +1,141 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.ets"
|
||||
- "**/*.ts"
|
||||
- "**/module.json5"
|
||||
---
|
||||
# HarmonyOS / ArkTS セキュリティ
|
||||
|
||||
> このファイルは [common/security.md](../common/security.md) を HarmonyOS 固有のセキュリティプラクティスで拡張します。
|
||||
|
||||
## パーミッション管理
|
||||
|
||||
### module.json5 でのパーミッション宣言
|
||||
|
||||
パーミッションが必要なすべてのシステム API 呼び出しを宣言する必要がある:
|
||||
|
||||
```json5
|
||||
{
|
||||
"module": {
|
||||
"requestPermissions": [
|
||||
{
|
||||
"name": "ohos.permission.INTERNET",
|
||||
"reason": "$string:internet_permission_reason",
|
||||
"usedScene": {
|
||||
"abilities": ["EntryAbility"],
|
||||
"when": "always"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### パーミッションチェックリスト
|
||||
|
||||
システム API を呼び出す前に確認する:
|
||||
|
||||
- [ ] パーミッションが `module.json5` に宣言されている
|
||||
- [ ] パーミッション理由の文字列がリソースで定義されている(ユーザー向けパーミッションの場合)
|
||||
- [ ] 機密性の高いパーミッション(カメラ、位置情報など)に対してランタイムパーミッションリクエストが実装されている
|
||||
- [ ] API 呼び出し前にパーミッションを確認し、拒否時の適切なフォールバックがある
|
||||
|
||||
### ランタイムパーミッションリクエスト
|
||||
|
||||
```typescript
|
||||
import { abilityAccessCtrl, bundleManager, Permissions } from '@kit.AbilityKit';
|
||||
|
||||
async function checkAndRequestPermission(permission: Permissions): Promise<boolean> {
|
||||
const atManager = abilityAccessCtrl.createAtManager();
|
||||
const bundleInfo = await bundleManager.getBundleInfoForSelf(
|
||||
bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION
|
||||
);
|
||||
const tokenId = bundleInfo.appInfo.accessTokenId;
|
||||
const grantStatus = await atManager.checkAccessToken(tokenId, permission);
|
||||
|
||||
if (grantStatus === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const result = await atManager.requestPermissionsFromUser(getContext(), [permission]);
|
||||
return result.authResults[0] === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED;
|
||||
}
|
||||
```
|
||||
|
||||
## シークレット管理
|
||||
|
||||
- API キー、トークン、パスワードを `.ets`/`.ts` ソースファイルに**絶対にハードコードしない**
|
||||
- 機密性の低い設定には HarmonyOS Preferences API を使用する
|
||||
- 機密性の高い認証情報には HarmonyOS キーストアを使用する
|
||||
- 環境固有の設定はビルドプロファイルで管理する
|
||||
|
||||
```typescript
|
||||
// BAD: ハードコードされたシークレット
|
||||
const API_KEY: string = 'sk-xxxxxxxxxxxx';
|
||||
|
||||
// GOOD: ビルドプロファイル設定から取得(機密性なし)
|
||||
import { BuildProfile } from 'BuildProfile';
|
||||
const endpoint = BuildProfile.API_ENDPOINT;
|
||||
|
||||
// GOOD: HUKS を使用してキー素材を露出せずにデータを暗号化/復号化する
|
||||
import { huks } from '@kit.UniversalKeystoreKit';
|
||||
async function decryptWithKeystore(alias: string, nonce: Uint8Array, aad: Uint8Array, cipherData: Uint8Array): Promise<Uint8Array> {
|
||||
const options: huks.HuksOptions = {
|
||||
properties: [
|
||||
{ tag: huks.HuksTag.HUKS_TAG_ALGORITHM, value: huks.HuksKeyAlg.HUKS_ALG_AES },
|
||||
{ tag: huks.HuksTag.HUKS_TAG_PURPOSE, value: huks.HuksKeyPurpose.HUKS_KEY_PURPOSE_DECRYPT },
|
||||
{ tag: huks.HuksTag.HUKS_TAG_BLOCK_MODE, value: huks.HuksCipherMode.HUKS_MODE_GCM },
|
||||
{ tag: huks.HuksTag.HUKS_TAG_PADDING, value: huks.HuksKeyPadding.HUKS_PADDING_NONE },
|
||||
{ tag: huks.HuksTag.HUKS_TAG_NONCE, value: nonce },
|
||||
{ tag: huks.HuksTag.HUKS_TAG_ASSOCIATED_DATA, value: aad }
|
||||
],
|
||||
inData: cipherData
|
||||
};
|
||||
const handle = await huks.initSession(alias, options);
|
||||
const result = await huks.finishSession(handle.handle, options);
|
||||
return result.outData;
|
||||
}
|
||||
```
|
||||
|
||||
## 入力バリデーション
|
||||
|
||||
- 処理前にすべてのユーザー入力を検証する
|
||||
- インジェクションを防ぐため、UI に表示する前にデータをサニタイズする
|
||||
- ナビゲーション前にディープリンクのパラメータを検証する
|
||||
|
||||
```typescript
|
||||
// ナビゲーション前に検証する
|
||||
function handleDeepLink(uri: string): void {
|
||||
const allowedPaths: string[] = ['detail', 'settings', 'profile'];
|
||||
const parsed = new URL(uri);
|
||||
const path = parsed.pathname.replace('/', '');
|
||||
|
||||
if (!allowedPaths.includes(path)) {
|
||||
hilog.warn(0x0000, 'DeepLink', 'Invalid deep link path: %{public}s', path);
|
||||
return;
|
||||
}
|
||||
|
||||
navPathStack.pushPath({ name: path });
|
||||
}
|
||||
```
|
||||
|
||||
## ネットワークセキュリティ
|
||||
|
||||
- ネットワークリクエストには常に HTTPS を使用する
|
||||
- サーバー証明書を検証する
|
||||
- リクエストのタイムアウトとリトライポリシーを実装する
|
||||
- ネットワークリクエスト/レスポンスのログに機密データ(トークン、ユーザー認証情報)を記録しない
|
||||
|
||||
## データストレージセキュリティ
|
||||
|
||||
- 機密性の高いローカルデータには暗号化されたプリファレンスを使用する
|
||||
- 不要になった機密データはメモリから消去する
|
||||
- 適切なデータライフサイクル管理を実装する
|
||||
- ストレージメカニズムを選択する際にデータ分類(公開、内部、機密)を考慮する
|
||||
|
||||
## 依存関係のセキュリティ
|
||||
|
||||
- 信頼できるソース(公式 ohpm レジストリ)からの依存関係のみを使用する
|
||||
- `oh-package.json5` の依存関係バージョンを確認する
|
||||
- サードパーティライブラリの既知の脆弱性を定期的に確認する
|
||||
- 予期しない更新を避けるために依存関係バージョンを固定する
|
||||
126
docs/ja-JP/rules/arkts/testing.md
Normal file
126
docs/ja-JP/rules/arkts/testing.md
Normal file
@@ -0,0 +1,126 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.ets"
|
||||
- "**/*.ts"
|
||||
- "**/ohosTest/**"
|
||||
---
|
||||
# HarmonyOS / ArkTS テスト
|
||||
|
||||
> このファイルは [common/testing.md](../common/testing.md) を HarmonyOS 固有のテストプラクティスで拡張します。
|
||||
|
||||
## テストフレームワーク
|
||||
|
||||
HarmonyOS は `@ohos.test` 機能を持つ組み込みテストフレームワークを使用する:
|
||||
|
||||
- **ユニットテスト**: `src/ohosTest/ets/test/` に配置
|
||||
- **UI テスト**: コンポーネントテストには `@ohos.UiTest` を使用
|
||||
- **インストルメントテスト**: デバイス/エミュレーターで実行
|
||||
|
||||
## テストディレクトリ構成
|
||||
|
||||
```
|
||||
module/
|
||||
|-- src/
|
||||
| |-- main/ets/ # プロダクションコード
|
||||
| |-- ohosTest/ets/ # テストコード
|
||||
| |-- test/
|
||||
| | |-- Ability.test.ets
|
||||
| | |-- List.test.ets
|
||||
| |-- TestAbility.ets
|
||||
| |-- TestRunner.ets
|
||||
```
|
||||
|
||||
## テストの実行
|
||||
|
||||
```bash
|
||||
# モジュールのすべてのテストを実行する
|
||||
hvigorw testHap -p product=default
|
||||
|
||||
# 接続されたデバイスでテストを実行する
|
||||
hdc shell aa test -b com.example.app -m entry_test -s unittest /ets/TestRunner/OpenHarmonyTestRunner
|
||||
```
|
||||
|
||||
## ユニットテストの例
|
||||
|
||||
```typescript
|
||||
import { describe, it, expect } from '@ohos/hypium';
|
||||
|
||||
export default function UserViewModelTest() {
|
||||
describe('UserViewModel', () => {
|
||||
it('should_initialize_with_empty_state', 0, () => {
|
||||
const vm = new UserViewModel();
|
||||
expect(vm.userName).assertEqual('');
|
||||
expect(vm.isLoading).assertFalse();
|
||||
});
|
||||
|
||||
it('should_update_user_name', 0, () => {
|
||||
const vm = new UserViewModel();
|
||||
vm.updateUserName('Alice');
|
||||
expect(vm.userName).assertEqual('Alice');
|
||||
});
|
||||
|
||||
it('should_handle_empty_input', 0, () => {
|
||||
const vm = new UserViewModel();
|
||||
vm.updateUserName('');
|
||||
expect(vm.userName).assertEqual('');
|
||||
expect(vm.hasError).assertFalse();
|
||||
});
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## UI テストの例
|
||||
|
||||
```typescript
|
||||
import { describe, it, expect } from '@ohos/hypium';
|
||||
import { Driver, ON } from '@ohos.UiTest';
|
||||
|
||||
export default function HomePageUITest() {
|
||||
describe('HomePage_UI', () => {
|
||||
it('should_display_title', 0, async () => {
|
||||
const driver = Driver.create();
|
||||
await driver.delayMs(1000);
|
||||
|
||||
const title = await driver.findComponent(ON.text('Home'));
|
||||
expect(title !== null).assertTrue();
|
||||
});
|
||||
|
||||
it('should_navigate_to_detail_on_click', 0, async () => {
|
||||
const driver = Driver.create();
|
||||
const button = await driver.findComponent(ON.id('detailButton'));
|
||||
await button.click();
|
||||
await driver.delayMs(500);
|
||||
|
||||
const detailTitle = await driver.findComponent(ON.text('Detail'));
|
||||
expect(detailTitle !== null).assertTrue();
|
||||
});
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## HarmonyOS 向け TDD ワークフロー
|
||||
|
||||
HarmonyOS に適応した標準 TDD サイクルに従う:
|
||||
|
||||
1. **RED**: `ohosTest/ets/test/` に失敗するテストを書く
|
||||
2. **GREEN**: `main/ets/` にテストを通過するための最小限のコードを実装する
|
||||
3. **REFACTOR**: テストをグリーンに保ちながらクリーンアップする
|
||||
4. **ビルド**: `hvigorw assembleHap` を実行してコンパイルを確認する
|
||||
5. **VERIFY**: デバイス/エミュレーターでテストを実行する
|
||||
|
||||
## テストカバレッジ要件
|
||||
|
||||
- すべての重要なアプリケーションコード(ViewModels、サービス、ユーティリティ)で最低 80% のカバレッジ
|
||||
- **ユニットテスト**: すべてのユーティリティ関数、ViewModel ロジック、データモデル
|
||||
- **統合テスト**: API 呼び出し、データベース操作、クロスモジュールインタラクション
|
||||
- **E2E / UI テスト**: 重要なユーザーフロー(ログイン、ナビゲーション、データ送信)
|
||||
- エッジケースのテスト: 空のデータ、ネットワークエラー、パーミッション拒否
|
||||
|
||||
## テストのベストプラクティス
|
||||
|
||||
- テストを独立させる — テスト間で共有のミュータブルな状態を持たない
|
||||
- ユニットテストではネットワーク呼び出しとシステム API をモックする
|
||||
- 意味のあるテスト名を使用する: `should_[期待される動作]_when_[条件]`
|
||||
- V2 状態管理のリアクティビティをテストする: `@Trace` プロパティが UI 更新をトリガーすることを確認する
|
||||
- Navigation フローをテストする: `NavPathStack` のプッシュ/ポップ/置き換え操作を確認する
|
||||
- フレームワーク内部のテストは避ける — ビジネスロジックとユーザーが見える動作に集中する
|
||||
124
docs/ja-JP/rules/common/code-review.md
Normal file
124
docs/ja-JP/rules/common/code-review.md
Normal file
@@ -0,0 +1,124 @@
|
||||
# コードレビュー基準
|
||||
|
||||
## 目的
|
||||
|
||||
コードレビューは、コードがマージされる前に品質、セキュリティ、保守性を確保します。このルールは、コードレビューの実施タイミングと方法を定義します。
|
||||
|
||||
## レビューのタイミング
|
||||
|
||||
**必須レビュートリガー:**
|
||||
|
||||
- コードの作成または修正後
|
||||
- 共有ブランチへのコミット前
|
||||
- セキュリティに関わるコードが変更された場合(認証、決済、ユーザーデータ)
|
||||
- アーキテクチャの変更時
|
||||
- プルリクエストのマージ前
|
||||
|
||||
**レビュー前の要件:**
|
||||
|
||||
レビューを依頼する前に、以下を確認してください:
|
||||
|
||||
- すべての自動チェック(CI/CD)が通過している
|
||||
- マージコンフリクトが解決されている
|
||||
- ブランチがターゲットブランチと最新状態である
|
||||
|
||||
## レビューチェックリスト
|
||||
|
||||
コードを完了とマークする前に:
|
||||
|
||||
- [ ] コードが読みやすく、適切に命名されている
|
||||
- [ ] 関数が焦点を絞っている(50行未満)
|
||||
- [ ] ファイルが凝集している(800行未満)
|
||||
- [ ] 深いネストがない(4レベル以上)
|
||||
- [ ] エラーが明示的に処理されている
|
||||
- [ ] ハードコードされたシークレットや認証情報がない
|
||||
- [ ] console.logやデバッグ文がない
|
||||
- [ ] 新機能にテストが存在する
|
||||
- [ ] テストカバレッジが最低80%を満たしている
|
||||
|
||||
## セキュリティレビュートリガー
|
||||
|
||||
**以下の場合はsecurity-reviewerエージェントを使用してください:**
|
||||
|
||||
- 認証または認可コード
|
||||
- ユーザー入力の処理
|
||||
- データベースクエリ
|
||||
- ファイルシステム操作
|
||||
- 外部APIコール
|
||||
- 暗号化操作
|
||||
- 決済または金融コード
|
||||
|
||||
## レビュー重大度レベル
|
||||
|
||||
| レベル | 意味 | アクション |
|
||||
|--------|------|------------|
|
||||
| CRITICAL | セキュリティ脆弱性またはデータ損失リスク | **BLOCK** - マージ前に修正必須 |
|
||||
| HIGH | バグまたは重大な品質問題 | **WARN** - マージ前に修正すべき |
|
||||
| MEDIUM | 保守性の懸念 | **INFO** - 修正を検討 |
|
||||
| LOW | スタイルまたは軽微な提案 | **NOTE** - 任意 |
|
||||
|
||||
## エージェントの使用
|
||||
|
||||
コードレビューには以下のエージェントを使用してください:
|
||||
|
||||
| エージェント | 目的 |
|
||||
|-------------|------|
|
||||
| **code-reviewer** | 一般的なコード品質、パターン、ベストプラクティス |
|
||||
| **security-reviewer** | セキュリティ脆弱性、OWASP Top 10 |
|
||||
| **typescript-reviewer** | TypeScript/JavaScript固有の問題 |
|
||||
| **python-reviewer** | Python固有の問題 |
|
||||
| **go-reviewer** | Go固有の問題 |
|
||||
| **rust-reviewer** | Rust固有の問題 |
|
||||
|
||||
## レビューワークフロー
|
||||
|
||||
```
|
||||
1. git diffを実行して変更を理解する
|
||||
2. セキュリティチェックリストを最初に確認する
|
||||
3. コード品質チェックリストをレビューする
|
||||
4. 関連するテストを実行する
|
||||
5. カバレッジ >= 80%を検証する
|
||||
6. 詳細レビューに適切なエージェントを使用する
|
||||
```
|
||||
|
||||
## よくある問題の検出
|
||||
|
||||
### セキュリティ
|
||||
|
||||
- ハードコードされた認証情報(APIキー、パスワード、トークン)
|
||||
- SQLインジェクション(クエリでの文字列連結)
|
||||
- XSS脆弱性(エスケープされていないユーザー入力)
|
||||
- パストラバーサル(未サニタイズのファイルパス)
|
||||
- CSRF保護の欠如
|
||||
- 認証バイパス
|
||||
|
||||
### コード品質
|
||||
|
||||
- 大きな関数(50行超)- より小さく分割
|
||||
- 大きなファイル(800行超)- モジュールを抽出
|
||||
- 深いネスト(4レベル超)- 早期リターンを使用
|
||||
- エラー処理の欠如 - 明示的に処理
|
||||
- ミューテーションパターン - イミュータブルな操作を優先
|
||||
- テストの欠如 - テストカバレッジを追加
|
||||
|
||||
### パフォーマンス
|
||||
|
||||
- N+1クエリ - JOINまたはバッチングを使用
|
||||
- ページネーションの欠如 - クエリにLIMITを追加
|
||||
- 制約のないクエリ - 制約を追加
|
||||
- キャッシュの欠如 - 高コスト操作をキャッシュ
|
||||
|
||||
## 承認基準
|
||||
|
||||
- **承認**: CRITICALまたはHIGHの問題なし
|
||||
- **警告**: HIGHの問題のみ(注意してマージ)
|
||||
- **ブロック**: CRITICALの問題が検出
|
||||
|
||||
## 他のルールとの統合
|
||||
|
||||
このルールは以下と連携します:
|
||||
|
||||
- [testing.md](testing.md) - テストカバレッジ要件
|
||||
- [security.md](security.md) - セキュリティチェックリスト
|
||||
- [git-workflow.md](git-workflow.md) - コミット規約
|
||||
- [agents.md](agents.md) - エージェント委任
|
||||
44
docs/ja-JP/rules/common/development-workflow.md
Normal file
44
docs/ja-JP/rules/common/development-workflow.md
Normal file
@@ -0,0 +1,44 @@
|
||||
# 開発ワークフロー
|
||||
|
||||
> このファイルは [common/git-workflow.md](./git-workflow.md) を拡張し、Git操作の前に行われるフル機能開発プロセスを説明します。
|
||||
|
||||
機能実装ワークフローは、開発パイプラインを説明します:調査、計画、TDD、コードレビュー、そしてGitへのコミット。
|
||||
|
||||
## 機能実装ワークフロー
|
||||
|
||||
0. **調査と再利用** _(新規実装の前に必須)_
|
||||
- **まずGitHubコード検索:** 何か新しいものを書く前に、`gh search repos` と `gh search code` を実行して既存の実装、テンプレート、パターンを見つける。
|
||||
- **次にライブラリドキュメント:** Context7またはベンダーの公式ドキュメントを使用して、API動作、パッケージ使用方法、バージョン固有の詳細を実装前に確認する。
|
||||
- **最初の2つが不十分な場合のみExa:** GitHub検索と公式ドキュメントの後、より広範なウェブ調査や発見のためにExaを使用する。
|
||||
- **パッケージレジストリを確認:** ユーティリティコードを書く前にnpm、PyPI、crates.ioなどのレジストリを検索する。手作りのソリューションよりも実績のあるライブラリを優先。
|
||||
- **適応可能な実装を検索:** 問題の80%以上を解決し、フォーク、移植、またはラップできるオープンソースプロジェクトを探す。
|
||||
- 要件を満たす場合、完全な新規コードよりも実績のあるアプローチの採用や移植を優先する。
|
||||
|
||||
1. **まず計画**
|
||||
- **planner**エージェントを使用して実装計画を作成
|
||||
- コーディング前に計画ドキュメントを生成:PRD、アーキテクチャ、system_design、tech_doc、task_list
|
||||
- 依存関係とリスクを特定
|
||||
- フェーズに分割
|
||||
|
||||
2. **TDDアプローチ**
|
||||
- **tdd-guide**エージェントを使用
|
||||
- まずテストを書く(RED)
|
||||
- テストを通すように実装(GREEN)
|
||||
- リファクタリング(IMPROVE)
|
||||
- 80%以上のカバレッジを検証
|
||||
|
||||
3. **コードレビュー**
|
||||
- コード作成直後に**code-reviewer**エージェントを使用
|
||||
- CRITICALとHIGHの問題に対処
|
||||
- 可能な場合はMEDIUMの問題も修正
|
||||
|
||||
4. **コミットとプッシュ**
|
||||
- 詳細なコミットメッセージ
|
||||
- Conventional Commitsフォーマットに従う
|
||||
- コミットメッセージのフォーマットとPRプロセスについては[git-workflow.md](./git-workflow.md)を参照
|
||||
|
||||
5. **レビュー前チェック**
|
||||
- すべての自動チェック(CI/CD)が通過していることを確認
|
||||
- マージコンフリクトを解決
|
||||
- ブランチがターゲットブランチと最新状態であることを確認
|
||||
- これらのチェックが通過した後にのみレビューを依頼
|
||||
44
docs/ja-JP/rules/cpp/coding-style.md
Normal file
44
docs/ja-JP/rules/cpp/coding-style.md
Normal file
@@ -0,0 +1,44 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.cpp"
|
||||
- "**/*.hpp"
|
||||
- "**/*.cc"
|
||||
- "**/*.hh"
|
||||
- "**/*.cxx"
|
||||
- "**/*.h"
|
||||
- "**/CMakeLists.txt"
|
||||
---
|
||||
# C++ コーディングスタイル
|
||||
|
||||
> このファイルは [common/coding-style.md](../common/coding-style.md) を C++ 固有のコンテンツで拡張します。
|
||||
|
||||
## モダン C++(C++17/20/23)
|
||||
|
||||
- C スタイルの構文よりも**モダン C++ の機能**を優先する
|
||||
- コンテキストから型が明らかな場合は `auto` を使用する
|
||||
- コンパイル時の定数には `constexpr` を使用する
|
||||
- 構造化バインディングを使用する: `auto [key, value] = map_entry;`
|
||||
|
||||
## リソース管理
|
||||
|
||||
- **RAII を徹底する** — 手動での `new`/`delete` は禁止
|
||||
- 独占的な所有権には `std::unique_ptr` を使用する
|
||||
- 共有所有権が本当に必要な場合のみ `std::shared_ptr` を使用する
|
||||
- 生の `new` の代わりに `std::make_unique` / `std::make_shared` を使用する
|
||||
|
||||
## 命名規則
|
||||
|
||||
- 型/クラス: `PascalCase`
|
||||
- 関数/メソッド: `snake_case` または `camelCase`(プロジェクトの規約に従う)
|
||||
- 定数: `kPascalCase` または `UPPER_SNAKE_CASE`
|
||||
- 名前空間: `lowercase`
|
||||
- メンバー変数: `snake_case_`(末尾アンダースコア)または `m_` プレフィックス
|
||||
|
||||
## フォーマット
|
||||
|
||||
- **clang-format** を使用する — スタイルの議論は不要
|
||||
- コミット前に `clang-format -i <file>` を実行する
|
||||
|
||||
## 参考
|
||||
|
||||
包括的な C++ コーディング標準とガイドラインについてはスキル: `cpp-coding-standards` を参照。
|
||||
39
docs/ja-JP/rules/cpp/hooks.md
Normal file
39
docs/ja-JP/rules/cpp/hooks.md
Normal file
@@ -0,0 +1,39 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.cpp"
|
||||
- "**/*.hpp"
|
||||
- "**/*.cc"
|
||||
- "**/*.hh"
|
||||
- "**/*.cxx"
|
||||
- "**/*.h"
|
||||
- "**/CMakeLists.txt"
|
||||
---
|
||||
# C++ フック
|
||||
|
||||
> このファイルは [common/hooks.md](../common/hooks.md) を C++ 固有のコンテンツで拡張します。
|
||||
|
||||
## ビルドフック
|
||||
|
||||
C++ の変更をコミットする前にこれらのチェックを実行する:
|
||||
|
||||
```bash
|
||||
# フォーマットチェック
|
||||
clang-format --dry-run --Werror src/*.cpp src/*.hpp
|
||||
|
||||
# 静的解析
|
||||
clang-tidy src/*.cpp -- -std=c++17
|
||||
|
||||
# ビルド
|
||||
cmake --build build
|
||||
|
||||
# テスト
|
||||
ctest --test-dir build --output-on-failure
|
||||
```
|
||||
|
||||
## 推奨 CI パイプライン
|
||||
|
||||
1. **clang-format** — フォーマットチェック
|
||||
2. **clang-tidy** — 静的解析
|
||||
3. **cppcheck** — 追加解析
|
||||
4. **cmake ビルド** — コンパイル
|
||||
5. **ctest** — サニタイザーを使用したテスト実行
|
||||
51
docs/ja-JP/rules/cpp/patterns.md
Normal file
51
docs/ja-JP/rules/cpp/patterns.md
Normal file
@@ -0,0 +1,51 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.cpp"
|
||||
- "**/*.hpp"
|
||||
- "**/*.cc"
|
||||
- "**/*.hh"
|
||||
- "**/*.cxx"
|
||||
- "**/*.h"
|
||||
- "**/CMakeLists.txt"
|
||||
---
|
||||
# C++ パターン
|
||||
|
||||
> このファイルは [common/patterns.md](../common/patterns.md) を C++ 固有のコンテンツで拡張します。
|
||||
|
||||
## RAII(Resource Acquisition Is Initialization)
|
||||
|
||||
リソースのライフタイムをオブジェクトのライフタイムに結びつける:
|
||||
|
||||
```cpp
|
||||
class FileHandle {
|
||||
public:
|
||||
explicit FileHandle(const std::string& path) : file_(std::fopen(path.c_str(), "r")) {}
|
||||
~FileHandle() { if (file_) std::fclose(file_); }
|
||||
FileHandle(const FileHandle&) = delete;
|
||||
FileHandle& operator=(const FileHandle&) = delete;
|
||||
private:
|
||||
std::FILE* file_;
|
||||
};
|
||||
```
|
||||
|
||||
## 5の法則/0の法則
|
||||
|
||||
- **0の法則**: カスタムデストラクタ、コピー/ムーブコンストラクタ、代入が不要なクラスを優先する
|
||||
- **5の法則**: デストラクタ/コピーコンストラクタ/コピー代入/ムーブコンストラクタ/ムーブ代入のいずれかを定義する場合、5つすべてを定義する
|
||||
|
||||
## 値セマンティクス
|
||||
|
||||
- 小さい/トリビアルな型は値渡しにする
|
||||
- 大きな型は `const&` で渡す
|
||||
- 値で返す(RVO/NRVO に依存する)
|
||||
- シンクパラメータにはムーブセマンティクスを使用する
|
||||
|
||||
## エラーハンドリング
|
||||
|
||||
- 例外的な状況には例外を使用する
|
||||
- 存在しない可能性のある値には `std::optional` を使用する
|
||||
- 想定される失敗には `std::expected`(C++23)または結果型を使用する
|
||||
|
||||
## 参考
|
||||
|
||||
包括的な C++ パターンとアンチパターンについてはスキル: `cpp-coding-standards` を参照。
|
||||
51
docs/ja-JP/rules/cpp/security.md
Normal file
51
docs/ja-JP/rules/cpp/security.md
Normal file
@@ -0,0 +1,51 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.cpp"
|
||||
- "**/*.hpp"
|
||||
- "**/*.cc"
|
||||
- "**/*.hh"
|
||||
- "**/*.cxx"
|
||||
- "**/*.h"
|
||||
- "**/CMakeLists.txt"
|
||||
---
|
||||
# C++ セキュリティ
|
||||
|
||||
> このファイルは [common/security.md](../common/security.md) を C++ 固有のコンテンツで拡張します。
|
||||
|
||||
## メモリ安全性
|
||||
|
||||
- 生の `new`/`delete` は絶対に使用しない — スマートポインタを使用する
|
||||
- C スタイルの配列は絶対に使用しない — `std::array` または `std::vector` を使用する
|
||||
- `malloc`/`free` は絶対に使用しない — C++ のアロケーションを使用する
|
||||
- 絶対に必要な場合を除き `reinterpret_cast` を避ける
|
||||
|
||||
## バッファオーバーフロー
|
||||
|
||||
- `char*` の代わりに `std::string` を使用する
|
||||
- 安全性が重要な場合の境界チェック付きアクセスには `.at()` を使用する
|
||||
- `strcpy`、`strcat`、`sprintf` は絶対に使用しない — `std::string` または `fmt::format` を使用する
|
||||
|
||||
## 未定義動作
|
||||
|
||||
- 変数を必ず初期化する
|
||||
- 符号付き整数のオーバーフローを避ける
|
||||
- NULL または dangling ポインタのデリファレンスを絶対に行わない
|
||||
- CI でサニタイザーを使用する:
|
||||
```bash
|
||||
cmake -DCMAKE_CXX_FLAGS="-fsanitize=address,undefined" ..
|
||||
```
|
||||
|
||||
## 静的解析
|
||||
|
||||
- 自動チェックには **clang-tidy** を使用する:
|
||||
```bash
|
||||
clang-tidy --checks='*' src/*.cpp
|
||||
```
|
||||
- 追加の解析には **cppcheck** を使用する:
|
||||
```bash
|
||||
cppcheck --enable=all src/
|
||||
```
|
||||
|
||||
## 参考
|
||||
|
||||
詳細なセキュリティガイドラインについてはスキル: `cpp-coding-standards` を参照。
|
||||
44
docs/ja-JP/rules/cpp/testing.md
Normal file
44
docs/ja-JP/rules/cpp/testing.md
Normal file
@@ -0,0 +1,44 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.cpp"
|
||||
- "**/*.hpp"
|
||||
- "**/*.cc"
|
||||
- "**/*.hh"
|
||||
- "**/*.cxx"
|
||||
- "**/*.h"
|
||||
- "**/CMakeLists.txt"
|
||||
---
|
||||
# C++ テスト
|
||||
|
||||
> このファイルは [common/testing.md](../common/testing.md) を C++ 固有のコンテンツで拡張します。
|
||||
|
||||
## フレームワーク
|
||||
|
||||
**CMake/CTest** と組み合わせた **GoogleTest**(gtest/gmock)を使用する。
|
||||
|
||||
## テストの実行
|
||||
|
||||
```bash
|
||||
cmake --build build && ctest --test-dir build --output-on-failure
|
||||
```
|
||||
|
||||
## カバレッジ
|
||||
|
||||
```bash
|
||||
cmake -DCMAKE_CXX_FLAGS="--coverage" -DCMAKE_EXE_LINKER_FLAGS="--coverage" ..
|
||||
cmake --build .
|
||||
ctest --output-on-failure
|
||||
lcov --capture --directory . --output-file coverage.info
|
||||
```
|
||||
|
||||
## サニタイザー
|
||||
|
||||
CI では常にサニタイザーを使用してテストを実行する:
|
||||
|
||||
```bash
|
||||
cmake -DCMAKE_CXX_FLAGS="-fsanitize=address,undefined" ..
|
||||
```
|
||||
|
||||
## 参考
|
||||
|
||||
詳細な C++ テストパターン、TDD ワークフロー、GoogleTest/GMock の使用方法についてはスキル: `cpp-testing` を参照。
|
||||
72
docs/ja-JP/rules/csharp/coding-style.md
Normal file
72
docs/ja-JP/rules/csharp/coding-style.md
Normal file
@@ -0,0 +1,72 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.cs"
|
||||
- "**/*.csx"
|
||||
---
|
||||
# C# コーディングスタイル
|
||||
|
||||
> このファイルは [common/coding-style.md](../common/coding-style.md) を C# 固有のコンテンツで拡張します。
|
||||
|
||||
## 標準
|
||||
|
||||
- 現在の .NET の規約に従い、nullable 参照型を有効にする
|
||||
- パブリックおよびインターナル API に明示的なアクセス修飾子を優先する
|
||||
- ファイルはそこで定義されている主要な型に合わせて構成する
|
||||
|
||||
## 型とモデル
|
||||
|
||||
- イミュータブルな値のようなモデルには `record` または `record struct` を優先する
|
||||
- ID とライフサイクルを持つエンティティや型には `class` を使用する
|
||||
- サービス境界と抽象化には `interface` を使用する
|
||||
- アプリケーションコードで `dynamic` を避ける; ジェネリクスや明示的なモデルを優先する
|
||||
|
||||
```csharp
|
||||
public sealed record UserDto(Guid Id, string Email);
|
||||
|
||||
public interface IUserRepository
|
||||
{
|
||||
Task<UserDto?> FindByIdAsync(Guid id, CancellationToken cancellationToken);
|
||||
}
|
||||
```
|
||||
|
||||
## イミュータビリティ
|
||||
|
||||
- 共有状態には `init` セッター、コンストラクタパラメータ、イミュータブルコレクションを優先する
|
||||
- 更新された状態を生成する際に入力モデルをインプレースでミューテートしない
|
||||
|
||||
```csharp
|
||||
public sealed record UserProfile(string Name, string Email);
|
||||
|
||||
public static UserProfile Rename(UserProfile profile, string name) =>
|
||||
profile with { Name = name };
|
||||
```
|
||||
|
||||
## 非同期とエラーハンドリング
|
||||
|
||||
- `.Result` や `.Wait()` のようなブロッキング呼び出しよりも `async`/`await` を優先する
|
||||
- パブリックな非同期 API を通じて `CancellationToken` を渡す
|
||||
- 特定の例外をスローし、構造化されたプロパティでログを記録する
|
||||
|
||||
```csharp
|
||||
public async Task<Order> LoadOrderAsync(
|
||||
Guid orderId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await repository.FindAsync(orderId, cancellationToken)
|
||||
?? throw new InvalidOperationException($"Order {orderId} was not found.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to load order {OrderId}", orderId);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## フォーマット
|
||||
|
||||
- フォーマットとアナライザーの修正には `dotnet format` を使用する
|
||||
- `using` ディレクティブを整理し、未使用のインポートを削除する
|
||||
- 読みやすさを保てる場合のみ式本体メンバーを優先する
|
||||
25
docs/ja-JP/rules/csharp/hooks.md
Normal file
25
docs/ja-JP/rules/csharp/hooks.md
Normal file
@@ -0,0 +1,25 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.cs"
|
||||
- "**/*.csx"
|
||||
- "**/*.csproj"
|
||||
- "**/*.sln"
|
||||
- "**/Directory.Build.props"
|
||||
- "**/Directory.Build.targets"
|
||||
---
|
||||
# C# フック
|
||||
|
||||
> このファイルは [common/hooks.md](../common/hooks.md) を C# 固有のコンテンツで拡張します。
|
||||
|
||||
## PostToolUse フック
|
||||
|
||||
`~/.claude/settings.json` で設定する:
|
||||
|
||||
- **dotnet format**: 編集した C# ファイルを自動フォーマットし、アナライザーの修正を適用する
|
||||
- **dotnet build**: 編集後もソリューションやプロジェクトがコンパイルできることを確認する
|
||||
- **dotnet test --no-build**: 動作変更後に最も近い関連テストプロジェクトを再実行する
|
||||
|
||||
## ストップフック
|
||||
|
||||
- 広範な C# 変更を含むセッションを終了する前に最終的な `dotnet build` を実行する
|
||||
- 変更された `appsettings*.json` ファイルについてシークレットがコミットされないよう警告する
|
||||
50
docs/ja-JP/rules/csharp/patterns.md
Normal file
50
docs/ja-JP/rules/csharp/patterns.md
Normal file
@@ -0,0 +1,50 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.cs"
|
||||
- "**/*.csx"
|
||||
---
|
||||
# C# パターン
|
||||
|
||||
> このファイルは [common/patterns.md](../common/patterns.md) を C# 固有のコンテンツで拡張します。
|
||||
|
||||
## API レスポンスパターン
|
||||
|
||||
```csharp
|
||||
public sealed record ApiResponse<T>(
|
||||
bool Success,
|
||||
T? Data = default,
|
||||
string? Error = null,
|
||||
object? Meta = null);
|
||||
```
|
||||
|
||||
## リポジトリパターン
|
||||
|
||||
```csharp
|
||||
public interface IRepository<T>
|
||||
{
|
||||
Task<IReadOnlyList<T>> FindAllAsync(CancellationToken cancellationToken);
|
||||
Task<T?> FindByIdAsync(Guid id, CancellationToken cancellationToken);
|
||||
Task<T> CreateAsync(T entity, CancellationToken cancellationToken);
|
||||
Task<T> UpdateAsync(T entity, CancellationToken cancellationToken);
|
||||
Task DeleteAsync(Guid id, CancellationToken cancellationToken);
|
||||
}
|
||||
```
|
||||
|
||||
## オプションパターン
|
||||
|
||||
コードベース全体で生の文字列を読み取る代わりに、設定に強く型付けされたオプションを使用する。
|
||||
|
||||
```csharp
|
||||
public sealed class PaymentsOptions
|
||||
{
|
||||
public const string SectionName = "Payments";
|
||||
public required string BaseUrl { get; init; }
|
||||
public required string ApiKeySecretName { get; init; }
|
||||
}
|
||||
```
|
||||
|
||||
## 依存性注入
|
||||
|
||||
- サービス境界でインターフェースに依存する
|
||||
- コンストラクタを集中させる。サービスに依存関係が多すぎる場合は責任を分割する
|
||||
- ライフタイムを意図的に登録する: ステートレス/共有サービスにはシングルトン、リクエストデータにはスコープ、軽量な純粋ワーカーにはトランジエント
|
||||
58
docs/ja-JP/rules/csharp/security.md
Normal file
58
docs/ja-JP/rules/csharp/security.md
Normal file
@@ -0,0 +1,58 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.cs"
|
||||
- "**/*.csx"
|
||||
- "**/*.csproj"
|
||||
- "**/appsettings*.json"
|
||||
---
|
||||
# C# セキュリティ
|
||||
|
||||
> このファイルは [common/security.md](../common/security.md) を C# 固有のコンテンツで拡張します。
|
||||
|
||||
## シークレット管理
|
||||
|
||||
- API キー、トークン、接続文字列をソースコードに絶対にハードコードしない
|
||||
- ローカル開発には環境変数とユーザーシークレットを使用し、本番環境ではシークレットマネージャーを使用する
|
||||
- `appsettings.*.json` に実際の認証情報を含めない
|
||||
|
||||
```csharp
|
||||
// BAD
|
||||
const string ApiKey = "sk-live-123";
|
||||
|
||||
// GOOD
|
||||
var apiKey = builder.Configuration["OpenAI:ApiKey"]
|
||||
?? throw new InvalidOperationException("OpenAI:ApiKey is not configured.");
|
||||
```
|
||||
|
||||
## SQL インジェクション対策
|
||||
|
||||
- ADO.NET、Dapper、EF Core でのパラメータ化クエリを常に使用する
|
||||
- ユーザー入力を SQL 文字列に絶対に連結しない
|
||||
- 動的クエリ構成を使用する前に並べ替えフィールドとフィルタ演算子を検証する
|
||||
|
||||
```csharp
|
||||
const string sql = "SELECT * FROM Orders WHERE CustomerId = @customerId";
|
||||
await connection.QueryAsync<Order>(sql, new { customerId });
|
||||
```
|
||||
|
||||
## 入力バリデーション
|
||||
|
||||
- アプリケーション境界で DTO を検証する
|
||||
- データアノテーション、FluentValidation、または明示的なガード句を使用する
|
||||
- ビジネスロジックを実行する前に無効なモデル状態を拒否する
|
||||
|
||||
## 認証と認可
|
||||
|
||||
- カスタムトークン解析の代わりにフレームワークの認証ハンドラーを優先する
|
||||
- エンドポイントまたはハンドラー境界で認可ポリシーを適用する
|
||||
- 生のトークン、パスワード、PII を絶対にログに記録しない
|
||||
|
||||
## エラーハンドリング
|
||||
|
||||
- クライアント向けの安全なメッセージを返す
|
||||
- 詳細な例外はサーバー側で構造化されたコンテキストと共にログに記録する
|
||||
- API レスポンスにスタックトレース、SQL テキスト、ファイルシステムパスを露出しない
|
||||
|
||||
## 参考
|
||||
|
||||
より広範なアプリケーションセキュリティレビューチェックリストについてはスキル: `security-review` を参照。
|
||||
46
docs/ja-JP/rules/csharp/testing.md
Normal file
46
docs/ja-JP/rules/csharp/testing.md
Normal file
@@ -0,0 +1,46 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.cs"
|
||||
- "**/*.csx"
|
||||
- "**/*.csproj"
|
||||
---
|
||||
# C# テスト
|
||||
|
||||
> このファイルは [common/testing.md](../common/testing.md) を C# 固有のコンテンツで拡張します。
|
||||
|
||||
## テストフレームワーク
|
||||
|
||||
- ユニットテストと統合テストには **xUnit** を優先する
|
||||
- 読みやすいアサーションには **FluentAssertions** を使用する
|
||||
- 依存関係のモックには **Moq** または **NSubstitute** を使用する
|
||||
- 統合テストで実際のインフラが必要な場合は **Testcontainers** を使用する
|
||||
|
||||
## テスト構成
|
||||
|
||||
- `tests/` 以下で `src/` 構造を反映させる
|
||||
- ユニット、統合、エンドツーエンドのカバレッジを明確に分離する
|
||||
- 実装の詳細ではなく動作でテストに名前を付ける
|
||||
|
||||
```csharp
|
||||
public sealed class OrderServiceTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task FindByIdAsync_ReturnsOrder_WhenOrderExists()
|
||||
{
|
||||
// Arrange
|
||||
// Act
|
||||
// Assert
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## ASP.NET Core 統合テスト
|
||||
|
||||
- API 統合カバレッジには `WebApplicationFactory<TEntryPoint>` を使用する
|
||||
- ミドルウェアをバイパスするのではなく、HTTP を通じて認証、バリデーション、シリアライゼーションをテストする
|
||||
|
||||
## カバレッジ
|
||||
|
||||
- 行カバレッジ 80% 以上を目標とする
|
||||
- ドメインロジック、バリデーション、認証、失敗パスにカバレッジを集中させる
|
||||
- 利用可能な場合はカバレッジ収集を有効にして CI で `dotnet test` を実行する
|
||||
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 でブロックする
|
||||
112
docs/ja-JP/rules/fsharp/coding-style.md
Normal file
112
docs/ja-JP/rules/fsharp/coding-style.md
Normal file
@@ -0,0 +1,112 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.fs"
|
||||
- "**/*.fsx"
|
||||
---
|
||||
# F# コーディングスタイル
|
||||
|
||||
> このファイルは [common/coding-style.md](../common/coding-style.md) を F# 固有のコンテンツで拡張します。
|
||||
|
||||
## 標準
|
||||
|
||||
- 標準的な F# の規約に従い、正確性のために型システムを活用する
|
||||
- デフォルトでイミュータビリティを優先する。パフォーマンス上の理由がある場合のみ `mutable` を使用する
|
||||
- モジュールを焦点を絞り、凝集性を保つ
|
||||
|
||||
## 型とモデル
|
||||
|
||||
- ドメインモデリングにはクラス階層よりも判別共用体を優先する
|
||||
- 名前付きフィールドを持つデータにはレコードを使用する
|
||||
- プリミティブ型に対する型安全なラッパーには単一ケース共用体を使用する
|
||||
- 相互運用またはミュータブルなステートが必要でない限り、クラスの使用を避ける
|
||||
|
||||
```fsharp
|
||||
type EmailAddress = EmailAddress of string
|
||||
|
||||
type OrderStatus =
|
||||
| Pending
|
||||
| Confirmed of confirmedAt: DateTimeOffset
|
||||
| Shipped of trackingNumber: string
|
||||
| Cancelled of reason: string
|
||||
|
||||
type Order =
|
||||
{ Id: Guid
|
||||
CustomerId: string
|
||||
Status: OrderStatus
|
||||
Items: OrderItem list }
|
||||
```
|
||||
|
||||
## イミュータビリティ
|
||||
|
||||
- レコードはデフォルトでイミュータブル。更新には `with` 式を使用する
|
||||
- ミュータブルなコレクションよりも `list`、`map`、`set` を優先する
|
||||
- ドメインロジックで `ref` セルとミュータブルフィールドを避ける
|
||||
|
||||
```fsharp
|
||||
let rename (profile: UserProfile) newName =
|
||||
{ profile with Name = newName }
|
||||
```
|
||||
|
||||
## 関数スタイル
|
||||
|
||||
- 大きなメソッドよりも小さく合成可能な関数を優先する
|
||||
- パイプ演算子 `|>` を使用して読みやすいデータパイプラインを構築する
|
||||
- if/else チェーンよりもパターンマッチングを優先する
|
||||
- null の代わりに `Option` を使用する。失敗する可能性のある操作には `Result` を使用する
|
||||
|
||||
```fsharp
|
||||
let processOrder order =
|
||||
order
|
||||
|> validateItems
|
||||
|> Result.bind calculateTotal
|
||||
|> Result.map applyDiscount
|
||||
|> Result.mapError OrderError
|
||||
```
|
||||
|
||||
## 非同期とエラーハンドリング
|
||||
|
||||
- .NET の非同期 API との相互運用には `task { }` を使用する
|
||||
- F# ネイティブの非同期ワークフローには `async { }` を使用する
|
||||
- パブリック非同期 API を通じて `CancellationToken` を伝播する
|
||||
- 予期されるエラーには例外ではなく `Result` とRailway指向プログラミングを優先する
|
||||
|
||||
```fsharp
|
||||
let loadOrderAsync (orderId: Guid) (ct: CancellationToken) =
|
||||
task {
|
||||
let! order = repository.FindAsync(orderId, ct)
|
||||
return
|
||||
order
|
||||
|> Option.defaultWith (fun () ->
|
||||
failwith $"Order {orderId} was not found.")
|
||||
}
|
||||
```
|
||||
|
||||
## フォーマット
|
||||
|
||||
- 自動フォーマットには `fantomas` を使用する
|
||||
- 意味のある空白を優先する。不要な括弧を避ける
|
||||
- 未使用の `open` 宣言を削除する
|
||||
|
||||
### open 宣言の順序
|
||||
|
||||
`open` 文を4つのセクションに空行で区切ってグループ化し、各セクション内は辞書順で並べる:
|
||||
|
||||
1. `System.*`
|
||||
2. `Microsoft.*`
|
||||
3. サードパーティの名前空間
|
||||
4. ファーストパーティ / プロジェクトの名前空間
|
||||
|
||||
```fsharp
|
||||
open System
|
||||
open System.Collections.Generic
|
||||
open System.Threading.Tasks
|
||||
|
||||
open Microsoft.AspNetCore.Http
|
||||
open Microsoft.Extensions.Logging
|
||||
|
||||
open FsCheck.Xunit
|
||||
open Swensen.Unquote
|
||||
|
||||
open MyApp.Domain
|
||||
open MyApp.Infrastructure
|
||||
```
|
||||
26
docs/ja-JP/rules/fsharp/hooks.md
Normal file
26
docs/ja-JP/rules/fsharp/hooks.md
Normal file
@@ -0,0 +1,26 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.fs"
|
||||
- "**/*.fsx"
|
||||
- "**/*.fsproj"
|
||||
- "**/*.sln"
|
||||
- "**/*.slnx"
|
||||
- "**/Directory.Build.props"
|
||||
- "**/Directory.Build.targets"
|
||||
---
|
||||
# F# フック
|
||||
|
||||
> このファイルは [common/hooks.md](../common/hooks.md) を F# 固有のコンテンツで拡張します。
|
||||
|
||||
## PostToolUse フック
|
||||
|
||||
`~/.claude/settings.json` で設定する:
|
||||
|
||||
- **fantomas**: 編集された F# ファイルを自動フォーマット
|
||||
- **dotnet build**: 編集後にソリューションまたはプロジェクトが引き続きコンパイルされることを確認する
|
||||
- **dotnet test --no-build**: 動作の変更後に最も近い関連テストプロジェクトを再実行する
|
||||
|
||||
## Stop フック
|
||||
|
||||
- 広範な F# の変更を伴うセッションを終了する前に最終的な `dotnet build` を実行する
|
||||
- 変更された `appsettings*.json` ファイルに対して警告を出し、シークレットがコミットされないようにする
|
||||
111
docs/ja-JP/rules/fsharp/patterns.md
Normal file
111
docs/ja-JP/rules/fsharp/patterns.md
Normal file
@@ -0,0 +1,111 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.fs"
|
||||
- "**/*.fsx"
|
||||
---
|
||||
# F# パターン
|
||||
|
||||
> このファイルは [common/patterns.md](../common/patterns.md) を F# 固有のコンテンツで拡張します。
|
||||
|
||||
## エラーハンドリングのための Result 型
|
||||
|
||||
予期されるエラーには例外の代わりに Railway指向プログラミングで `Result<'T, 'TError>` を使用する。
|
||||
|
||||
```fsharp
|
||||
type OrderError =
|
||||
| InvalidCustomer of string
|
||||
| EmptyItems
|
||||
| ItemOutOfStock of sku: string
|
||||
|
||||
let validateOrder (request: CreateOrderRequest) : Result<ValidatedOrder, OrderError> =
|
||||
if String.IsNullOrWhiteSpace request.CustomerId then
|
||||
Error(InvalidCustomer "CustomerId is required")
|
||||
elif request.Items |> List.isEmpty then
|
||||
Error EmptyItems
|
||||
else
|
||||
Ok { CustomerId = request.CustomerId; Items = request.Items }
|
||||
```
|
||||
|
||||
## 欠損値のための Option
|
||||
|
||||
null の代わりに `Option<'T>` を優先する。変換には `Option.map`、`Option.bind`、`Option.defaultValue` を使用する。
|
||||
|
||||
```fsharp
|
||||
let findUser (id: Guid) : User option =
|
||||
users |> Map.tryFind id
|
||||
|
||||
let getUserEmail userId =
|
||||
findUser userId
|
||||
|> Option.map (fun u -> u.Email)
|
||||
|> Option.defaultValue "unknown@example.com"
|
||||
```
|
||||
|
||||
## ドメインモデリングのための判別共用体
|
||||
|
||||
ビジネスの状態を明示的にモデル化する。コンパイラが網羅的なハンドリングを強制する。
|
||||
|
||||
```fsharp
|
||||
type PaymentState =
|
||||
| AwaitingPayment of amount: decimal
|
||||
| Paid of paidAt: DateTimeOffset * transactionId: string
|
||||
| Refunded of refundedAt: DateTimeOffset * reason: string
|
||||
| Failed of error: string
|
||||
|
||||
let describePayment = function
|
||||
| AwaitingPayment amount -> $"Awaiting payment of {amount:C}"
|
||||
| Paid (at, txn) -> $"Paid at {at} (txn: {txn})"
|
||||
| Refunded (at, reason) -> $"Refunded at {at}: {reason}"
|
||||
| Failed error -> $"Payment failed: {error}"
|
||||
```
|
||||
|
||||
## コンピュテーション式
|
||||
|
||||
コンピュテーション式を使用して、失敗する可能性のある順次操作を簡略化する。
|
||||
|
||||
```fsharp
|
||||
let placeOrder request =
|
||||
result {
|
||||
let! validated = validateOrder request
|
||||
let! inventory = checkInventory validated.Items
|
||||
let! order = createOrder validated inventory
|
||||
return order
|
||||
}
|
||||
```
|
||||
|
||||
## モジュールの構成
|
||||
|
||||
- 関連する関数をクラスではなくモジュールにグループ化する
|
||||
- 名前の衝突を防ぐために `[<RequireQualifiedAccess>]` を使用する
|
||||
- モジュールは小さく、単一の責任に集中させる
|
||||
|
||||
```fsharp
|
||||
[<RequireQualifiedAccess>]
|
||||
module Order =
|
||||
let create customerId items = { Id = Guid.NewGuid(); CustomerId = customerId; Items = items; Status = Pending }
|
||||
let confirm order = { order with Status = Confirmed(DateTimeOffset.UtcNow) }
|
||||
let cancel reason order = { order with Status = Cancelled reason }
|
||||
```
|
||||
|
||||
## 依存性注入
|
||||
|
||||
- 依存関係を関数パラメータまたはレコード of 関数として定義する
|
||||
- 主に .NET ライブラリとの境界でのみインターフェースを使用する
|
||||
- パイプラインへの依存関係注入には部分適用を優先する
|
||||
|
||||
```fsharp
|
||||
type OrderDeps =
|
||||
{ FindOrder: Guid -> Task<Order option>
|
||||
SaveOrder: Order -> Task<unit>
|
||||
SendNotification: Order -> Task<unit> }
|
||||
|
||||
let processOrder (deps: OrderDeps) orderId =
|
||||
task {
|
||||
match! deps.FindOrder orderId with
|
||||
| None -> return Error "Order not found"
|
||||
| Some order ->
|
||||
let confirmed = Order.confirm order
|
||||
do! deps.SaveOrder confirmed
|
||||
do! deps.SendNotification confirmed
|
||||
return Ok confirmed
|
||||
}
|
||||
```
|
||||
76
docs/ja-JP/rules/fsharp/security.md
Normal file
76
docs/ja-JP/rules/fsharp/security.md
Normal file
@@ -0,0 +1,76 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.fs"
|
||||
- "**/*.fsx"
|
||||
- "**/*.fsproj"
|
||||
- "**/appsettings*.json"
|
||||
---
|
||||
# F# セキュリティ
|
||||
|
||||
> このファイルは [common/security.md](../common/security.md) を F# 固有のコンテンツで拡張します。
|
||||
|
||||
## シークレット管理
|
||||
|
||||
- ソースコードに API キー、トークン、接続文字列をハードコードしない
|
||||
- ローカル開発には環境変数とユーザーシークレットを使用し、本番環境ではシークレットマネージャーを使用する
|
||||
- `appsettings.*.json` に実際の認証情報を含めない
|
||||
|
||||
```fsharp
|
||||
// BAD
|
||||
let apiKey = "sk-live-123"
|
||||
|
||||
// GOOD
|
||||
let apiKey =
|
||||
configuration["OpenAI:ApiKey"]
|
||||
|> Option.ofObj
|
||||
|> Option.defaultWith (fun () -> failwith "OpenAI:ApiKey is not configured.")
|
||||
```
|
||||
|
||||
## SQL インジェクション対策
|
||||
|
||||
- ADO.NET、Dapper、または EF Core でパラメータ化クエリを常に使用する
|
||||
- ユーザー入力を SQL 文字列に連結しない
|
||||
- 動的クエリ合成を使用する前に、並び替えフィールドとフィルター演算子を検証する
|
||||
|
||||
```fsharp
|
||||
let findByCustomer (connection: IDbConnection) customerId =
|
||||
task {
|
||||
let sql = "SELECT * FROM Orders WHERE CustomerId = @customerId"
|
||||
return! connection.QueryAsync<Order>(sql, {| customerId = customerId |})
|
||||
}
|
||||
```
|
||||
|
||||
## 入力バリデーション
|
||||
|
||||
- 型を使用してアプリケーション境界で入力を検証する
|
||||
- 検証済みの値には単一ケース判別共用体を使用する
|
||||
- 無効な入力がドメインロジックに入る前に拒否する
|
||||
|
||||
```fsharp
|
||||
type ValidatedEmail = private ValidatedEmail of string
|
||||
|
||||
module ValidatedEmail =
|
||||
let create (input: string) =
|
||||
if System.Text.RegularExpressions.Regex.IsMatch(input, @"^[^@]+@[^@]+\.[^@]+$") then
|
||||
Ok(ValidatedEmail input)
|
||||
else
|
||||
Error "Invalid email address"
|
||||
|
||||
let value (ValidatedEmail v) = v
|
||||
```
|
||||
|
||||
## 認証と認可
|
||||
|
||||
- カスタムトークン解析ではなくフレームワークの認証ハンドラーを優先する
|
||||
- エンドポイントまたはハンドラー境界で認可ポリシーを強制する
|
||||
- 生のトークン、パスワード、PII をログに記録しない
|
||||
|
||||
## エラーハンドリング
|
||||
|
||||
- クライアントに返す安全なメッセージを返す
|
||||
- 詳細な例外はサーバーサイドで構造化コンテキストと共にログに記録する
|
||||
- API レスポンスにスタックトレース、SQL テキスト、ファイルシステムパスを公開しない
|
||||
|
||||
## 参考資料
|
||||
|
||||
より広範なアプリケーションセキュリティレビューチェックリストはスキル `security-review` を参照。
|
||||
62
docs/ja-JP/rules/fsharp/testing.md
Normal file
62
docs/ja-JP/rules/fsharp/testing.md
Normal file
@@ -0,0 +1,62 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.fs"
|
||||
- "**/*.fsx"
|
||||
- "**/*.fsproj"
|
||||
---
|
||||
# F# テスト
|
||||
|
||||
> このファイルは [common/testing.md](../common/testing.md) を F# 固有のコンテンツで拡張します。
|
||||
|
||||
## テストフレームワーク
|
||||
|
||||
- F# フレンドリーなアサーションのために **xUnit** と **FsUnit.xUnit** を優先する
|
||||
- 明確な失敗メッセージを持つクォーテーションベースのアサーションには **Unquote** を使用する
|
||||
- プロパティベーステストには **FsCheck.xUnit** を使用する
|
||||
- 依存関係のモックには **NSubstitute** または関数スタブを使用する
|
||||
- インテグレーションテストで実際のインフラが必要な場合は **Testcontainers** を使用する
|
||||
|
||||
## テストの構成
|
||||
|
||||
- `tests/` 配下に `src/` の構造を反映させる
|
||||
- ユニット、インテグレーション、エンドツーエンドのカバレッジを明確に分離する
|
||||
- 実装の詳細ではなく、振る舞いでテストに名前を付ける
|
||||
|
||||
```fsharp
|
||||
open Xunit
|
||||
open Swensen.Unquote
|
||||
|
||||
[<Fact>]
|
||||
let ``リクエストが有効な場合、PlaceOrder は成功を返す`` () =
|
||||
let request = { CustomerId = "cust-123"; Items = [ validItem ] }
|
||||
let result = OrderService.placeOrder request
|
||||
test <@ Result.isOk result @>
|
||||
|
||||
[<Fact>]
|
||||
let ``アイテムが空の場合、PlaceOrder はエラーを返す`` () =
|
||||
let request = { CustomerId = "cust-123"; Items = [] }
|
||||
let result = OrderService.placeOrder request
|
||||
test <@ Result.isError result @>
|
||||
```
|
||||
|
||||
## FsCheck を使ったプロパティベーステスト
|
||||
|
||||
```fsharp
|
||||
open FsCheck.Xunit
|
||||
|
||||
[<Property>]
|
||||
let ``注文合計が負になることはない`` (items: OrderItem list) =
|
||||
let total = Order.calculateTotal items
|
||||
total >= 0m
|
||||
```
|
||||
|
||||
## ASP.NET Core インテグレーションテスト
|
||||
|
||||
- API インテグレーションカバレッジには `WebApplicationFactory<TEntryPoint>` を使用する
|
||||
- ミドルウェアをバイパスするのではなく、HTTP を通じて認証、バリデーション、シリアライゼーションをテストする
|
||||
|
||||
## カバレッジ
|
||||
|
||||
- 80%以上の行カバレッジを目標とする
|
||||
- ドメインロジック、バリデーション、認証、失敗パスのカバレッジに重点を置く
|
||||
- 利用可能な場合はカバレッジ収集を有効にして CI で `dotnet test` を実行する
|
||||
32
docs/ja-JP/rules/golang/coding-style.md
Normal file
32
docs/ja-JP/rules/golang/coding-style.md
Normal file
@@ -0,0 +1,32 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.go"
|
||||
- "**/go.mod"
|
||||
- "**/go.sum"
|
||||
---
|
||||
# Go コーディングスタイル
|
||||
|
||||
> このファイルは [common/coding-style.md](../common/coding-style.md) を Go 固有のコンテンツで拡張します。
|
||||
|
||||
## フォーマット
|
||||
|
||||
- **gofmt** と **goimports** は必須 — スタイルの議論は不要
|
||||
|
||||
## 設計原則
|
||||
|
||||
- インターフェースを受け取り、構造体を返す
|
||||
- インターフェースは小さく保つ(1〜3メソッド)
|
||||
|
||||
## エラーハンドリング
|
||||
|
||||
常にコンテキスト付きでエラーをラップする:
|
||||
|
||||
```go
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create user: %w", err)
|
||||
}
|
||||
```
|
||||
|
||||
## リファレンス
|
||||
|
||||
スキル: `golang-patterns` で包括的な Go のイディオムとパターンを参照してください。
|
||||
17
docs/ja-JP/rules/golang/hooks.md
Normal file
17
docs/ja-JP/rules/golang/hooks.md
Normal file
@@ -0,0 +1,17 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.go"
|
||||
- "**/go.mod"
|
||||
- "**/go.sum"
|
||||
---
|
||||
# Go フック
|
||||
|
||||
> このファイルは [common/hooks.md](../common/hooks.md) を Go 固有のコンテンツで拡張します。
|
||||
|
||||
## PostToolUse フック
|
||||
|
||||
`~/.claude/settings.json` で設定:
|
||||
|
||||
- **gofmt/goimports**: 編集後に `.go` ファイルを自動フォーマット
|
||||
- **go vet**: `.go` ファイル編集後に静的解析を実行
|
||||
- **staticcheck**: 変更されたパッケージに対して拡張静的チェックを実行
|
||||
45
docs/ja-JP/rules/golang/patterns.md
Normal file
45
docs/ja-JP/rules/golang/patterns.md
Normal file
@@ -0,0 +1,45 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.go"
|
||||
- "**/go.mod"
|
||||
- "**/go.sum"
|
||||
---
|
||||
# Go パターン
|
||||
|
||||
> このファイルは [common/patterns.md](../common/patterns.md) を Go 固有のコンテンツで拡張します。
|
||||
|
||||
## Functional Options
|
||||
|
||||
```go
|
||||
type Option func(*Server)
|
||||
|
||||
func WithPort(port int) Option {
|
||||
return func(s *Server) { s.port = port }
|
||||
}
|
||||
|
||||
func NewServer(opts ...Option) *Server {
|
||||
s := &Server{port: 8080}
|
||||
for _, opt := range opts {
|
||||
opt(s)
|
||||
}
|
||||
return s
|
||||
}
|
||||
```
|
||||
|
||||
## 小さなインターフェース
|
||||
|
||||
インターフェースは実装される場所ではなく、使用される場所で定義する。
|
||||
|
||||
## 依存性注入
|
||||
|
||||
コンストラクタ関数を使用して依存関係を注入する:
|
||||
|
||||
```go
|
||||
func NewUserService(repo UserRepository, logger Logger) *UserService {
|
||||
return &UserService{repo: repo, logger: logger}
|
||||
}
|
||||
```
|
||||
|
||||
## リファレンス
|
||||
|
||||
スキル: `golang-patterns` で並行処理、エラーハンドリング、パッケージ構成を含む包括的な Go パターンを参照してください。
|
||||
34
docs/ja-JP/rules/golang/security.md
Normal file
34
docs/ja-JP/rules/golang/security.md
Normal file
@@ -0,0 +1,34 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.go"
|
||||
- "**/go.mod"
|
||||
- "**/go.sum"
|
||||
---
|
||||
# Go セキュリティ
|
||||
|
||||
> このファイルは [common/security.md](../common/security.md) を Go 固有のコンテンツで拡張します。
|
||||
|
||||
## シークレット管理
|
||||
|
||||
```go
|
||||
apiKey := os.Getenv("OPENAI_API_KEY")
|
||||
if apiKey == "" {
|
||||
log.Fatal("OPENAI_API_KEY not configured")
|
||||
}
|
||||
```
|
||||
|
||||
## セキュリティスキャン
|
||||
|
||||
- **gosec** を使用して静的セキュリティ解析を実行:
|
||||
```bash
|
||||
gosec ./...
|
||||
```
|
||||
|
||||
## Context とタイムアウト
|
||||
|
||||
タイムアウト制御には常に `context.Context` を使用する:
|
||||
|
||||
```go
|
||||
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
```
|
||||
31
docs/ja-JP/rules/golang/testing.md
Normal file
31
docs/ja-JP/rules/golang/testing.md
Normal file
@@ -0,0 +1,31 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.go"
|
||||
- "**/go.mod"
|
||||
- "**/go.sum"
|
||||
---
|
||||
# Go テスト
|
||||
|
||||
> このファイルは [common/testing.md](../common/testing.md) を Go 固有のコンテンツで拡張します。
|
||||
|
||||
## フレームワーク
|
||||
|
||||
標準の `go test` と**テーブル駆動テスト**を使用する。
|
||||
|
||||
## 競合検出
|
||||
|
||||
常に `-race` フラグを付けて実行する:
|
||||
|
||||
```bash
|
||||
go test -race ./...
|
||||
```
|
||||
|
||||
## カバレッジ
|
||||
|
||||
```bash
|
||||
go test -cover ./...
|
||||
```
|
||||
|
||||
## リファレンス
|
||||
|
||||
スキル: `golang-testing` で詳細な Go テストパターンとヘルパーを参照してください。
|
||||
114
docs/ja-JP/rules/java/coding-style.md
Normal file
114
docs/ja-JP/rules/java/coding-style.md
Normal file
@@ -0,0 +1,114 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.java"
|
||||
---
|
||||
# Java コーディングスタイル
|
||||
|
||||
> このファイルは [common/coding-style.md](../common/coding-style.md) を Java 固有のコンテンツで拡張します。
|
||||
|
||||
## フォーマット
|
||||
|
||||
- **google-java-format** または **Checkstyle**(Google または Sun スタイル)で強制
|
||||
- ファイルごとに1つの public トップレベル型
|
||||
- 一貫したインデント: 2 または 4 スペース(プロジェクト標準に合わせる)
|
||||
- メンバー順序: 定数、フィールド、コンストラクタ、public メソッド、protected、private
|
||||
|
||||
## 不変性
|
||||
|
||||
- 値型には `record` を優先(Java 16+)
|
||||
- フィールドはデフォルトで `final` にする — 可変状態は必要な場合のみ使用
|
||||
- public API からは防御的コピーを返す: `List.copyOf()`、`Map.copyOf()`、`Set.copyOf()`
|
||||
- コピーオンライト: 既存のインスタンスを変更するのではなく、新しいインスタンスを返す
|
||||
|
||||
```java
|
||||
// GOOD — 不変の値型
|
||||
public record OrderSummary(Long id, String customerName, BigDecimal total) {}
|
||||
|
||||
// GOOD — final フィールド、setter なし
|
||||
public class Order {
|
||||
private final Long id;
|
||||
private final List<LineItem> items;
|
||||
|
||||
public List<LineItem> getItems() {
|
||||
return List.copyOf(items);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 命名
|
||||
|
||||
標準的な Java の慣例に従う:
|
||||
- `PascalCase` — クラス、インターフェース、レコード、列挙型
|
||||
- `camelCase` — メソッド、フィールド、パラメータ、ローカル変数
|
||||
- `SCREAMING_SNAKE_CASE` — `static final` 定数
|
||||
- パッケージ: すべて小文字、逆ドメイン(`com.example.app.service`)
|
||||
|
||||
## モダン Java 機能
|
||||
|
||||
明確さを向上させるモダンな言語機能を使用する:
|
||||
- **レコード** — DTO と値型(Java 16+)
|
||||
- **シールドクラス** — 閉じた型階層(Java 17+)
|
||||
- **パターンマッチング** — `instanceof` で明示的キャスト不要(Java 16+)
|
||||
- **テキストブロック** — 複数行文字列(SQL、JSON テンプレート)(Java 15+)
|
||||
- **Switch 式** — アロー構文(Java 14+)
|
||||
- **switch でのパターンマッチング** — 網羅的なシールド型処理(Java 21+)
|
||||
|
||||
```java
|
||||
// パターンマッチング instanceof
|
||||
if (shape instanceof Circle c) {
|
||||
return Math.PI * c.radius() * c.radius();
|
||||
}
|
||||
|
||||
// シールド型階層
|
||||
public sealed interface PaymentMethod permits CreditCard, BankTransfer, Wallet {}
|
||||
|
||||
// Switch 式
|
||||
String label = switch (status) {
|
||||
case ACTIVE -> "Active";
|
||||
case SUSPENDED -> "Suspended";
|
||||
case CLOSED -> "Closed";
|
||||
};
|
||||
```
|
||||
|
||||
## Optional の使い方
|
||||
|
||||
- 結果がない可能性がある検索メソッドから `Optional<T>` を返す
|
||||
- `map()`、`flatMap()`、`orElseThrow()` を使用する — `isPresent()` なしで `get()` を呼ばない
|
||||
- `Optional` をフィールド型やメソッドパラメータとして使用しない
|
||||
|
||||
```java
|
||||
// GOOD
|
||||
return repository.findById(id)
|
||||
.map(ResponseDto::from)
|
||||
.orElseThrow(() -> new OrderNotFoundException(id));
|
||||
|
||||
// BAD — パラメータとしての Optional
|
||||
public void process(Optional<String> name) {}
|
||||
```
|
||||
|
||||
## エラーハンドリング
|
||||
|
||||
- ドメインエラーには非チェック例外を優先
|
||||
- `RuntimeException` を継承するドメイン固有の例外を作成
|
||||
- トップレベルハンドラ以外では広範な `catch (Exception e)` を避ける
|
||||
- 例外メッセージにコンテキストを含める
|
||||
|
||||
```java
|
||||
public class OrderNotFoundException extends RuntimeException {
|
||||
public OrderNotFoundException(Long id) {
|
||||
super("Order not found: id=" + id);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## ストリーム
|
||||
|
||||
- 変換にはストリームを使用する; パイプラインは短く保つ(最大3〜4操作)
|
||||
- 可読性がある場合はメソッド参照を優先: `.map(Order::getTotal)`
|
||||
- ストリーム操作での副作用を避ける
|
||||
- 複雑なロジックの場合、入り組んだストリームパイプラインよりもループを優先
|
||||
|
||||
## リファレンス
|
||||
|
||||
スキル: `java-coding-standards` で完全なコーディング規約と例を参照してください。
|
||||
スキル: `jpa-patterns` で JPA/Hibernate エンティティ設計パターンを参照してください。
|
||||
18
docs/ja-JP/rules/java/hooks.md
Normal file
18
docs/ja-JP/rules/java/hooks.md
Normal file
@@ -0,0 +1,18 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.java"
|
||||
- "**/pom.xml"
|
||||
- "**/build.gradle"
|
||||
- "**/build.gradle.kts"
|
||||
---
|
||||
# Java フック
|
||||
|
||||
> このファイルは [common/hooks.md](../common/hooks.md) を Java 固有のコンテンツで拡張します。
|
||||
|
||||
## PostToolUse フック
|
||||
|
||||
`~/.claude/settings.json` で設定:
|
||||
|
||||
- **google-java-format**: 編集後に `.java` ファイルを自動フォーマット
|
||||
- **checkstyle**: Java ファイル編集後にスタイルチェックを実行
|
||||
- **./mvnw compile** または **./gradlew compileJava**: 変更後にコンパイルを検証
|
||||
147
docs/ja-JP/rules/java/patterns.md
Normal file
147
docs/ja-JP/rules/java/patterns.md
Normal file
@@ -0,0 +1,147 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.java"
|
||||
---
|
||||
# Java パターン
|
||||
|
||||
> このファイルは [common/patterns.md](../common/patterns.md) を Java 固有のコンテンツで拡張します。
|
||||
|
||||
## リポジトリパターン
|
||||
|
||||
データアクセスをインターフェースの背後にカプセル化する:
|
||||
|
||||
```java
|
||||
public interface OrderRepository {
|
||||
Optional<Order> findById(Long id);
|
||||
List<Order> findAll();
|
||||
Order save(Order order);
|
||||
void deleteById(Long id);
|
||||
}
|
||||
```
|
||||
|
||||
具象実装がストレージの詳細を処理する(JPA、JDBC、テスト用インメモリ)。
|
||||
|
||||
## サービス層
|
||||
|
||||
ビジネスロジックはサービスクラスに配置する; コントローラとリポジトリは薄く保つ:
|
||||
|
||||
```java
|
||||
public class OrderService {
|
||||
private final OrderRepository orderRepository;
|
||||
private final PaymentGateway paymentGateway;
|
||||
|
||||
public OrderService(OrderRepository orderRepository, PaymentGateway paymentGateway) {
|
||||
this.orderRepository = orderRepository;
|
||||
this.paymentGateway = paymentGateway;
|
||||
}
|
||||
|
||||
public OrderSummary placeOrder(CreateOrderRequest request) {
|
||||
var order = Order.from(request);
|
||||
paymentGateway.charge(order.total());
|
||||
var saved = orderRepository.save(order);
|
||||
return OrderSummary.from(saved);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## コンストラクタインジェクション
|
||||
|
||||
常にコンストラクタインジェクションを使用する — フィールドインジェクションは使用しない:
|
||||
|
||||
```java
|
||||
// GOOD — コンストラクタインジェクション(テスト可能、不変)
|
||||
public class NotificationService {
|
||||
private final EmailSender emailSender;
|
||||
|
||||
public NotificationService(EmailSender emailSender) {
|
||||
this.emailSender = emailSender;
|
||||
}
|
||||
}
|
||||
|
||||
// BAD — フィールドインジェクション(リフレクションなしではテスト不可、フレームワークの魔法が必要)
|
||||
public class NotificationService {
|
||||
@Inject // or @Autowired
|
||||
private EmailSender emailSender;
|
||||
}
|
||||
```
|
||||
|
||||
## DTO マッピング
|
||||
|
||||
DTO にはレコードを使用する。サービス/コントローラの境界でマッピングする:
|
||||
|
||||
```java
|
||||
public record OrderResponse(Long id, String customer, BigDecimal total) {
|
||||
public static OrderResponse from(Order order) {
|
||||
return new OrderResponse(order.getId(), order.getCustomerName(), order.getTotal());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Builder パターン
|
||||
|
||||
オプションパラメータが多いオブジェクトに使用する:
|
||||
|
||||
```java
|
||||
public class SearchCriteria {
|
||||
private final String query;
|
||||
private final int page;
|
||||
private final int size;
|
||||
private final String sortBy;
|
||||
|
||||
private SearchCriteria(Builder builder) {
|
||||
this.query = builder.query;
|
||||
this.page = builder.page;
|
||||
this.size = builder.size;
|
||||
this.sortBy = builder.sortBy;
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
private String query = "";
|
||||
private int page = 0;
|
||||
private int size = 20;
|
||||
private String sortBy = "id";
|
||||
|
||||
public Builder query(String query) { this.query = query; return this; }
|
||||
public Builder page(int page) { this.page = page; return this; }
|
||||
public Builder size(int size) { this.size = size; return this; }
|
||||
public Builder sortBy(String sortBy) { this.sortBy = sortBy; return this; }
|
||||
public SearchCriteria build() { return new SearchCriteria(this); }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## ドメインモデルのシールド型
|
||||
|
||||
```java
|
||||
public sealed interface PaymentResult permits PaymentSuccess, PaymentFailure {
|
||||
record PaymentSuccess(String transactionId, BigDecimal amount) implements PaymentResult {}
|
||||
record PaymentFailure(String errorCode, String message) implements PaymentResult {}
|
||||
}
|
||||
|
||||
// 網羅的な処理(Java 21+)
|
||||
String message = switch (result) {
|
||||
case PaymentSuccess s -> "Paid: " + s.transactionId();
|
||||
case PaymentFailure f -> "Failed: " + f.errorCode();
|
||||
};
|
||||
```
|
||||
|
||||
## API レスポンスエンベロープ
|
||||
|
||||
一貫した API レスポンス:
|
||||
|
||||
```java
|
||||
public record ApiResponse<T>(boolean success, T data, String error) {
|
||||
public static <T> ApiResponse<T> ok(T data) {
|
||||
return new ApiResponse<>(true, data, null);
|
||||
}
|
||||
public static <T> ApiResponse<T> error(String message) {
|
||||
return new ApiResponse<>(false, null, message);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## リファレンス
|
||||
|
||||
スキル: `springboot-patterns` で Spring Boot アーキテクチャパターンを参照してください。
|
||||
スキル: `quarkus-patterns` で REST、Panache、メッセージングを含む Quarkus アーキテクチャパターンを参照してください。
|
||||
スキル: `jpa-patterns` でエンティティ設計とクエリ最適化を参照してください。
|
||||
101
docs/ja-JP/rules/java/security.md
Normal file
101
docs/ja-JP/rules/java/security.md
Normal file
@@ -0,0 +1,101 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.java"
|
||||
---
|
||||
# Java セキュリティ
|
||||
|
||||
> このファイルは [common/security.md](../common/security.md) を Java 固有のコンテンツで拡張します。
|
||||
|
||||
## シークレット管理
|
||||
|
||||
- API キー、トークン、認証情報をソースコードにハードコードしない
|
||||
- 環境変数を使用する: `System.getenv("API_KEY")`
|
||||
- 本番環境のシークレットにはシークレットマネージャ(Vault、AWS Secrets Manager)を使用する
|
||||
- シークレットを含むローカル設定ファイルは `.gitignore` に追加する
|
||||
|
||||
```java
|
||||
// BAD
|
||||
private static final String API_KEY = "sk-abc123...";
|
||||
|
||||
// GOOD — 環境変数
|
||||
String apiKey = System.getenv("PAYMENT_API_KEY");
|
||||
Objects.requireNonNull(apiKey, "PAYMENT_API_KEY must be set");
|
||||
```
|
||||
|
||||
## SQL インジェクション防止
|
||||
|
||||
- 常にパラメータ化クエリを使用する — ユーザー入力を SQL に連結しない
|
||||
- `PreparedStatement` またはフレームワークのパラメータ化クエリ API を使用する
|
||||
- ネイティブクエリで使用される入力はすべて検証・サニタイズする
|
||||
|
||||
```java
|
||||
// BAD — 文字列連結による SQL インジェクション
|
||||
Statement stmt = conn.createStatement();
|
||||
String sql = "SELECT * FROM orders WHERE name = '" + name + "'";
|
||||
stmt.executeQuery(sql);
|
||||
|
||||
// GOOD — パラメータ化クエリの PreparedStatement
|
||||
PreparedStatement ps = conn.prepareStatement("SELECT * FROM orders WHERE name = ?");
|
||||
ps.setString(1, name);
|
||||
|
||||
// GOOD — JDBC テンプレート
|
||||
jdbcTemplate.query("SELECT * FROM orders WHERE name = ?", mapper, name);
|
||||
```
|
||||
|
||||
## 入力検証
|
||||
|
||||
- 処理前にシステム境界ですべてのユーザー入力を検証する
|
||||
- バリデーションフレームワーク使用時は DTO に Bean Validation(`@NotNull`、`@NotBlank`、`@Size`)を使用する
|
||||
- ファイルパスとユーザー提供文字列は使用前にサニタイズする
|
||||
- 検証に失敗した入力は明確なエラーメッセージで拒否する
|
||||
|
||||
```java
|
||||
// プレーン Java での手動検証
|
||||
public Order createOrder(String customerName, BigDecimal amount) {
|
||||
if (customerName == null || customerName.isBlank()) {
|
||||
throw new IllegalArgumentException("Customer name is required");
|
||||
}
|
||||
if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) {
|
||||
throw new IllegalArgumentException("Amount must be positive");
|
||||
}
|
||||
return new Order(customerName, amount);
|
||||
}
|
||||
```
|
||||
|
||||
## 認証と認可
|
||||
|
||||
- 独自の暗号化を実装しない — 確立されたライブラリを使用する
|
||||
- パスワードは bcrypt または Argon2 で保存する、MD5/SHA1 は使用しない
|
||||
- サービス境界で認可チェックを強制する
|
||||
- ログから機密データを消去する — パスワード、トークン、PII をログに記録しない
|
||||
|
||||
## 依存関係のセキュリティ
|
||||
|
||||
- `mvn dependency:tree` または `./gradlew dependencies` で推移的依存関係を監査する
|
||||
- OWASP Dependency-Check または Snyk を使用して既知の CVE をスキャンする
|
||||
- 依存関係を最新に保つ — Dependabot または Renovate を設定する
|
||||
|
||||
## エラーメッセージ
|
||||
|
||||
- API レスポンスにスタックトレース、内部パス、SQL エラーを公開しない
|
||||
- ハンドラ境界で例外を安全な汎用クライアントメッセージにマッピングする
|
||||
- 詳細なエラーはサーバー側でログに記録する; クライアントには汎用メッセージを返す
|
||||
|
||||
```java
|
||||
// 詳細をログに記録し、汎用メッセージを返す
|
||||
try {
|
||||
return orderService.findById(id);
|
||||
} catch (OrderNotFoundException ex) {
|
||||
log.warn("Order not found: id={}", id);
|
||||
return ApiResponse.error("Resource not found"); // 汎用、内部情報なし
|
||||
} catch (Exception ex) {
|
||||
log.error("Unexpected error processing order id={}", id, ex);
|
||||
return ApiResponse.error("Internal server error"); // ex.getMessage() を公開しない
|
||||
}
|
||||
```
|
||||
|
||||
## リファレンス
|
||||
|
||||
スキル: `springboot-security` で Spring Security の認証・認可パターンを参照してください。
|
||||
スキル: `quarkus-security` で JWT/OIDC、RBAC、CDI を含む Quarkus セキュリティを参照してください。
|
||||
スキル: `security-review` で一般的なセキュリティチェックリストを参照してください。
|
||||
133
docs/ja-JP/rules/java/testing.md
Normal file
133
docs/ja-JP/rules/java/testing.md
Normal file
@@ -0,0 +1,133 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.java"
|
||||
---
|
||||
# Java テスト
|
||||
|
||||
> このファイルは [common/testing.md](../common/testing.md) を Java 固有のコンテンツで拡張します。
|
||||
|
||||
## テストフレームワーク
|
||||
|
||||
- **JUnit 5**(`@Test`、`@ParameterizedTest`、`@Nested`、`@DisplayName`)
|
||||
- **AssertJ** — 流暢なアサーション(`assertThat(result).isEqualTo(expected)`)
|
||||
- **Mockito** — 依存関係のモック
|
||||
- **Testcontainers** — データベースやサービスを必要とする統合テスト
|
||||
|
||||
## テストの構成
|
||||
|
||||
```
|
||||
src/test/java/com/example/app/
|
||||
service/ # サービス層のユニットテスト
|
||||
controller/ # Web 層 / API テスト
|
||||
repository/ # データアクセステスト
|
||||
integration/ # クロスレイヤー統合テスト
|
||||
```
|
||||
|
||||
`src/main/java` のパッケージ構造を `src/test/java` にミラーリングする。
|
||||
|
||||
## ユニットテストパターン
|
||||
|
||||
```java
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class OrderServiceTest {
|
||||
|
||||
@Mock
|
||||
private OrderRepository orderRepository;
|
||||
|
||||
private OrderService orderService;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
orderService = new OrderService(orderRepository);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("findById returns order when exists")
|
||||
void findById_existingOrder_returnsOrder() {
|
||||
var order = new Order(1L, "Alice", BigDecimal.TEN);
|
||||
when(orderRepository.findById(1L)).thenReturn(Optional.of(order));
|
||||
|
||||
var result = orderService.findById(1L);
|
||||
|
||||
assertThat(result.customerName()).isEqualTo("Alice");
|
||||
verify(orderRepository).findById(1L);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("findById throws when order not found")
|
||||
void findById_missingOrder_throws() {
|
||||
when(orderRepository.findById(99L)).thenReturn(Optional.empty());
|
||||
|
||||
assertThatThrownBy(() -> orderService.findById(99L))
|
||||
.isInstanceOf(OrderNotFoundException.class)
|
||||
.hasMessageContaining("99");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## パラメータ化テスト
|
||||
|
||||
```java
|
||||
@ParameterizedTest
|
||||
@CsvSource({
|
||||
"100.00, 10, 90.00",
|
||||
"50.00, 0, 50.00",
|
||||
"200.00, 25, 150.00"
|
||||
})
|
||||
@DisplayName("discount applied correctly")
|
||||
void applyDiscount(BigDecimal price, int pct, BigDecimal expected) {
|
||||
assertThat(PricingUtils.discount(price, pct)).isEqualByComparingTo(expected);
|
||||
}
|
||||
```
|
||||
|
||||
## 統合テスト
|
||||
|
||||
Testcontainers を使用した実データベース統合:
|
||||
|
||||
```java
|
||||
@Testcontainers
|
||||
class OrderRepositoryIT {
|
||||
|
||||
@Container
|
||||
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16");
|
||||
|
||||
private OrderRepository repository;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
var dataSource = new PGSimpleDataSource();
|
||||
dataSource.setUrl(postgres.getJdbcUrl());
|
||||
dataSource.setUser(postgres.getUsername());
|
||||
dataSource.setPassword(postgres.getPassword());
|
||||
repository = new JdbcOrderRepository(dataSource);
|
||||
}
|
||||
|
||||
@Test
|
||||
void save_and_findById() {
|
||||
var saved = repository.save(new Order(null, "Bob", BigDecimal.ONE));
|
||||
var found = repository.findById(saved.getId());
|
||||
assertThat(found).isPresent();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Spring Boot 統合テストについては、スキル: `springboot-tdd` を参照してください。
|
||||
Quarkus 統合テストについては、スキル: `quarkus-tdd` を参照してください。
|
||||
|
||||
## テスト命名
|
||||
|
||||
`@DisplayName` を使った説明的な名前:
|
||||
- メソッド名には `methodName_scenario_expectedBehavior()`
|
||||
- レポート用に `@DisplayName("人間が読める説明")`
|
||||
|
||||
## カバレッジ
|
||||
|
||||
- 行カバレッジ 80% 以上を目標
|
||||
- カバレッジレポートには JaCoCo を使用
|
||||
- サービスとドメインロジックに集中する — 自明な getter/設定クラスはスキップ
|
||||
|
||||
## リファレンス
|
||||
|
||||
スキル: `springboot-tdd` で MockMvc と Testcontainers を使った Spring Boot TDD パターンを参照してください。
|
||||
スキル: `quarkus-tdd` で REST Assured と Dev Services を使った Quarkus TDD パターンを参照してください。
|
||||
スキル: `java-coding-standards` でテスト要件を参照してください。
|
||||
86
docs/ja-JP/rules/kotlin/coding-style.md
Normal file
86
docs/ja-JP/rules/kotlin/coding-style.md
Normal file
@@ -0,0 +1,86 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.kt"
|
||||
- "**/*.kts"
|
||||
---
|
||||
# Kotlin コーディングスタイル
|
||||
|
||||
> このファイルは [common/coding-style.md](../common/coding-style.md) を Kotlin 固有のコンテンツで拡張します。
|
||||
|
||||
## フォーマット
|
||||
|
||||
- **ktlint** または **Detekt** でスタイルを強制
|
||||
- 公式 Kotlin コードスタイル(`gradle.properties` に `kotlin.code.style=official`)
|
||||
|
||||
## 不変性
|
||||
|
||||
- `var` よりも `val` を優先 — デフォルトは `val`、ミューテーションが必要な場合のみ `var` を使用
|
||||
- 値型には `data class` を使用する; public API では不変コレクション(`List`、`Map`、`Set`)を使用
|
||||
- 状態更新にはコピーオンライト: `state.copy(field = newValue)`
|
||||
|
||||
## 命名
|
||||
|
||||
Kotlin の慣例に従う:
|
||||
- `camelCase` — 関数とプロパティ
|
||||
- `PascalCase` — クラス、インターフェース、オブジェクト、型エイリアス
|
||||
- `SCREAMING_SNAKE_CASE` — 定数(`const val` または `@JvmStatic`)
|
||||
- インターフェースの接頭辞は振る舞いで付ける、`I` ではない: `Clickable` であって `IClickable` ではない
|
||||
|
||||
## Null 安全性
|
||||
|
||||
- `!!` は使用しない — `?.`、`?:`、`requireNotNull()`、または `checkNotNull()` を優先
|
||||
- スコープ付き null 安全操作には `?.let {}` を使用
|
||||
- 正当に結果がない可能性がある関数からは nullable 型を返す
|
||||
|
||||
```kotlin
|
||||
// BAD
|
||||
val name = user!!.name
|
||||
|
||||
// GOOD
|
||||
val name = user?.name ?: "Unknown"
|
||||
val name = requireNotNull(user) { "User must be set before accessing name" }.name
|
||||
```
|
||||
|
||||
## シールド型
|
||||
|
||||
閉じた状態階層のモデリングにはシールドクラス/インターフェースを使用する:
|
||||
|
||||
```kotlin
|
||||
sealed interface UiState<out T> {
|
||||
data object Loading : UiState<Nothing>
|
||||
data class Success<T>(val data: T) : UiState<T>
|
||||
data class Error(val message: String) : UiState<Nothing>
|
||||
}
|
||||
```
|
||||
|
||||
シールド型に対しては常に網羅的な `when` を使用する — `else` ブランチは使わない。
|
||||
|
||||
## 拡張関数
|
||||
|
||||
ユーティリティ操作には拡張関数を使用するが、発見しやすくする:
|
||||
- レシーバー型にちなんだファイル名にする(`StringExt.kt`、`FlowExt.kt`)
|
||||
- スコープを限定する — `Any` や過度に汎用的な型に拡張を追加しない
|
||||
|
||||
## スコープ関数
|
||||
|
||||
適切なスコープ関数を使用する:
|
||||
- `let` — null チェック + 変換: `user?.let { greet(it) }`
|
||||
- `run` — レシーバーを使って結果を計算: `service.run { fetch(config) }`
|
||||
- `apply` — オブジェクトの設定: `builder.apply { timeout = 30 }`
|
||||
- `also` — 副作用: `result.also { log(it) }`
|
||||
- スコープ関数の深いネストは避ける(最大2レベル)
|
||||
|
||||
## エラーハンドリング
|
||||
|
||||
- `Result<T>` またはカスタムシールド型を使用
|
||||
- throwable コードのラッピングには `runCatching {}` を使用
|
||||
- `CancellationException` は絶対にキャッチしない — 常に再スローする
|
||||
- 制御フローに `try-catch` を使用しない
|
||||
|
||||
```kotlin
|
||||
// BAD — 制御フローに例外を使用
|
||||
val user = try { repository.getUser(id) } catch (e: NotFoundException) { null }
|
||||
|
||||
// GOOD — nullable 戻り値
|
||||
val user: User? = repository.findUser(id)
|
||||
```
|
||||
17
docs/ja-JP/rules/kotlin/hooks.md
Normal file
17
docs/ja-JP/rules/kotlin/hooks.md
Normal file
@@ -0,0 +1,17 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.kt"
|
||||
- "**/*.kts"
|
||||
- "**/build.gradle.kts"
|
||||
---
|
||||
# Kotlin フック
|
||||
|
||||
> このファイルは [common/hooks.md](../common/hooks.md) を Kotlin 固有のコンテンツで拡張します。
|
||||
|
||||
## PostToolUse フック
|
||||
|
||||
`~/.claude/settings.json` で設定:
|
||||
|
||||
- **ktfmt/ktlint**: 編集後に `.kt` と `.kts` ファイルを自動フォーマット
|
||||
- **detekt**: Kotlin ファイル編集後に静的解析を実行
|
||||
- **./gradlew build**: 変更後にコンパイルを検証
|
||||
146
docs/ja-JP/rules/kotlin/patterns.md
Normal file
146
docs/ja-JP/rules/kotlin/patterns.md
Normal file
@@ -0,0 +1,146 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.kt"
|
||||
- "**/*.kts"
|
||||
---
|
||||
# Kotlin パターン
|
||||
|
||||
> このファイルは [common/patterns.md](../common/patterns.md) を Kotlin および Android/KMP 固有のコンテンツで拡張します。
|
||||
|
||||
## 依存性注入
|
||||
|
||||
コンストラクタインジェクションを優先する。Koin(KMP)または Hilt(Android のみ)を使用:
|
||||
|
||||
```kotlin
|
||||
// Koin — モジュール宣言
|
||||
val dataModule = module {
|
||||
single<ItemRepository> { ItemRepositoryImpl(get(), get()) }
|
||||
factory { GetItemsUseCase(get()) }
|
||||
viewModelOf(::ItemListViewModel)
|
||||
}
|
||||
|
||||
// Hilt — アノテーション
|
||||
@HiltViewModel
|
||||
class ItemListViewModel @Inject constructor(
|
||||
private val getItems: GetItemsUseCase
|
||||
) : ViewModel()
|
||||
```
|
||||
|
||||
## ViewModel パターン
|
||||
|
||||
単一の状態オブジェクト、イベントシンク、単方向データフロー:
|
||||
|
||||
```kotlin
|
||||
data class ScreenState(
|
||||
val items: List<Item> = emptyList(),
|
||||
val isLoading: Boolean = false
|
||||
)
|
||||
|
||||
class ScreenViewModel(private val useCase: GetItemsUseCase) : ViewModel() {
|
||||
private val _state = MutableStateFlow(ScreenState())
|
||||
val state = _state.asStateFlow()
|
||||
|
||||
fun onEvent(event: ScreenEvent) {
|
||||
when (event) {
|
||||
is ScreenEvent.Load -> load()
|
||||
is ScreenEvent.Delete -> delete(event.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## リポジトリパターン
|
||||
|
||||
- `suspend` 関数は `Result<T>` またはカスタムエラー型を返す
|
||||
- リアクティブストリームには `Flow`
|
||||
- ローカルとリモートのデータソースを調整する
|
||||
|
||||
```kotlin
|
||||
interface ItemRepository {
|
||||
suspend fun getById(id: String): Result<Item>
|
||||
suspend fun getAll(): Result<List<Item>>
|
||||
fun observeAll(): Flow<List<Item>>
|
||||
}
|
||||
```
|
||||
|
||||
## UseCase パターン
|
||||
|
||||
単一責任、`operator fun invoke`:
|
||||
|
||||
```kotlin
|
||||
class GetItemUseCase(private val repository: ItemRepository) {
|
||||
suspend operator fun invoke(id: String): Result<Item> {
|
||||
return repository.getById(id)
|
||||
}
|
||||
}
|
||||
|
||||
class GetItemsUseCase(private val repository: ItemRepository) {
|
||||
suspend operator fun invoke(): Result<List<Item>> {
|
||||
return repository.getAll()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## expect/actual(KMP)
|
||||
|
||||
プラットフォーム固有の実装に使用:
|
||||
|
||||
```kotlin
|
||||
// commonMain
|
||||
expect fun platformName(): String
|
||||
expect class SecureStorage {
|
||||
fun save(key: String, value: String)
|
||||
fun get(key: String): String?
|
||||
}
|
||||
|
||||
// androidMain
|
||||
actual fun platformName(): String = "Android"
|
||||
actual class SecureStorage {
|
||||
actual fun save(key: String, value: String) { /* EncryptedSharedPreferences */ }
|
||||
actual fun get(key: String): String? = null /* ... */
|
||||
}
|
||||
|
||||
// iosMain
|
||||
actual fun platformName(): String = "iOS"
|
||||
actual class SecureStorage {
|
||||
actual fun save(key: String, value: String) { /* Keychain */ }
|
||||
actual fun get(key: String): String? = null /* ... */
|
||||
}
|
||||
```
|
||||
|
||||
## コルーチンパターン
|
||||
|
||||
- ViewModel では `viewModelScope`、構造化された子作業には `coroutineScope` を使用
|
||||
- コールド Flow から StateFlow への変換には `stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), initialValue)` を使用
|
||||
- 子の失敗を独立させる場合は `supervisorScope` を使用
|
||||
|
||||
## DSL を使った Builder パターン
|
||||
|
||||
```kotlin
|
||||
class HttpClientConfig {
|
||||
var baseUrl: String = ""
|
||||
var timeout: Long = 30_000
|
||||
private val interceptors = mutableListOf<Interceptor>()
|
||||
|
||||
fun interceptor(block: () -> Interceptor) {
|
||||
interceptors.add(block())
|
||||
}
|
||||
}
|
||||
|
||||
fun httpClient(block: HttpClientConfig.() -> Unit): HttpClient {
|
||||
val config = HttpClientConfig().apply(block)
|
||||
return HttpClient(config)
|
||||
}
|
||||
|
||||
// 使用例
|
||||
val client = httpClient {
|
||||
baseUrl = "https://api.example.com"
|
||||
timeout = 15_000
|
||||
interceptor { AuthInterceptor(tokenProvider) }
|
||||
}
|
||||
```
|
||||
|
||||
## リファレンス
|
||||
|
||||
スキル: `kotlin-coroutines-flows` で詳細なコルーチンパターンを参照してください。
|
||||
スキル: `android-clean-architecture` でモジュールとレイヤーパターンを参照してください。
|
||||
82
docs/ja-JP/rules/kotlin/security.md
Normal file
82
docs/ja-JP/rules/kotlin/security.md
Normal file
@@ -0,0 +1,82 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.kt"
|
||||
- "**/*.kts"
|
||||
---
|
||||
# Kotlin セキュリティ
|
||||
|
||||
> このファイルは [common/security.md](../common/security.md) を Kotlin および Android/KMP 固有のコンテンツで拡張します。
|
||||
|
||||
## シークレット管理
|
||||
|
||||
- API キー、トークン、認証情報をソースコードにハードコードしない
|
||||
- ローカル開発のシークレットには `local.properties`(git で無視)を使用する
|
||||
- リリースビルドには CI シークレットから生成される `BuildConfig` フィールドを使用する
|
||||
- ランタイムのシークレット保存には `EncryptedSharedPreferences`(Android)または Keychain(iOS)を使用する
|
||||
|
||||
```kotlin
|
||||
// BAD
|
||||
val apiKey = "sk-abc123..."
|
||||
|
||||
// GOOD — BuildConfig から(ビルド時に生成)
|
||||
val apiKey = BuildConfig.API_KEY
|
||||
|
||||
// GOOD — ランタイム時にセキュアストレージから
|
||||
val token = secureStorage.get("auth_token")
|
||||
```
|
||||
|
||||
## ネットワークセキュリティ
|
||||
|
||||
- HTTPS のみを使用する — クリアテキストをブロックするため `network_security_config.xml` を設定する
|
||||
- 機密性の高いエンドポイントには OkHttp の `CertificatePinner` または Ktor 相当で証明書ピンニングを行う
|
||||
- すべての HTTP クライアントにタイムアウトを設定する — デフォルト(無限の場合がある)のまま放置しない
|
||||
- すべてのサーバーレスポンスを使用前に検証・サニタイズする
|
||||
|
||||
```xml
|
||||
<!-- res/xml/network_security_config.xml -->
|
||||
<network-security-config>
|
||||
<base-config cleartextTrafficPermitted="false" />
|
||||
</network-security-config>
|
||||
```
|
||||
|
||||
## 入力検証
|
||||
|
||||
- 処理や API 送信前にすべてのユーザー入力を検証する
|
||||
- Room/SQLDelight にはパラメータ化クエリを使用する — ユーザー入力を SQL に連結しない
|
||||
- パストラバーサルを防ぐためユーザー入力のファイルパスをサニタイズする
|
||||
|
||||
```kotlin
|
||||
// BAD — SQL インジェクション
|
||||
@Query("SELECT * FROM items WHERE name = '$input'")
|
||||
|
||||
// GOOD — パラメータ化
|
||||
@Query("SELECT * FROM items WHERE name = :input")
|
||||
fun findByName(input: String): List<ItemEntity>
|
||||
```
|
||||
|
||||
## データ保護
|
||||
|
||||
- Android では機密性の高いキーバリューデータに `EncryptedSharedPreferences` を使用する
|
||||
- 明示的なフィールド名で `@Serializable` を使用する — 内部プロパティ名を漏洩させない
|
||||
- 不要になった機密データはメモリからクリアする
|
||||
- シリアライズされたクラスの名前マングリングを防ぐため `@Keep` または ProGuard ルールを使用する
|
||||
|
||||
## 認証
|
||||
|
||||
- トークンはプレーンな SharedPreferences ではなくセキュアストレージに保存する
|
||||
- 適切な 401/403 ハンドリングでトークンリフレッシュを実装する
|
||||
- ログアウト時にすべての認証状態をクリアする(トークン、キャッシュされたユーザーデータ、Cookie)
|
||||
- 機密性の高い操作にはバイオメトリクス認証(`BiometricPrompt`)を使用する
|
||||
|
||||
## ProGuard / R8
|
||||
|
||||
- すべてのシリアライズされたモデル(`@Serializable`、Gson、Moshi)の Keep ルール
|
||||
- リフレクションベースのライブラリ(Koin、Retrofit)の Keep ルール
|
||||
- リリースビルドをテストする — 難読化はシリアライズを無言で壊す可能性がある
|
||||
|
||||
## WebView セキュリティ
|
||||
|
||||
- 明示的に必要でない限り JavaScript を無効にする: `settings.javaScriptEnabled = false`
|
||||
- WebView にロードする前に URL を検証する
|
||||
- 機密データにアクセスする `@JavascriptInterface` メソッドを公開しない
|
||||
- `WebViewClient.shouldOverrideUrlLoading()` を使用してナビゲーションを制御する
|
||||
128
docs/ja-JP/rules/kotlin/testing.md
Normal file
128
docs/ja-JP/rules/kotlin/testing.md
Normal file
@@ -0,0 +1,128 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.kt"
|
||||
- "**/*.kts"
|
||||
---
|
||||
# Kotlin テスト
|
||||
|
||||
> このファイルは [common/testing.md](../common/testing.md) を Kotlin および Android/KMP 固有のコンテンツで拡張します。
|
||||
|
||||
## テストフレームワーク
|
||||
|
||||
- **kotlin.test** — マルチプラットフォーム(KMP)用(`@Test`、`assertEquals`、`assertTrue`)
|
||||
- **JUnit 4/5** — Android 固有のテスト用
|
||||
- **Turbine** — Flow と StateFlow のテスト用
|
||||
- **kotlinx-coroutines-test** — コルーチンテスト用(`runTest`、`TestDispatcher`)
|
||||
|
||||
## Turbine を使った ViewModel テスト
|
||||
|
||||
```kotlin
|
||||
@Test
|
||||
fun `loading state emitted then data`() = runTest {
|
||||
val repo = FakeItemRepository()
|
||||
repo.addItem(testItem)
|
||||
val viewModel = ItemListViewModel(GetItemsUseCase(repo))
|
||||
|
||||
viewModel.state.test {
|
||||
assertEquals(ItemListState(), awaitItem()) // 初期状態
|
||||
viewModel.onEvent(ItemListEvent.Load)
|
||||
assertTrue(awaitItem().isLoading) // ローディング中
|
||||
assertEquals(listOf(testItem), awaitItem().items) // ロード完了
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## モックよりもフェイクを優先
|
||||
|
||||
モッキングフレームワークよりも手書きのフェイクを優先する:
|
||||
|
||||
```kotlin
|
||||
class FakeItemRepository : ItemRepository {
|
||||
private val items = mutableListOf<Item>()
|
||||
var fetchError: Throwable? = null
|
||||
|
||||
override suspend fun getAll(): Result<List<Item>> {
|
||||
fetchError?.let { return Result.failure(it) }
|
||||
return Result.success(items.toList())
|
||||
}
|
||||
|
||||
override fun observeAll(): Flow<List<Item>> = flowOf(items.toList())
|
||||
|
||||
fun addItem(item: Item) { items.add(item) }
|
||||
}
|
||||
```
|
||||
|
||||
## コルーチンテスト
|
||||
|
||||
```kotlin
|
||||
@Test
|
||||
fun `parallel operations complete`() = runTest {
|
||||
val repo = FakeRepository()
|
||||
val result = loadDashboard(repo)
|
||||
advanceUntilIdle()
|
||||
assertNotNull(result.items)
|
||||
assertNotNull(result.stats)
|
||||
}
|
||||
```
|
||||
|
||||
`runTest` を使用する — 仮想時間を自動的に進め、`TestScope` を提供する。
|
||||
|
||||
## Ktor MockEngine
|
||||
|
||||
```kotlin
|
||||
val mockEngine = MockEngine { request ->
|
||||
when (request.url.encodedPath) {
|
||||
"/api/items" -> respond(
|
||||
content = Json.encodeToString(testItems),
|
||||
headers = headersOf(HttpHeaders.ContentType, ContentType.Application.Json.toString())
|
||||
)
|
||||
else -> respondError(HttpStatusCode.NotFound)
|
||||
}
|
||||
}
|
||||
|
||||
val client = HttpClient(mockEngine) {
|
||||
install(ContentNegotiation) { json() }
|
||||
}
|
||||
```
|
||||
|
||||
## Room/SQLDelight テスト
|
||||
|
||||
- Room: インメモリテストには `Room.inMemoryDatabaseBuilder()` を使用
|
||||
- SQLDelight: JVM テストには `JdbcSqliteDriver(JdbcSqliteDriver.IN_MEMORY)` を使用
|
||||
|
||||
```kotlin
|
||||
@Test
|
||||
fun `insert and query items`() = runTest {
|
||||
val driver = JdbcSqliteDriver(JdbcSqliteDriver.IN_MEMORY)
|
||||
Database.Schema.create(driver)
|
||||
val db = Database(driver)
|
||||
|
||||
db.itemQueries.insert("1", "Sample Item", "description")
|
||||
val items = db.itemQueries.getAll().executeAsList()
|
||||
assertEquals(1, items.size)
|
||||
}
|
||||
```
|
||||
|
||||
## テスト命名
|
||||
|
||||
バッククォートで囲んだ説明的な名前を使用する:
|
||||
|
||||
```kotlin
|
||||
@Test
|
||||
fun `search with empty query returns all items`() = runTest { }
|
||||
|
||||
@Test
|
||||
fun `delete item emits updated list without deleted item`() = runTest { }
|
||||
```
|
||||
|
||||
## テストの構成
|
||||
|
||||
```
|
||||
src/
|
||||
├── commonTest/kotlin/ # 共有テスト(ViewModel、UseCase、Repository)
|
||||
├── androidUnitTest/kotlin/ # Android ユニットテスト(JUnit)
|
||||
├── androidInstrumentedTest/kotlin/ # インストルメンテッドテスト(Room、UI)
|
||||
└── iosTest/kotlin/ # iOS 固有のテスト
|
||||
```
|
||||
|
||||
最低限のテストカバレッジ: すべての機能に対して ViewModel + UseCase。
|
||||
46
docs/ja-JP/rules/perl/coding-style.md
Normal file
46
docs/ja-JP/rules/perl/coding-style.md
Normal file
@@ -0,0 +1,46 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.pl"
|
||||
- "**/*.pm"
|
||||
- "**/*.t"
|
||||
- "**/*.psgi"
|
||||
- "**/*.cgi"
|
||||
---
|
||||
# Perl コーディングスタイル
|
||||
|
||||
> このファイルは [common/coding-style.md](../common/coding-style.md) を Perl 固有のコンテンツで拡張します。
|
||||
|
||||
## 標準
|
||||
|
||||
- 常に `use v5.36`(`strict`、`warnings`、`say`、サブルーチンシグネチャを有効化)
|
||||
- サブルーチンシグネチャを使用する — `@_` を手動で展開しない
|
||||
- 明示的な改行付きの `print` よりも `say` を優先
|
||||
|
||||
## 不変性
|
||||
|
||||
- **Moo** で `is => 'ro'` と **Types::Standard** をすべての属性に使用
|
||||
- blessed ハッシュリファレンスを直接使用しない — 常に Moo/Moose アクセサを使用
|
||||
- **OO オーバーライドノート**: `builder` または `default` を持つ Moo の `has` 属性は、計算された読み取り専用値として許容される
|
||||
|
||||
## フォーマット
|
||||
|
||||
以下の設定で **perltidy** を使用:
|
||||
|
||||
```
|
||||
-i=4 # 4スペースインデント
|
||||
-l=100 # 100文字の行長
|
||||
-ce # cuddled else
|
||||
-bar # 開き波括弧は常に右
|
||||
```
|
||||
|
||||
## リンティング
|
||||
|
||||
**perlcritic** をテーマ `core`、`pbp`、`security` で重大度 3 で使用する。
|
||||
|
||||
```bash
|
||||
perlcritic --severity 3 --theme 'core || pbp || security' lib/
|
||||
```
|
||||
|
||||
## リファレンス
|
||||
|
||||
スキル: `perl-patterns` で包括的なモダン Perl のイディオムとベストプラクティスを参照してください。
|
||||
22
docs/ja-JP/rules/perl/hooks.md
Normal file
22
docs/ja-JP/rules/perl/hooks.md
Normal file
@@ -0,0 +1,22 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.pl"
|
||||
- "**/*.pm"
|
||||
- "**/*.t"
|
||||
- "**/*.psgi"
|
||||
- "**/*.cgi"
|
||||
---
|
||||
# Perl フック
|
||||
|
||||
> このファイルは [common/hooks.md](../common/hooks.md) を Perl 固有のコンテンツで拡張します。
|
||||
|
||||
## PostToolUse フック
|
||||
|
||||
`~/.claude/settings.json` で設定:
|
||||
|
||||
- **perltidy**: 編集後に `.pl` と `.pm` ファイルを自動フォーマット
|
||||
- **perlcritic**: `.pm` ファイル編集後にリントチェックを実行
|
||||
|
||||
## 警告
|
||||
|
||||
- スクリプト以外の `.pm` ファイルでの `print` について警告する — `say` またはロギングモジュール(例: `Log::Any`)を使用すること
|
||||
76
docs/ja-JP/rules/perl/patterns.md
Normal file
76
docs/ja-JP/rules/perl/patterns.md
Normal file
@@ -0,0 +1,76 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.pl"
|
||||
- "**/*.pm"
|
||||
- "**/*.t"
|
||||
- "**/*.psgi"
|
||||
- "**/*.cgi"
|
||||
---
|
||||
# Perl パターン
|
||||
|
||||
> このファイルは [common/patterns.md](../common/patterns.md) を Perl 固有のコンテンツで拡張します。
|
||||
|
||||
## リポジトリパターン
|
||||
|
||||
**DBI** または **DBIx::Class** をインターフェースの背後に使用する:
|
||||
|
||||
```perl
|
||||
package MyApp::Repo::User;
|
||||
use Moo;
|
||||
|
||||
has dbh => (is => 'ro', required => 1);
|
||||
|
||||
sub find_by_id ($self, $id) {
|
||||
my $sth = $self->dbh->prepare('SELECT * FROM users WHERE id = ?');
|
||||
$sth->execute($id);
|
||||
return $sth->fetchrow_hashref;
|
||||
}
|
||||
```
|
||||
|
||||
## DTO / 値オブジェクト
|
||||
|
||||
**Moo** クラスと **Types::Standard** を使用する(Python の dataclass に相当):
|
||||
|
||||
```perl
|
||||
package MyApp::DTO::User;
|
||||
use Moo;
|
||||
use Types::Standard qw(Str Int);
|
||||
|
||||
has name => (is => 'ro', isa => Str, required => 1);
|
||||
has email => (is => 'ro', isa => Str, required => 1);
|
||||
has age => (is => 'ro', isa => Int);
|
||||
```
|
||||
|
||||
## リソース管理
|
||||
|
||||
- 常に `autodie` 付きの **3引数 open** を使用する
|
||||
- ファイル操作には **Path::Tiny** を使用する
|
||||
|
||||
```perl
|
||||
use autodie;
|
||||
use Path::Tiny;
|
||||
|
||||
my $content = path('config.json')->slurp_utf8;
|
||||
```
|
||||
|
||||
## モジュールインターフェース
|
||||
|
||||
`Exporter 'import'` と `@EXPORT_OK` を使用する — `@EXPORT` は使用しない:
|
||||
|
||||
```perl
|
||||
use Exporter 'import';
|
||||
our @EXPORT_OK = qw(parse_config validate_input);
|
||||
```
|
||||
|
||||
## 依存関係管理
|
||||
|
||||
再現可能なインストールのために **cpanfile** + **carton** を使用する:
|
||||
|
||||
```bash
|
||||
carton install
|
||||
carton exec prove -lr t/
|
||||
```
|
||||
|
||||
## リファレンス
|
||||
|
||||
スキル: `perl-patterns` で包括的なモダン Perl のパターンとイディオムを参照してください。
|
||||
69
docs/ja-JP/rules/perl/security.md
Normal file
69
docs/ja-JP/rules/perl/security.md
Normal file
@@ -0,0 +1,69 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.pl"
|
||||
- "**/*.pm"
|
||||
- "**/*.t"
|
||||
- "**/*.psgi"
|
||||
- "**/*.cgi"
|
||||
---
|
||||
# Perl セキュリティ
|
||||
|
||||
> このファイルは [common/security.md](../common/security.md) を Perl 固有のコンテンツで拡張します。
|
||||
|
||||
## 汚染モード
|
||||
|
||||
- すべての CGI/Web 向けスクリプトで `-T` フラグを使用する
|
||||
- 外部コマンド実行前に `%ENV`(`$ENV{PATH}`、`$ENV{CDPATH}` など)をサニタイズする
|
||||
|
||||
## 入力検証
|
||||
|
||||
- アンテイントには許可リスト正規表現を使用する — `/(.*)/s` は絶対に使用しない
|
||||
- すべてのユーザー入力を明示的なパターンで検証する:
|
||||
|
||||
```perl
|
||||
if ($input =~ /\A([a-zA-Z0-9_-]+)\z/) {
|
||||
my $clean = $1;
|
||||
}
|
||||
```
|
||||
|
||||
## ファイル I/O
|
||||
|
||||
- **3引数 open のみ** — 2引数 open は使用しない
|
||||
- `Cwd::realpath` でパストラバーサルを防止する:
|
||||
|
||||
```perl
|
||||
use Cwd 'realpath';
|
||||
my $safe_path = realpath($user_path);
|
||||
die "Path traversal" unless $safe_path =~ m{\A/allowed/directory/};
|
||||
```
|
||||
|
||||
## プロセス実行
|
||||
|
||||
- **リスト形式の `system()`** を使用する — 単一文字列形式は使用しない
|
||||
- 出力キャプチャには **IPC::Run3** を使用する
|
||||
- 変数補間付きのバッククォートは使用しない
|
||||
|
||||
```perl
|
||||
system('grep', '-r', $pattern, $directory); # 安全
|
||||
```
|
||||
|
||||
## SQL インジェクション防止
|
||||
|
||||
常に DBI プレースホルダを使用する — SQL に補間しない:
|
||||
|
||||
```perl
|
||||
my $sth = $dbh->prepare('SELECT * FROM users WHERE email = ?');
|
||||
$sth->execute($email);
|
||||
```
|
||||
|
||||
## セキュリティスキャン
|
||||
|
||||
**perlcritic** をセキュリティテーマで重大度 4 以上で実行する:
|
||||
|
||||
```bash
|
||||
perlcritic --severity 4 --theme security lib/
|
||||
```
|
||||
|
||||
## リファレンス
|
||||
|
||||
スキル: `perl-security` で包括的な Perl セキュリティパターン、汚染モード、安全な I/O を参照してください。
|
||||
54
docs/ja-JP/rules/perl/testing.md
Normal file
54
docs/ja-JP/rules/perl/testing.md
Normal file
@@ -0,0 +1,54 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.pl"
|
||||
- "**/*.pm"
|
||||
- "**/*.t"
|
||||
- "**/*.psgi"
|
||||
- "**/*.cgi"
|
||||
---
|
||||
# Perl テスト
|
||||
|
||||
> このファイルは [common/testing.md](../common/testing.md) を Perl 固有のコンテンツで拡張します。
|
||||
|
||||
## フレームワーク
|
||||
|
||||
新規プロジェクトには **Test2::V0** を使用する(Test::More ではない):
|
||||
|
||||
```perl
|
||||
use Test2::V0;
|
||||
|
||||
is($result, 42, 'answer is correct');
|
||||
|
||||
done_testing;
|
||||
```
|
||||
|
||||
## ランナー
|
||||
|
||||
```bash
|
||||
prove -l t/ # lib/ を @INC に追加
|
||||
prove -lr -j8 t/ # 再帰的、8並列ジョブ
|
||||
```
|
||||
|
||||
`lib/` を `@INC` に含めるため、常に `-l` を使用する。
|
||||
|
||||
## カバレッジ
|
||||
|
||||
**Devel::Cover** を使用する — 80% 以上を目標:
|
||||
|
||||
```bash
|
||||
cover -test
|
||||
```
|
||||
|
||||
## モック
|
||||
|
||||
- **Test::MockModule** — 既存モジュールのメソッドをモック
|
||||
- **Test::MockObject** — ゼロからテストダブルを作成
|
||||
|
||||
## 注意点
|
||||
|
||||
- テストファイルは常に `done_testing` で終了する
|
||||
- `prove` で `-l` フラグを忘れない
|
||||
|
||||
## リファレンス
|
||||
|
||||
スキル: `perl-testing` で Test2::V0、prove、Devel::Cover を使った詳細な Perl TDD パターンを参照してください。
|
||||
40
docs/ja-JP/rules/php/coding-style.md
Normal file
40
docs/ja-JP/rules/php/coding-style.md
Normal file
@@ -0,0 +1,40 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.php"
|
||||
- "**/composer.json"
|
||||
---
|
||||
# PHP コーディングスタイル
|
||||
|
||||
> このファイルは [common/coding-style.md](../common/coding-style.md) を PHP 固有のコンテンツで拡張します。
|
||||
|
||||
## 標準
|
||||
|
||||
- **PSR-12** のフォーマットと命名規則に従う。
|
||||
- アプリケーションコードでは `declare(strict_types=1);` を優先する。
|
||||
- 新しいコードが許す限り、スカラー型ヒント、戻り値の型、型付きプロパティをあらゆる箇所で使用する。
|
||||
|
||||
## 不変性
|
||||
|
||||
- サービス境界を越えるデータには不変の DTO と値オブジェクトを優先する。
|
||||
- 可能な場合はリクエスト/レスポンスペイロードに `readonly` プロパティまたは不変コンストラクタを使用する。
|
||||
- 単純なマップには配列を使用する; ビジネスクリティカルな構造は明示的なクラスに昇格させる。
|
||||
|
||||
## フォーマット
|
||||
|
||||
- フォーマットには **PHP-CS-Fixer** または **Laravel Pint** を使用する。
|
||||
- 静的解析には **PHPStan** または **Psalm** を使用する。
|
||||
- Composer スクリプトをチェックインし、ローカルと CI で同じコマンドが実行されるようにする。
|
||||
|
||||
## インポート
|
||||
|
||||
- 参照されるすべてのクラス、インターフェース、トレイトに `use` 文を追加する。
|
||||
- プロジェクトが明示的に完全修飾名を好む場合を除き、グローバル名前空間への依存を避ける。
|
||||
|
||||
## エラーハンドリング
|
||||
|
||||
- 例外的な状態には例外をスローする; 新しいコードでは隠れたエラーチャネルとして `false`/`null` を返すことを避ける。
|
||||
- フレームワーク/リクエスト入力をドメインロジックに到達する前に検証済み DTO に変換する。
|
||||
|
||||
## リファレンス
|
||||
|
||||
スキル: `backend-patterns` でより広範なサービス/リポジトリの階層化ガイダンスを参照してください。
|
||||
24
docs/ja-JP/rules/php/hooks.md
Normal file
24
docs/ja-JP/rules/php/hooks.md
Normal file
@@ -0,0 +1,24 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.php"
|
||||
- "**/composer.json"
|
||||
- "**/phpstan.neon"
|
||||
- "**/phpstan.neon.dist"
|
||||
- "**/psalm.xml"
|
||||
---
|
||||
# PHP フック
|
||||
|
||||
> このファイルは [common/hooks.md](../common/hooks.md) を PHP 固有のコンテンツで拡張します。
|
||||
|
||||
## PostToolUse フック
|
||||
|
||||
`~/.claude/settings.json` で設定:
|
||||
|
||||
- **Pint / PHP-CS-Fixer**: 編集された `.php` ファイルを自動フォーマット。
|
||||
- **PHPStan / Psalm**: 型付きコードベースで PHP 編集後に静的解析を実行。
|
||||
- **PHPUnit / Pest**: 編集が動作に影響する場合、対象ファイルやモジュールのテストを実行。
|
||||
|
||||
## 警告
|
||||
|
||||
- 編集されたファイルに残された `var_dump`、`dd`、`dump`、`die()` について警告する。
|
||||
- 編集された PHP ファイルが生 SQL を追加したり CSRF/セッション保護を無効化している場合に警告する。
|
||||
33
docs/ja-JP/rules/php/patterns.md
Normal file
33
docs/ja-JP/rules/php/patterns.md
Normal file
@@ -0,0 +1,33 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.php"
|
||||
- "**/composer.json"
|
||||
---
|
||||
# PHP パターン
|
||||
|
||||
> このファイルは [common/patterns.md](../common/patterns.md) を PHP 固有のコンテンツで拡張します。
|
||||
|
||||
## 薄いコントローラ、明示的なサービス
|
||||
|
||||
- コントローラはトランスポートに集中する: 認証、バリデーション、シリアライゼーション、ステータスコード。
|
||||
- ビジネスルールは HTTP ブートストラップなしでテストしやすいアプリケーション/ドメインサービスに移動する。
|
||||
|
||||
## DTO と値オブジェクト
|
||||
|
||||
- 形状の重い連想配列をリクエスト、コマンド、外部 API ペイロード用の DTO に置き換える。
|
||||
- 金額、識別子、日付範囲、その他の制約のある概念には値オブジェクトを使用する。
|
||||
|
||||
## 依存性注入
|
||||
|
||||
- フレームワークのグローバルではなく、インターフェースまたは狭いサービス契約に依存する。
|
||||
- サービスロケータ検索なしでテスト可能になるよう、コンストラクタ経由で協力オブジェクトを渡す。
|
||||
|
||||
## 境界
|
||||
|
||||
- モデル層が永続化以上のことをしている場合、ORM モデルをドメイン判断から分離する。
|
||||
- サードパーティ SDK を小さなアダプタでラップし、コードベースの残りが彼らの契約ではなく自身の契約に依存するようにする。
|
||||
|
||||
## リファレンス
|
||||
|
||||
スキル: `api-design` でエンドポイントの規約とレスポンス形状のガイダンスを参照してください。
|
||||
スキル: `laravel-patterns` で Laravel 固有のアーキテクチャガイダンスを参照してください。
|
||||
37
docs/ja-JP/rules/php/security.md
Normal file
37
docs/ja-JP/rules/php/security.md
Normal file
@@ -0,0 +1,37 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.php"
|
||||
- "**/composer.lock"
|
||||
- "**/composer.json"
|
||||
---
|
||||
# PHP セキュリティ
|
||||
|
||||
> このファイルは [common/security.md](../common/security.md) を PHP 固有のコンテンツで拡張します。
|
||||
|
||||
## 入力と出力
|
||||
|
||||
- フレームワーク境界でリクエスト入力を検証する(`FormRequest`、Symfony Validator、または明示的な DTO バリデーション)。
|
||||
- テンプレートではデフォルトで出力をエスケープする; 生の HTML レンダリングは正当化が必要な例外として扱う。
|
||||
- バリデーションなしにクエリパラメータ、Cookie、ヘッダー、アップロードされたファイルのメタデータを信頼しない。
|
||||
|
||||
## データベースの安全性
|
||||
|
||||
- すべての動的クエリにプリペアドステートメント(`PDO`、Doctrine、Eloquent クエリビルダ)を使用する。
|
||||
- コントローラ/ビューでの文字列構築 SQL を避ける。
|
||||
- ORM のマスアサインメントを慎重にスコープし、書き込み可能なフィールドをホワイトリストにする。
|
||||
|
||||
## シークレットと依存関係
|
||||
|
||||
- 環境変数またはシークレットマネージャからシークレットをロードする、コミットされた設定ファイルからは読み込まない。
|
||||
- CI で `composer audit` を実行し、依存関係追加前に新しいパッケージメンテナーの信頼性を確認する。
|
||||
- メジャーバージョンは意図的に固定し、放棄されたパッケージは速やかに削除する。
|
||||
|
||||
## 認証とセッションの安全性
|
||||
|
||||
- パスワード保存には `password_hash()` / `password_verify()` を使用する。
|
||||
- 認証と権限変更後にセッション識別子を再生成する。
|
||||
- 状態変更を伴う Web リクエストに CSRF 保護を強制する。
|
||||
|
||||
## リファレンス
|
||||
|
||||
スキル: `laravel-security` で Laravel 固有のセキュリティガイダンスを参照してください。
|
||||
39
docs/ja-JP/rules/php/testing.md
Normal file
39
docs/ja-JP/rules/php/testing.md
Normal file
@@ -0,0 +1,39 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.php"
|
||||
- "**/phpunit.xml"
|
||||
- "**/phpunit.xml.dist"
|
||||
- "**/composer.json"
|
||||
---
|
||||
# PHP テスト
|
||||
|
||||
> このファイルは [common/testing.md](../common/testing.md) を PHP 固有のコンテンツで拡張します。
|
||||
|
||||
## フレームワーク
|
||||
|
||||
デフォルトのテストフレームワークとして **PHPUnit** を使用する。プロジェクトに **Pest** が設定されている場合、新しいテストには Pest を優先し、フレームワークの混在を避ける。
|
||||
|
||||
## カバレッジ
|
||||
|
||||
```bash
|
||||
vendor/bin/phpunit --coverage-text
|
||||
# または
|
||||
vendor/bin/pest --coverage
|
||||
```
|
||||
|
||||
CI では **pcov** または **Xdebug** を優先し、カバレッジ閾値は暗黙知ではなく CI で管理する。
|
||||
|
||||
## テストの構成
|
||||
|
||||
- 高速なユニットテストとフレームワーク/データベース統合テストを分離する。
|
||||
- フィクスチャには大きな手書き配列ではなくファクトリ/ビルダーを使用する。
|
||||
- HTTP/コントローラテストはトランスポートとバリデーションに集中する; ビジネスルールはサービスレベルのテストに移動する。
|
||||
|
||||
## Inertia
|
||||
|
||||
プロジェクトが Inertia.js を使用している場合、生の JSON アサーションではなく `AssertableInertia` 付きの `assertInertia` でコンポーネント名とプロップスを検証することを優先する。
|
||||
|
||||
## リファレンス
|
||||
|
||||
スキル: `tdd-workflow` でリポジトリ全体の RED -> GREEN -> REFACTOR ループを参照してください。
|
||||
スキル: `laravel-tdd` で Laravel 固有のテストパターン(PHPUnit と Pest)を参照してください。
|
||||
42
docs/ja-JP/rules/python/coding-style.md
Normal file
42
docs/ja-JP/rules/python/coding-style.md
Normal file
@@ -0,0 +1,42 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.py"
|
||||
- "**/*.pyi"
|
||||
---
|
||||
# Python コーディングスタイル
|
||||
|
||||
> このファイルは [common/coding-style.md](../common/coding-style.md) を Python 固有のコンテンツで拡張します。
|
||||
|
||||
## 標準
|
||||
|
||||
- **PEP 8** 規約に従う
|
||||
- すべての関数シグネチャに**型アノテーション**を使用する
|
||||
|
||||
## 不変性
|
||||
|
||||
不変データ構造を優先する:
|
||||
|
||||
```python
|
||||
from dataclasses import dataclass
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class User:
|
||||
name: str
|
||||
email: str
|
||||
|
||||
from typing import NamedTuple
|
||||
|
||||
class Point(NamedTuple):
|
||||
x: float
|
||||
y: float
|
||||
```
|
||||
|
||||
## フォーマット
|
||||
|
||||
- **black** — コードフォーマット
|
||||
- **isort** — インポートの並べ替え
|
||||
- **ruff** — リンティング
|
||||
|
||||
## リファレンス
|
||||
|
||||
スキル: `python-patterns` で包括的な Python のイディオムとパターンを参照してください。
|
||||
58
docs/ja-JP/rules/python/fastapi.md
Normal file
58
docs/ja-JP/rules/python/fastapi.md
Normal file
@@ -0,0 +1,58 @@
|
||||
---
|
||||
paths:
|
||||
- "**/app/**/*.py"
|
||||
- "**/fastapi/**/*.py"
|
||||
- "**/*_api.py"
|
||||
---
|
||||
# FastAPI ルール
|
||||
|
||||
FastAPI プロジェクトでは、一般的な Python ルールと併せてこれらのルールを使用してください。
|
||||
|
||||
## 構造
|
||||
|
||||
- アプリの構築は `create_app()` に配置する。
|
||||
- ルーターは薄く保つ; 永続化とビジネスロジックはサービスまたは CRUD ヘルパーに移動する。
|
||||
- リクエストスキーマ、更新スキーマ、レスポンススキーマは分離する。
|
||||
- データベースセッションと認証は依存関係に配置する。
|
||||
|
||||
## 非同期
|
||||
|
||||
- I/O を実行するエンドポイントには `async def` を使用する。
|
||||
- 非同期エンドポイントからは非同期データベースクライアントと HTTP クライアントを使用する。
|
||||
- 非同期ルートから `requests`、同期 SQLAlchemy セッション、またはブロッキングファイル/ネットワーク操作を呼び出さない。
|
||||
|
||||
## 依存性注入
|
||||
|
||||
```python
|
||||
@router.get("/users/{user_id}")
|
||||
async def get_user(
|
||||
user_id: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
...
|
||||
```
|
||||
|
||||
ルートハンドラ内で `SessionLocal()` や長寿命クライアントを作成しない。
|
||||
|
||||
## スキーマ
|
||||
|
||||
- レスポンスモデルにパスワード、パスワードハッシュ、アクセストークン、リフレッシュトークン、内部認証状態を含めない。
|
||||
- アプリケーションデータを返すエンドポイントには `response_model` を使用する。
|
||||
- 手書きのバリデーションの代わりに、Pydantic でルールを表現できる場合はフィールド制約を使用する。
|
||||
|
||||
## セキュリティ
|
||||
|
||||
- CORS オリジンは環境固有にする。
|
||||
- ワイルドカードオリジンと認証情報付き CORS を組み合わせない。
|
||||
- JWT の有効期限、発行者、オーディエンス、アルゴリズムを検証する。
|
||||
- 認証および書き込み負荷の高いエンドポイントにレート制限を適用する。
|
||||
- ログから認証情報、Cookie、Authorization ヘッダー、トークンを除去する。
|
||||
|
||||
## テスト
|
||||
|
||||
- `Depends` で使用される正確な依存関係をオーバーライドする。
|
||||
- テスト後に `app.dependency_overrides` をクリアする。
|
||||
- 非同期アプリケーションには非同期テストクライアントを優先する。
|
||||
|
||||
スキル: `fastapi-patterns` を参照してください。
|
||||
19
docs/ja-JP/rules/python/hooks.md
Normal file
19
docs/ja-JP/rules/python/hooks.md
Normal file
@@ -0,0 +1,19 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.py"
|
||||
- "**/*.pyi"
|
||||
---
|
||||
# Python フック
|
||||
|
||||
> このファイルは [common/hooks.md](../common/hooks.md) を Python 固有のコンテンツで拡張します。
|
||||
|
||||
## PostToolUse フック
|
||||
|
||||
`~/.claude/settings.json` で設定:
|
||||
|
||||
- **black/ruff**: 編集後に `.py` ファイルを自動フォーマット
|
||||
- **mypy/pyright**: `.py` ファイル編集後に型チェックを実行
|
||||
|
||||
## 警告
|
||||
|
||||
- 編集されたファイル内の `print()` 文について警告する(代わりに `logging` モジュールを使用)
|
||||
39
docs/ja-JP/rules/python/patterns.md
Normal file
39
docs/ja-JP/rules/python/patterns.md
Normal file
@@ -0,0 +1,39 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.py"
|
||||
- "**/*.pyi"
|
||||
---
|
||||
# Python パターン
|
||||
|
||||
> このファイルは [common/patterns.md](../common/patterns.md) を Python 固有のコンテンツで拡張します。
|
||||
|
||||
## Protocol(ダックタイピング)
|
||||
|
||||
```python
|
||||
from typing import Protocol
|
||||
|
||||
class Repository(Protocol):
|
||||
def find_by_id(self, id: str) -> dict | None: ...
|
||||
def save(self, entity: dict) -> dict: ...
|
||||
```
|
||||
|
||||
## DTO としての Dataclass
|
||||
|
||||
```python
|
||||
from dataclasses import dataclass
|
||||
|
||||
@dataclass
|
||||
class CreateUserRequest:
|
||||
name: str
|
||||
email: str
|
||||
age: int | None = None
|
||||
```
|
||||
|
||||
## コンテキストマネージャとジェネレータ
|
||||
|
||||
- リソース管理にはコンテキストマネージャ(`with` 文)を使用する
|
||||
- 遅延評価とメモリ効率の良いイテレーションにはジェネレータを使用する
|
||||
|
||||
## リファレンス
|
||||
|
||||
スキル: `python-patterns` でデコレータ、並行処理、パッケージ構成を含む包括的なパターンを参照してください。
|
||||
30
docs/ja-JP/rules/python/security.md
Normal file
30
docs/ja-JP/rules/python/security.md
Normal file
@@ -0,0 +1,30 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.py"
|
||||
- "**/*.pyi"
|
||||
---
|
||||
# Python セキュリティ
|
||||
|
||||
> このファイルは [common/security.md](../common/security.md) を Python 固有のコンテンツで拡張します。
|
||||
|
||||
## シークレット管理
|
||||
|
||||
```python
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
api_key = os.environ["OPENAI_API_KEY"] # 未設定の場合 KeyError を発生
|
||||
```
|
||||
|
||||
## セキュリティスキャン
|
||||
|
||||
- **bandit** を使用して静的セキュリティ解析を実行:
|
||||
```bash
|
||||
bandit -r src/
|
||||
```
|
||||
|
||||
## リファレンス
|
||||
|
||||
スキル: `django-security` で Django 固有のセキュリティガイドラインを参照してください(該当する場合)。
|
||||
38
docs/ja-JP/rules/python/testing.md
Normal file
38
docs/ja-JP/rules/python/testing.md
Normal file
@@ -0,0 +1,38 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.py"
|
||||
- "**/*.pyi"
|
||||
---
|
||||
# Python テスト
|
||||
|
||||
> このファイルは [common/testing.md](../common/testing.md) を Python 固有のコンテンツで拡張します。
|
||||
|
||||
## フレームワーク
|
||||
|
||||
テストフレームワークとして **pytest** を使用する。
|
||||
|
||||
## カバレッジ
|
||||
|
||||
```bash
|
||||
pytest --cov=src --cov-report=term-missing
|
||||
```
|
||||
|
||||
## テストの構成
|
||||
|
||||
テスト分類には `pytest.mark` を使用する:
|
||||
|
||||
```python
|
||||
import pytest
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_calculate_total():
|
||||
...
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_database_connection():
|
||||
...
|
||||
```
|
||||
|
||||
## リファレンス
|
||||
|
||||
スキル: `python-testing` で詳細な pytest パターンとフィクスチャを参照してください。
|
||||
46
docs/ja-JP/rules/ruby/coding-style.md
Normal file
46
docs/ja-JP/rules/ruby/coding-style.md
Normal file
@@ -0,0 +1,46 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.rb"
|
||||
- "**/*.rake"
|
||||
- "**/Gemfile"
|
||||
- "**/*.gemspec"
|
||||
- "**/config.ru"
|
||||
---
|
||||
# Ruby コーディングスタイル
|
||||
|
||||
> このファイルは [common/coding-style.md](../common/coding-style.md) を Ruby および Rails 固有のコンテンツで拡張します。
|
||||
|
||||
## 標準
|
||||
|
||||
- プロジェクトが既に古いサポート対象ランタイムを固定していない限り、新しい Rails 開発では **Ruby 3.3+** をターゲットにする。
|
||||
- 本番環境では起動時間、メモリ、リクエスト/ジョブのスループットを測定した後にのみ **YJIT** を有効にする。
|
||||
- プロジェクトがその規約を使用している場合、新しい Ruby ファイルに `# frozen_string_literal: true` を追加する。
|
||||
- 巧妙なメタプログラミングよりも明快な Ruby を優先する。DSL を多用するコードは狭く、テストされた境界の背後に隔離する。
|
||||
|
||||
## フォーマットとリンティング
|
||||
|
||||
- プロジェクトのチェックイン済み RuboCop 設定を使用する。Rails 8+ アプリでは `rubocop-rails-omakase` から始め、コードベースに実際の規約がある場合にのみカスタマイズする。
|
||||
- フォーマッタ/リンターのコマンドは binstub またはスクリプトの背後に配置し、CI とローカルの実行を一致させる:
|
||||
|
||||
```bash
|
||||
bundle exec rubocop
|
||||
bundle exec rubocop -A
|
||||
```
|
||||
|
||||
- 例外が狭く、文書化されており、コードで明確に表現するのが困難でない限り、インラインで cop を無効にしない。
|
||||
|
||||
## Rails スタイル
|
||||
|
||||
- カスタム構造を追加する前に、Rails の命名規則とディレクトリ規約に従う。
|
||||
- コントローラはトランスポートに焦点を当てる: 認証、認可、パラメータ処理、レスポンスの形状。
|
||||
- 再利用可能なドメインロジックは、デフォルトの儀式としてではなく、実際の複雑さに基づいてモデル、concerns、サービスオブジェクト、クエリオブジェクト、またはフォームオブジェクトに配置する。
|
||||
- グローバルにインストールされたコマンドよりも `bin/rails`、`bin/rake`、およびチェックイン済み binstub を優先する。
|
||||
|
||||
## エラーハンドリング
|
||||
|
||||
- 特定の例外を rescue する。広範な `rescue StandardError` ブロックは、再スローするか、運用者に十分なコンテキストを保持する場合を除いて避ける。
|
||||
- 運用イベントには `ActiveSupport::Notifications` またはアプリのロガーを使用する。コミット済みのアプリケーションコードに `puts`、`pp`、`debugger` を残さない。
|
||||
|
||||
## 参考
|
||||
|
||||
サービス/リポジトリの階層化ガイダンスについてはスキル: `backend-patterns` を参照。
|
||||
37
docs/ja-JP/rules/ruby/hooks.md
Normal file
37
docs/ja-JP/rules/ruby/hooks.md
Normal file
@@ -0,0 +1,37 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.rb"
|
||||
- "**/*.rake"
|
||||
- "**/Gemfile"
|
||||
- "**/Gemfile.lock"
|
||||
- "**/config/routes.rb"
|
||||
---
|
||||
# Ruby フック
|
||||
|
||||
> このファイルは [common/hooks.md](../common/hooks.md) を Ruby および Rails 固有のコンテンツで拡張します。
|
||||
|
||||
## PostToolUse フック
|
||||
|
||||
binstub とチェックイン済みツールを優先するようにプロジェクトローカルのフックを設定する:
|
||||
|
||||
- **RuboCop**: Ruby 編集後に `bundle exec rubocop -A <file>` またはプロジェクトのより安全なフォーマッタコマンドを実行する。
|
||||
- **Brakeman**: セキュリティに関わる Rails の変更後に `bundle exec brakeman --no-pager` を実行する。
|
||||
- **テスト**: 変更されたファイルに対して最も狭い範囲の `bin/rails test ...` または `bundle exec rspec ...` コマンドを実行する。
|
||||
- **Bundler audit**: `Gemfile` または `Gemfile.lock` が変更され、プロジェクトに bundler-audit がインストールされている場合、`bundle exec bundle-audit check --update` を実行する。
|
||||
|
||||
## 警告
|
||||
|
||||
- アプリケーションコードにコミットされた `debugger`、`binding.irb`、`binding.pry`、`puts`、`pp`、`p` の呼び出しに対して警告する。
|
||||
- 編集が CSRF 保護を無効にしたり、マスアサインメントを拡大したり、パラメータ化なしで生の SQL を追加した場合に警告する。
|
||||
- マイグレーションが可逆的なパスや文書化されたロールアウト計画なしにデータを破壊的に変更する場合に警告する。
|
||||
|
||||
## CI ゲートの提案
|
||||
|
||||
```bash
|
||||
bundle exec rubocop
|
||||
bundle exec brakeman --no-pager
|
||||
bin/rails test
|
||||
bundle exec rspec
|
||||
```
|
||||
|
||||
プロジェクトに存在するコマンドのみを使用する。メンテナーの承認なしに新しいフック依存関係をインストールしない。
|
||||
44
docs/ja-JP/rules/ruby/patterns.md
Normal file
44
docs/ja-JP/rules/ruby/patterns.md
Normal file
@@ -0,0 +1,44 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.rb"
|
||||
- "**/*.rake"
|
||||
- "**/Gemfile"
|
||||
- "**/app/**/*.erb"
|
||||
- "**/config/routes.rb"
|
||||
---
|
||||
# Ruby パターン
|
||||
|
||||
> このファイルは [common/patterns.md](../common/patterns.md) を Ruby および Rails 固有のコンテンツで拡張します。
|
||||
|
||||
## まず Rails Way
|
||||
|
||||
- 小規模および中規模の機能には、プレーンな Rails MVC と Active Record の規約から始める。
|
||||
- モデル/コントローラの境界が複数の責務を担っている場合に、サービスオブジェクト、クエリオブジェクト、フォームオブジェクト、デコレータ、またはプレゼンターを導入する。
|
||||
- 抽出したオブジェクトには `Manager` や `Processor` のような汎用的なレイヤー名ではなく、実行するビジネス操作にちなんだ名前を付ける。
|
||||
|
||||
## 永続化
|
||||
|
||||
- マルチホスト本番 Rails アプリでは、既存プラットフォームが MySQL や SQLite を使用する明確な理由がない限り PostgreSQL を優先する。
|
||||
- Rails 8 の SQLite ベースのデフォルトは、シングルホストまたは小規模なデプロイメントに適しているが、共有マルチサービスシステムに自動的に適合するわけではない。
|
||||
- 生の SQL はクエリオブジェクトまたはモデルスコープの背後に配置し、すべての動的値をパラメータ化する。
|
||||
|
||||
## バックグラウンドジョブとランタイムサービス
|
||||
|
||||
- グリーンフィールドの Rails 8 アプリで、適度なスループットとシンプルなデプロイメントが必要な場合は **Solid Queue** を使用する。
|
||||
- 成熟したオブザーバビリティ、高スループット、既存の Redis インフラストラクチャ、または Pro/Enterprise 機能が必要な場合は **Sidekiq** を使用する。
|
||||
- **Solid Cache** と **Solid Cable** はそのデプロイメントモデルがアプリに適合する場合に使用する。共有クロスサービス動作、高ファンアウト、または高度なデータ構造が重要な場合は Redis を使用する。
|
||||
|
||||
## フロントエンド
|
||||
|
||||
- サーバーレンダリングの Rails アプリには Turbo、Stimulus、Importmap、Propshaft を使用した **Hotwire** を優先する。
|
||||
- インタラクションの複雑さ、既存のプロダクトアーキテクチャ、またはチームのオーナーシップが追加のクライアントサーフェスを正当化する場合は、React、Vue、Inertia.js、または個別の SPA を使用する。
|
||||
- ビューコンポーネント、パーシャル、プレゼンターはレンダリングの判断に集中させる。永続化と認可をテンプレートに含めない。
|
||||
|
||||
## 認証
|
||||
|
||||
- シンプルなセッション認証とパスワードリセットのニーズには Rails 8 認証ジェネレータを使用する。
|
||||
- OAuth、MFA、confirmable/lockable フロー、マルチモデル認証、または大規模な既存 Devise フットプリントが要件に含まれる場合は Devise または他の確立された認証システムを使用する。
|
||||
|
||||
## 参考
|
||||
|
||||
サービス境界とアダプターパターンについてはスキル: `backend-patterns` を参照。
|
||||
51
docs/ja-JP/rules/ruby/security.md
Normal file
51
docs/ja-JP/rules/ruby/security.md
Normal file
@@ -0,0 +1,51 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.rb"
|
||||
- "**/*.rake"
|
||||
- "**/Gemfile"
|
||||
- "**/Gemfile.lock"
|
||||
- "**/config/routes.rb"
|
||||
- "**/config/credentials*.yml.enc"
|
||||
---
|
||||
# Ruby セキュリティ
|
||||
|
||||
> このファイルは [common/security.md](../common/security.md) を Ruby および Rails 固有のコンテンツで拡張します。
|
||||
|
||||
## Rails デフォルト
|
||||
|
||||
- 状態を変更するブラウザリクエストでは CSRF 保護を有効にしておく。
|
||||
- マスアサインメントの前に strong parameters または型付き境界オブジェクトを使用する。
|
||||
- シークレットは Rails credentials、環境変数、またはシークレットマネージャーに保存する。平文のキー、トークン、プライベート資格情報、またはコピーした `.env` 値をコミットしない。
|
||||
|
||||
## SQL と Active Record
|
||||
|
||||
- Active Record クエリ API とパラメータ化された SQL を優先する。
|
||||
- リクエスト、Cookie、ヘッダー、ジョブ、または Webhook の値を SQL 文字列に補間しない。
|
||||
- モデルコールバックのスコープを慎重に設定する。セキュリティに関わる副作用は明示的にし、テストでカバーする。
|
||||
|
||||
## 認証とセッション
|
||||
|
||||
- シンプルなセッション認証には Rails 8 認証ジェネレータを使用する。OAuth、MFA、confirmable、lockable、マルチモデル認証、または既存の Devise 規約が必要な場合は Devise を使用する。
|
||||
- サインインと権限変更後にセッションをローテーションする。
|
||||
- アカウント回復フローは有効期限、ワンタイムトークン、レート制限、および監査ログで保護する。
|
||||
|
||||
## 依存関係
|
||||
|
||||
- ロックファイルが変更された時に依存関係チェックを実行する:
|
||||
|
||||
```bash
|
||||
bundle audit check --update
|
||||
bundle exec brakeman --no-pager
|
||||
```
|
||||
|
||||
- 新しい gem については、メンテナーの活動状況、ネイティブ拡張のリスク、推移的依存関係、および Rails コアで同じ動作を実装できるかどうかを確認する。
|
||||
|
||||
## Web セーフティ
|
||||
|
||||
- デフォルトでテンプレート出力をエスケープする。`html_safe`、`raw`、およびカスタムサニタイザーはセキュリティに関わるコードとして扱う。
|
||||
- ファイルアップロードはコンテンツタイプ、拡張子、サイズ、および保存先で検証する。
|
||||
- バックグラウンドジョブ、Webhook、Action Cable メッセージ、および Turbo Stream 入力は信頼されない境界として扱う。
|
||||
|
||||
## 参考
|
||||
|
||||
セキュア・バイ・デフォルトのレビューパターンについてはスキル: `security-review` を参照。
|
||||
51
docs/ja-JP/rules/ruby/testing.md
Normal file
51
docs/ja-JP/rules/ruby/testing.md
Normal file
@@ -0,0 +1,51 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.rb"
|
||||
- "**/*.rake"
|
||||
- "**/Gemfile"
|
||||
- "**/test/**/*.rb"
|
||||
- "**/spec/**/*.rb"
|
||||
- "**/config/routes.rb"
|
||||
---
|
||||
# Ruby テスト
|
||||
|
||||
> このファイルは [common/testing.md](../common/testing.md) を Ruby および Rails 固有のコンテンツで拡張します。
|
||||
|
||||
## フレームワーク
|
||||
|
||||
- Rails アプリがデフォルトの Rails テストスタックに従っている場合は **Minitest** を使用する。
|
||||
- プロジェクトで既に確立されている場合、またはチームがそれに関する明確な本番規約を持っている場合は **RSpec** を使用する。
|
||||
- マイグレーションの理由なしに、同じ機能領域内で Minitest と RSpec を混在させない。
|
||||
|
||||
## テストピラミッド
|
||||
|
||||
- 高速なドメインロジックはモデル、サービス、クエリ、ポリシー、ジョブのテストに配置する。
|
||||
- HTTP コントラクト、認証動作、リダイレクト、ステータスコード、レスポンスの形状にはリクエスト/コントローラテストを使用する。
|
||||
- ブラウザ依存の重要なフローにのみ Capybara を使用したシステムテストを使用する。焦点を絞り、安定させる。
|
||||
- バックグラウンドジョブは動作のユニットテストとキュー/エンキューコントラクトの統合テストでカバーする。
|
||||
|
||||
## フィクスチャとファクトリ
|
||||
|
||||
- Rails フィクスチャがプロジェクトのデフォルトで、データグラフが小さい場合はそれを使用する。
|
||||
- シナリオが明示的なオブジェクト構築や複雑なトレイトを必要とする場合は `factory_bot` を使用する。
|
||||
- テストデータはアサートされる動作の近くに配置する。セットアップコストを隠すグローバルフィクスチャを避ける。
|
||||
|
||||
## コマンド
|
||||
|
||||
プロジェクトローカルのコマンドを優先する:
|
||||
|
||||
```bash
|
||||
bin/rails test
|
||||
bin/rails test test/models/user_test.rb
|
||||
bundle exec rspec
|
||||
bundle exec rspec spec/models/user_spec.rb
|
||||
```
|
||||
|
||||
## カバレッジ
|
||||
|
||||
- カバレッジが強制される場合は SimpleCov を使用する。しきい値は CI に設定し、低価値なテストでブランチカバレッジを水増ししない。
|
||||
- バグ修正では、本番コードを変更する前にリグレッションテストを追加する。
|
||||
|
||||
## 参考
|
||||
|
||||
リポジトリ全体の RED -> GREEN -> REFACTOR ループについてはスキル: `tdd-workflow` を参照。
|
||||
151
docs/ja-JP/rules/rust/coding-style.md
Normal file
151
docs/ja-JP/rules/rust/coding-style.md
Normal file
@@ -0,0 +1,151 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.rs"
|
||||
---
|
||||
# Rust コーディングスタイル
|
||||
|
||||
> このファイルは [common/coding-style.md](../common/coding-style.md) を Rust 固有のコンテンツで拡張します。
|
||||
|
||||
## フォーマット
|
||||
|
||||
- 強制には **rustfmt** を使用 — コミット前に必ず `cargo fmt` を実行する
|
||||
- リントには **clippy** を使用 — `cargo clippy -- -D warnings`(警告をエラーとして扱う)
|
||||
- 4スペースインデント(rustfmt デフォルト)
|
||||
- 最大行幅: 100文字(rustfmt デフォルト)
|
||||
|
||||
## 不変性
|
||||
|
||||
Rust の変数はデフォルトで不変 — これを活用する:
|
||||
|
||||
- デフォルトで `let` を使用する。ミューテーションが必要な場合にのみ `let mut` を使用する
|
||||
- その場でのミューテーションよりも新しい値を返すことを優先する
|
||||
- 関数が割り当てる必要があるかどうかわからない場合は `Cow<'_, T>` を使用する
|
||||
|
||||
```rust
|
||||
use std::borrow::Cow;
|
||||
|
||||
// 良い例 — デフォルトで不変、新しい値を返す
|
||||
fn normalize(input: &str) -> Cow<'_, str> {
|
||||
if input.contains(' ') {
|
||||
Cow::Owned(input.replace(' ', "_"))
|
||||
} else {
|
||||
Cow::Borrowed(input)
|
||||
}
|
||||
}
|
||||
|
||||
// 悪い例 — 不要なミューテーション
|
||||
fn normalize_bad(input: &mut String) {
|
||||
*input = input.replace(' ', "_");
|
||||
}
|
||||
```
|
||||
|
||||
## 命名
|
||||
|
||||
標準的な Rust の規約に従う:
|
||||
- 関数、メソッド、変数、モジュール、クレートには `snake_case`
|
||||
- 型、トレイト、列挙型、型パラメータには `PascalCase`(UpperCamelCase)
|
||||
- 定数とスタティックには `SCREAMING_SNAKE_CASE`
|
||||
- ライフタイム: 短い小文字(`'a`、`'de`)— 複雑な場合は説明的な名前(`'input`)
|
||||
|
||||
## 所有権と借用
|
||||
|
||||
- デフォルトで借用(`&T`)する。格納または消費する必要がある場合にのみ所有権を取得する
|
||||
- 根本原因を理解せずにボローチェッカーを満たすためにクローンしない
|
||||
- 関数パラメータでは `String` よりも `&str`、`Vec<T>` よりも `&[T]` を受け入れる
|
||||
- `String` を所有する必要があるコンストラクタには `impl Into<String>` を使用する
|
||||
|
||||
```rust
|
||||
// 良い例 — 所有権が不要な場合は借用する
|
||||
fn word_count(text: &str) -> usize {
|
||||
text.split_whitespace().count()
|
||||
}
|
||||
|
||||
// 良い例 — Into を使用してコンストラクタで所有権を取得する
|
||||
fn new(name: impl Into<String>) -> Self {
|
||||
Self { name: name.into() }
|
||||
}
|
||||
|
||||
// 悪い例 — &str で十分なのに String を取得する
|
||||
fn word_count_bad(text: String) -> usize {
|
||||
text.split_whitespace().count()
|
||||
}
|
||||
```
|
||||
|
||||
## エラーハンドリング
|
||||
|
||||
- 伝搬には `Result<T, E>` と `?` を使用する — 本番コードでは `unwrap()` を使わない
|
||||
- **ライブラリ**: `thiserror` で型付きエラーを定義する
|
||||
- **アプリケーション**: 柔軟なエラーコンテキストには `anyhow` を使用する
|
||||
- `.with_context(|| format!("failed to ..."))?` でコンテキストを追加する
|
||||
- `unwrap()` / `expect()` はテストと本当に到達不可能な状態にのみ使用する
|
||||
|
||||
```rust
|
||||
// 良い例 — thiserror によるライブラリエラー
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum ConfigError {
|
||||
#[error("failed to read config: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
#[error("invalid config format: {0}")]
|
||||
Parse(String),
|
||||
}
|
||||
|
||||
// 良い例 — anyhow によるアプリケーションエラー
|
||||
use anyhow::Context;
|
||||
|
||||
fn load_config(path: &str) -> anyhow::Result<Config> {
|
||||
let content = std::fs::read_to_string(path)
|
||||
.with_context(|| format!("failed to read {path}"))?;
|
||||
toml::from_str(&content)
|
||||
.with_context(|| format!("failed to parse {path}"))
|
||||
}
|
||||
```
|
||||
|
||||
## ループよりもイテレータ
|
||||
|
||||
変換にはイテレータチェーンを優先する。複雑な制御フローにはループを使用する:
|
||||
|
||||
```rust
|
||||
// 良い例 — 宣言的で合成可能
|
||||
let active_emails: Vec<&str> = users.iter()
|
||||
.filter(|u| u.is_active)
|
||||
.map(|u| u.email.as_str())
|
||||
.collect();
|
||||
|
||||
// 良い例 — 早期リターンを伴う複雑なロジックにはループ
|
||||
for user in &users {
|
||||
if let Some(verified) = verify_email(&user.email)? {
|
||||
send_welcome(&verified)?;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## モジュール構成
|
||||
|
||||
型ごとではなく、ドメインごとに整理する:
|
||||
|
||||
```text
|
||||
src/
|
||||
├── main.rs
|
||||
├── lib.rs
|
||||
├── auth/ # ドメインモジュール
|
||||
│ ├── mod.rs
|
||||
│ ├── token.rs
|
||||
│ └── middleware.rs
|
||||
├── orders/ # ドメインモジュール
|
||||
│ ├── mod.rs
|
||||
│ ├── model.rs
|
||||
│ └── service.rs
|
||||
└── db/ # インフラストラクチャ
|
||||
├── mod.rs
|
||||
└── pool.rs
|
||||
```
|
||||
|
||||
## 可視性
|
||||
|
||||
- デフォルトはプライベート。内部共有には `pub(crate)` を使用する
|
||||
- クレートのパブリック API の一部であるものだけに `pub` を付ける
|
||||
- `lib.rs` からパブリック API を再エクスポートする
|
||||
|
||||
## 参考
|
||||
|
||||
包括的な Rust のイディオムとパターンについてはスキル: `rust-patterns` を参照。
|
||||
16
docs/ja-JP/rules/rust/hooks.md
Normal file
16
docs/ja-JP/rules/rust/hooks.md
Normal file
@@ -0,0 +1,16 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.rs"
|
||||
- "**/Cargo.toml"
|
||||
---
|
||||
# Rust フック
|
||||
|
||||
> このファイルは [common/hooks.md](../common/hooks.md) を Rust 固有のコンテンツで拡張します。
|
||||
|
||||
## PostToolUse フック
|
||||
|
||||
`~/.claude/settings.json` で設定する:
|
||||
|
||||
- **cargo fmt**: `.rs` ファイルを編集後に自動フォーマットする
|
||||
- **cargo clippy**: Rust ファイルの編集後にリントチェックを実行する
|
||||
- **cargo check**: 変更後にコンパイルを検証する(`cargo build` よりも高速)
|
||||
168
docs/ja-JP/rules/rust/patterns.md
Normal file
168
docs/ja-JP/rules/rust/patterns.md
Normal file
@@ -0,0 +1,168 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.rs"
|
||||
---
|
||||
# Rust パターン
|
||||
|
||||
> このファイルは [common/patterns.md](../common/patterns.md) を Rust 固有のコンテンツで拡張します。
|
||||
|
||||
## トレイトを使ったリポジトリパターン
|
||||
|
||||
データアクセスをトレイトの背後にカプセル化する:
|
||||
|
||||
```rust
|
||||
pub trait OrderRepository: Send + Sync {
|
||||
fn find_by_id(&self, id: u64) -> Result<Option<Order>, StorageError>;
|
||||
fn find_all(&self) -> Result<Vec<Order>, StorageError>;
|
||||
fn save(&self, order: &Order) -> Result<Order, StorageError>;
|
||||
fn delete(&self, id: u64) -> Result<(), StorageError>;
|
||||
}
|
||||
```
|
||||
|
||||
具象実装がストレージの詳細を処理する(Postgres、SQLite、テスト用インメモリ)。
|
||||
|
||||
## サービスレイヤー
|
||||
|
||||
サービス構造体にビジネスロジックを配置する。コンストラクタ経由で依存関係を注入する:
|
||||
|
||||
```rust
|
||||
pub struct OrderService {
|
||||
repo: Box<dyn OrderRepository>,
|
||||
payment: Box<dyn PaymentGateway>,
|
||||
}
|
||||
|
||||
impl OrderService {
|
||||
pub fn new(repo: Box<dyn OrderRepository>, payment: Box<dyn PaymentGateway>) -> Self {
|
||||
Self { repo, payment }
|
||||
}
|
||||
|
||||
pub fn place_order(&self, request: CreateOrderRequest) -> anyhow::Result<OrderSummary> {
|
||||
let order = Order::from(request);
|
||||
self.payment.charge(order.total())?;
|
||||
let saved = self.repo.save(&order)?;
|
||||
Ok(OrderSummary::from(saved))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 型安全のための Newtype パターン
|
||||
|
||||
引数の取り違えを防ぐために個別のラッパー型を使用する:
|
||||
|
||||
```rust
|
||||
struct UserId(u64);
|
||||
struct OrderId(u64);
|
||||
|
||||
fn get_order(user: UserId, order: OrderId) -> anyhow::Result<Order> {
|
||||
// 呼び出し側で user と order の ID を誤って入れ替えることができない
|
||||
todo!()
|
||||
}
|
||||
```
|
||||
|
||||
## 列挙型ステートマシン
|
||||
|
||||
状態を列挙型としてモデリングする — 不正な状態を表現不可能にする:
|
||||
|
||||
```rust
|
||||
enum ConnectionState {
|
||||
Disconnected,
|
||||
Connecting { attempt: u32 },
|
||||
Connected { session_id: String },
|
||||
Failed { reason: String, retries: u32 },
|
||||
}
|
||||
|
||||
fn handle(state: &ConnectionState) {
|
||||
match state {
|
||||
ConnectionState::Disconnected => connect(),
|
||||
ConnectionState::Connecting { attempt } if *attempt > 3 => abort(),
|
||||
ConnectionState::Connecting { .. } => wait(),
|
||||
ConnectionState::Connected { session_id } => use_session(session_id),
|
||||
ConnectionState::Failed { retries, .. } if *retries < 5 => retry(),
|
||||
ConnectionState::Failed { reason, .. } => log_failure(reason),
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
ビジネス上重要な列挙型では常に網羅的にマッチする — ワイルドカード `_` は使わない。
|
||||
|
||||
## ビルダーパターン
|
||||
|
||||
多数のオプションパラメータを持つ構造体に使用する:
|
||||
|
||||
```rust
|
||||
pub struct ServerConfig {
|
||||
host: String,
|
||||
port: u16,
|
||||
max_connections: usize,
|
||||
}
|
||||
|
||||
impl ServerConfig {
|
||||
pub fn builder(host: impl Into<String>, port: u16) -> ServerConfigBuilder {
|
||||
ServerConfigBuilder {
|
||||
host: host.into(),
|
||||
port,
|
||||
max_connections: 100,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ServerConfigBuilder {
|
||||
host: String,
|
||||
port: u16,
|
||||
max_connections: usize,
|
||||
}
|
||||
|
||||
impl ServerConfigBuilder {
|
||||
pub fn max_connections(mut self, n: usize) -> Self {
|
||||
self.max_connections = n;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(self) -> ServerConfig {
|
||||
ServerConfig {
|
||||
host: self.host,
|
||||
port: self.port,
|
||||
max_connections: self.max_connections,
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 拡張性制御のための Sealed トレイト
|
||||
|
||||
プライベートモジュールを使用してトレイトをシールし、外部からの実装を防止する:
|
||||
|
||||
```rust
|
||||
mod private {
|
||||
pub trait Sealed {}
|
||||
}
|
||||
|
||||
pub trait Format: private::Sealed {
|
||||
fn encode(&self, data: &[u8]) -> Vec<u8>;
|
||||
}
|
||||
|
||||
pub struct Json;
|
||||
impl private::Sealed for Json {}
|
||||
impl Format for Json {
|
||||
fn encode(&self, data: &[u8]) -> Vec<u8> { todo!() }
|
||||
}
|
||||
```
|
||||
|
||||
## API レスポンスエンベロープ
|
||||
|
||||
ジェネリック列挙型を使用した一貫した API レスポンス:
|
||||
|
||||
```rust
|
||||
#[derive(Debug, serde::Serialize)]
|
||||
#[serde(tag = "status")]
|
||||
pub enum ApiResponse<T: serde::Serialize> {
|
||||
#[serde(rename = "ok")]
|
||||
Ok { data: T },
|
||||
#[serde(rename = "error")]
|
||||
Error { message: String },
|
||||
}
|
||||
```
|
||||
|
||||
## 参考
|
||||
|
||||
所有権、トレイト、ジェネリクス、並行性、非同期を含む包括的なパターンについてはスキル: `rust-patterns` を参照。
|
||||
141
docs/ja-JP/rules/rust/security.md
Normal file
141
docs/ja-JP/rules/rust/security.md
Normal file
@@ -0,0 +1,141 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.rs"
|
||||
---
|
||||
# Rust セキュリティ
|
||||
|
||||
> このファイルは [common/security.md](../common/security.md) を Rust 固有のコンテンツで拡張します。
|
||||
|
||||
## シークレット管理
|
||||
|
||||
- API キー、トークン、資格情報をソースコードにハードコードしない
|
||||
- 環境変数を使用する: `std::env::var("API_KEY")`
|
||||
- 必要なシークレットが起動時に欠落している場合は即座に失敗する
|
||||
- `.env` ファイルは `.gitignore` に含める
|
||||
|
||||
```rust
|
||||
// 悪い例
|
||||
const API_KEY: &str = "sk-abc123...";
|
||||
|
||||
// 良い例 — 早期バリデーション付きの環境変数
|
||||
fn load_api_key() -> anyhow::Result<String> {
|
||||
std::env::var("PAYMENT_API_KEY")
|
||||
.context("PAYMENT_API_KEY must be set")
|
||||
}
|
||||
```
|
||||
|
||||
## SQL インジェクション防止
|
||||
|
||||
- 常にパラメータ化クエリを使用する — ユーザー入力を SQL 文字列にフォーマットしない
|
||||
- バインドパラメータ付きのクエリビルダーまたは ORM(sqlx、diesel、sea-orm)を使用する
|
||||
|
||||
```rust
|
||||
// 悪い例 — フォーマット文字列による SQL インジェクション
|
||||
let query = format!("SELECT * FROM users WHERE name = '{name}'");
|
||||
sqlx::query(&query).fetch_one(&pool).await?;
|
||||
|
||||
// 良い例 — sqlx によるパラメータ化クエリ
|
||||
// プレースホルダ構文はバックエンドにより異なる: Postgres: $1 | MySQL: ? | SQLite: $1
|
||||
sqlx::query("SELECT * FROM users WHERE name = $1")
|
||||
.bind(&name)
|
||||
.fetch_one(&pool)
|
||||
.await?;
|
||||
```
|
||||
|
||||
## 入力バリデーション
|
||||
|
||||
- 処理前にシステム境界ですべてのユーザー入力を検証する
|
||||
- 型システムを使用して不変条件を強制する(newtype パターン)
|
||||
- バリデーションではなくパースする — 境界で非構造化データを型付き構造体に変換する
|
||||
- 無効な入力は明確なエラーメッセージで拒否する
|
||||
|
||||
```rust
|
||||
// バリデーションではなくパースする — 無効な状態は表現不可能
|
||||
pub struct Email(String);
|
||||
|
||||
impl Email {
|
||||
pub fn parse(input: &str) -> Result<Self, ValidationError> {
|
||||
let trimmed = input.trim();
|
||||
let at_pos = trimmed.find('@')
|
||||
.filter(|&p| p > 0 && p < trimmed.len() - 1)
|
||||
.ok_or_else(|| ValidationError::InvalidEmail(input.to_string()))?;
|
||||
let domain = &trimmed[at_pos + 1..];
|
||||
if trimmed.len() > 254 || !domain.contains('.') {
|
||||
return Err(ValidationError::InvalidEmail(input.to_string()));
|
||||
}
|
||||
// 本番環境では、バリデーション済みメールクレート(例: `email_address`)の使用を推奨
|
||||
Ok(Self(trimmed.to_string()))
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## アンセーフコード
|
||||
|
||||
- `unsafe` ブロックを最小限にする — 安全な抽象化を優先する
|
||||
- すべての `unsafe` ブロックには不変条件を説明する `// SAFETY:` コメントが必要
|
||||
- 利便性のためにボローチェッカーを迂回するために `unsafe` を使用しない
|
||||
- レビュー時にすべての `unsafe` コードを監査する — 正当な理由なしに使用するのは危険信号である
|
||||
- C ライブラリには `safe` な FFI ラッパーを優先する
|
||||
|
||||
```rust
|
||||
// 良い例 — safety コメントが必要なすべての不変条件を文書化
|
||||
let widget: &Widget = {
|
||||
// SAFETY: `ptr` は non-null、アライン済み、初期化された Widget を指し、
|
||||
// そのライフタイム中にミュータブル参照やミューテーションは存在しない。
|
||||
unsafe { &*ptr }
|
||||
};
|
||||
|
||||
// 悪い例 — safety の正当化がない
|
||||
unsafe { &*ptr }
|
||||
```
|
||||
|
||||
## 依存関係のセキュリティ
|
||||
|
||||
- `cargo audit` を実行して依存関係の既知の CVE をスキャンする
|
||||
- `cargo deny check` でライセンスとアドバイザリのコンプライアンスを確認する
|
||||
- `cargo tree` で推移的依存関係を監査する
|
||||
- 依存関係を最新に保つ — Dependabot または Renovate を設定する
|
||||
- 依存関係数を最小限にする — 新しいクレートを追加する前に評価する
|
||||
|
||||
```bash
|
||||
# セキュリティ監査
|
||||
cargo audit
|
||||
|
||||
# アドバイザリ、重複バージョン、制限付きライセンスの拒否
|
||||
cargo deny check
|
||||
|
||||
# 依存関係ツリーの検査
|
||||
cargo tree
|
||||
cargo tree -d # 重複のみ表示
|
||||
```
|
||||
|
||||
## エラーメッセージ
|
||||
|
||||
- API レスポンスに内部パス、スタックトレース、データベースエラーを公開しない
|
||||
- 詳細なエラーはサーバー側でログに記録する。クライアントには汎用メッセージを返す
|
||||
- 構造化されたサーバー側ロギングには `tracing` または `log` を使用する
|
||||
|
||||
```rust
|
||||
// エラーを適切なステータスコードと汎用メッセージにマッピングする
|
||||
// (例では axum を使用。レスポンス型はフレームワークに合わせて調整する)
|
||||
match order_service.find_by_id(id) {
|
||||
Ok(order) => Ok((StatusCode::OK, Json(order))),
|
||||
Err(ServiceError::NotFound(_)) => {
|
||||
tracing::info!(order_id = id, "order not found");
|
||||
Err((StatusCode::NOT_FOUND, "Resource not found"))
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!(order_id = id, error = %e, "unexpected error");
|
||||
Err((StatusCode::INTERNAL_SERVER_ERROR, "Internal server error"))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 参考
|
||||
|
||||
アンセーフコードのガイドラインと所有権パターンについてはスキル: `rust-patterns` を参照。
|
||||
一般的なセキュリティチェックリストについてはスキル: `security-review` を参照。
|
||||
154
docs/ja-JP/rules/rust/testing.md
Normal file
154
docs/ja-JP/rules/rust/testing.md
Normal file
@@ -0,0 +1,154 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.rs"
|
||||
---
|
||||
# Rust テスト
|
||||
|
||||
> このファイルは [common/testing.md](../common/testing.md) を Rust 固有のコンテンツで拡張します。
|
||||
|
||||
## テストフレームワーク
|
||||
|
||||
- ユニットテストには `#[cfg(test)]` モジュール内の **`#[test]`** を使用する
|
||||
- パラメータ化テストとフィクスチャには **rstest** を使用する
|
||||
- プロパティベーステストには **proptest** を使用する
|
||||
- トレイトベースのモッキングには **mockall** を使用する
|
||||
- 非同期テストには **`#[tokio::test]`** を使用する
|
||||
|
||||
## テストの構成
|
||||
|
||||
```text
|
||||
my_crate/
|
||||
├── src/
|
||||
│ ├── lib.rs # #[cfg(test)] モジュール内のユニットテスト
|
||||
│ ├── auth/
|
||||
│ │ └── mod.rs # #[cfg(test)] mod tests { ... }
|
||||
│ └── orders/
|
||||
│ └── service.rs # #[cfg(test)] mod tests { ... }
|
||||
├── tests/ # 統合テスト(各ファイル = 個別のバイナリ)
|
||||
│ ├── api_test.rs
|
||||
│ ├── db_test.rs
|
||||
│ └── common/ # 共有テストユーティリティ
|
||||
│ └── mod.rs
|
||||
└── benches/ # Criterion ベンチマーク
|
||||
└── benchmark.rs
|
||||
```
|
||||
|
||||
ユニットテストは同じファイル内の `#[cfg(test)]` モジュールに配置する。統合テストは `tests/` に配置する。
|
||||
|
||||
## ユニットテストのパターン
|
||||
|
||||
```rust
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn creates_user_with_valid_email() {
|
||||
let user = User::new("Alice", "alice@example.com").unwrap();
|
||||
assert_eq!(user.name, "Alice");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_invalid_email() {
|
||||
let result = User::new("Bob", "not-an-email");
|
||||
assert!(result.is_err());
|
||||
assert!(result.unwrap_err().to_string().contains("invalid email"));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## パラメータ化テスト
|
||||
|
||||
```rust
|
||||
use rstest::rstest;
|
||||
|
||||
#[rstest]
|
||||
#[case("hello", 5)]
|
||||
#[case("", 0)]
|
||||
#[case("rust", 4)]
|
||||
fn test_string_length(#[case] input: &str, #[case] expected: usize) {
|
||||
assert_eq!(input.len(), expected);
|
||||
}
|
||||
```
|
||||
|
||||
## 非同期テスト
|
||||
|
||||
```rust
|
||||
#[tokio::test]
|
||||
async fn fetches_data_successfully() {
|
||||
let client = TestClient::new().await;
|
||||
let result = client.get("/data").await;
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
```
|
||||
|
||||
## mockall によるモッキング
|
||||
|
||||
本番コードでトレイトを定義し、テストモジュールでモックを生成する:
|
||||
|
||||
```rust
|
||||
// 本番トレイト — 統合テストがインポートできるように pub にする
|
||||
pub trait UserRepository {
|
||||
fn find_by_id(&self, id: u64) -> Option<User>;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use mockall::predicate::eq;
|
||||
|
||||
mockall::mock! {
|
||||
pub Repo {}
|
||||
impl UserRepository for Repo {
|
||||
fn find_by_id(&self, id: u64) -> Option<User>;
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn service_returns_user_when_found() {
|
||||
let mut mock = MockRepo::new();
|
||||
mock.expect_find_by_id()
|
||||
.with(eq(42))
|
||||
.times(1)
|
||||
.returning(|_| Some(User { id: 42, name: "Alice".into() }));
|
||||
|
||||
let service = UserService::new(Box::new(mock));
|
||||
let user = service.get_user(42).unwrap();
|
||||
assert_eq!(user.name, "Alice");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## テストの命名
|
||||
|
||||
シナリオを説明する記述的な名前を使用する:
|
||||
- `creates_user_with_valid_email()`
|
||||
- `rejects_order_when_insufficient_stock()`
|
||||
- `returns_none_when_not_found()`
|
||||
|
||||
## カバレッジ
|
||||
|
||||
- 80%以上の行カバレッジを目標にする
|
||||
- カバレッジレポートには **cargo-llvm-cov** を使用する
|
||||
- ビジネスロジックに集中する — 生成コードと FFI バインディングは除外する
|
||||
|
||||
```bash
|
||||
cargo llvm-cov # サマリー
|
||||
cargo llvm-cov --html # HTML レポート
|
||||
cargo llvm-cov --fail-under-lines 80 # しきい値以下で失敗
|
||||
```
|
||||
|
||||
## テストコマンド
|
||||
|
||||
```bash
|
||||
cargo test # すべてのテストを実行
|
||||
cargo test -- --nocapture # println 出力を表示
|
||||
cargo test test_name # パターンに一致するテストを実行
|
||||
cargo test --lib # ユニットテストのみ
|
||||
cargo test --test api_test # 特定の統合テスト(tests/api_test.rs)
|
||||
cargo test --doc # ドキュメントテストのみ
|
||||
```
|
||||
|
||||
## 参考
|
||||
|
||||
プロパティベーステスト、フィクスチャ、Criterion によるベンチマークを含む包括的なテストパターンについてはスキル: `rust-testing` を参照。
|
||||
47
docs/ja-JP/rules/swift/coding-style.md
Normal file
47
docs/ja-JP/rules/swift/coding-style.md
Normal file
@@ -0,0 +1,47 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.swift"
|
||||
- "**/Package.swift"
|
||||
---
|
||||
# Swift コーディングスタイル
|
||||
|
||||
> このファイルは [common/coding-style.md](../common/coding-style.md) を Swift 固有のコンテンツで拡張します。
|
||||
|
||||
## フォーマット
|
||||
|
||||
- 自動フォーマットには **SwiftFormat**、スタイル強制には **SwiftLint** を使用する
|
||||
- Xcode 16+ には代替として `swift-format` がバンドルされている
|
||||
|
||||
## 不変性
|
||||
|
||||
- `var` よりも `let` を優先する — すべてを `let` で定義し、コンパイラが要求する場合にのみ `var` に変更する
|
||||
- デフォルトで値セマンティクスの `struct` を使用する。同一性や参照セマンティクスが必要な場合にのみ `class` を使用する
|
||||
|
||||
## 命名
|
||||
|
||||
[Apple API デザインガイドライン](https://www.swift.org/documentation/api-design-guidelines/) に従う:
|
||||
|
||||
- 使用箇所での明確さ — 不要な単語を省く
|
||||
- メソッドとプロパティは型ではなく役割にちなんだ名前を付ける
|
||||
- グローバル定数よりも `static let` を定数に使用する
|
||||
|
||||
## エラーハンドリング
|
||||
|
||||
型付き throws(Swift 6+)とパターンマッチングを使用する:
|
||||
|
||||
```swift
|
||||
func load(id: String) throws(LoadError) -> Item {
|
||||
guard let data = try? read(from: path) else {
|
||||
throw .fileNotFound(id)
|
||||
}
|
||||
return try decode(data)
|
||||
}
|
||||
```
|
||||
|
||||
## 並行性
|
||||
|
||||
Swift 6 の厳格な並行性チェックを有効にする。以下を優先する:
|
||||
|
||||
- 隔離境界をまたぐデータには `Sendable` 値型
|
||||
- 共有ミュータブル状態にはアクター
|
||||
- 非構造化 `Task {}` よりも構造化された並行性(`async let`、`TaskGroup`)
|
||||
20
docs/ja-JP/rules/swift/hooks.md
Normal file
20
docs/ja-JP/rules/swift/hooks.md
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.swift"
|
||||
- "**/Package.swift"
|
||||
---
|
||||
# Swift フック
|
||||
|
||||
> このファイルは [common/hooks.md](../common/hooks.md) を Swift 固有のコンテンツで拡張します。
|
||||
|
||||
## PostToolUse フック
|
||||
|
||||
`~/.claude/settings.json` で設定する:
|
||||
|
||||
- **SwiftFormat**: `.swift` ファイルを編集後に自動フォーマットする
|
||||
- **SwiftLint**: `.swift` ファイルの編集後にリントチェックを実行する
|
||||
- **swift build**: 編集後に変更されたパッケージを型チェックする
|
||||
|
||||
## 警告
|
||||
|
||||
`print()` 文にフラグを立てる — 本番コードでは代わりに `os.Logger` または構造化ロギングを使用する。
|
||||
66
docs/ja-JP/rules/swift/patterns.md
Normal file
66
docs/ja-JP/rules/swift/patterns.md
Normal file
@@ -0,0 +1,66 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.swift"
|
||||
- "**/Package.swift"
|
||||
---
|
||||
# Swift パターン
|
||||
|
||||
> このファイルは [common/patterns.md](../common/patterns.md) を Swift 固有のコンテンツで拡張します。
|
||||
|
||||
## プロトコル指向設計
|
||||
|
||||
小さく焦点を絞ったプロトコルを定義する。共有デフォルトにはプロトコル拡張を使用する:
|
||||
|
||||
```swift
|
||||
protocol Repository: Sendable {
|
||||
associatedtype Item: Identifiable & Sendable
|
||||
func find(by id: Item.ID) async throws -> Item?
|
||||
func save(_ item: Item) async throws
|
||||
}
|
||||
```
|
||||
|
||||
## 値型
|
||||
|
||||
- データ転送オブジェクトとモデルには構造体を使用する
|
||||
- 異なる状態をモデリングするには関連値付きの列挙型を使用する:
|
||||
|
||||
```swift
|
||||
enum LoadState<T: Sendable>: Sendable {
|
||||
case idle
|
||||
case loading
|
||||
case loaded(T)
|
||||
case failed(Error)
|
||||
}
|
||||
```
|
||||
|
||||
## アクターパターン
|
||||
|
||||
ロックやディスパッチキューの代わりに、共有ミュータブル状態にはアクターを使用する:
|
||||
|
||||
```swift
|
||||
actor Cache<Key: Hashable & Sendable, Value: Sendable> {
|
||||
private var storage: [Key: Value] = [:]
|
||||
|
||||
func get(_ key: Key) -> Value? { storage[key] }
|
||||
func set(_ key: Key, value: Value) { storage[key] = value }
|
||||
}
|
||||
```
|
||||
|
||||
## 依存性注入
|
||||
|
||||
デフォルトパラメータ付きでプロトコルを注入する — 本番ではデフォルトを使用し、テストではモックを注入する:
|
||||
|
||||
```swift
|
||||
struct UserService {
|
||||
private let repository: any UserRepository
|
||||
|
||||
init(repository: any UserRepository = DefaultUserRepository()) {
|
||||
self.repository = repository
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 参考
|
||||
|
||||
アクターベースの永続化パターンについてはスキル: `swift-actor-persistence` を参照。
|
||||
プロトコルベースの DI とテストについてはスキル: `swift-protocol-di-testing` を参照。
|
||||
33
docs/ja-JP/rules/swift/security.md
Normal file
33
docs/ja-JP/rules/swift/security.md
Normal file
@@ -0,0 +1,33 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.swift"
|
||||
- "**/Package.swift"
|
||||
---
|
||||
# Swift セキュリティ
|
||||
|
||||
> このファイルは [common/security.md](../common/security.md) を Swift 固有のコンテンツで拡張します。
|
||||
|
||||
## シークレット管理
|
||||
|
||||
- 機密データ(トークン、パスワード、キー)には **Keychain Services** を使用する — `UserDefaults` は使わない
|
||||
- ビルド時のシークレットには環境変数または `.xcconfig` ファイルを使用する
|
||||
- ソースにシークレットをハードコードしない — 逆コンパイルツールで容易に抽出される
|
||||
|
||||
```swift
|
||||
let apiKey = ProcessInfo.processInfo.environment["API_KEY"]
|
||||
guard let apiKey, !apiKey.isEmpty else {
|
||||
fatalError("API_KEY not configured")
|
||||
}
|
||||
```
|
||||
|
||||
## トランスポートセキュリティ
|
||||
|
||||
- App Transport Security(ATS)はデフォルトで強制される — 無効にしない
|
||||
- 重要なエンドポイントには証明書ピンニングを使用する
|
||||
- すべてのサーバー証明書を検証する
|
||||
|
||||
## 入力バリデーション
|
||||
|
||||
- 表示前にすべてのユーザー入力をサニタイズしてインジェクションを防止する
|
||||
- 強制アンラップではなく、バリデーション付きの `URL(string:)` を使用する
|
||||
- 外部ソース(API、ディープリンク、ペーストボード)からのデータは処理前に検証する
|
||||
45
docs/ja-JP/rules/swift/testing.md
Normal file
45
docs/ja-JP/rules/swift/testing.md
Normal file
@@ -0,0 +1,45 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.swift"
|
||||
- "**/Package.swift"
|
||||
---
|
||||
# Swift テスト
|
||||
|
||||
> このファイルは [common/testing.md](../common/testing.md) を Swift 固有のコンテンツで拡張します。
|
||||
|
||||
## フレームワーク
|
||||
|
||||
新しいテストには **Swift Testing**(`import Testing`)を使用する。`@Test` と `#expect` を使用する:
|
||||
|
||||
```swift
|
||||
@Test("User creation validates email")
|
||||
func userCreationValidatesEmail() throws {
|
||||
#expect(throws: ValidationError.invalidEmail) {
|
||||
try User(email: "not-an-email")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## テストの分離
|
||||
|
||||
各テストは新しいインスタンスを取得する — `init` でセットアップし、`deinit` でティアダウンする。テスト間で共有ミュータブル状態を持たない。
|
||||
|
||||
## パラメータ化テスト
|
||||
|
||||
```swift
|
||||
@Test("Validates formats", arguments: ["json", "xml", "csv"])
|
||||
func validatesFormat(format: String) throws {
|
||||
let parser = try Parser(format: format)
|
||||
#expect(parser.isValid)
|
||||
}
|
||||
```
|
||||
|
||||
## カバレッジ
|
||||
|
||||
```bash
|
||||
swift test --enable-code-coverage
|
||||
```
|
||||
|
||||
## 参考
|
||||
|
||||
プロトコルベースの依存性注入と Swift Testing によるモックパターンについてはスキル: `swift-protocol-di-testing` を参照。
|
||||
199
docs/ja-JP/rules/typescript/coding-style.md
Normal file
199
docs/ja-JP/rules/typescript/coding-style.md
Normal file
@@ -0,0 +1,199 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.ts"
|
||||
- "**/*.tsx"
|
||||
- "**/*.js"
|
||||
- "**/*.jsx"
|
||||
---
|
||||
# TypeScript/JavaScript コーディングスタイル
|
||||
|
||||
> このファイルは [common/coding-style.md](../common/coding-style.md) を TypeScript/JavaScript 固有のコンテンツで拡張します。
|
||||
|
||||
## 型とインターフェース
|
||||
|
||||
パブリック API、共有モデル、コンポーネント props を明示的、可読的、再利用可能にするために型を使用する。
|
||||
|
||||
### パブリック API
|
||||
|
||||
- エクスポートされる関数、共有ユーティリティ、パブリッククラスメソッドにパラメータ型と戻り値型を追加する
|
||||
- 明白なローカル変数の型は TypeScript に推論させる
|
||||
- 繰り返されるインラインオブジェクトシェイプは名前付き型またはインターフェースに抽出する
|
||||
|
||||
```typescript
|
||||
// 間違い: 明示的な型のないエクスポート関数
|
||||
export function formatUser(user) {
|
||||
return `${user.firstName} ${user.lastName}`
|
||||
}
|
||||
|
||||
// 正しい: パブリック API での明示的な型
|
||||
interface User {
|
||||
firstName: string
|
||||
lastName: string
|
||||
}
|
||||
|
||||
export function formatUser(user: User): string {
|
||||
return `${user.firstName} ${user.lastName}`
|
||||
}
|
||||
```
|
||||
|
||||
### インターフェース vs 型エイリアス
|
||||
|
||||
- 拡張または実装される可能性のあるオブジェクトシェイプには `interface` を使用する
|
||||
- ユニオン、インターセクション、タプル、マップ型、ユーティリティ型には `type` を使用する
|
||||
- 相互運用性のために `enum` が必要でない限り、`enum` よりも文字列リテラルユニオンを優先する
|
||||
|
||||
```typescript
|
||||
interface User {
|
||||
id: string
|
||||
email: string
|
||||
}
|
||||
|
||||
type UserRole = 'admin' | 'member'
|
||||
type UserWithRole = User & {
|
||||
role: UserRole
|
||||
}
|
||||
```
|
||||
|
||||
### `any` の回避
|
||||
|
||||
- アプリケーションコードで `any` を避ける
|
||||
- 外部または信頼されない入力には `unknown` を使用し、安全にナローイングする
|
||||
- 値の型が呼び出し側に依存する場合はジェネリクスを使用する
|
||||
|
||||
```typescript
|
||||
// 間違い: any は型安全性を除去する
|
||||
function getErrorMessage(error: any) {
|
||||
return error.message
|
||||
}
|
||||
|
||||
// 正しい: unknown は安全なナローイングを強制する
|
||||
function getErrorMessage(error: unknown): string {
|
||||
if (error instanceof Error) {
|
||||
return error.message
|
||||
}
|
||||
|
||||
return 'Unexpected error'
|
||||
}
|
||||
```
|
||||
|
||||
### React Props
|
||||
|
||||
- コンポーネント props は名前付き `interface` または `type` で定義する
|
||||
- コールバック props は明示的に型付けする
|
||||
- 特定の理由がない限り `React.FC` を使用しない
|
||||
|
||||
```typescript
|
||||
interface User {
|
||||
id: string
|
||||
email: string
|
||||
}
|
||||
|
||||
interface UserCardProps {
|
||||
user: User
|
||||
onSelect: (id: string) => void
|
||||
}
|
||||
|
||||
function UserCard({ user, onSelect }: UserCardProps) {
|
||||
return <button onClick={() => onSelect(user.id)}>{user.email}</button>
|
||||
}
|
||||
```
|
||||
|
||||
### JavaScript ファイル
|
||||
|
||||
- `.js` および `.jsx` ファイルでは、型が明確さを向上させ TypeScript への移行が実用的でない場合に JSDoc を使用する
|
||||
- JSDoc をランタイム動作と整合させる
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* @param {{ firstName: string, lastName: string }} user
|
||||
* @returns {string}
|
||||
*/
|
||||
export function formatUser(user) {
|
||||
return `${user.firstName} ${user.lastName}`
|
||||
}
|
||||
```
|
||||
|
||||
## 不変性
|
||||
|
||||
不変な更新にはスプレッド演算子を使用する:
|
||||
|
||||
```typescript
|
||||
interface User {
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
|
||||
// 間違い: ミューテーション
|
||||
function updateUser(user: User, name: string): User {
|
||||
user.name = name // ミューテーション!
|
||||
return user
|
||||
}
|
||||
|
||||
// 正しい: 不変性
|
||||
function updateUser(user: Readonly<User>, name: string): User {
|
||||
return {
|
||||
...user,
|
||||
name
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## エラーハンドリング
|
||||
|
||||
async/await と try-catch を使用し、unknown エラーを安全にナローイングする:
|
||||
|
||||
```typescript
|
||||
interface User {
|
||||
id: string
|
||||
email: string
|
||||
}
|
||||
|
||||
declare function riskyOperation(userId: string): Promise<User>
|
||||
|
||||
function getErrorMessage(error: unknown): string {
|
||||
if (error instanceof Error) {
|
||||
return error.message
|
||||
}
|
||||
|
||||
return 'Unexpected error'
|
||||
}
|
||||
|
||||
const logger = {
|
||||
error: (message: string, error: unknown) => {
|
||||
// 本番用ロガー(例: pino や winston)に置き換えてください。
|
||||
}
|
||||
}
|
||||
|
||||
async function loadUser(userId: string): Promise<User> {
|
||||
try {
|
||||
const result = await riskyOperation(userId)
|
||||
return result
|
||||
} catch (error: unknown) {
|
||||
logger.error('Operation failed', error)
|
||||
throw new Error(getErrorMessage(error))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 入力バリデーション
|
||||
|
||||
スキーマベースのバリデーションには Zod を使用し、スキーマから型を推論する:
|
||||
|
||||
```typescript
|
||||
import { z } from 'zod'
|
||||
|
||||
const userSchema = z.object({
|
||||
email: z.string().email(),
|
||||
age: z.number().int().min(0).max(150)
|
||||
})
|
||||
|
||||
type UserInput = z.infer<typeof userSchema>
|
||||
|
||||
const validated: UserInput = userSchema.parse(input)
|
||||
```
|
||||
|
||||
## Console.log
|
||||
|
||||
- 本番コードに `console.log` 文を残さない
|
||||
- 代わりに適切なロギングライブラリを使用する
|
||||
- 自動検出についてはフックを参照
|
||||
22
docs/ja-JP/rules/typescript/hooks.md
Normal file
22
docs/ja-JP/rules/typescript/hooks.md
Normal file
@@ -0,0 +1,22 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.ts"
|
||||
- "**/*.tsx"
|
||||
- "**/*.js"
|
||||
- "**/*.jsx"
|
||||
---
|
||||
# TypeScript/JavaScript フック
|
||||
|
||||
> このファイルは [common/hooks.md](../common/hooks.md) を TypeScript/JavaScript 固有のコンテンツで拡張します。
|
||||
|
||||
## PostToolUse フック
|
||||
|
||||
`~/.claude/settings.json` で設定する:
|
||||
|
||||
- **Prettier**: JS/TS ファイルを編集後に自動フォーマットする
|
||||
- **TypeScript チェック**: `.ts`/`.tsx` ファイルの編集後に `tsc` を実行する
|
||||
- **console.log 警告**: 編集されたファイルの `console.log` について警告する
|
||||
|
||||
## Stop フック
|
||||
|
||||
- **console.log 監査**: セッション終了前にすべての変更されたファイルで `console.log` をチェックする
|
||||
52
docs/ja-JP/rules/typescript/patterns.md
Normal file
52
docs/ja-JP/rules/typescript/patterns.md
Normal file
@@ -0,0 +1,52 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.ts"
|
||||
- "**/*.tsx"
|
||||
- "**/*.js"
|
||||
- "**/*.jsx"
|
||||
---
|
||||
# TypeScript/JavaScript パターン
|
||||
|
||||
> このファイルは [common/patterns.md](../common/patterns.md) を TypeScript/JavaScript 固有のコンテンツで拡張します。
|
||||
|
||||
## API レスポンスフォーマット
|
||||
|
||||
```typescript
|
||||
interface ApiResponse<T> {
|
||||
success: boolean
|
||||
data?: T
|
||||
error?: string
|
||||
meta?: {
|
||||
total: number
|
||||
page: number
|
||||
limit: number
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## カスタムフックパターン
|
||||
|
||||
```typescript
|
||||
export function useDebounce<T>(value: T, delay: number): T {
|
||||
const [debouncedValue, setDebouncedValue] = useState<T>(value)
|
||||
|
||||
useEffect(() => {
|
||||
const handler = setTimeout(() => setDebouncedValue(value), delay)
|
||||
return () => clearTimeout(handler)
|
||||
}, [value, delay])
|
||||
|
||||
return debouncedValue
|
||||
}
|
||||
```
|
||||
|
||||
## リポジトリパターン
|
||||
|
||||
```typescript
|
||||
interface Repository<T> {
|
||||
findAll(filters?: Filters): Promise<T[]>
|
||||
findById(id: string): Promise<T | null>
|
||||
create(data: CreateDto): Promise<T>
|
||||
update(id: string, data: UpdateDto): Promise<T>
|
||||
delete(id: string): Promise<void>
|
||||
}
|
||||
```
|
||||
28
docs/ja-JP/rules/typescript/security.md
Normal file
28
docs/ja-JP/rules/typescript/security.md
Normal file
@@ -0,0 +1,28 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.ts"
|
||||
- "**/*.tsx"
|
||||
- "**/*.js"
|
||||
- "**/*.jsx"
|
||||
---
|
||||
# TypeScript/JavaScript セキュリティ
|
||||
|
||||
> このファイルは [common/security.md](../common/security.md) を TypeScript/JavaScript 固有のコンテンツで拡張します。
|
||||
|
||||
## シークレット管理
|
||||
|
||||
```typescript
|
||||
// 絶対にダメ: ハードコードされたシークレット
|
||||
const apiKey = "sk-proj-xxxxx"
|
||||
|
||||
// 常に: 環境変数
|
||||
const apiKey = process.env.OPENAI_API_KEY
|
||||
|
||||
if (!apiKey) {
|
||||
throw new Error('OPENAI_API_KEY not configured')
|
||||
}
|
||||
```
|
||||
|
||||
## エージェントサポート
|
||||
|
||||
- 包括的なセキュリティ監査には **security-reviewer** スキルを使用する
|
||||
18
docs/ja-JP/rules/typescript/testing.md
Normal file
18
docs/ja-JP/rules/typescript/testing.md
Normal file
@@ -0,0 +1,18 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.ts"
|
||||
- "**/*.tsx"
|
||||
- "**/*.js"
|
||||
- "**/*.jsx"
|
||||
---
|
||||
# TypeScript/JavaScript テスト
|
||||
|
||||
> このファイルは [common/testing.md](../common/testing.md) を TypeScript/JavaScript 固有のコンテンツで拡張します。
|
||||
|
||||
## E2E テスト
|
||||
|
||||
重要なユーザーフローの E2E テストフレームワークとして **Playwright** を使用する。
|
||||
|
||||
## エージェントサポート
|
||||
|
||||
- **e2e-runner** - Playwright E2E テストスペシャリスト
|
||||
96
docs/ja-JP/rules/web/coding-style.md
Normal file
96
docs/ja-JP/rules/web/coding-style.md
Normal file
@@ -0,0 +1,96 @@
|
||||
> このファイルは [common/coding-style.md](../common/coding-style.md) を Web 固有のフロントエンドコンテンツで拡張します。
|
||||
|
||||
# Web コーディングスタイル
|
||||
|
||||
## ファイル構成
|
||||
|
||||
ファイルタイプではなく、機能またはサーフェスエリアごとに整理する:
|
||||
|
||||
```text
|
||||
src/
|
||||
├── components/
|
||||
│ ├── hero/
|
||||
│ │ ├── Hero.tsx
|
||||
│ │ ├── HeroVisual.tsx
|
||||
│ │ └── hero.css
|
||||
│ ├── scrolly-section/
|
||||
│ │ ├── ScrollySection.tsx
|
||||
│ │ ├── StickyVisual.tsx
|
||||
│ │ └── scrolly.css
|
||||
│ └── ui/
|
||||
│ ├── Button.tsx
|
||||
│ ├── SurfaceCard.tsx
|
||||
│ └── AnimatedText.tsx
|
||||
├── hooks/
|
||||
│ ├── useReducedMotion.ts
|
||||
│ └── useScrollProgress.ts
|
||||
├── lib/
|
||||
│ ├── animation.ts
|
||||
│ └── color.ts
|
||||
└── styles/
|
||||
├── tokens.css
|
||||
├── typography.css
|
||||
└── global.css
|
||||
```
|
||||
|
||||
## CSS カスタムプロパティ
|
||||
|
||||
デザイントークンを変数として定義する。パレット、タイポグラフィ、スペーシングを繰り返しハードコードしない:
|
||||
|
||||
```css
|
||||
:root {
|
||||
--color-surface: oklch(98% 0 0);
|
||||
--color-text: oklch(18% 0 0);
|
||||
--color-accent: oklch(68% 0.21 250);
|
||||
|
||||
--text-base: clamp(1rem, 0.92rem + 0.4vw, 1.125rem);
|
||||
--text-hero: clamp(3rem, 1rem + 7vw, 8rem);
|
||||
|
||||
--space-section: clamp(4rem, 3rem + 5vw, 10rem);
|
||||
|
||||
--duration-fast: 150ms;
|
||||
--duration-normal: 300ms;
|
||||
--ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
```
|
||||
|
||||
## アニメーション専用プロパティ
|
||||
|
||||
コンポジタフレンドリーなモーションを優先する:
|
||||
- `transform`
|
||||
- `opacity`
|
||||
- `clip-path`
|
||||
- `filter`(控えめに)
|
||||
|
||||
レイアウトに紐づくプロパティのアニメーションを避ける:
|
||||
- `width`
|
||||
- `height`
|
||||
- `top`
|
||||
- `left`
|
||||
- `margin`
|
||||
- `padding`
|
||||
- `border`
|
||||
- `font-size`
|
||||
|
||||
## セマンティック HTML ファースト
|
||||
|
||||
```html
|
||||
<header>
|
||||
<nav aria-label="Main navigation">...</nav>
|
||||
</header>
|
||||
<main>
|
||||
<section aria-labelledby="hero-heading">
|
||||
<h1 id="hero-heading">...</h1>
|
||||
</section>
|
||||
</main>
|
||||
<footer>...</footer>
|
||||
```
|
||||
|
||||
セマンティック要素が存在するときに、汎用的な `div` ラッパースタックに頼らない。
|
||||
|
||||
## 命名
|
||||
|
||||
- コンポーネント: PascalCase(`ScrollySection`、`SurfaceCard`)
|
||||
- フック: `use` プレフィックス(`useReducedMotion`)
|
||||
- CSS クラス: kebab-case またはユーティリティクラス
|
||||
- アニメーションタイムライン: 意図を含む camelCase(`heroRevealTl`)
|
||||
63
docs/ja-JP/rules/web/design-quality.md
Normal file
63
docs/ja-JP/rules/web/design-quality.md
Normal file
@@ -0,0 +1,63 @@
|
||||
> このファイルは [common/patterns.md](../common/patterns.md) を Web 固有のデザイン品質ガイダンスで拡張します。
|
||||
|
||||
# Web デザイン品質基準
|
||||
|
||||
## アンチテンプレートポリシー
|
||||
|
||||
汎用的なテンプレートに見える UI をリリースしない。フロントエンドの出力は意図的で、主張があり、プロダクトに固有であるべきである。
|
||||
|
||||
### 禁止パターン
|
||||
|
||||
- 均一なスペーシングで階層のないデフォルトのカードグリッド
|
||||
- 中央揃えの見出し、グラデーションブロブ、汎用 CTA の定番ヒーローセクション
|
||||
- 完成したデザインとして出す未変更のライブラリデフォルト
|
||||
- レイヤリング、深度、モーションのないフラットレイアウト
|
||||
- すべてのコンポーネントで均一な角丸、スペーシング、シャドウ
|
||||
- 1つのアクセントカラーだけの安全なグレー・オン・ホワイトのスタイリング
|
||||
- サイドバー + カード + チャートで視点のないダッシュボード量産レイアウト
|
||||
- 意図的な理由なしに使用されるデフォルトフォントスタック
|
||||
|
||||
### 必要な品質
|
||||
|
||||
すべての意味のあるフロントエンドサーフェスは、以下のうち少なくとも4つを示すべきである:
|
||||
|
||||
1. スケールコントラストによる明確な階層
|
||||
2. 均一なパディングではなく、スペーシングの意図的なリズム
|
||||
3. オーバーラップ、シャドウ、サーフェス、またはモーションによる深度やレイヤリング
|
||||
4. 個性と実際のペアリング戦略を持つタイポグラフィ
|
||||
5. 装飾的ではなく、セマンティックに使用される色
|
||||
6. デザインされたと感じるホバー、フォーカス、アクティブ状態
|
||||
7. 必要に応じたグリッドを打ち破るエディトリアルまたはベントーレイアウト
|
||||
8. ビジュアルの方向性に合ったテクスチャ、グレイン、または雰囲気
|
||||
9. 注意をそらすのではなく、フローを明確にするモーション
|
||||
10. 後付けではなく、デザインシステムの一部として扱われるデータビジュアライゼーション
|
||||
|
||||
## フロントエンドコードを書く前に
|
||||
|
||||
1. 具体的なスタイルの方向性を選ぶ。「クリーンミニマル」のような曖昧なデフォルトを避ける。
|
||||
2. パレットを意図的に定義する。
|
||||
3. タイポグラフィを意図的に選択する。
|
||||
4. 少なくとも少数の実際のリファレンスを集める。
|
||||
5. 関連する ECC のデザイン/フロントエンドスキルを使用する。
|
||||
|
||||
## 価値あるスタイルの方向性
|
||||
|
||||
- エディトリアル / マガジン
|
||||
- ネオブルータリズム
|
||||
- 実際の深度を持つグラスモーフィズム
|
||||
- 規律あるコントラストのダークラグジュアリーまたはライトラグジュアリー
|
||||
- ベントーレイアウト
|
||||
- スクローリーテリング
|
||||
- 3D 統合
|
||||
- スイス / インターナショナル
|
||||
- レトロフューチャリズム
|
||||
|
||||
自動的にダークモードをデフォルトにしない。プロダクトが実際に求めるビジュアルの方向性を選択する。
|
||||
|
||||
## コンポーネントチェックリスト
|
||||
|
||||
- [ ] デフォルトの Tailwind や shadcn テンプレートに見えないか?
|
||||
- [ ] 意図的なホバー/フォーカス/アクティブ状態があるか?
|
||||
- [ ] 均一な強調ではなく階層を使用しているか?
|
||||
- [ ] 実際のプロダクトのスクリーンショットで信憑性があるか?
|
||||
- [ ] 両テーマをサポートする場合、ライトとダークの両方が意図的に感じられるか?
|
||||
129
docs/ja-JP/rules/web/hooks.md
Normal file
129
docs/ja-JP/rules/web/hooks.md
Normal file
@@ -0,0 +1,129 @@
|
||||
> このファイルは [common/hooks.md](../common/hooks.md) を Web 固有のフック推奨事項で拡張します。
|
||||
|
||||
# Web フック
|
||||
|
||||
## 推奨 PostToolUse フック
|
||||
|
||||
プロジェクトローカルのツールを優先する。リモートの使い捨てパッケージ実行にフックを接続しない。
|
||||
|
||||
### 保存時フォーマット
|
||||
|
||||
編集後にプロジェクトの既存フォーマッタエントリポイントを使用する:
|
||||
|
||||
```json
|
||||
{
|
||||
"hooks": {
|
||||
"PostToolUse": [
|
||||
{
|
||||
"matcher": "Write|Edit",
|
||||
"command": "pnpm prettier --write \"$FILE_PATH\"",
|
||||
"description": "Format edited frontend files"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`yarn prettier` や `npm exec prettier --` による同等のローカルコマンドも、リポジトリが所有する依存関係を使用する場合は問題ない。
|
||||
|
||||
### リントチェック
|
||||
|
||||
```json
|
||||
{
|
||||
"hooks": {
|
||||
"PostToolUse": [
|
||||
{
|
||||
"matcher": "Write|Edit",
|
||||
"command": "pnpm eslint --fix \"$FILE_PATH\"",
|
||||
"description": "Run ESLint on edited frontend files"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 型チェック
|
||||
|
||||
`--incremental` を使用して再実行時に前回の `.tsbuildinfo` を再利用する(変更のないコードでは30-60秒ではなく1-3秒)。`timeout` でラップして、停止した tsc が OS によって回収されるようにする — これにより、編集が tsc の完了よりも速く発生した場合のマルチプロセス蓄積を防止する。
|
||||
|
||||
```json
|
||||
{
|
||||
"hooks": {
|
||||
"PostToolUse": [
|
||||
{
|
||||
"matcher": "Write|Edit",
|
||||
"command": "timeout 60 pnpm tsc --noEmit --pretty false --incremental --tsBuildInfoFile node_modules/.cache/tsc-hook.tsbuildinfo",
|
||||
"description": "Type-check after frontend edits (incremental + timeout-capped)"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**両方のフラグが重要な理由:**
|
||||
- `--incremental` なしでは、すべての編集でプログラム全体をゼロから再チェックする。実際の Next.js プロジェクトでは、これが急速に積み重なる: 5-10秒間隔の編集 + 30-60秒の tsc 実行 = N個の並行 tsc プロセス。
|
||||
- `timeout` なしでは、ハングした tsc(推移的依存関係の変更、再帰型で停止した型チェッカー)は終了せず、親シェルが終了したときに孤児になる。
|
||||
- `--tsBuildInfoFile` が必要なのは、`--noEmit` が通常 buildinfo の書き込みを抑制するため。パスを明示的に指定することでインクリメンタルが機能し続ける。
|
||||
|
||||
Windows で GNU coreutils がない場合は、`timeout 60` を PowerShell ラッパーに置き換えるか、Stop/SessionEnd フックに頼って停滞した tsc プロセスを掃除する。
|
||||
|
||||
### CSS リント
|
||||
|
||||
```json
|
||||
{
|
||||
"hooks": {
|
||||
"PostToolUse": [
|
||||
{
|
||||
"matcher": "Write|Edit",
|
||||
"command": "pnpm stylelint --fix \"$FILE_PATH\"",
|
||||
"description": "Lint edited stylesheets"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## PreToolUse フック
|
||||
|
||||
### ファイルサイズガード
|
||||
|
||||
まだ存在しない可能性のあるファイルからではなく、ツール入力コンテンツからの巨大な書き込みをブロックする:
|
||||
|
||||
```json
|
||||
{
|
||||
"hooks": {
|
||||
"PreToolUse": [
|
||||
{
|
||||
"matcher": "Write",
|
||||
"command": "node -e \"let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{const i=JSON.parse(d);const c=i.tool_input?.content||'';const lines=c.split('\\n').length;if(lines>800){console.error('[Hook] BLOCKED: File exceeds 800 lines ('+lines+' lines)');console.error('[Hook] Split into smaller modules');process.exit(2)}console.log(d)})\"",
|
||||
"description": "Block writes that exceed 800 lines"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Stop フック
|
||||
|
||||
### 最終ビルド検証
|
||||
|
||||
```json
|
||||
{
|
||||
"hooks": {
|
||||
"Stop": [
|
||||
{
|
||||
"command": "pnpm build",
|
||||
"description": "Verify the production build at session end"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 順序
|
||||
|
||||
推奨順序:
|
||||
1. フォーマット
|
||||
2. リント
|
||||
3. 型チェック
|
||||
4. ビルド検証
|
||||
79
docs/ja-JP/rules/web/patterns.md
Normal file
79
docs/ja-JP/rules/web/patterns.md
Normal file
@@ -0,0 +1,79 @@
|
||||
> このファイルは [common/patterns.md](../common/patterns.md) を Web 固有のパターンで拡張します。
|
||||
|
||||
# Web パターン
|
||||
|
||||
## コンポーネントコンポジション
|
||||
|
||||
### コンパウンドコンポーネント
|
||||
|
||||
関連する UI が状態とインタラクションのセマンティクスを共有する場合、コンパウンドコンポーネントを使用する:
|
||||
|
||||
```tsx
|
||||
<Tabs defaultValue="overview">
|
||||
<Tabs.List>
|
||||
<Tabs.Trigger value="overview">Overview</Tabs.Trigger>
|
||||
<Tabs.Trigger value="settings">Settings</Tabs.Trigger>
|
||||
</Tabs.List>
|
||||
<Tabs.Content value="overview">...</Tabs.Content>
|
||||
<Tabs.Content value="settings">...</Tabs.Content>
|
||||
</Tabs>
|
||||
```
|
||||
|
||||
- 親が状態を所有する
|
||||
- 子はコンテキスト経由で消費する
|
||||
- 複雑なウィジェットでは props のバケツリレーよりもこれを優先する
|
||||
|
||||
### レンダープロップ / スロット
|
||||
|
||||
- 動作は共有されるがマークアップを変える必要がある場合、レンダープロップまたはスロットパターンを使用する
|
||||
- キーボードハンドリング、ARIA、フォーカスロジックはヘッドレスレイヤーに保持する
|
||||
|
||||
### コンテナ / プレゼンテーション分離
|
||||
|
||||
- コンテナコンポーネントがデータ読み込みと副作用を所有する
|
||||
- プレゼンテーションコンポーネントは props を受け取り UI をレンダリングする
|
||||
- プレゼンテーションコンポーネントは純粋に保つべきである
|
||||
|
||||
## 状態管理
|
||||
|
||||
これらを個別に扱う:
|
||||
|
||||
| 関心事 | ツール |
|
||||
|--------|--------|
|
||||
| サーバー状態 | TanStack Query、SWR、tRPC |
|
||||
| クライアント状態 | Zustand、Jotai、signals |
|
||||
| URL 状態 | search params、route segments |
|
||||
| フォーム状態 | React Hook Form または同等のもの |
|
||||
|
||||
- サーバー状態をクライアントストアに複製しない
|
||||
- 冗長な計算済み状態を保存する代わりに値を導出する
|
||||
|
||||
## 状態としての URL
|
||||
|
||||
共有可能な状態を URL に永続化する:
|
||||
- フィルタ
|
||||
- ソート順
|
||||
- ページネーション
|
||||
- アクティブタブ
|
||||
- 検索クエリ
|
||||
|
||||
## データフェッチ
|
||||
|
||||
### Stale-While-Revalidate
|
||||
|
||||
- キャッシュされたデータを即座に返す
|
||||
- バックグラウンドで再バリデーションする
|
||||
- 手作りする代わりに既存のライブラリを優先する
|
||||
|
||||
### 楽観的更新
|
||||
|
||||
- 現在の状態のスナップショットを取る
|
||||
- 楽観的な更新を適用する
|
||||
- 失敗時にロールバックする
|
||||
- ロールバック時に可視的なエラーフィードバックを出す
|
||||
|
||||
### 並列ローディング
|
||||
|
||||
- 独立したデータを並列にフェッチする
|
||||
- 親子のリクエストウォーターフォールを避ける
|
||||
- 正当な理由がある場合、次のルートや状態をプリフェッチする
|
||||
64
docs/ja-JP/rules/web/performance.md
Normal file
64
docs/ja-JP/rules/web/performance.md
Normal file
@@ -0,0 +1,64 @@
|
||||
> このファイルは [common/performance.md](../common/performance.md) を Web 固有のパフォーマンスコンテンツで拡張します。
|
||||
|
||||
# Web パフォーマンスルール
|
||||
|
||||
## Core Web Vitals 目標
|
||||
|
||||
| メトリクス | 目標 |
|
||||
|-----------|------|
|
||||
| LCP | < 2.5秒 |
|
||||
| INP | < 200ms |
|
||||
| CLS | < 0.1 |
|
||||
| FCP | < 1.5秒 |
|
||||
| TBT | < 200ms |
|
||||
|
||||
## バンドルバジェット
|
||||
|
||||
| ページタイプ | JS バジェット(gzip 圧縮後) | CSS バジェット |
|
||||
|-------------|---------------------------|--------------|
|
||||
| ランディングページ | < 150kb | < 30kb |
|
||||
| アプリページ | < 300kb | < 50kb |
|
||||
| マイクロサイト | < 80kb | < 15kb |
|
||||
|
||||
## ローディング戦略
|
||||
|
||||
1. 正当な場合、クリティカルなアバブ・ザ・フォールド CSS をインライン化する
|
||||
2. ヒーロー画像とプライマリフォントのみをプリロードする
|
||||
3. 非クリティカルな CSS や JS を遅延読み込みする
|
||||
4. 重いライブラリを動的インポートする
|
||||
|
||||
```js
|
||||
const gsapModule = await import('gsap');
|
||||
const { ScrollTrigger } = await import('gsap/ScrollTrigger');
|
||||
```
|
||||
|
||||
## 画像最適化
|
||||
|
||||
- 明示的な `width` と `height`
|
||||
- ヒーローメディアのみに `loading="eager"` と `fetchpriority="high"`
|
||||
- ビロウ・ザ・フォールドのアセットには `loading="lazy"`
|
||||
- フォールバック付きで AVIF または WebP を優先する
|
||||
- レンダリングサイズを大幅に超えるソース画像を配信しない
|
||||
|
||||
## フォント読み込み
|
||||
|
||||
- 明確な例外がない限り、フォントファミリーは最大2つ
|
||||
- `font-display: swap`
|
||||
- 可能な場合はサブセット化する
|
||||
- 本当にクリティカルなウェイト/スタイルのみをプリロードする
|
||||
|
||||
## アニメーションパフォーマンス
|
||||
|
||||
- コンポジタフレンドリーなプロパティのみをアニメーションする
|
||||
- `will-change` は狭い範囲で使用し、完了時に削除する
|
||||
- シンプルなトランジションには CSS を優先する
|
||||
- JS モーションには `requestAnimationFrame` または確立されたアニメーションライブラリを使用する
|
||||
- スクロールハンドラの乱発を避ける。IntersectionObserver または行儀の良いライブラリを使用する
|
||||
|
||||
## パフォーマンスチェックリスト
|
||||
|
||||
- [ ] すべての画像に明示的なサイズがある
|
||||
- [ ] 意図しないレンダーブロッキングリソースがない
|
||||
- [ ] 動的コンテンツによるレイアウトシフトがない
|
||||
- [ ] モーションがコンポジタフレンドリーなプロパティにとどまっている
|
||||
- [ ] サードパーティスクリプトが async/defer で読み込まれ、必要な場合のみ使用されている
|
||||
57
docs/ja-JP/rules/web/security.md
Normal file
57
docs/ja-JP/rules/web/security.md
Normal file
@@ -0,0 +1,57 @@
|
||||
> このファイルは [common/security.md](../common/security.md) を Web 固有のセキュリティコンテンツで拡張します。
|
||||
|
||||
# Web セキュリティルール
|
||||
|
||||
## コンテンツセキュリティポリシー
|
||||
|
||||
本番環境では常に CSP を設定する。
|
||||
|
||||
### ノンスベースの CSP
|
||||
|
||||
`'unsafe-inline'` の代わりに、スクリプトにはリクエストごとのノンスを使用する。
|
||||
|
||||
```text
|
||||
Content-Security-Policy:
|
||||
default-src 'self';
|
||||
script-src 'self' 'nonce-{RANDOM}' https://cdn.jsdelivr.net;
|
||||
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
|
||||
img-src 'self' data: https:;
|
||||
font-src 'self' https://fonts.gstatic.com;
|
||||
connect-src 'self' https://*.example.com;
|
||||
frame-src 'none';
|
||||
object-src 'none';
|
||||
base-uri 'self';
|
||||
```
|
||||
|
||||
オリジンはプロジェクトに合わせて調整する。このブロックをそのままコピーして使わない。
|
||||
|
||||
## XSS 防止
|
||||
|
||||
- サニタイズされていない HTML を注入しない
|
||||
- サニタイズなしで `innerHTML` / `dangerouslySetInnerHTML` を使用しない
|
||||
- 動的テンプレート値をエスケープする
|
||||
- どうしても必要な場合は、検証済みのローカルサニタイザーでユーザー HTML をサニタイズする
|
||||
|
||||
## サードパーティスクリプト
|
||||
|
||||
- 非同期で読み込む
|
||||
- CDN から配信する場合は SRI を使用する
|
||||
- 四半期ごとに監査する
|
||||
- 実用的な場合、重要な依存関係にはセルフホスティングを優先する
|
||||
|
||||
## HTTPS とヘッダー
|
||||
|
||||
```text
|
||||
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
|
||||
X-Content-Type-Options: nosniff
|
||||
X-Frame-Options: DENY
|
||||
Referrer-Policy: strict-origin-when-cross-origin
|
||||
Permissions-Policy: camera=(), microphone=(), geolocation=()
|
||||
```
|
||||
|
||||
## フォーム
|
||||
|
||||
- 状態を変更するフォームには CSRF 保護
|
||||
- 送信エンドポイントにはレート制限
|
||||
- クライアント側とサーバー側の両方でバリデーション
|
||||
- 重い CAPTCHA デフォルトよりもハニーポットや軽量なアンチアビューズ制御を優先する
|
||||
55
docs/ja-JP/rules/web/testing.md
Normal file
55
docs/ja-JP/rules/web/testing.md
Normal file
@@ -0,0 +1,55 @@
|
||||
> このファイルは [common/testing.md](../common/testing.md) を Web 固有のテストコンテンツで拡張します。
|
||||
|
||||
# Web テストルール
|
||||
|
||||
## 優先順位
|
||||
|
||||
### 1. ビジュアルリグレッション
|
||||
|
||||
- 主要なブレークポイントでスクリーンショットを撮る: 320、768、1024、1440
|
||||
- ヒーローセクション、スクローリーテリングセクション、および意味のある状態をテストする
|
||||
- ビジュアル重視の作業には Playwright スクリーンショットを使用する
|
||||
- 両テーマが存在する場合は両方をテストする
|
||||
|
||||
### 2. アクセシビリティ
|
||||
|
||||
- 自動アクセシビリティチェックを実行する
|
||||
- キーボードナビゲーションをテストする
|
||||
- 動作軽減の動作を検証する
|
||||
- カラーコントラストを検証する
|
||||
|
||||
### 3. パフォーマンス
|
||||
|
||||
- 意味のあるページに対して Lighthouse または同等のものを実行する
|
||||
- [performance.md](performance.md) の CWV 目標を維持する
|
||||
|
||||
### 4. クロスブラウザ
|
||||
|
||||
- 最低: Chrome、Firefox、Safari
|
||||
- スクロール、モーション、フォールバック動作をテストする
|
||||
|
||||
### 5. レスポンシブ
|
||||
|
||||
- 320、375、768、1024、1440、1920 でテストする
|
||||
- オーバーフローがないことを検証する
|
||||
- タッチインタラクションを検証する
|
||||
|
||||
## E2E の形式
|
||||
|
||||
```ts
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test('landing hero loads', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await expect(page.locator('h1')).toBeVisible();
|
||||
});
|
||||
```
|
||||
|
||||
- 不安定なタイムアウトベースのアサーションを避ける
|
||||
- 決定的な待機を優先する
|
||||
|
||||
## ユニットテスト
|
||||
|
||||
- ユーティリティ、データ変換、カスタムフックをテストする
|
||||
- 高度にビジュアルなコンポーネントでは、壊れやすいマークアップアサーションよりもビジュアルリグレッションの方がシグナルが高いことが多い
|
||||
- ビジュアルリグレッションはカバレッジ目標を補完するものであり、置き換えるものではない
|
||||
Reference in New Issue
Block a user