diff --git a/CHANGELOG.md b/CHANGELOG.md index 9154a7631..50ebca5e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/lib/rubocop/ast.rb b/lib/rubocop/ast.rb index e2d17fdbd..253e80a3f 100644 --- a/lib/rubocop/ast.rb +++ b/lib/rubocop/ast.rb @@ -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' diff --git a/lib/rubocop/ast/builder.rb b/lib/rubocop/ast/builder.rb index 5a77d677f..c80c29519 100644 --- a/lib/rubocop/ast/builder.rb +++ b/lib/rubocop/ast/builder.rb @@ -48,6 +48,7 @@ class Builder < Parser::Builders::Default or: OrNode, pair: PairNode, regexp: RegexpNode, + rescue: RescueNode, resbody: ResbodyNode, return: ReturnNode, csend: SendNode, diff --git a/lib/rubocop/ast/node/resbody_node.rb b/lib/rubocop/ast/node/resbody_node.rb index 62c7cebc8..6585df4cf 100644 --- a/lib/rubocop/ast/node/resbody_node.rb +++ b/lib/rubocop/ast/node/resbody_node.rb @@ -13,12 +13,33 @@ def body node_parts[2] end + # Returns an array of all the exceptions in the `rescue` clause. + # + # @return [Array] 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 diff --git a/lib/rubocop/ast/node/rescue_node.rb b/lib/rubocop/ast/node/rescue_node.rb new file mode 100644 index 000000000..0d1ce26da --- /dev/null +++ b/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] 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] 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 diff --git a/spec/rubocop/ast/resbody_node_spec.rb b/spec/rubocop/ast/resbody_node_spec.rb index a37bc697f..eb9f0ef85 100644 --- a/spec/rubocop/ast/resbody_node_spec.rb +++ b/spec/rubocop/ast/resbody_node_spec.rb @@ -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' } @@ -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 diff --git a/spec/rubocop/ast/rescue_node_spec.rb b/spec/rubocop/ast/rescue_node_spec.rb new file mode 100644 index 000000000..531df87cf --- /dev/null +++ b/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