forked from rubocop/rubocop
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request rubocop#1728 from ypresto/return-from-iteration-block
Add NonLocalExitFromIterator lint and fix other cops complained by it
- Loading branch information
Showing
10 changed files
with
242 additions
and
11 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
# encoding: utf-8 | ||
|
||
module RuboCop | ||
module Cop | ||
module Lint | ||
# This cop checks for non-local exit from iterator, without return value. | ||
# It warns only when satisfies all of these: `return` doesn't have return | ||
# value, block followed by method chain, and block have arguments. | ||
# | ||
# @example | ||
# | ||
# class ItemApi | ||
# rescue_from ValidationError do |e| # non-iteration block with arg | ||
# return message: 'validation error' unless e.errors # allowd | ||
# error_array = e.errors.map do |error| # block with method chain | ||
# return if error.suppress? # warned | ||
# return "#{error.param}: invalid" unless error.message # allowed | ||
# "#{error.param}: #{error.message}" | ||
# end | ||
# message: 'validation error', errors: error_array | ||
# end | ||
# | ||
# def update_items | ||
# transaction do # block without arguments | ||
# return unless update_necessary? # allowed | ||
# find_each do |item| # block without method chain | ||
# return if item.stock == 0 # false-negative... | ||
# item.update!(foobar: true) | ||
# end | ||
# end | ||
# end | ||
# end | ||
# | ||
class NonLocalExitFromIterator < Cop | ||
MSG = 'Non-local exit from iterator, without return value. ' \ | ||
'`next`, `break`, `Array#find`, `Array#any?`, etc. is preferred.' | ||
|
||
def on_return(return_node) | ||
return if return_value?(return_node) | ||
return_node.each_ancestor(:block) do |block_node| | ||
send_node, args_node, _body_node = *block_node | ||
next if args_node.children.empty? | ||
if chained_send?(send_node) | ||
add_offense(return_node, :keyword) | ||
break | ||
end | ||
end | ||
end | ||
|
||
def return_value?(return_node) | ||
!return_node.children.empty? | ||
end | ||
|
||
def chained_send?(send_node) | ||
receiver_node, _selector_node = *send_node | ||
!receiver_node.nil? | ||
end | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
132 changes: 132 additions & 0 deletions
132
spec/rubocop/cop/lint/non_local_exit_from_iterator_spec.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,132 @@ | ||
# encoding: utf-8 | ||
|
||
require 'spec_helper' | ||
|
||
describe RuboCop::Cop::Lint::NonLocalExitFromIterator do | ||
subject(:cop) { described_class.new } | ||
|
||
context 'inspection' do | ||
before do | ||
inspect_source(cop, source) | ||
end | ||
|
||
let(:message) do | ||
'Non-local exit from iterator, without return value. ' \ | ||
'`next`, `break`, `Array#find`, `Array#any?`, etc. is preferred.' | ||
end | ||
|
||
shared_examples_for 'offense detector' do | ||
it 'registers an offense' do | ||
expect(cop.offenses.size).to eq(1) | ||
expect(cop.offenses.first.message).to eq(message) | ||
expect(cop.offenses.first.severity.name).to eq(:warning) | ||
expect(cop.highlights).to eq(['return']) | ||
end | ||
end | ||
|
||
context 'when block is followed by method chain' do | ||
context 'and has single argument' do | ||
let(:source) { <<-END } | ||
items.each do |item| | ||
return if item.stock == 0 | ||
item.update!(foobar: true) | ||
end | ||
END | ||
|
||
it_behaves_like('offense detector') | ||
it { expect(cop.offenses.first.line).to eq(2) } | ||
end | ||
|
||
context 'and has multiple arguments' do | ||
let(:source) { <<-END } | ||
items.each_with_index do |item, i| | ||
return if item.stock == 0 | ||
item.update!(foobar: true) | ||
end | ||
END | ||
|
||
it_behaves_like('offense detector') | ||
it { expect(cop.offenses.first.line).to eq(2) } | ||
end | ||
|
||
context 'and has no argument' do | ||
let(:source) { <<-END } | ||
item.with_lock do | ||
return if item.stock == 0 | ||
item.update!(foobar: true) | ||
end | ||
END | ||
|
||
it { expect(cop.offenses).to be_empty } | ||
end | ||
end | ||
|
||
context 'when block is not followed by method chain' do | ||
let(:source) { <<-END } | ||
transaction do | ||
return unless update_necessary? | ||
find_each do |item| | ||
return if item.stock == 0 # false-negative... | ||
item.update!(foobar: true) | ||
end | ||
end | ||
END | ||
|
||
it { expect(cop.offenses).to be_empty } | ||
end | ||
|
||
context 'when block is lambda' do | ||
let(:source) { <<-END } | ||
items.each(lambda do |item| | ||
return if item.stock == 0 | ||
item.update!(foobar: true) | ||
end) | ||
items.each -> (item) { | ||
return if item.stock == 0 | ||
item.update!(foobar: true) | ||
} | ||
END | ||
|
||
it { expect(cop.offenses).to be_empty } | ||
end | ||
|
||
context 'when block in middle of nest is followed by method chain' do | ||
let(:source) { <<-END } | ||
transaction do | ||
return unless update_necessary? | ||
items.each do |item| | ||
return if item.nil? | ||
item.with_lock do | ||
return if item.stock == 0 | ||
item.very_complicated_update_operation! | ||
end | ||
end | ||
end | ||
END | ||
|
||
it 'registers offenses' do | ||
expect(cop.offenses.size).to eq(2) | ||
expect(cop.offenses[0].message).to eq(message) | ||
expect(cop.offenses[0].severity.name).to eq(:warning) | ||
expect(cop.offenses[0].line).to eq(4) | ||
expect(cop.offenses[1].message).to eq(message) | ||
expect(cop.offenses[1].severity.name).to eq(:warning) | ||
expect(cop.offenses[1].line).to eq(6) | ||
expect(cop.highlights).to eq(%w(return return)) | ||
end | ||
end | ||
|
||
context 'when return with value' do | ||
let(:source) { <<-END } | ||
def find_first_sold_out_item(items) | ||
items.each do |item| | ||
return item if item.stock == 0 | ||
item.foobar! | ||
end | ||
end | ||
END | ||
|
||
it { expect(cop.offenses).to be_empty } | ||
end | ||
end | ||
end |