Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add NonLocalExitFromIterator lint and fix other cops complained by it #1728

Merged
merged 2 commits into from
Mar 23, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
* New cop `Performance/ReverseEach` to convert `reverse.each` to `reverse_each`. ([@rrosenblum][])
* [#1281](https://github.com/bbatsov/rubocop/issues/1281): `IfUnlessModifier` cop does auto-correction. ([@lumeet][])
* New cop `Performance/Detect` to detect usage of `select.first`, `select.last`, `find_all.first`, and `find_all.last` and convert them to use `detect` instead. ([@palkan][], [@rrosenblum][])
* [#1728](https://github.com/bbatsov/rubocop/pull/1728): New cop `NonLocalExitFromIterator` checks for misused `return` in block. ([@ypresto][])

### Bugs fixed

Expand All @@ -38,6 +39,7 @@
* [#1579](https://github.com/bbatsov/rubocop/issues/1579): Fix handling of similar-looking blocks in `BlockAlignment`. ([@lumeet][])
* [#1676](https://github.com/bbatsov/rubocop/pull/1676): Fix auto-correct in `Lambda` when a new multi-line lambda is used as an argument. ([@lumeet][])
* [#1656](https://github.com/bbatsov/rubocop/issues/1656): Fix bug that would include hidden directories implicitly. ([@jonas054][])
* [#1728](https://github.com/bbatsov/rubocop/pull/1728): Fix bug in `LiteralInInterpolation` and `AssignmentInCondition`. ([@ypresto][])

### Changes

Expand Down Expand Up @@ -1310,3 +1312,4 @@
[@zvkemp]: https://github.com/zvkemp
[@vassilevsky]: https://github.com/vassilevsky
[@gerry3]: https://github.com/gerry3
[@ypresto]: https://github.com/ypresto
4 changes: 4 additions & 0 deletions config/enabled.yml
Original file line number Diff line number Diff line change
Expand Up @@ -891,6 +891,10 @@ Lint/Loop:
StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#loop-with-break'
Enabled: true

Lint/NonLocalExitFromIterator:
Description: 'Do not use return in iterator to cause non-local exit.'
Enabled: true

Lint/ParenthesesAsGroupedExpression:
Description: >-
Checks for method calls with a space before the opening
Expand Down
1 change: 1 addition & 0 deletions lib/rubocop.rb
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@
require 'rubocop/cop/lint/literal_in_condition'
require 'rubocop/cop/lint/literal_in_interpolation'
require 'rubocop/cop/lint/loop'
require 'rubocop/cop/lint/non_local_exit_from_iterator'
require 'rubocop/cop/lint/parentheses_as_grouped_expression'
require 'rubocop/cop/lint/require_parentheses'
require 'rubocop/cop/lint/rescue_exception'
Expand Down
20 changes: 16 additions & 4 deletions lib/rubocop/cop/lint/assignment_in_condition.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ class AssignmentInCondition < Cop
include SafeAssignment

MSG = 'Assignment in condition - you probably meant to use `==`.'
ASGN_TYPES = [:begin, *EQUALS_ASGN_NODES, :send]

def on_if(node)
check(node)
Expand All @@ -29,22 +30,33 @@ def check(node)

# assignments inside blocks are not what we're looking for
return if condition.type == :block

condition.each_node(:begin, *EQUALS_ASGN_NODES, :send) do |asgn_node|
traverse_node(condition, ASGN_TYPES) do |asgn_node|
if asgn_node.type == :send
_receiver, method_name, *_args = *asgn_node
return if method_name != :[]=
next :skip_children if method_name != :[]=
end

# skip safe assignment nodes if safe assignment is allowed
return if safe_assignment_allowed? && safe_assignment?(asgn_node)
if safe_assignment_allowed? && safe_assignment?(asgn_node)
next :skip_children
end

# assignment nodes from shorthand ops like ||= don't have operator
if asgn_node.type != :begin && asgn_node.loc.operator
add_offense(asgn_node, :operator)
end
end
end

# each_node/visit_descendants_with_types with :skip_children
def traverse_node(node, types, &block)
result = yield node if types.include?(node.type)
# return to skip all descendant nodes
return if result == :skip_children
node.children.each do |child|
traverse_node(child, types, &block) if child.is_a?(Astrolabe::Node)
end
end
end
end
end
Expand Down
2 changes: 1 addition & 1 deletion lib/rubocop/cop/lint/literal_in_interpolation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ def on_dstr(node)
final_node = begin_node.children.last
next unless final_node
# handle strings like __FILE__
return if special_string?(final_node)
next if special_string?(final_node)
next unless LITERALS.include?(final_node.type)

add_offense(final_node, :expression)
Expand Down
61 changes: 61 additions & 0 deletions lib/rubocop/cop/lint/non_local_exit_from_iterator.rb
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
10 changes: 4 additions & 6 deletions lib/rubocop/cop/style/unneeded_capital_w.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,11 @@ def on_array(node)
private

def on_percent_literal(node)
node.children.each do |string|
if string.type == :dstr ||
string.loc.expression.source =~ StringHelp::ESCAPED_CHAR_REGEXP
return
end
requires_interpolation = node.children.any? do |string|
string.type == :dstr ||
string.loc.expression.source =~ StringHelp::ESCAPED_CHAR_REGEXP
end
add_offense(node, :expression)
add_offense(node, :expression) unless requires_interpolation
end

def autocorrect(node)
Expand Down
14 changes: 14 additions & 0 deletions spec/rubocop/cop/lint/assignment_in_condition_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,14 @@
expect(cop.offenses).to be_empty
end

it 'registers an offense for assignment after == in condition' do
inspect_source(cop,
['if test == 10 || foobar = 1',
'end'
])
expect(cop.offenses.size).to eq(1)
end

it 'accepts = in a block that is called in a condition' do
inspect_source(cop,
'return 1 if any_errors? { o = inspect(file) }')
Expand All @@ -90,6 +98,12 @@
expect(cop.offenses).to be_empty
end

it 'registers an offense for assignment after ||= in condition' do
inspect_source(cop,
'raise StandardError unless (foo ||= bar) || a = b')
expect(cop.offenses.size).to eq(1)
end

context 'safe assignment is allowed' do
it 'accepts = in condition surrounded with braces' do
inspect_source(cop,
Expand Down
6 changes: 6 additions & 0 deletions spec/rubocop/cop/lint/literal_in_interpolation_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,10 @@
inspect_source(cop, '"this is #{__FILE__} silly"')
expect(cop.offenses).to be_empty
end

it 'registers an offense for interpolation after __FILE__' do
inspect_source(cop,
'"this is the #{__FILE__} #{1}"')
expect(cop.offenses.size).to eq(1)
end
end
132 changes: 132 additions & 0 deletions spec/rubocop/cop/lint/non_local_exit_from_iterator_spec.rb
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