feat: ai-antipattern ポリシーに冗長な条件分岐パターン検出を追加

AI が生成しがちな if/else で同一関数を引数の有無のみ変えて
呼び出すパターンを REJECT 基準として定義。ai_review フェーズで
検出し、reviewers に到達する前に修正できるようにする。
既存のデッドコード例も TAKT 固有コードから汎用パターンに置換。
This commit is contained in:
nrslib 2026-02-28 17:48:10 +09:00
parent ff31d9cb0c
commit 45663342c6
2 changed files with 72 additions and 16 deletions

View File

@ -47,6 +47,34 @@ AI often repeats the same patterns, including mistakes.
| Inconsistent implementation | Same logic implemented differently across files | | Inconsistent implementation | Same logic implemented differently across files |
| Boilerplate explosion | Unnecessary repetition that could be abstracted | | Boilerplate explosion | Unnecessary repetition that could be abstracted |
## Redundant Conditional Branch Detection
AI tends to generate if/else blocks that call the same function with only argument differences.
| Pattern | Example | Verdict |
|---------|---------|---------|
| Branch differs only in argument presence | `if (x) f(a, b, c) else f(a, b)` | REJECT |
| Branch differs only in options | `if (x) f(a, {opt: x}) else f(a)` | REJECT |
| Redundant else without using return value | `if (x) { f(a, x); return; } f(a);` | REJECT |
```typescript
// REJECT - both branches call the same function, differing only in the 3rd argument
if (options.format !== undefined) {
await processFile(input, output, { format: options.format });
} else {
await processFile(input, output);
}
// OK - extract the conditional into a variable, then make a single call
const formatOpt = options.format !== undefined ? { format: options.format } : undefined;
await processFile(input, output, formatOpt);
```
Verification approach:
1. Find if/else blocks calling the same function
2. If the only difference is optional argument presence, unify with ternary or spread syntax
3. If branches have different preprocessing, store results in a variable and make a single call
## Context Fitness Assessment ## Context Fitness Assessment
Does the code fit this specific project? Does the code fit this specific project?
@ -102,18 +130,18 @@ Logical dead code detection:
AI tends to add "just in case" defensive code, but when considering caller constraints, it may be unreachable. Code that is syntactically reachable but logically unreachable due to call chain preconditions should be removed. AI tends to add "just in case" defensive code, but when considering caller constraints, it may be unreachable. Code that is syntactically reachable but logically unreachable due to call chain preconditions should be removed.
```typescript ```typescript
// REJECT - callers are only from interactive menus that require TTY // REJECT - callers always require interactive input
// This function is never called from non-TTY environments // This function is never called from non-interactive environments
function showFullDiff(cwd: string, branch: string): void { function displayResult(data: ResultData): void {
const usePager = process.stdin.isTTY === true; const isInteractive = process.stdin.isTTY === true;
// usePager is always true (callers assume TTY) // isInteractive is always true (callers assume TTY)
const pager = usePager ? 'less -R' : 'cat'; // else branch is unreachable const output = isInteractive ? formatRich(data) : formatPlain(data); // else branch is unreachable
} }
// OK - understands caller constraints and removes unnecessary branching // OK - understands caller constraints and removes unnecessary branching
function showFullDiff(cwd: string, branch: string): void { function displayResult(data: ResultData): void {
// Only called from interactive menus, so TTY is always present // Only called from interactive menus, so TTY is always present
spawnSync('git', ['diff', ...], { env: { GIT_PAGER: 'less -R' } }); console.log(formatRich(data));
} }
``` ```

View File

@ -47,6 +47,34 @@ AIは同じパターンを、間違いも含めて繰り返すことが多い。
| 一貫性のない実装 | ファイル間で異なる方法で実装された同じロジック | | 一貫性のない実装 | ファイル間で異なる方法で実装された同じロジック |
| ボイラープレートの爆発 | 抽象化できる不要な繰り返し | | ボイラープレートの爆発 | 抽象化できる不要な繰り返し |
## 冗長な条件分岐パターン検出
AIは条件分岐で同一関数を引数の差異のみで呼び分けるコードを生成しがちである。
| パターン | 例 | 判定 |
|---------|-----|------|
| 引数の有無のみの分岐 | `if (x) f(a, b, c) else f(a, b)` | REJECT |
| オプション有無の分岐 | `if (x) f(a, {opt: x}) else f(a)` | REJECT |
| 戻り値を使わない冗長な else | `if (x) { f(a, x); return; } f(a);` | REJECT |
```typescript
// REJECT - 両ブランチが同一関数を呼び出し、第3引数の有無のみが異なる
if (options.format !== undefined) {
await processFile(input, output, { format: options.format });
} else {
await processFile(input, output);
}
// OK - 三項演算子で統一
const formatOpt = options.format !== undefined ? { format: options.format } : undefined;
await processFile(input, output, formatOpt);
```
検証アプローチ:
1. if/else ブロックで同一関数を呼び出している箇所を探す
2. 差異がオプション引数の有無のみなら、三項演算子やスプレッド構文で統一
3. 分岐ごとに異なる前処理がある場合は、変数に結果を格納してから単一呼び出しに統一
## コンテキスト適合性評価 ## コンテキスト適合性評価
コードはこの特定のプロジェクトに合っているか? コードはこの特定のプロジェクトに合っているか?
@ -118,18 +146,18 @@ AIは新しいコードを追加するが、不要になったコードの削除
AIは「念のため」の防御コードを追加しがちだが、呼び出し元の制約を考慮すると到達不能な場合がある。構文的には到達可能でも、呼び出しチェーンの前提条件により論理的に到達しないコードは削除する。 AIは「念のため」の防御コードを追加しがちだが、呼び出し元の制約を考慮すると到達不能な場合がある。構文的には到達可能でも、呼び出しチェーンの前提条件により論理的に到達しないコードは削除する。
```typescript ```typescript
// REJECT - 呼び出し元がTTY必須のインタラクティブメニュー経由のみ // REJECT - 呼び出し元がインタラクティブ入力を前提としている
// TTYがない環境からこの関数が呼ばれることはない // 非インタラクティブ環境からこの関数が呼ばれることはない
function showFullDiff(cwd: string, branch: string): void { function displayResult(data: ResultData): void {
const usePager = process.stdin.isTTY === true; const isInteractive = process.stdin.isTTY === true;
// usePager は常に true呼び出し元がTTYを前提としている // isInteractive は常に true呼び出し元がTTYを前提としている
const pager = usePager ? 'less -R' : 'cat'; // else節は到達不能 const output = isInteractive ? formatRich(data) : formatPlain(data); // else節は到達不能
} }
// OK - 呼び出し元の制約を理解し、不要な分岐を排除 // OK - 呼び出し元の制約を理解し、不要な分岐を排除
function showFullDiff(cwd: string, branch: string): void { function displayResult(data: ResultData): void {
// インタラクティブメニューからのみ呼ばれるためTTYは常に存在する // インタラクティブメニューからのみ呼ばれるためTTYは常に存在する
spawnSync('git', ['diff', ...], { env: { GIT_PAGER: 'less -R' } }); console.log(formatRich(data));
} }
``` ```