RuboCop 1.13へバージョンアップ後、undefined method codepointsとなったときの解決法
はじめに
以前、RuboCopを1.11から1.13へバージョンアップを行いました。
その後GitHub Actionsのreviewdogを実行すると、RuboCop実行時にundefined methodとなってしまいました。
今回はそのエラーと解決法を備忘録として残します。
使用バージョン
RuboCop 1.13
RuboCop AST 1.4.1
現象
GitHub Actionsでreviewdogを使用してRuboCopを実行すると、Style/NegatedIfElseCondition検知時に、undefined method codepoints for nil:NilClass
となりました。
Running rubocop with reviewdog 🐶 ...
An error occurred while Style/NegatedIfElseCondition cop was inspecting
・・・
undefined method `codepoints' for nil:NilClass
/opt/hostedtoolcache/Ruby/2.6.7/x64/lib/ruby/gems/2.6.0/gems/unicode-display_width-2.0.0/lib/unicode/display_width.rb:11:in `of'
/opt/hostedtoolcache/Ruby/2.6.7/x64/lib/ruby/gems/2.6.0/gems/rubocop-1.15.0/lib/rubocop/formatter/clang_style_formatter.rb:55:in `report_highlighted_area'
/opt/hostedtoolcache/Ruby/2.6.7/x64/lib/ruby/gems/2.6.0/gems/rubocop-1.15.0/lib/rubocop/formatter/clang_style_formatter.rb:31:in `report_offense'
/opt/hostedtoolcache/Ruby/2.6.7/x64/lib/ruby/gems/2.6.0/gems/rubocop-1.15.0/lib/rubocop/formatter/clang_style_formatter.rb:12:in `block in report_file'
・・・
先に結論
以下のどちらかを行うことで解決します
- RuboCopを1.14以上にバージョンアップ
- RuboCop ASTを1.4.2以上にバージョン
調査
問題が起きたコード
エラーログを見ると、undefined method codepoints for nil:NilClass
となったコードは、Unicode::DisplayWidthのgemのコードを指していました。
def self.of(string, ambiguous = 1, overwrite = {}, options = {})
res = string.codepoints.inject(0){ |total_width, codepoint|
index_or_value = INDEX
codepoint_depth_offset = codepoint
DEPTHS.each{ |depth|
index_or_value = index_or_value[codepoint_depth_offset / depth]
codepoint_depth_offset = codepoint_depth_offset % depth
break unless index_or_value.is_a? Array
}
width = index_or_value.is_a?(Array) ? index_or_value[codepoint_depth_offset] : index_or_value
width = ambiguous if width == :A
total_width + (overwrite[codepoint] || width || 1)
}
res -= emoji_extra_width_of(string, ambiguous, overwrite) if options[:emoji]
res < 0 ? 0 : res
end
ここのres = string.codepoints.inject(0)
でundefined methodとなっているようです。
string
は引数のため、呼び出し元であるRuboCopのコードを確認します。
def report_highlighted_area(highlighted_area)
space_area = highlighted_area.source_buffer.slice(0...highlighted_area.begin_pos)
source_area = highlighted_area.source
output.puts("#{' ' * Unicode::DisplayWidth.of(space_area)}" \
"#{'^' * Unicode::DisplayWidth.of(source_area)}")
end
エラーログから、呼び出し元はUnicode::DisplayWidth.of(source_area)
のようです。
このsource_area
のセット元であるhighlighted_area.source
がnilだったのが問題のようです。
このhighlighted_area
はreport_highlighted_area
メソッドの引数のため、呼び出し元をたどります。
def report_offense(file, offense)
output.printf(
"%<path>s:%<line>d:%<column>d: %<severity>s: %<message>s\n",
path: cyan(smart_path(file)),
line: offense.line,
column: offense.real_column,
severity: colored_severity_code(offense),
message: message(offense)
)
begin
return unless valid_line?(offense)
report_line(offense.location)
report_highlighted_area(offense.highlighted_area)
rescue IndexError
# range is not on a valid line; perhaps the source file is empty
end
end
呼び出し元はreport_highlighted_area(offense.highlighted_area)
となっています。
なお、このoffense
はRuboCop::Cop::Offenseクラスとなります。このクラスではRuboCopのスタイル違反を表す役割を持っています。
それでは、offense.highlighted_area
が何を指しているのか、このOffenseクラスのhighlighted_area
メソッドを見てみます。
# @api public
#
# @return [Parser::Source::Range]
# the range of the code that is highlighted
def highlighted_area
Parser::Source::Range.new(source_line, column, column + column_length)
end
Parser::Source::Rangeでは、指定ソースに対する文字の範囲を表すようです。highligted_area
メソッドでは、RuboCopで検知したソースとその範囲を持たせたクラスを返すことになります。
今回、このParser::Source::Rangeクラスのsource
メソッドでnil
となっているので、source
メソッドを見てみます。
##
# @return [String] all source code covered by this range.
#
def source
@source_buffer.slice(self.begin_pos...self.end_pos)
end
Parser::Source::Rangeクラスの初期化時に渡したソースを指定範囲でスライスしています。今回はこの結果がnil
となったようです。
この結果から、RuboCopでの検知はしたけれども、検知したコードをうまく取れないということが考えられます。
Style/NegatedIfElseConditionの変更の確認
コードを見ていった結果、検知したコードを取れないことが分かったので、次はStyle/NegatedIfElseConditionの変更やエラー内容をもとに確認していきました。
その中で、1つ気になるIssueがありました。
このIssueを大まかにまとめると、if-else分岐の範囲取得ではコメントを含んだ範囲を取得するのですが、https://github.com/rubocop/rubocop-ast/issues/179
で記載されているast_with_comments
メソッドの影響により、範囲取得が間違ってしまうようです。
if !a_condition
# comment # <== start
flag = false #
do_a #
end #
#
if !another_condition #
# comment #
flag = false #
do_b # <== end
else
do_c
end
このIssueは以下Pull Requestにより、ast_with_comments
メソッドを使用しない方法へ変更されました。
なお、このPull RequestはRuboCop 1.14に反映されているようです。
また、RuboCop ASTでも、ast_with_comments
メソッドを修正するコミットが行われ、反映されました。
このコミットはRuboCop AST 1.4.2で反映されているようです。
今回の、Style/NegatedIfElseConditionが検知したコードは、RuboCopの以下RSpecに記載されているようなパターンと一致していました。
it 'works with duplicate nodes' do
expect_offense(<<~RUBY)
# outer comment
do_a
if !condition
^^^^^^^^^^^^^ Invert the negated condition and swap the if-else branches.
# comment
do_a
else
do_c
end
RUBY
そのため、これらPull Requestを反映されたバージョンにバージョンアップすることで解決可能と分かりました。
解決法
調査の結果、RuboCopやRuboCop ASTの指定バージョンに問題があることが分かったので、以下方法のどちらかを行うことで解決できます。
- RuboCopを1.14以上にバージョンアップ
- RuboCop ASTを1.4.2以上にバージョンアップ
おわりに
RuboCopバージョンアップ時の問題と解決法を記載しました。
調査を進めていくなかで、Style/NegatedIfElseConditionでは、コードのみを捉えた検知ではなく、コメント範囲を含んだ部分まで検知しているのかと初めて知りました。
この記事が誰かのお役に立てれば幸いです。
Discussion