VIMのaleでのrubocopを軽快に

rubocop軽快化のためのツール

前提

素のrubocopの起動時間を確認しておく。

$ time rubocop -v
0.80.1
rubocop -v  0.56s user 0.20s system 95% cpu 0.794 total

rubocop-daemon

rubocopをdaemonとして起動しておくことができる。

rubocop-daemon-wrapperを使うと、daemonを自動的に起動してくれて、その上でdaemonとやり取りするので起動時間分だけ実行時間が短くなる。

$ rubocop_daemon_wrapper=$(gem contents rubocop-daemon | grep rubocop-daemon-wrapper)

$ time $rubocop_daemon_wrapper -v
0.80.1
$rubocop_daemon_wrapper -v  0.69s user 0.26s system 96% cpu 0.988 total

$ time $rubocop_daemon_wrapper -v
0.80.1
$rubocop_daemon_wrapper -v  0.01s user 0.01s system 69% cpu 0.031 total

daemonはプロジェクトごとに起動されるようなので、使用しているrubocopのバージョンが異なる複数のプロジェクトがあっても問題ない(はず)。

spring-commands-rubocop

Railsでおなじみのspring経由でrubocopを実行できるようにする。

あらかじめgemをロードした状態のプロセスを用意しておいて、そこからrubocopを起動するのでロード分だけ実行時間が短くなる。

$ time spring rubocop -v
Running via Spring preloader in process 94344
0.80.1
spring rubocop -v  0.14s user 0.06s system 11% cpu 1.764 total

$ time spring rubocop -v
Running via Spring preloader in process 94377
0.80.1
spring rubocop -v  0.13s user 0.06s system 70% cpu 0.275 total

ただしプロジェクトによっては逆に遅くなったりすることもある。(rubocopに必要のないものまでロードした状態になるから?)

上の例はrails newしただけの状態でのもの。

VIMのaleの設定

背景

aleではrubocopの実行コマンド名をg:ale_ruby_rubocop_executableb:ale_ruby_rubocop_executableにより指定できる。

しかし、これらの変数にはコマンド名しか指定できないので、springを経由させたいときにはspring rubocopではなくてbin/rubocopのようにspring binstubを指定しなければならない。

また、rubocopをRailsプロジェクト以外でも使う場合、実行コマンド名をbin/rubocopに固定するわけにはいかない。

さらに、プロジェクトの事情によってはbin/rubocopがなかったり、Gemfileにrubocop-daemonを追加できなかったりすることもある。

そのため、プロジェクトごとに、あるいは特定のプロジェクトに属さないファイルに応じて、rubocopの実行コマンドを調整してやる必要が出てくる。

設定

というわけで設定を考えてみた。

alevim-bundlerをインストールしておく。

function! s:set_ale_ruby_rubocop_executable()
  let l:project = bundler#project()
  if empty(l:project) || !l:project.has('rubocop')
    return
  endif

  let l:project_root = l:project['root']
  let l:project_rubocop_daemon_wrapper = l:project_root.'/../rubocop-daemon-wrapper.rb'
  let l:project_rubocop_spring_wrapper = l:project_root.'/../rubocop-spring-wrapper.sh'
  let l:rubocop_binstub = l:project_root.'/bin/rubocop'
  if executable(l:project_rubocop_daemon_wrapper)
    unlet $RUBOCOP_DAEMON_USE_BUNDLER
    let b:ale_ruby_rubocop_executable = l:project_rubocop_daemon_wrapper
  elseif l:project.has('rubocop-daemon')
    let $RUBOCOP_DAEMON_USE_BUNDLER = '1'
    let b:ale_ruby_rubocop_executable = l:project.paths()['rubocop-daemon'].'/bin/rubocop-daemon-wrapper'
  elseif executable(l:project_rubocop_spring_wrapper)
    let b:ale_ruby_rubocop_executable = l:project_rubocop_spring_wrapper
  elseif l:project.has('spring-commands-rubocop') && executable(l:rubocop_binstub)
    let b:ale_ruby_rubocop_executable = l:rubocop_binstub
  else
    let b:ale_ruby_rubocop_executable = 'bundle'
  endif
endfunction

augroup my_ale_ruby_rubocop_setting
 au!
 au FileType ruby :call s:set_ale_ruby_rubocop_executable()
augroup END

../rubocop-daemon-wrapper.rb../rubocop-spring-wrapper.shは、諸事情ある場合の回避策として必要に応じて作成しておく。たとえば以下のような内容でどうだろう。

#!/usr/bin/env ruby
# rubocop-daemon-wrapper.rb
require 'rubygems'

if $0 == __FILE__
  bin_dir = Gem::Specification.find_by_name('rubocop-daemon')&.bin_dir
  abort 'rubocop-daemon.gem not found' unless bin_dir

  ENV['RUBYOPT'] = "-r #{File.expand_path(__FILE__).sub(/\.rb\z/, '')}"
  exec(File.join(bin_dir, '../bin/rubocop-daemon-wrapper'), *ARGV)
end

def activate_rubocop_gems(lockfile_path)
  in_gem = in_spec = false
  File.foreach(lockfile_path) do |line|
    case line
    when /^Gem/
      in_gem = true
    when /^\S/
      break if in_gem
    when /^  specs:/
      in_spec = in_gem
    when /^  \S/
      break if in_spec
    when /^    (?<name>rubocop\S*) \((?<version>[.\d]+)\)$/
      m = Regexp.last_match
      gem m[:name], "=#{m[:version]}"
    end
  end
end

dir = Dir.pwd
while dir.start_with?(__dir__)
  begin
    activate_rubocop_gems("#{dir}/Gemfile.lock")
    break
  rescue SystemCallError
    dir = File.dirname(dir)
  end
end
#!/bin/sh
# rubocop-spring-wrapper.sh
exec spring rubocop "$@"

../rubocop-daemon-wrapper.rbを使うと少し遅くなるが、それでも素のrubocopよりは速い。

$ time ../rubocop-daemon-wrapper.rb -v
0.80.1
../rubocop-daemon-wrapper.rb -v  0.61s user 0.23s system 97% cpu 0.866 total

$ time ../rubocop-daemon-wrapper.rb -v
0.80.1
../rubocop-daemon-wrapper.rb -v  0.07s user 0.05s system 88% cpu 0.137 total

(あれ、初回起動が素のrubocop-daemon-wrapperを実行するより速いのはなぜだろう? gemのアクティベートをはしょっているから?)