0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Formatter & Lint チェックのCIを作成してみた。

Last updated at Posted at 2024-06-06

これは何?

  • VSCodeでのPythonのLint、Formatterの設定方法
  • GitHub ActionsでのLintチェックの導入方法
  • pre-commit を使ったLintチェックの設定方法

Goal (Where)

  • チーム全員が共通のコードフォーマットにて開発できる。
  • 低レベルなバグや不要なコードに気が付きやすくする。
  • そのための方法はできるだけ自動化されている。

目的 (Why)

  • 静的解析を行うことで、低レベルなバグやミスの発生を抑える。
    • 低レベルなバグ: 未使用のimportや構文ミスなど
  • チームの全員が共通のルールでコードを書くことにより、コードチェックのコストを減らす。
  • コードフォーマットについて考える、作業する工数を減らす。
環境
  • Python 3.9 (Django)
  • VSCode

前提

Lintとは?

lint とは、主にC言語ソースコードに対し、コンパイラよりも詳細かつ厳密なチェックを行なうプログラムである。静的解析ツールとも呼ばれる。以下に挙げるように、コンパイラではチェックされないが、バグの原因になるような曖昧な記述についても警告される。構文(シンタックス)レベルのチェックだけでなく、意味(セマンティクス)レベルのチェックまで実行するものもある。
Wikipedia lint (https://ja.wikipedia.org/wiki/Lint)

フォーマッターとは?

コンピュータプログラムの開発ツールの一種で、開発者がプログラミング言語で記述したソースコードを、一定のルールに従って整形してくれるものをフォーマッターという。
同じ動作を行うプログラムでも、行頭にどのくらいスペースを空けるかなど、書き方には無数のバリエーションがあり、人によって好みの書き方が異なることも多い。フォーマッターはコードの書き方に一定の決まり(コーディング規約)を設け、これに従ってコードの意味を変えずに書式を調整する。
IT用語辞典 フォーマッター (https://e-words.jp/w/フォーマッター.html)

  • Lintは元々C言語用に作成された静的解析ツールであり、現在は他言語のコードに対して構文チェックや静的解析を行なうプログラムも含めて lintと呼ぶ。
  • 意味上は、lintはコード構文などの静的解析を行うことで不要なコードの検知や低レベルのバグの防止を行い、フォーマッターはコードフォーマットを整形する役目を持つ。

簡単に調べてみましたが、一言で lint と言ってもパッケージによって実行できる範囲は違うため、あまり言葉にとらわれず、必要な機能を軸に考えた方がいい気もしました。

行ったこと

基本的には全員が最初から、IDEにて共通の設定をしていれば問題ない気もしましたが、プロジェクトが進んでいく間に、未フォーマットのコードが紛れ込む可能性もありそのため、GitHub ActionsでもLintチェックを行うこととしました。

VSCodeでの設定

上記の3つは全て、VSCodeのExtensionが用意されているため、もしチームがVSCodeを使用していればすぐに導入することができるようになります。未導入の人には導入を促すポップアップが出るように、rootディレクトリに以下ファイルを作成、設定しました。

.vscode/extensions.json
{
  "recommendations": [
    "ms-python.python",
    "ms-python.flake8",
    "ms-python.black",
    "ms-python.vscode-pylance",
    "ms-python.isort"
  ]
}

またExtensionsの設定を .vscode/settings.json に設定しています。
Djangoのため、いくつかのファイルを対象外としています。

.vscode/settings.json
{
  "python.languageServer": "Pylance",
  "[python]": {
    "editor.defaultFormatter": "ms-python.black-formatter",
    "editor.formatOnSave": true,
    "editor.codeActionsOnSave": {
      "source.organizeImports": "explicit"
    }
  },

  "black-formatter.args": ["--line-length", "120", "--force-exclude", "(migrations|deploy)"],
  "isort.args": ["--profile", "black", "--extend-skip", "migrations", "--extend-skip", "deploy"],
  "flake8.args": [
    "--max-line-length=120",
    "--ignore=E203,W503,W504",
    "--extend-exclude=migrations,deploy"
  ]
}

GitHub Workflowの設定

今回はPRの作成、更新、再開に合わせてVSCodeの設定と同じフォーマットチェックをし、フォーマット整形すべき箇所があれば、 FAILEDさせるようにしたいと思います。

.github/workflows/lint.yml
name: Lint

on:
  workflow_dispatch:
  pull_request:
    branches: []
    types: [opened, synchronize, reopened]

jobs:
  build:
    runs-on: ubuntu-20.04
    env:
      MAX_LINE_LENGTH: 120
    strategy:
      max-parallel: 4

    steps:
      - name: Set up Python 3.9.6
        uses: actions/setup-python@v5
        with:
          python-version: '3.9.6'
      - name: Install Dependencies
        run: |
          python -m pip install --upgrade pip
          pip install black flake8 isort

      - name: Checkout to PR branch
        uses: actions/checkout@v4
        with:
          ref: ${{ github.event.pull_request.head.ref }}
          fetch-depth: 0

      - name: Fetch base branch
        run: git fetch origin ${{ github.base_ref }}:${{ github.base_ref }}

      - name: Check diff files and lint
        run: |
          DIFF_FILES=$(git diff --name-only --diff-filter=d ${{ github.base_ref}} HEAD | (grep '\.py$' || true))
          if [ -z "$DIFF_FILES" ]; then
          echo "No files to lint"
          exit 0
          fi
          echo "Running flake8 lint check for only changed files"
          for FILE in $DIFF_FILES; do
            flake8 $FILE || (echo "flake8 found issues in $FILE" && exit 1)
          done

          echo "Running black format check for all files"
          black . --check \
          --line-length=${MAX_LINE_LENGTH}  \
          --force-exclude="(migrations|deploy)" || (echo "black found issues." && exit 1)

          echo "Running isort format check for all files"
          isort . --check --profile="black" \
          --extend-skip="migrations" \
          --extend-skip="deploy" || (echo "isort found issues." && exit 1)

flake8 の設定は別でファイルを用意して設定を行いました。

.flake8
[flake8]
max-line-length = 120
extend-ignore = E203,W503,W504
extend-exclude = node_modules,__init__.py,migrations,deploy,.venv

検証

設定がうまくできているかの検証です。以下2点で問題ないことを確認しました。

  • VSCodeではファイル保存時に、自動的に black , isort によるフォーマットがされること。
    • 設定した対象外ファイルではフォーマットがされないこと。
  • GitHub Actionsにて動作していること。

補足

場合によっては、GitHub Actionsを使用できないこともあると思います。自分のプロジェクトでも一時的に無料枠がなくなり、月を跨ぐまで使用できなくなる事態が起こりました。その際は、localでのcommit時に動かす pre-commit が解決策となりました。

パッケージインストール

チーム開発で他の人とバージョンを合わせるため requirements.txt ファイルに記載することが多いかと思います。

requirements.txt
# Formatting and linting
black~=24.4.0
flake8~=7.0.0
isort~=5.13.0
pip install -r requirements.txt

pre-commit スクリプト作成、設定

  • 以下ファイルをrootディレクトリに作成し、 ./pre-commit install を実施。
    • その後はgitにてコミットコマンドを実行するたびに、スクリプトが実行され、成功したら commit 、失敗したら commit しないという動作が実現できます。
pre-commit.sh
#!/usr/bin/env bash

# This is a pre-commit hook that validates code formatting.
#
# Install this by running the script with an argument of "install",
# which installs a symlink to .git/hooks/precommit:
# $ ln -s ../../hooks/pre-commit .git/hooks/pre-commit

root="$(git rev-parse --show-toplevel 2>/dev/null)"

set -e

# Some sanity checking.
[[ -n "$root" ]]

# Installation.
if [[ "$1" == "install" ]]; then
    hook="$root"/.git/hooks/pre-commit
    if [[ ! -e "$hook" ]]; then
        ln -s ../../pre-commit "$hook"
        echo "Installed git pre-commit hook at $hook"
    else
        echo "Hook already installed"
    fi
    exit
fi

save_merge_files() {
    # Move MERGE_[HEAD|MODE|MSG] files to the root directory, and let `git stash push` save them.
    for f in "$root"/.git/MERGE_*; do
        if [[ -e "$f" ]]; then
            t=$(basename $f)
            mv -f "$f" "$t"
        fi
    done
}

restore_merge_files() {
    # Moves MERGE files restored by `git stash pop` back into .git/ directory.
    for f in MERGE_*; do
        if [[ -e "$f" ]]; then
            if [[ -e "${root}/.git/${f}" ]]; then
                echo "Failed to restore ${f}. File already exists" 1>&2
            else
                mv -f "$f" "${root}/.git/${f}"
            fi
        fi
    done
}

# Check formatting.
echo "Check formatting"
stashfile=$(mktemp .pre-commit.stashXXXXXX)
trap 'set +e;git stash pop -q; rm -f "$stashfile"; restore_merge_files' EXIT
save_merge_files
git stash push -k -u -q -m "pre-commit stash"

len=120
if ! errors=($(black --line-length=${len} --force-exclude="(node_modules|deploy|migrations)" --check .)); then
    echo "Formatting errors found."
    exit 1
fi

if ! errors=($(isort . --check --profile="black" --extend-skip=deploy --extend-skip=migrations)); then
    echo "Import order errors found."
    exit 1
fi

if ! errors=($(flake8)); then
    echo "flake8 found issues."
    exit 1
fi

echo "Run pre-commit checks"

check() {
    msg="$1"
    shift
    if "$@"; then
        echo "$msg: OK"
    else
        res=$?
        echo "-----------------------"
        echo "$msg: FAILED ($res)"
        echo "  $@"
        exit $res
    fi
}

最後に

ここまで読んでいただきありがとうございました。
LintFormatterを初めて設定してみましたが、自動化により細かいミスを防ぐことができ、また開発者も改行などの細かいコーディング規約を意識することなく、コーディングできるというメリットがあると思います。
他の言語でも基本的な設定方法は同じかと思うので、今後はかならず実施していきたいと思います。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?