mcpField
C
code-quality

code-quality

github.com/cognitedataテスト・品質62.0k 回インストール1 日前に更新
リポジトリ

code-quality について

code-quality は、TypeScriptアプリをレビューするスキルです。lintと型チェックの後、型安全性、コンポーネント構成、重複ロジック、依存性注入、テスト容易性、命名、デッドコードを調べ、ファイル名と行番号付きで報告します。

主な機能

  1. 01lintと厳格なTypeScript型チェック
  2. 02型安全性と状態モデリングの確認
  3. 03コンポーネント規模と責務の確認
  4. 04重複ロジックと依存関係の検出
  5. 05テスト容易性と実装パターンのレビュー
  6. 06命名規則とデッドコードの確認

活用シーン

  1. 01TypeScriptアプリの品質レビュー
  2. 02安全でない型や重複コードの検出
  3. 03依存性注入とテストパターンの評価
  4. 04ファイル名・行番号付きレビュー結果の作成

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/cognitedata/builder-skills --skill code-quality

インストール後、Claude Code で/code-qualityを入力して読み込まれたか確認します。

詳細

コード品質レビュー

$ARGUMENTS(引数が指定されていない場合はアプリ全体)をレビューし、コード品質の問題を確認します。以下の各ステップを順番に進め、すべての指摘事項をファイルパスと行番号とともに報告してください。


ステップ 1 — まずリンターを実行する

コードを手動で読む前に、自動ツールでベースラインを確認します。

pnpm run lint

すべてのエラーと警告を列挙してください。続行する前に、すべてのエラーを修正してください。リンターエラーは妥協できません。警告も、文書化された例外がない限り、確認して解決してください。

隠れた型の問題を検出するため、TypeScript コンパイラーも strict モードで実行してください。

pnpm exec tsc --noEmit

すべての型エラーを列挙してください。これらは必ず修正してください。


ステップ 2 — TypeScript の型安全性

2a — any 型をなくす

コードベース全体で any の使用箇所を検索してください。

grep -rn --include="*.ts" --include="*.tsx" -E ": any|as any|<any>" src/

該当箇所はすべて、適切な型に置き換えてください。よく使われる置き換えは次のとおりです。

代わりに使うもの使用するもの
外部からの不明なデータに対する any型ガードまたは Zod parse と組み合わせた unknown
イベントハンドラーに対する anyReact.ChangeEvent<HTMLInputElement>、React.MouseEvent など
CDF レスポンスに対する anySDK 独自のレスポンス型(@cognite/sdk からインポート)
配列に対する any[]正しいジェネリック型を指定した T[]
as any キャスト適切な型の絞り込み、または明示的なオーバーロード関数シグネチャ

目標は src/ 内の any をゼロにすることです。サードパーティーライブラリの都合で any が必要な場合は、型付きアダプター関数で呼び出しをラップし、アプリに any が漏れないようにしてください。

2b — 表現できない状態を作る

無効な状態がコンパイル時にエラーになるよう、型システムを活用してください。到達可能な状態が少ないほど、コードの読み取りや変更が容易になります。

ブランド型 — プリミティブ型にブランドを付け、取り違えを防ぎます。境界で一度検証し、以降のコードではその型を信頼します。

type PhoneNumber = string & { __brand: "PhoneNumber" };

function parsePhone(input: string): PhoneNumber {
  if (!/^\+?\d{10,15}$/.test(input)) throw new Error(`Invalid: ${input}`);
  return input as PhoneNumber;
}

プロジェクトでブランド型をネイティブにサポートするライブラリ(Effect など)を使用している場合は、独自実装ではなく、そのライブラリのプリミティブを使用してください。

フラグの集合より判別可能なユニオン — 真偽値とオプショナル型の組み合わせを、網羅性のあるユニオンに置き換えます。

// Don't — invalid combos representable
type State = { loading: boolean; user?: User; error?: string };

// Do — only valid states exist
type State =
  | { status: "loading" }
  | { status: "success"; user: User }
  | { status: "error"; error: string };

フラグの集合になっているパターンを検索してください。

grep -rn --include="*.ts" --include="*.tsx" -E "loading\?|isLoading.*isError|isSuccess.*isError" src/

有効な組み合わせが限られる真偽値フラグを組み合わせた型をすべて指摘してください。これらは判別可能なユニオンにする必要があります。

2c — 型をエンドツーエンドで引き継ぐ

DB スキーマ → サーバー → クライアントで、型を手作業で重複させずに共有する必要があります。導出できる型を再記述しないでください。新しいインターフェースを書く前に、Pick、Omit、Parameters、ReturnType、Awaited、typeof を使ってください。

// Don't — duplicate shape, drifts when the row changes
type UserSummary = { id: string; email: Email };
function renderUser(u: UserSummary) { /* ... */ }

// Do — derive from the source of truth
type User = Awaited<ReturnType<typeof db.query.users.findFirst>>;
function renderUser(u: Pick<User, "id" | "email">) { /* ... */ }
# Find manually duplicated type shapes
grep -rn --include="*.ts" --include="*.tsx" -E "^(export )?type \w+Summary|^(export )?interface \w+DTO" src/

SDK または DB の型にすでに存在するフィールドを手動で再記述しているインターフェースを指摘してください。これらには Pick/Omit を使用する必要があります。

2d — 位置引数ではなくオブジェクトを渡す

同じプリミティブ型の引数を 2 つ以上取る関数では、呼び出し側が引数を気づかずに入れ替えないよう、名前付きプロパティを持つオブジェクトを受け取る必要があります。

// Don't — swap two args, still compiles
sendEmail("Welcome!", "Hi there");

// Do — order-independent, self-documenting
sendEmail({ to: "[email protected]", subject: "Welcome!", body: "Hi there" });
# Find functions with multiple string/number parameters (potential swap bugs)
grep -rn --include="*.ts" --include="*.tsx" -E "^\s*(export\s+)?(function|const)\s+\w+\s*\([^)]*string[^)]*string" src/

ステップ 3 — コンポーネントのサイズと単一責任を確認する

すべての .tsx ファイルを行数とともに列挙してください。

node -e "const fs=require('fs'),path=require('path');function walk(d){return fs.readdirSync(d,{withFileTypes:true}).flatMap(e=>{const p=path.join(d,e.name);return e.isDirectory()?walk(p):p.endsWith('.tsx')?[p]:[]})}walk('src').map(p=>({p,l:fs.readFileSync(p,'utf8').split('\n').length})).sort((a,b)=>b.l-a.l).forEach(({l,p})=>console.log(l,p))"

150 行を超えるコンポーネントファイルをすべて指摘してください。それぞれを読み、次の点を確認してください。

  • 複数の役割を担っていませんか?(データ取得と UI の描画、フォーム状態の処理など)
  • データ取得ロジックをカスタムフック(useAssetData)に移せますか?
  • サブセクションを名前付きサブコンポーネントとして切り出せますか?

本当に分離が明確になる場合に限り、分割してください。行数を減らすこと自体を目的に分割しないでください。適切な名前が付いた 200 行のコンポーネントは、名前のよくない 60 行のコンポーネント 3 つより優れています。


ステップ 4 — 重複ロジックを見つけて削除する(DRY)

フック、ユーティリティ、コンポーネントにコピー&ペーストされたパターンがないか検索してください。

# Find repeated fetch patterns
grep -rn --include="*.ts" --include="*.tsx" -E "sdk\.(assets|timeseries|events|files)\.(list|retrieve)" src/

# Find repeated formatting functions
grep -rn --include="*.ts" --include="*.tsx" -E "toLocaleDateString|toLocaleString|new Date\(" src/

# Find repeated className strings longer than 40 chars
grep -rn --include="*.tsx" -E 'className="[^"]{40,}"' src/

重複が見つかった場合は、次のように対応してください。

  • 純粋関数の場合は src/utils/ に切り出す
  • React の状態またはエフェクトを含む場合は src/hooks/ に切り出す
  • JSX の場合は共有コンポーネントに切り出す

ステップ 5 — 外部呼び出しで依存性注入を徹底する

コンポーネントやフックから CDF クライアントを直接インポートしてはいけません。SDK クライアントはコンテキスト(useCogniteClient() または prop 経由)から取得し、コンポーネントを単独でテストできるようにする必要があります。

grep -rn --include="*.ts" --include="*.tsx" -E "new CogniteClient|createCogniteClient" src/

アプリのブートストラップ/認証設定ファイル以外で、クライアントを直接生成している箇所をすべて指摘してください。常に次のパターンを使用してください。

// GOOD — client comes from context
export function useMyData() {
  const sdk = useCogniteClient(); // from Flows auth context
  // ...
}

// BAD — direct construction inside a hook or component
const sdk = new CogniteClient({ project: "my-project", ... });

同様に、Atlas ツールはグローバルなシングルトンをインポートするのではなく、フックから提供された ref を execute のクロージャー経由で受け取り、依存関係を利用する必要があります。


ステップ 6 — コーディングパターンとテスト可能性を確認する

コードベースが Flows アプリのレビューで求められる、次の 3 つの中核的なパターンに従っているか確認してください。これらのパターンにより、コードのテスト可能性、保守性、一貫性が保たれます。

6a — React コンテキストによる依存性注入

フックはコンテキスト型を通じて依存関係を宣言し、直接インポートするのではなく、useContext 経由で利用する必要があります。これにより、モジュールレベルのモックを使わずにテストできます。

# Find hooks that import other hooks/services directly (potential DI violation)
grep -rn --include="*.ts" --include="*.tsx" -E "^import.*from\s+['\"]\.\./" src/hooks/

# Find hooks that use useContext for dependency injection (good pattern)
grep -rn --include="*.ts" --include="*.tsx" "useContext" src/hooks/

推奨されるパターン:

// GOOD — injectable via context
const defaultDependencies = { useDataSource, useAnalytics };
export type UseMyHookContextType = typeof defaultDependencies;
export const UseMyHookContext = createContext<UseMyHookContextType>(defaultDependencies);
export function useMyHook() {
  const { useDataSource } = useContext(UseMyHookContext);
}

// BAD — hard-coded import, requires vi.mock to test
import { useDataSource } from '../data/useDataSource';
export function useMyHook() { const data = useDataSource(); }

React 以外のコード(ユーティリティ、サービス)では、一部の依存関係をオーバーライドできるファクトリー関数を使用してください。

type Deps = { serviceFactory: () => SomeService };
const defaultDeps: Deps = { serviceFactory: () => new SomeServiceImpl() };
export const doSomething = async (props: Props, depOverrides?: Partial<Deps>) => {
  const deps = { ...defaultDeps, ...depOverrides };
  const service = deps.serviceFactory();
};

コンテキスト経由で依存関係を受け取らずに直接インポートしているフックをすべて指摘してください。現在テストが存在していても、これらはテスト可能性に関する懸念事項です。

6b — インターフェースベースのサービス

サービスクラスは明示的な TypeScript インターフェースを実装する必要があります。これにより、本番コードの差し替えが可能になり、テスト用の代替実装を型安全にできます。

# Find service/class definitions and check for interface implementations
grep -rn --include="*.ts" --include="*.tsx" -E "class\s+\w+(Service|Client|Repository|Manager)" src/

# Find unsafe casts in production AND test code
grep -rn --include="*.ts" --include="*.tsx" "as unknown as" src/

次の項目を指摘してください。

  • 明示的なインターフェースを実装していないサービスクラス
  • 本番コードまたはテストコードにある as unknown as T キャスト — インターフェース設計に問題があることを示しています
6c — ViewModel パターン

ページレベルのフック(useSomethingViewModel)では、ビジネスロジックをプレゼンテーションから分離する必要があります。UI コンポーネントが受け取るのはデータとコールバックのみとし、データ取得、サイドエフェクトのロジック、SDK の直接呼び出しを含めてはいけません。

# Find page/view components
grep -rn --include="*.tsx" --include="*.ts" -l "useQuery\|useMutation\|sdk\.\|client\." src/pages/ src/views/ 2>/dev/null

# Find ViewModel hooks
grep -rn --include="*.ts" --include="*.tsx" -l "ViewModel" src/hooks/ 2>/dev/null

次の項目を指摘してください。

  • useQuery、useMutation、または SDK の直接呼び出しを含むページコンポーネント — これらのロジックは ViewModel フックに配置する必要があります
  • 複雑なデータロジックを持つページに ViewModel フックがないこと
6d — テストモックの品質
# Find vi.mock usage — each should have a comment justifying why context injection wasn't used
grep -rn --include="*.ts" --include="*.tsx" "vi\.mock" src/

# Find unsafe test casts
grep -rn --include="*.ts" --include="*.tsx" "as unknown as" src/ | grep -E "\.test\.|\.spec\."

次の項目を指摘してください。

  • コンテキストへの依存性注入が不可能だった理由を説明するコメントがない vi.mock の使用
  • テストファイルにある as unknown as T キャスト — 本番コードのインターフェース設計に問題があることを示しています

ステップ 7 — 命名規則を確認する

代表的なファイルをいくつか読み、次の規則に従っているか確認してください。

成果物規則例
ファイルとディレクトリkebab-caseasset-panel.tsx、use-asset-data.ts
React コンポーネントPascalCaseAssetPanel、NavigationBar
変数、関数、フックcamelCaseisLoading、fetchAssets、useAssetData
定数(モジュールレベル)SCREAMING_SNAKE_CASEMAX_ITEMS、AGENT_EXTERNAL_ID
TypeScript の型とインターフェースPascalCaseAssetNode、ChartConfig
ブール値の変数助動詞を接頭辞にするisLoading、hasError、canEdit

よくある違反を検索してください。

# TSX components not in PascalCase (filename starts with lowercase)
node -e "const fs=require('fs'),path=require('path');function walk(d){return fs.readdirSync(d,{withFileTypes:true}).flatMap(e=>{const p=path.join(d,e.name);return e.isDirectory()?walk(p):p.endsWith('.tsx')?[p]:[]})}walk('src').filter(p=>/^[a-z]/.test(path.basename(p))).forEach(p=>console.log(p))"

# Hook files not prefixed with "use"
node -e "const fs=require('fs');fs.readdirSync('src/hooks').filter(f=>f.endsWith('.ts')&&!f.startsWith('use')).forEach(f=>console.log('src/hooks/'+f))"

ステップ 8 — 不要なコードを削除する

# Find commented-out code blocks (3+ consecutive commented lines)
Get-ChildItem -Recurse -Include "*.ts","*.tsx" src | ForEach-Object {
    $file = $_; $lines = Get-Content $file.FullName
    $count = 0; $startLine = 0
    for ($i = 0; $i -lt $lines.Count; $i++) {
        if ($lines[$i] -match '^\s*//') {
            if ($count -eq 0) { $startLine = $i + 1 }
            $count++
        } else {
            if ($count -ge 3) { "$($file.FullName):$startLine — $count consecutive comment lines" }
            $count = 0
        }
    }
    if ($count -ge 3) { "$($file.FullName):$startLine — $count consecutive comment lines" }
}

# Find console.log/debug statements
grep -rn --include="*.tsx" --include="*.ts" -E "console\.(log|debug|warn|error|info)" src/

# Find TODO/FIXME/HACK comments
grep -rn --include="*.tsx" --include="*.ts" -E "(TODO|FIXME|HACK|XXX):" src/

到達不能なページ(ルーターで定義されているものの、そのコンポーネントがインポートもレンダリングもされないページ)や、まったく使われていないファイルを検索してください。

# Find all .ts/.tsx files and check if they are imported anywhere
for file in $(find src -name "*.ts" -o -name "*.tsx" | grep -v ".test." | grep -v ".spec." | grep -v "node_modules"); do
  basename=$(basename "$file" | sed 's/\.[^.]*$//')
  imports=$(grep -rn --include="*.ts" --include="*.tsx" "$basename" src/ | grep -v "$file" | wc -l)
  if [ "$imports" -eq 0 ]; then
    echo "UNUSED: $file"
  fi
done

# Find route definitions and verify their components are imported
grep -rn --include="*.tsx" --include="*.ts" -E "path:\s*['\"]|<Route" src/

ルール:

  • 出荷前に console.log と console.debug を削除してください(console.error には適切なエラーロギングを使用してください)。
  • コメントアウトされたコードブロックは削除してください。バージョン管理で履歴を保持できます。
  • 現在のスプリントより前から残っている TODO と FIXME のコメントは、解決するか、追跡対象の課題に置き換えてください。
  • 未使用のインポートはリンターで検出されます(ステップ 1)。削除済みであることを確認してください。

承認の必須条件: 到達不能なページ、まったく使われていないファイル、大規模な不要コードブロックは、承認前に必ず削除してください。これらは承認を妨げる指摘事項です。


ステップ 9 — ファイルとエクスポートの構造を確認する

各機能領域は、一貫した構造に従う必要があります。アプリのレイアウトが次のパターンに一致しているか確認してください。

src/
├── components/         # Shared presentational components
│   └── <name>/
│       ├── <name>.tsx
│       └── index.ts    # re-exports the public API
├── hooks/              # Custom hooks (each file = one hook)
├── utils/              # Pure utility functions (no React)
├── contexts/           # React context providers
├── pages/ or views/    # Route-level components
└── types/              # Shared TypeScript types

以下を指摘してください:

  • ページコンポーネントに直接書かれているビジネスロジック(フックに置くべきです)
  • コンポーネントファイル内に置かれているユーティリティ関数(utils/ に置くべきです)
  • 複数のファイルで使われているのに、コンポーネントファイル内にインラインで定義されている型(types/ に置くべきです)
  • コンポーネントディレクトリに index.ts のバレルファイルがない(インポートが冗長になります)

ステップ 10 — 調査結果を報告する

カテゴリごとにまとめた構造化レポートを作成してください。

カテゴリファイル行問題推奨事項
TypeScriptsrc/hooks/useData.ts18response as any キャスト@cognite/sdk から NodeItem 型をインポートして使用する
サイズsrc/components/Dashboard.tsx—340 行で、取得処理とレンダリング処理が混在しているuseDashboardData フック(約 120 行)に切り出す
DRYsrc/components/A.tsx、src/components/B.tsx45、62日付フォーマッターが重複しているsrc/utils/formatDate.ts に切り出す
命名src/hooks/data.ts—ファイル名が use で始まっていないuseData.ts に変更する
デッドコードsrc/App.tsx88console.log("debug response", data)削除する

ステップ内で問題が見つからなかった場合は、そのステップについて「問題は見つかりませんでした」と記載してください。黙ってステップを省略しないでください。


完了

カテゴリごとの調査結果の合計数をまとめ、最初に対処すべき影響の大きい項目を列挙してください。any 型と lint エラーは、いずれもブロッカーとして扱い、別々に列挙してください。

コメント