のーずいだんぷ

主に自分用メモですが、もしかしたら誰かの役に立つかもしれません

複数言語が混在したプロジェクトでlinterとformatterをgit-commitにフックする

はじめに

linter&formatterの導入を解説した記事はかなりたくさんネット上にあって非常に助かるのだが、いずれもシンプルか自分のニーズにずれている部分があるため自分で書くこととした。

この手の話は、IDEを使えばいいじゃん、で片付きそうであるがチームで共通化させようとするとエディタの好みは様々であるし、そもそもエディタの設定を共通化すること自体が結構難しいと思う。

というわけで、現在も一定のニーズはあると思っているので、今後誰かの役に立てれば幸いである。

複数言語をすべてgit-hookで連携するのはしんどい

業務で使用しているプロジェクトは多くの場合、UIとバックエンドが混在したリポジトリだったりで、単一言語で構成されるものは少ないのではないかと思う。

その場合ひとつのツールでgit-hookを連携したくなるもので、今回はPython製のpre-commitを使用することでそれが解決できたのその備忘録として残しておく。

git-hook連携ツール:pre-commitを使用する

pre-commitはしっかりしたドキュメントが以下にある。

pre-commit.com

このツールの良いところは、複数言語対応しているところで、公式ページには以下の言語が紹介されていた。

  • conda
  • docker
  • docker_image
  • fail
  • golang
  • node
  • python
  • python_venv
  • ruby
  • rust
  • swift
  • pcre
  • pygrep
  • script
  • system

実は上記以外にも連携させる裏技があるようで、例えばScalaなんかもできそうだったのだが、今回はうまく行かなかったので、後日チャレンジとしたい。


インストール

$ pip install pre-commit

設定ファイル

ルートディレクトリに.pre-commit-config.yamlを作成する。

例えば、例としてpre-commit自身が使用している設定を見ると次のようになっている。

repos:
-   repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v2.1.0
    hooks:
    -   id: trailing-whitespace
    -   id: end-of-file-fixer
    -   id: check-docstring-first
    -   id: check-json
    -   id: check-yaml
    -   id: debug-statements
    -   id: name-tests-test
    -   id: requirements-txt-fixer
    -   id: double-quote-string-fixer
-   repo: https://gitlab.com/pycqa/flake8
    rev: 3.7.7
    hooks:
    -   id: flake8
-   repo: https://github.com/pre-commit/mirrors-autopep8
    rev: v1.4.3
    hooks:
    -   id: autopep8
-   repo: https://github.com/pre-commit/pre-commit
    rev: v1.14.4
    hooks:
    -   id: validate_manifest
-   repo: https://github.com/asottile/pyupgrade
    rev: v1.12.0
    hooks:
    -   id: pyupgrade
-   repo: https://github.com/asottile/reorder_python_imports
    rev: v1.4.0
    hooks:
    -   id: reorder-python-imports
        language_version: python3
-   repo: https://github.com/asottile/add-trailing-comma
    rev: v1.0.0
    hooks:
    -   id: add-trailing-comma
-   repo: meta
    hooks:
    -   id: check-hooks-apply
    -   id: check-useless-excludes

見るとなんとなくわかると思うが、repoでgithubのレポジトリURLを指定している。これでpre-commitがrevで指定したバージョンを初めて実行するときにinstallしてくれる。

ちなみに、すべてのhookは以下にまとまっているので探すのは簡単である。

設定の反映

$ pre-commit install

単純な実行

pre-commitはgit-hookとして以外にも、各ツールのラッパーCLIを持っているので、簡単に実行が可能。

# srcディレクトリ以下の`.py`に対してflake8を実行
$ pre-commit run flake8 --files src/*

# リポジトリ全てのファイルに対して全てのhookを実行
$ pre-commit run --all-files

汎用的なチェック

冒頭の例でも記載したが、pre-commit-hooksリポジトリには汎用的なcheker(formatter)が用意されており、かなり便利だったので今回使用したものを記載しておく。

  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v2.4.0
    hooks:
      - id: detect-aws-credentials
        args: ["--allow-missing-credentials"]
      - id: trailing-whitespace
      - id: end-of-file-fixer
      - id: check-yaml
      - id: check-added-large-files
      - id: check-toml
      - id: detect-private-key
      - id: check-json
  • trailing-whitespace -> 末尾の空白除去
  • end-of-file-fixer -> ファイル末尾に改行挿入
  • check-yaml -> YAMLファイルの構文チェッカー
  • check-toml -> TOMLファイルの構文チェッカー
  • check-json -> JSONファイルの構文チェッカー
  • detect-aws-credentials -> AWSのシークレットアクセスキーが紛れ込んでないかチェック
  • detect-private-key -> 秘密鍵が紛れ込んでいないかチェック

個人的に機密情報のチェッカーはかなりありがたかった。

どこまで検知してくれるのかはまだわかっていない(AWSはセットしているプロファイルからだけ?)ので過信は禁物だが、ちゃんと検知してくれるならかなりありがたいチェックだ。

各言語の方針

Python

調べたところ、Pythonの静的解析ツールは非常に沢山あってよくわからん。

今回は以下を参考にさせていただき、次のツールを使うこととした。

blog.hirokiky.org

formatter

  • black
  • isort

linter

  • flake8

ちなみにisortに関しては、pythonファイル内で、sys.path.append(..)みたいにスクリプト内でPYTHONPATHを調整していると順序を書き換えられてうまく動かなる。そもそもスクリプト内での調整自体が推奨されていないとのことなので、この際に別の手を考えたい。

この場合準備すべき点は2つ

  1. .pre-commit-config.yamlにフックの設定
  2. 各ツールの設定ファイルを作成

pre-commitへのhook設置

hook設置は冒頭で説明したとおり、基本の構成でよい。 一点注意があるとすれば、今回はblackとisortの設定ファイルをpyproject.tomlで管理するのだが、isortに関しては引数(additional_dependencies...intall時の引数となるもの)にtomlを指定してやる必要がある。

  - repo: https://github.com/ambv/black
    rev: stable
    hooks:
      - id: black
        verbose: true

  - repo: https://gitlab.com/pycqa/flake8
    rev: 3.7.7
    hooks:
      - id: flake8
        verbose: true

  - repo: https://github.com/pre-commit/mirrors-isort
    rev: v4.3.20
    hooks:
      - id: isort
        verbose: true
        additional_dependencies: [toml]

各設定ファイル

  • .flake8
[flake8]
ignore = E203,W503,W504
max-line-length = 99
max-complexity = 18
select = B,C,E,F,W,T4,B9
  • pyproject.toml
[tool.black]
line-length = 99
include = '\.pyi?$'
exclude = '''
/(
    \.git
  | \.hg
  | \.mypy_cache
  | \.tox
  | \.venv
  | _build
  | buck-out
  | build
  | dist
  | \.idea
  | node
  | project
  | \.sbt
  | node_modules
  | out
  | package.json
  | package-lock.json
  | \.gitignore
)/
'''

[tool.isort]
include_trailing_comma = true
line_length = 99
multi_line_output = 3
force_grid_wrap = 0
use_parentheses = true

ここでの注意は3点ある。

  1. tool.*は意味のある書き方
  2. .flake8max-line-lengthpyproject.tomlline-lengthと合わせる。
  3. flake8とblackはいくつか競合する項目がある。ignoreのとおり無視する必要がある。 詳細はblackのgithubを見ると記載があるので参考にされたい。

Node.js

node.jsについてはJavaScriptとして、フロントエンドで使用するとき同様に

  • linter -> eslint
  • formatter -> prettier

で対応した。

ここもやることはPython同様に

  1. .pre-commit-config.yamlにフックの設定
  2. 各ツールの設定ファイルを作成

で進めていく。

1. pre-commitへフック設置

先程も説明したとおり、additional_dependenciesがinstallする際のオプションのような役割となる。 eslintのプラグインも同様に当該ディレクティブへリストで記載する。

  - repo: https://github.com/prettier/prettier
    rev: 1.19.1
    hooks:
      - id: prettier
        verbose: true

  - repo: https://github.com/pre-commit/mirrors-eslint
    rev: v6.8.0
    hooks:
      - id: eslint
        verbose: true
        args: ["--fix"]
        additional_dependencies:
          - eslint@6.8.0
          - eslint-config-google@0.14.0
          - eslint-plugin-node@11.0.0
          - eslint-config-prettier@6.9.0

ここで新しいディレクティブ:argsが出現しているが、これはCLI実行時のオプションと等価で、この場合eslint --fixを実行したのと同じ結果が得られる。

あと、eslintに関してはverbosetureにしたほうが良い。これによりwarnが表示されるようになる。

個人的にはいくつかerrorとしたくないルールもあるので、その時々でwarnを見ながら意図したもの確認するようにした。

2. 設定ファイルの作成

設定自体は色々なたたき台がネット上に用意されているが、今回はNode.jsとして使用する初めての経験だったので記載しておく。

{
  "env": {
    "es6": true,
    "node": true
  },
  "extends": [
    "eslint:recommended",
    "google",
    "plugin:node/recommended",
    "prettier"
  ],
  "parserOptions": {
    "sourceType": "module"
  },
  "plugins": ["node"],
  "rules": {
    "camelcase": "warn",
    "no-empty": "warn",
    "no-undef": "warn",
    "no-var": "warn",
    "node/no-unsupported-features": [
      "error",
      {
        "version": 8
      }
    ],
    "require-jsdoc": "off"
  }
}

終わりに

今回はこれらをイチから作成したが、正直かなりめんどくさかった。

今後はcookiecutterのテンプレートを自作して、それに取り込むとか考えたい。

後はScalafmtとの連携についても後日調査したいと思う。

参考

ツール集

GitHub - pre-commit/pre-commit: A framework for managing and maintaining multi-language pre-commit hooks.

GitHub - pre-commit/pre-commit-hooks: Some out-of-the-box hooks for pre-commit

GitHub - prettier/prettier: Prettier is an opinionated code formatter.

GitHub - pre-commit/mirrors-eslint: Mirror of eslint node package for pre-commit.

GitHub - psf/black: The uncompromising Python code formatter

GitHub - pre-commit/mirrors-isort: Mirror of the isort package for pre-commit.

eslint-plugin

GitHub - google/eslint-config-google: ESLint shareable config for the Google JavaScript style guide

GitHub - mysticatea/eslint-plugin-node: Additional ESLint's rules for Node.js

GitHub - prettier/eslint-config-prettier: Turns off all rules that are unnecessary or might conflict with Prettier.

失敗したScalafmt + pre-commit

Reusable pre-commit hooks in Scala projects - SoftwareMill Tech Blog

Code formatting: scalafmt and the git pre-commit hook

その他

eslintの設定からprettierとの併用までの流れ - Qiita

ESLint 最初の一歩 - Qiita

Step by Stepで始めるESLint - Qiita

pre-commit時にformatterを実行する - Qiita

Python製のツールpre-commitでGitのpre-commit hookを楽々管理!! | Developers.IO

【Python】sys.pathに追加したディレクトリからimportする処理でPEP8に違反した際の対処 - Qiita

Pythonコードの安全を保つSAST(静的解析)ツール ~Bandit, Pyt~ - 好奇心の足跡