コード品質は、ソースコードそのものの読みやすさ、変更しやすさ、欠陥の入りにくさを指します。「品質を上げよう」と掛け声をかけても、何を測り、どの値を超えたらマージを止めるのかが決まっていなければ、レビューのたびに基準が揺れます。この記事では、規格上の位置づけ、よく使われる目安値とその出典、ESLintとJestでCIを失敗させる設定の実行結果、そして既存コードに品質ゲートを入れるときの判断基準を扱います。
まとめ
- コード品質は、ISO/IEC 25010の品質特性のうち主に保守性(モジュール性・再利用性・解析性・修正性・試験性)に対応します。
- 目安値には出典があります。循環的複雑度10はMcCabeの1976年論文、新規コードのカバレッジ80%と重複3%はSonarQube Serverの既定品質ゲート「Sonar way」の条件です。ESLintの
complexityルールの既定上限は10ではなく20です。 - カバレッジは行だけで見ないでください。1行に書いたif文だらけの関数では、行カバレッジ100%のまま分岐カバレッジが54.54%でした(Jest 30.5.2で実測)。
- CIでは「ツールが終了コード1を返す」ことが止める仕組みです。GitHubでは必須ステータスチェックに登録しないと、失敗してもマージできてしまいます。
- 既存コードには全体の閾値ではなく新規・変更コードだけに条件をかけます。既存コードが80%を大きく下回る状態で全体閾値を初日から強制すると、変更と無関係な未カバー箇所でもマージが止まります。
コード品質の定義と規格上の位置づけ
ソフトウェアの品質を定義する国際規格はISO/IEC 25010です。2023年の改訂で製品品質の特性は9つになり、安全性(safety)が加わりました。このうちソースコードを読んで判断できるのは主に保守性で、利用者から見える機能適合性や性能効率性とは別の軸です。特性の全体像はISO/IEC 25010とは?品質特性の一覧と2023年改訂(安全性の追加)まで解説にまとめています。
保守性の副特性ごとに、コード上の兆候と自動で測れる指標を並べると次のとおりです。
| 保守性の副特性 | コード上の兆候 | 自動で測れる指標 |
|---|---|---|
| モジュール性 | 1つの変更が多数のファイルに波及 | 依存の循環・結合度 |
| 再利用性 | 同じ処理のコピー | 重複行の割合 |
| 解析性 | 分岐が深く追えない関数 | 循環的複雑度・ネストの深さ |
| 修正性 | 直すと別の箇所が壊れる | 静的解析の指摘件数 |
| 試験性 | テストが書けない・通らない分岐 | 行・分岐カバレッジ |
ソースコードの計測方法そのものを定めた規格としては、ISO/IEC 5055:2021もあります。こちらはセキュリティ・信頼性・性能効率性・保守性の4つについて、ソースコード中の構造的な弱点を検出して数える方式です。ISO/IEC 25010は品質モデルを定義する規格で、自動計測の対象を限定するものではありません。可読性や命名の妥当性は、静的解析の指標だけでは判断しきれないため、レビューで補います。
コード品質を測る指標と目安値の出典
現場でよく見る目安値を、出典とあわせて整理します。数字だけが独り歩きしやすいので、「誰が何の前提で出した値か」を押さえておくと、チームで閾値を決めるときに説明がつきます。
| 指標 | 目安 | 出典 |
|---|---|---|
| 循環的複雑度(関数単位) | 10以下 | McCabe(1976) |
| ESLint complexity の既定上限 | 20 | ESLint 10.11.0 のルール実装 |
| 新規コードの指摘 | 0件 | SonarQube Server「Sonar way」 |
| 新規コードのカバレッジ | 80.0%以上 | SonarQube Server「Sonar way」 |
| 新規コードの重複率 | 3.0%以下 | SonarQube Server「Sonar way」 |
| 新規のセキュリティホットスポット | すべてレビュー済み | SonarQube Server「Sonar way」 |
循環的複雑度の目安10とESLint既定値20の差
循環的複雑度は、関数の中を通る独立した経路の数です。if・else if・&&・||などの分岐が1つ増えるごとに1ずつ上がります。上限10はThomas McCabeが1976年の論文「A Complexity Measure」で「妥当だが魔法の数ではない上限」として示した値です。一方、ESLintのcomplexityルールはオプションを省くと上限20で判定します(ESLint 10.11.0の実装でTHRESHOLD_DEFAULT = 20)。"complexity": "error"とだけ書くと、McCabeの目安の2倍まで通る点に注意してください。計算方法やツールごとの数え方の違いはサイクロマティック複雑度とは?計算方法・目安10の根拠とツールで値が違う理由で扱っています。
カバレッジ80%・重複3%を新規コードだけで数える理由
SonarQube Serverの既定品質ゲート「Sonar way」は、条件を4つとも新規コードにかけています。新規の指摘0件、新規のセキュリティホットスポットをすべてレビュー済み、新規コードのカバレッジ80.0%以上、新規コードの重複率3.0%以下です。SonarQube Cloudのドキュメントは、指摘0件の代わりに信頼性・セキュリティ・保守性の各レーティングAを条件に挙げています。さらに既定では、新規の行が20行に満たない変更では重複とカバレッジの条件を無視します。公式ドキュメントは、小さな変更で数行の未カバーが割合を大きく動かし、ゲート全体の判定を左右するのを避けるためだと説明しています(出典:SonarQube Server公式ドキュメント「Introduction to quality gates」)。
既存コード全体の数値を問わないのは、過去の負債を理由に今日の変更を止めないためです。重複の定義と解消手順はコードの重複とは何か? 定義と発生する典型的なケースを具体例と共に詳しく解説し、重複コード問題を理解する、SonarQube本体の機能と料金はSonarQube(ソナーキューブ)とは?できること・対応言語・料金とSonarLintとの違いをわかりやすく解説を参照してください。テスト工程の欠陥数で品質を見るならIPAの基準値を使う方法もあり、バグ密度・テスト密度の目安|IPA基準値と計算式・ゾーン分析にまとめています。
ESLintとJestでCIを失敗させる設定
CIで品質を止める仕組みは単純で、チェックツールが終了コード1を返せばジョブが失敗します。以下はNode.js 26.5.0、ESLint 10.11.0、Jest 30.5.2で実行した結果です。
ESLintで関数の複雑度を上限10に固定する設定
フラット設定(eslint.config.js)で上限を明示します。複雑度と一緒に、ネストの深さと引数の数も縛っておくと、分岐を別関数に逃がさずに深いネストへ押し込む書き方も止められます。
// eslint.config.js
export default [
{
files: ["src/**/*.js"],
rules: {
complexity: ["error", { max: 10 }],
"max-depth": ["error", 4],
"max-params": ["error", 4],
},
},
];
会員割引・年齢割引・クーポン・週末料金・カテゴリ別税率を1関数で計算する次のサンプルを対象にします。
// src/price.js
export function calcPrice(item, user, coupon, date) {
let price = item.price;
if (user.isMember) price *= 0.9;
if (user.age >= 65) price *= 0.95;
if (coupon && coupon.type === "fixed") price -= coupon.value;
else if (coupon && coupon.type === "rate") price *= 1 - coupon.value;
if (date.getDay() === 0 || date.getDay() === 6) price *= 1.1;
if (item.category === "food") price *= 1.08;
else if (item.category === "book") price *= 1.1;
if (price < 0) price = 0;
return Math.round(price);
}
掲載の設定でESLintを実行すると、次のように失敗します。
$ npx eslint src
/private/tmp/cq5209/src/price.js
1:8 error Function 'calcPrice' has a complexity of 12. Maximum allowed is 10 complexity
✖ 1 problem (1 error, 0 warnings)
$ echo $?
1
ifが6つ、else ifが2つ、&&が2つ、||が1つで計11、基本の1を足して12です。割引・料金・税率の3つの関数に分ければ、それぞれ上限内に収まります。
Jestのカバレッジ閾値でジョブを落とす設定
JestのcoverageThresholdを書くと、テストがすべて成功しても閾値を下回った時点で終了コード1になります。この例では行(lines)と分岐(branches)の両方を指定します。ただし、以下のglobal設定は収集対象全体に適用され、新規・変更行だけを判定する設定ではありません。既存コードで変更分だけを判定するには、SonarQubeなどの差分カバレッジ判定を別途用意します。
// jest.config.js
export default {
collectCoverage: true,
collectCoverageFrom: ["src/**/*.js"],
coverageThreshold: {
global: { branches: 80, lines: 80 },
},
};
先ほどの関数に「会員は1割引」のテストを1本だけ書き、NODE_OPTIONS=--experimental-vm-modules npx jestで実行します。
// src/price.test.js
import { calcPrice } from "./price.js";
test("会員は1割引", () => {
const item = { price: 1000, category: "other" };
const user = { isMember: true, age: 30 };
expect(calcPrice(item, user, null, new Date("2026-09-24"))).toBe(900);
});
結果は次のとおりです。
----------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
----------|---------|----------|---------|---------|-------------------
All files | 61.11 | 54.54 | 100 | 100 |
price.js | 61.11 | 54.54 | 100 | 100 | 3-10
----------|---------|----------|---------|---------|-------------------
Jest: Coverage for branches (54.54%) does not meet "global" threshold (80%)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
$ echo $?
1
行カバレッジは100%です。if文をすべて1行で書いているため、条件が偽で本体を飛ばしても「その行は実行された」と数えられます。lines: 80だけの設定なら、このテスト1本でCIは通っていました。実際に止めたのは分岐の条件です。C0・C1・C2の違いと目標の決め方はテストカバレッジとは?C0/C1/C2の網羅率と計測ツール・目標設定を実装者向けに解説で詳しく扱っています。
GitHub Actionsと必須ステータスチェックでマージを止める設定
上の2つをプルリクエストごとに実行するワークフローです。npm ciはロックファイルが無いと失敗するため、先にESLintとJestを開発依存へ追加し、生成されたpackage.jsonとpackage-lock.jsonをコミットしておきます。設定ファイルをexport defaultで書いているので、パッケージはESMにします。
npm install --save-dev --save-exact [email protected] [email protected]
npm pkg set type=module
JestのESMサポートは実験的機能のため、ワークフローではNODE_OPTIONSを渡しています。
# .github/workflows/quality.yml
name: quality
on:
pull_request:
jobs:
quality:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 24
- run: npm ci
- run: npx eslint src
- run: npx jest
env:
NODE_OPTIONS: --experimental-vm-modules
ワークフローを置いただけでは、失敗してもマージボタンは押せます。ブランチ保護ルールかルールセットで、ジョブ名qualityを必須ステータスチェックに登録して初めて止まります。GitHubのドキュメント「Troubleshooting required status checks」には、設定時に見落としやすい挙動が3つ書かれています。
- 必須に選べるのは、そのリポジトリで過去7日以内に成功したチェックだけです。ワークフローを入れた直後は、一度成功させてから登録します。
pathsなどのフィルターでワークフローごとスキップされると、チェックは「Pending」のまま残り、マージを塞ぎます。必須チェックにするワークフローにはパスフィルターをかけないでください。- ジョブが
ifの条件でスキップされると「Success」扱いになり、失敗したジョブにneedsで依存するジョブはスキップされてマージを止めない場合があります。必須にするジョブを他のジョブにぶら下げるならalways()を併用します。
チェックアウトの既定動作の変更点はactions/checkoutとは|v7の既定変更・主要入力と浅いクローンの壊れ方、カバレッジの推移をプルリクエストに表示したい場合はCodecovとは|GitHub Actions連携とcodecov.ymlの設定手順が参考になります。
既存コードへ品質ゲートを入れるときの判断基準
数年運用したリポジトリに、全体カバレッジ80%や全関数の複雑度10以下を初日から課すのは避けてください。既存コードが閾値を大きく下回る状態では、変更と無関係な箇所の違反でも毎回CIが失敗します。こうした運用では、ゲートの無効化や、アサーションの無いテストによる数値合わせを招くおそれがあります。
入れる順番は次のとおりです。
- フォーマッターとLintの自動修正をまとめて1回だけ適用し、以後は差分に整形が混ざらないようにします。
- Lintのルールは、既存違反が0件になるものから
errorにします。違反が残るルールはwarnで件数を記録し、現在の警告総数をNとしてnpx eslint src --max-warnings Nで上限を設定します。警告が減ったらNも引き下げます。 - カバレッジ・重複・新規の指摘は、プルリクエストの変更分だけに条件をかけます。SonarQubeはプルリクエストの解析で新規コードの条件だけを適用する設計です。
- 全体の数値は目標ではなく推移として見ます。新規コードの条件を守っても全体値が必ず改善するとは限らないため、カバレッジは上昇、重複率や指摘件数は減少しているかを継続して確認します。
判断に迷ったときの基準として、Googleのコードレビュー指針「The Standard of Code Review」の一文が使えます。「In general, reviewers should favor approving a CL once it is in a state where it definitely improves the overall code health of the system being worked on, even if the CL isn’t perfect.」完璧でなくても、全体の健全性を確実に上げる変更なら通す、という考え方です。触った箇所を少しだけ良くして戻す習慣はボーイスカウトルールとは?プログラミングでの実践範囲とGitでの差分分離、構造を変える手順はリファクタリングとは?24の観点とタイミング・進め方を参照してください。
品質ゲートをかけないほうがよい場面もあります。捨てる前提の検証用コード、コード生成ツールの出力、ベンダーから取り込んだライブラリは、collectCoverageFromやLintのignoresで計測対象から外します。生成物に閾値をかけると、生成器を直すべき問題を手作業のテストで埋めることになります。
機械に任せる項目とレビューで人が見る項目の分担
数えられるものは機械に、意味の判断は人に任せると、レビューの指摘がインデントや命名規則の揺れで埋まらなくなります。
| 項目 | 担当 | 手段 |
|---|---|---|
| 整形・インデント | 機械 | フォーマッター |
| 複雑度・ネスト・引数の数 | 機械 | Lint |
| カバレッジ・重複率 | 機械 | テストランナー・品質ゲート |
| 既知の脆弱性パターン | 機械 | SAST |
| 名前が意図を表しているか | 人 | レビュー |
| 設計・責務の置き場所 | 人 | レビュー |
| テストが仕様を検証しているか | 人 | レビュー |
最後の行が見落とされがちです。カバレッジは「実行されたか」しか数えないので、期待値を確かめないテストでも数字は上がります。脆弱性の自動検出をCIに入れる方法はSASTとは?DAST・SCAとの違いとCI/CDへの組み込み・導入判断を実装視点で解説、人が見る観点の決め方はコードレビューとは?目的・観点・進め方と実装現場の運用設計を解説、命名や読みやすさの原則はリーダブルコードとは?読みやすいコードの4原則と書籍のエッセンスにあります。
よくある質問
ソースコードの品質を上げるには何から始めればよいですか?
JavaScriptなら、フォーマッターとESLintをGitHub Actionsで実行し、そのジョブを必須ステータスチェックに登録するところから始めます。設定した整形ルールやLintルールへの違反を自動検出できるため、レビューで同じ指摘を繰り返す手間を減らせます。カバレッジや重複の条件は、その後に変更分だけへかけます。
コード品質の指標は何を見ればよいですか?
関数単位の循環的複雑度、新規コードのカバレッジ(分岐を含む)、新規コードの重複率、静的解析の新規指摘件数の4つが、自動で測れて閾値の出典もはっきりしています。可読性や設計の妥当性は数値化できないため、レビューで見ます。
カバレッジは何%を目標にすべきですか?
全体の目標値を1つ決めるより、新規コードに80%をかけるSonarQubeの既定から始めるのが無難です。行だけでなく分岐の閾値も指定してください。本文の例のように、行100%でも分岐は54.54%ということがあります。
コーディング規約とLintの違いは何ですか?
コーディング規約はチームで決めた書き方のルールの文書で、Lintはそのうち機械で判定できる部分を自動でチェックするツールです。例えば、関数の引数を4個以下にする規約は、ESLintのmax-paramsルールで検査できます。規約のうちLintの設定に落とせたものだけがCIで守られるので、規約を書いたら対応するルールを設定ファイルに入れます。
可読性・保守性・拡張性はどう違いますか?
可読性はコードを読んで意図がわかるか、保守性は不具合修正や変更を安全に行えるか、拡張性は機能を追加するときに既存部分を大きく直さずに済むかを指します。ISO/IEC 25010では可読性に近い性質は保守性の副特性「解析性」に当たります。拡張性という特性名は規格に無く、機能追加のしやすさなら保守性の「修正性」、処理量の増減への対応なら2023年版で柔軟性(flexibility)に追加された「スケーラビリティ」が近い概念です。