• #Python
  • #リンター
  • #テスト
  • #静的解析
  • #コード品質
開発未分類

リンターとテストの違い - Pythonカスタムリンター作成を例に

結論:リンターとテストは「いつ」「何を」検査するかが違う

本記事では「リンター」を静的解析ツールの代表例として扱う。厳密には、静的解析には型検査やデータフロー解析なども含まれ、リンターはその一部である。

項目リンター(静的解析の一種)テスト(動的検証)
実行タイミングコードを実行せずに検査コードを実際に実行して検査
検査対象コードの書き方・パターンコードの動作・出力
検出できること誤字、危険なパターン、スタイル違反バグ、期待と異なる出力、エッジケース
速度(一般的な傾向)速い場合が多い実行が必要な分、遅い場合が多い

※速度は解析の種類や規模により逆転することもある。ユニットテストは高速で、大規模な静的解析は重いケースもある。

具体例で理解する

リンターが検出する問題

# リンターは「この書き方は危険」と警告する
cursor.execute(f"SELECT * FROM users WHERE id = {user_id}")

リンターはコードを実行せず、構文解析やパターンマッチで「f-stringでSQLを組み立てている」ことを検出する。実際にSQLインジェクションが起きるかどうかは関係なく、危険な書き方そのものを指摘する。

テストが検出する問題

def calculate_tax(amount):
    return amount * 0.1

# テストコード
def test_calculate_tax():
    assert calculate_tax(1000) == 100  # 期待: 100
    assert calculate_tax(0) == 0       # 期待: 0

テストはコードを実際に実行して、入力に対する出力が期待通りかを検証する。

実践:プロジェクト固有のカスタムリンター作成

税理士業務アプリ「Tax Assistant」用に、プロジェクト固有のルールを持つリンターを作成した。

検出ルール

ルール検出内容理由
shiwake-typo「仕分け」→「仕訳」の誤字会計用語は「仕訳」が正しい
sql-injectionexecute()にf-stringを渡しパラメータなしSQLインジェクション脆弱性
no-float-money金額計算でfloat使用浮動小数点誤差を防ぐため
hardcoded-secretパスワード・APIキーのハードコードセキュリティリスク

実装例:仕訳誤字検出

def check_shiwake_typo(filename: str, source: str) -> Iterator[LintError]:
    """「仕分け」の誤字を検出。正しくは「仕訳」"""
    for i, line in enumerate(source.splitlines(), start=1):
        matches = list(re.finditer(r"仕分け", line))
        for match in matches:
            yield LintError(
                file=filename,
                line=i,
                column=match.start(),
                rule="shiwake-typo",
                message="「仕分け」→「仕訳」の誤字です",
                severity="error",
            )

このチェッカーは:

  • ファイルを1行ずつ読み込む
  • 正規表現で「仕分け」というパターンを探す
  • 見つかったら行番号と位置を記録

実際にPythonコードとして実行するのではなく、テキストとしてパターンマッチしている。

実装例:SQLインジェクション検出(AST解析)

class RawSQLChecker(ast.NodeVisitor):
    """execute()にf-stringを渡し、パラメータなしの場合のみ検出"""

    def visit_Call(self, node: ast.Call) -> None:
        if isinstance(node.func, ast.Attribute) and node.func.attr == "execute":
            if node.args:
                first_arg = node.args[0]
                has_params = len(node.args) >= 2

                # f-string でパラメータなしの場合のみ警告
                if isinstance(first_arg, ast.JoinedStr) and not has_params:
                    self.errors.append(LintError(...))

Pythonのastモジュールを使うと、コードを構文木として解析できる。これにより:

  • execute()という関数呼び出しを特定
  • 第1引数がf-stringかどうかを判定
  • 第2引数(パラメータ)があるかどうかを確認

構文木を静的に解析しているだけで、対象コードを実行しているわけではない。

誤検知(False Positive)との戦い

最初のリンターは22件のエラーを検出したが、確認すると多くが誤検知だった。

# 誤検知の例:このコードは安全
where_clause = f"WHERE {' AND '.join(conditions)}"
rows = conn.execute(f"""
    SELECT * FROM users
    {where_clause}
""", params)  # ← paramsでパラメータを渡している

このコードはf-stringを使っているが、実際の値はparamsで安全に渡している。また、where_clauseの構造部分(列名やAND演算子)はコード内で固定されており、ユーザー入力由来ではない。

「パラメータ渡しがある場合は除外」するロジックを追加した結果、22件 → 4件に減少した。

注意: SQL構造部分(列名、ORDER BY、演算子など)にユーザー入力を使う場合は、パラメータ化できないため別途ホワイトリスト検証が必要。

リンターとテストの使い分け

リンターが得意なこと

  • コーディングスタイルの統一
  • セキュリティリスクのあるパターン検出
  • プロジェクト固有のルール適用(用語の統一など)
  • 高速なフィードバック(CI/CDで毎回実行)

テストが得意なこと

  • ビジネスロジックの検証
  • エッジケースの確認
  • リファクタリング後の動作保証
  • 期待する入出力の明文化

両方使うのがベストプラクティス

開発フロー:
  1. コード編集
  2. リンター実行 → 書き方の問題を即座に検出
  3. テスト実行 → 動作の問題を検出
  4. コミット

まとめ

  • リンター:コードを実行せず、構文解析やパターンマッチで「書き方」を検査(静的解析の一種)
  • テスト:コードを実行して、「動作」を検証
  • プロジェクト固有のルールはカスタムリンターで実装できる
  • 誤検知を減らすには、検出ロジックの精度向上が必要
  • リンターとテストは役割が異なるので、両方使うのがベスト