perlcriticとのつきあい方
うたがわきき (utagawakiki@gmail.com)
自己紹介
utgwkk.icon
うたがわきき (utgwkk)
京都にいます
京大マイコンクラブ (KMC)
京都大学情報学研究科 (休学中)
株式会社はてな アルバイトエンジニア
アジェンダ
linterを使うモチベーション
perlcriticとは
perlcriticを導入する
perlcriticとのつきあい方
「やり方はひとつじゃない」とは言うけれども
いわゆるTIMTOWTDI
pronounced Tim Toady
集団開発で全員が好き勝手にコードを書いたらめちゃくちゃになる
ある程度の規律が欲しい
規律
社内wikiに文章化したコーディング規約
例外を捕捉するときにはTry::Tiny使う
一般に従うべき規則・原則
プライベートメソッドを別のクラスから呼ばない
AnotherClass->_private_method(...)
コードレビュー
規律に従っているか
ロジックが要求仕様を満たすか
バグ・パフォーマンス上の懸念がないか
などなど
考えることがけっこうある
人間が気にすることを減らしたい
「規律に従っているか」は前提にした上で、それ以降のレビューをやりたい・受けたい
人間が気にすることを減らしたい
機械相手に試行錯誤する環境を作るとレビューコストが大幅減
Perlでもlinterを使いたい
Perlでも人間が気にすることを減らしたい
そこでperlcritic
perlcriticとは
Perlのlinter
PPIを使ってプログラムをパースして、ポリシー違反していたら報告する PPI - Parse, Analyze and Manipulate Perl (without perl)
使ってみよう
こういうモジュールを用意する
code:Foo.pm
package Foo;
use strict;
use warnings;
our $VERSION = '0.01';
!!1;
使ってみた
code:_
% perlcritic -1 Foo.pm
Module does not end with "1;" at line 6, column 1. Must end with a recognizable true value. (Severity: 4)
モジュールが 1; で終わってない、と警告される
perlcriticの概念
.perlcriticrc
ポリシー (policy)
severity
.perlcriticrc
perlcriticの設定ファイル
.perlcriticrcの例
INIファイルっぽい?
code:.perlcriticrc
severity = 3
program-extensions = .pl .t
equivalent_modules = Test2::V0
equivalent_modules = Test2::V0
ポリシー
perlcriticにおけるコーディング規約の単位
ポリシーの作り方
Perl::Critic::Policy という名前空間以下に作る
Perl::Critic::Policy クラスを継承する
必要なメソッドを実装する
ポリシーにはseverityが設定されている
severity
日本語にするなら「厳しさ」「重大度」
数字と英語表現がある
1, 2, 3, 4, 5
severityの数字が大きいほど従うべきポリシー
この発表では数字表現を使います
適用するポリシーのseverityの最小値を設定する
-1 -2 -3 -4 -5 コマンドライン引数
severity パラメータ (perlcriticrc)
数字が小さいほど厳しくlintされる
たとえば -4 を渡したら、severityが4または5のポリシーが適用される
組み込みポリシーの例
severity 3
プライベートメソッドを別のクラスから呼ばない
AnotherClass->_private_method(...)
perlcriticを導入する
perlcriticの概念がわかってきましたね
概念はわかったけど……
どういうふうに導入していくとよいのか?
導入していく
severity パラメータをいくつに設定すべきか
プロジェクトに合った設定をするには
CIに組み込むには
severity パラメータをいくつに設定すべきか
……を決める前に
ポイント
severityが大きいほど従うべきポリシー
severityが小さいポリシーは個人の意見という雰囲気が出がち
そもそも『Perlベストプラクティス』に従うべきかどうか、という話題
枯れているというよりは単に内容が古いことがある
2006/8 発行
よく読んだら「ベストプラクティス」というよりは意見に近いものもある
いきなり厳しくしない
初期設定で severity 1 (最も厳しい) にすると…
めちゃくちゃポリシー違反してるみたいに見える
それはそう!!!
なぜなら今まで導入していなかったから
なぜなら最も厳しいから
あれもこれも対応しないといけないの!!!???
そうとは限らない
プロジェクトに合ったやり方を見つける
導入前にやったこと
どこでどういうポリシーに引っかかっていて、そのポリシーのseverityがいくつなのか、を見る
perlcritic -1 --top=1000 --verbose "%f:%l:%c:%m (%P, Severity: %s)\n" lib
--verbose で出力のフォーマットを変更できる (いわゆるverbose modeとは違う)
ポリシー違反をグルーピングして検討する
従わなくてよいポリシーは無効化する
ポリシーを設定する
以上を踏まえて、severityをいくつにしたらよいか考えて設定する
従わなくてよいポリシーは無効化する
パッケージに $VERSION 変数を設定するべき、というポリシー
ライブラリを作るなら設定すべきだと思う
一般のプロダクトにも設定すべきというわけではなさそう
code:disable-require-version-var
ポリシーを設定する
map のブロック内には1つの文だけを書くべきというポリシー
map { sleep 1; create_entry(blog => $blog) } (1..3) のような処理が頻出
記事の作成日時をずらして順不定にしないための工夫
code:increase-map-block-statements
max_statements = 5 # mapのブロック内に5つの文まで書けるようにする
severity パラメータをいくつに設定すべきか (1つの答え)
severityを3にする
3以上はバグを回避するのに有益なことが多い
2以下はけっこう意見という感じが強い
正規表現には必ず /m フラグを付けましょう、とか
とにかくマジックナンバーを回避しましょう、とか
CIに組み込む
reviewdogと連携するのが便利
PRの差分の範囲だけ警告する、ということができる
自前でperlcriticコマンドを叩いて結果を絞り込む、という手もあるけどreviewdogがやってくれるのが楽
perlcriticは遅い?
高速ということはなさそう
計算資源や並列数で「遅さ」が気にならないようにする
CIで走らせる
テスト (prove) とperlcriticのジョブを並列に走らせる
ジョブを分割して並列数を稼ぐ
perlcriticとのつきあい方
銀の弾丸は存在しない
人間に優しいコードを書く
linterを補助輪にする
銀の弾丸は存在しない
複雑なロジックのミスは検出できない
あくまでも、コーディング規約に従うコードを書くためのツール
おすすめ手法
テストを書く
ペアプロ・モブプロする
Perlプログラムのパースは難しい
useの有無でパース結果が変わる
+ の有無でパース結果が変わる
indirect notation
PPIは完全にPerlインタプリタをシミュレートできるわけではない
がんばっている
PPI - Parse, Analyze and Manipulate Perl (without perl)
人間に優しいコードを書く
「あとからコードを読む人のことを考える」
省略できるからといって省略しすぎない
機械に優しければ読む人間にもだいたい優しい
linterを補助輪にする
linterに従えば、よくある間違いを修正していける、というスタイル
linterが厳しすぎて警告だらけになるのはつらい
適度な厳しさで安心してコードを書きたい
おわりに
人間が気にすることを減らすために、どうやってperlcriticを導入するか、についてお話ししました
この発表がperlcritic導入の一歩につながれば幸いです
-----
没ゾーン
utgwkk.icon 2023/2/9: 社内向けの下書きには残っていたけど公開されていなかった、せっかくなので供養します
severity
gentle 以外のニュアンスの違いが分からない
gentle (5)
stern (4)
harsh (3)
cruel (2)
brutal (1)
テーマ
ポリシーに設定できるカテゴリ・タグみたいなもの
テーマの例
core
組み込みポリシーのテーマ
pbp
「『Perlベストプラクティス』の何ページを読みましょう」という警告を出す
bugs
バグを発見・防止するのを目的としたポリシーのテーマ
独自にテーマを作ることができる
作ったポリシーに utgwkk テーマを設定する、みたいなことができる
有効にするポリシーをテーマで設定する
-theme コマンドライン引数
theme パラメータ (perlcriticrc)
論理式を渡せる (!?)
code:theme
Operator Alternative Example
-----------------------------------------------------------------
&& and 'pbp && core'
|| or 'pbp || (bugs && security)'
! not 'pbp && ! (portability || complexity)'
これだけは設定しましょう
program-extensions = .pl .t
Perlプログラムにもいろいろあるはず
モジュール (.pm)
スクリプト (.pl)
テスト (.t)
未設定だと、ファイル名に関係なくモジュールだと思ってlintする
スクリプトに package 宣言はいらないですよね?
テストの末尾が 1; で終わる必要はないですよね
// eslint-disable-next-line 的なもの
## no critic (無効にするポリシー) と書くとできるぞ!!
code:pl
# 指定した行でだけ無効にする
no strict 'refs'; ## no critic (TestingAndDebugging::ProhibitNoStrict)
# これ以降の行で無効にする
## no critic (TestingAndDebugging::ProhibitNoWarnings)
{
no warnings 'redefine';
}
# これ以降の行で有効にする
## use critic (TestingAndDebugging::ProhibitNoWarnings)
従わなくてよいポリシーは無効化する
return undef; ではなく return; と書くべきというポリシー
「リストコンテキストで (undef) というリストが返っておかしくなる」
それよりもリストコンテキストで呼んだときに意図せず値がずれるほうが難しいと思う
foo(bar => baz(), qux => 1)
foo('bar', 'qux', 1)
コンテキストを強く意識しないといけないコードを書かない
Maybe[T] の気持ちで return undef; と書くのは不自然ではない
undef が返ったとき空リストにしたいなら (baz() // ()) と書けば実現できる
code:disable-prohibit-explicit-return-undef
ポリシーを設定する
use strict すべきというポリシー
これ自体に異論はない (ですよね?)
use したら use strict 相当のことが行われるモジュールがある
Test2::V0
code:use-test2-v0
equivalent_modules = Test2::V0
equivalent_modules = Test2::V0
サードパーティーポリシー
いろいろあります
おすすめサードーパーティーポリシー
Perlのおもしろ演算子の濫用を防ぐ
Data::Dumperをuseしてたら警告する
Try::Tinyを使っていたら入れたい
try-catchのようなものを提供するモジュール
try-catchを使うときは use Try::Tiny する
useしないとどうなる?
try-catchブロックの中から return しないようする
Try::Tinyあるある
try-catchブロックの後ろに ; を付ける
Try::Tinyあるある
パース結果が変わる例
code:1.pl
ok Foo->bar;
code:2.pl
ok +Foo->bar;
code:3.pl
use Test::More;
ok +Foo->bar;
Deparseする
code:sh
% perl -MO=Deparse -e 'ok Foo->bar;'
'Foo'->ok->bar;
-e syntax OK
% perl -MO=Deparse -e 'ok +Foo->bar;'
'ok' + 'Foo'->bar;
-e syntax OK
% perl -MO=Deparse -e 'use Test::More; ok +Foo->bar;'
use Test::More;
&ok(scalar 'Foo'->bar);
-e syntax OK
偽陽性もあるときはある
そういうもの!!
そもそもPerlプログラムを正しくパースするのが難しい
Test2::V0を使って書いたら回避できる
code:test.t
cmp_deeply $uri, isa('URI');
is $uri, object { prop isa => 'URI' };