monolithic kernel

CI で変更のあったファイルに対してのみ Perl::Critic を実行する

Perl でコードを書く上で、Perl::Critic によるコーディング規約のチェックはかなり便利です。しかし、Perl::Critic はとにかく遅くてつらい。

いやまあ、それでも便利ではあるのですが、CI の速度は開発のテンポを左右しますから、少しでも速いほうがいいよね。そこで、CI で必要なファイルにだけ Perl::Critic を実行する方法を試してみました。

さて、Perl::Critic を実行する必要のあるファイルとは何でしょうか。Perl::Critic はファイル単位でのチェックなので、前回チェックが通った時点から変更されたファイルが Perl::Critic を実行する必要のあるファイルです。

今どきのプロジェクトではだいたい Git でバージョン管理しているでしょうから、git diff を使えば変更されたファイルはすぐにわかります。master ではすべてのファイルをチェックする (= すべてのファイルがチェックを通過している) ことにすれば、master から派生した各ブランチでは master からの差分だけを見ればいいですね。

ということでやっていくと以下のような感じになります。いろいろ決め打ちなのは各自直してください。

use strict;
use warnings;
use List::Util qw(any);
use Perl::Critic;
use Test::More;
use Test::Perl::Critic -profile => 't/perlcritic.rc';

# Perl::Critic に直接関係するファイル
# これらのファイルが1つでも変更されたらすべてのファイルを実行し直す
use constant PERL_CRITIC_RELATED_FILES => +[
    't/perlcritic.rc',
    't/perlcritic.t',
];

sub get_current_branch {
    my $current_branch = `git rev-parse --abbrev-ref HEAD`;
    return if $?;
    chomp $current_branch;
    return $current_branch;
}

sub get_changed_files {
    my $diff = `git diff --name-only origin/master...HEAD`;
    return if $?;
    my %changed_files = map { $_ => 1 } split(/\n/, $diff);
    return \%changed_files;
}

sub needs_check_all_files {
    my ($current_branch, $changed_files) = @_;
    # ブランチ名を取得できないか master だったらすべてのファイルをチェックする
    return 1 if !$current_branch || $current_branch eq 'master';
    # 変更されたファイルのリストを取得できなかったらすべてのファイルをチェックする
    return 1 unless $changed_files;
    # 変更されたファイルに Perl::Critic 関係のものがあったらすべてのファイルをチェックする
    return 1 if any { $changed_files->{$_} } @{&PERL_CRITIC_RELATED_FILES()};
    # ここまでの条件に引っかからなかった場合は差分のみをチェックする
    return 0;
}

system qw/git fetch origin/;

my $current_branch = get_current_branch();
my $changed_files = get_changed_files();
my $check_all_files = needs_check_all_files($current_branch, $changed_files);

my @all_files = Perl::Critic::Utils::all_perl_files(qw(lib scripts t));
my @target_files;
if ($check_all_files) {
    # すべてのファイルをチェックする場合
    note 'check all files';
    @target_files = @all_files;
}
else {
    # 変更されたファイルのみをチェックする場合
    note 'check changed files';
    @target_files = grep { $changed_files->{$_} } @all_files;
}

if (@target_files) {
    # 対象ファイルが存在する場合はチェックを実行
    plan tests => scalar(@target_files);
    for my $file (@target_files) {
        critic_ok($file, $file);
    }
}
else {
    # 対象ファイルが存在しない場合は skip
    plan skip_all => 'no files changed';
}
done_testing();

Perl::Critic に限らず、ファイル単位で CI で何か見る場合全般に有効だと思いますので、ぜひぜひお試しください。