Skip to content

Commit

Permalink
Add RescueNode. Add ResbodyNode#exceptions and `ResbodyNode#branc…
Browse files Browse the repository at this point in the history
…h_index`
  • Loading branch information
fatkodima committed Aug 2, 2020
1 parent fd4198e commit 47beb71
Show file tree
Hide file tree
Showing 7 changed files with 254 additions and 0 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Expand Up @@ -2,6 +2,10 @@

## master (unreleased)

### New features

* [#88](https://github.com/rubocop-hq/rubocop-ast/pull/88): Add `RescueNode`. Add `ResbodyNode#exceptions` and `ResbodyNode#branch_index`. ([@fatkodima][])

## 0.3.0 (2020-08-01)

### New features
Expand Down
1 change: 1 addition & 0 deletions lib/rubocop/ast.rb
Expand Up @@ -47,6 +47,7 @@
require_relative 'ast/node/pair_node'
require_relative 'ast/node/range_node'
require_relative 'ast/node/regexp_node'
require_relative 'ast/node/rescue_node'
require_relative 'ast/node/resbody_node'
require_relative 'ast/node/return_node'
require_relative 'ast/node/self_class_node'
Expand Down
1 change: 1 addition & 0 deletions lib/rubocop/ast/builder.rb
Expand Up @@ -48,6 +48,7 @@ class Builder < Parser::Builders::Default
or: OrNode,
pair: PairNode,
regexp: RegexpNode,
rescue: RescueNode,
resbody: ResbodyNode,
return: ReturnNode,
csend: SendNode,
Expand Down
21 changes: 21 additions & 0 deletions lib/rubocop/ast/node/resbody_node.rb
Expand Up @@ -13,12 +13,33 @@ def body
node_parts[2]
end

# Returns an array of all the exceptions in the `rescue` clause.
#
# @return [Array<Node>] an array of exception nodes
def exceptions
exceptions_node = node_parts[0]
if exceptions_node.nil?
[]
elsif exceptions_node.array_type?
exceptions_node.values
else
[exceptions_node]
end
end

# Returns the exception variable of the `rescue` clause.
#
# @return [Node, nil] The exception variable of the `resbody`.
def exception_variable
node_parts[1]
end

# Returns the index of the `resbody` branch within the exception handling statement.
#
# @return [Integer] the index of the `resbody` branch
def branch_index
parent.resbody_branches.index(self)
end
end
end
end
49 changes: 49 additions & 0 deletions lib/rubocop/ast/node/rescue_node.rb
@@ -0,0 +1,49 @@
# frozen_string_literal: true

module RuboCop
module AST
# A node extension for `rescue` nodes. This will be used in place of a
# plain node when the builder constructs the AST, making its methods
# available to all `rescue` nodes within RuboCop.
class RescueNode < Node
# Returns the body of the rescue node.
#
# @return [Node, nil] The body of the rescue node.
def body
node_parts[0]
end

# Returns an array of all the rescue branches in the exception handling statement.
#
# @return [Array<ResbodyNode>] an array of `resbody` nodes
def resbody_branches
node_parts[1...-1]
end

# Returns an array of all the rescue branches in the exception handling statement.
#
# @return [Array<Node, nil>] an array of the bodies of the rescue branches
# and the else (if any). Note that these bodies could be nil.
def branches
bodies = resbody_branches.map(&:body)
bodies.push(else_branch) if else?
bodies
end

# Returns the else branch of the exception handling statement, if any.
#
# @return [Node] the else branch node of the exception handling statement
# @return [nil] if the exception handling statement does not have an else branch.
def else_branch
node_parts[-1]
end

# Checks whether this exception handling statement has an `else` branch.
#
# @return [Boolean] whether the exception handling statement has an `else` branch
def else?
loc.else
end
end
end
end
50 changes: 50 additions & 0 deletions spec/rubocop/ast/resbody_node_spec.rb
Expand Up @@ -13,6 +13,40 @@
it { expect(resbody_node.is_a?(described_class)).to be(true) }
end

describe '#exceptions' do
context 'without exception' do
let(:source) { <<~RUBY }
begin
rescue
end
RUBY

it { expect(resbody_node.exceptions.size).to eq(0) }
end

context 'with a single exception' do
let(:source) { <<~RUBY }
begin
rescue FooError
end
RUBY

it { expect(resbody_node.exceptions.size).to eq(1) }
it { expect(resbody_node.exceptions).to all(be_const_type) }
end

context 'with multiple exceptions' do
let(:source) { <<~RUBY }
begin
rescue FooError, BarError
end
RUBY

it { expect(resbody_node.exceptions.size).to eq(2) }
it { expect(resbody_node.exceptions).to all(be_const_type) }
end
end

describe '#exception_variable' do
context 'for an explicit rescue' do
let(:source) { 'begin; beginbody; rescue Error => ex; rescuebody; end' }
Expand All @@ -38,4 +72,20 @@

it { expect(resbody_node.body.sym_type?).to be(true) }
end

describe '#branch_index' do
let(:source) { <<~RUBY }
begin
rescue FooError then foo
rescue BarError, BazError then bar_and_baz
rescue QuuxError => e then quux
end
RUBY

let(:resbodies) { parse_source(source).ast.children.first.resbody_branches }

it { expect(resbodies[0].branch_index).to eq(0) }
it { expect(resbodies[1].branch_index).to eq(1) }
it { expect(resbodies[2].branch_index).to eq(2) }
end
end
128 changes: 128 additions & 0 deletions spec/rubocop/ast/rescue_node_spec.rb
@@ -0,0 +1,128 @@
# frozen_string_literal: true

RSpec.describe RuboCop::AST::RescueNode do
let(:ast) { parse_source(source).ast }
let(:rescue_node) { ast.children.first }

describe '.new' do
let(:source) { <<~RUBY }
begin
rescue => e
end
RUBY

it { expect(rescue_node.is_a?(described_class)).to be(true) }
end

describe '#body' do
let(:source) { <<~RUBY }
begin
foo
rescue => e
end
RUBY

it { expect(rescue_node.body.send_type?).to be(true) }
end

describe '#resbody_branches' do
let(:source) { <<~RUBY }
begin
rescue FooError then foo
rescue BarError, BazError then bar_and_baz
end
RUBY

it { expect(rescue_node.resbody_branches.size).to eq(2) }
it { expect(rescue_node.resbody_branches).to all(be_resbody_type) }
end

describe '#branches' do
context 'when there is an else' do
let(:source) { <<~RUBY }
begin
rescue FooError then foo
rescue BarError then # do nothing
else 'bar'
end
RUBY

it 'returns all the bodies' do
expect(rescue_node.branches).to match [be_send_type, nil, be_str_type]
end

context 'with an empty else' do
let(:source) { <<~RUBY }
begin
rescue FooError then foo
rescue BarError then # do nothing
else # do nothing
end
RUBY

it 'returns all the bodies' do
expect(rescue_node.branches).to match [be_send_type, nil, nil]
end
end
end

context 'when there is no else keyword' do
let(:source) { <<~RUBY }
begin
rescue FooError then foo
rescue BarError then # do nothing
end
RUBY

it 'returns only then rescue bodies' do
expect(rescue_node.branches).to match [be_send_type, nil]
end
end
end

describe '#else_branch' do
context 'without an else statement' do
let(:source) { <<~RUBY }
begin
rescue FooError then foo
end
RUBY

it { expect(rescue_node.else_branch.nil?).to be(true) }
end

context 'with an else statement' do
let(:source) { <<~RUBY }
begin
rescue FooError then foo
else bar
end
RUBY

it { expect(rescue_node.else_branch.send_type?).to be(true) }
end
end

describe '#else?' do
context 'without an else statement' do
let(:source) { <<~RUBY }
begin
rescue FooError then foo
end
RUBY

it { expect(rescue_node.else?).to be_falsey }
end

context 'with an else statement' do
let(:source) { <<~RUBY }
begin
rescue FooError then foo
else bar
end
RUBY

it { expect(rescue_node.else?).to be_truthy }
end
end
end

0 comments on commit 47beb71

Please sign in to comment.