❄️frozen_string_literal

こんにちは、mzpです。最近はBuckleScriptで、OCamlJavaScriptに変換して遊んでいます。

先日、Misoca開発チームでfrozen_string_literal を有効にするようにしたので、そのときの話を紹介したいと思います。

🔥有効にする前に起きたこと

Ruby 3.0からfrozen_string_literalが標準で有効になるという話もあって、一部のコードに # frozen_string_literal: true が登場するようになりました。

次第に、 # frozen_string_literal: true を書いてないと、レビューで指摘が入るようになりました。

f:id:mzp:20170411172750p:plain

f:id:mzp:20170411173000p:plain

🚓 Rubocopの設定変更

機械的にチェックできる項目をレビューで指摘するのは好きではないので、RubocopのStyle/FrozenStringLiteralCommenteを有効にし、自動でチェック・修正できるようにしました。

f:id:mzp:20170411173235p:plain

その際、既存のコードにはすべて ruboocp:disable をいれて、このルールに検知されないようにしました。

# 既存コードは frozen_string_literal: true を書いてなくても許す
%w(app lib spec config).each do |name|
  Dir["#{name}/**/*.rb"].each do |path|
    content = File.read(path, encoding: 'utf-8')
    if content !~ /\A# frozen_string_literal: true/
      File.write(path, "# rubocop:disable Style/FrozenStringLiteralComment\n" + content)
    end
  end
end

🔪Ripperによる自動書き換え

大半のファイルに rubocop:disable が書いてあるのは微妙かなと思ったので、安全に書き換えれるファイルでは frozen_string_literal を有効にするようにしました。

具体的にはRipperで全ファイルを走査して、文字列リテラルを使っていないファイルではfrozen_string_literal を有効にするようにしました。

f:id:mzp:20170411174755p:plain

require 'ripper'

class FindStringLiteral < Ripper::Filter
  def on_tstring_beg(_, data)
    data = true
  end

  def on_tstring_content(_, data)
    data = true
  end

  def heredoc_beg(_, data)
    data = true
  end
end

def string_literal?(path)
  content = File.read(path)
  FindStringLiteral.new(content).parse(false)
end

%w(app lib spec config).each do |name|
  Dir["#{name}/**/*.rb"].each do |path|
    unless string_literal?(path)
      content = File.read(path, encoding: 'utf-8')
      if content =~ %r{\A# rubocop:disable
Style/FrozenStringLiteralComment}
        File.write(path, content.gsub(/\A.*/, "# frozen_string_literal:
true"))
      end
    end
  end
end

🚀今後の予定

残ったファイルは自動では修正できないので、別の修正するたびにちょっとづつ書き直しています。ボーイスカウトルールです。

f:id:mzp:20170411175109p:plain

🔉宣伝

Misocaではコードを綺麗にしていくエンジニアを募集しています。