From f5f9076cac93d8536b132def7bc621244bdc0201 Mon Sep 17 00:00:00 2001 From: Marc-Andre Lafortune Date: Tue, 18 Aug 2020 15:25:03 -0400 Subject: [PATCH] Add `ConstNode` and some helper methods. --- CHANGELOG.md | 1 + lib/rubocop/ast.rb | 1 + lib/rubocop/ast/builder.rb | 1 + lib/rubocop/ast/node/const_node.rb | 63 +++++++++++++++++++++++++++++ spec/rubocop/ast/const_node_spec.rb | 45 +++++++++++++++++++++ 5 files changed, 111 insertions(+) create mode 100644 lib/rubocop/ast/node/const_node.rb create mode 100644 spec/rubocop/ast/const_node_spec.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index cdfc2d989..a8ad0a9aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ * [#88](https://github.com/rubocop-hq/rubocop-ast/pull/88): Add `RescueNode`. Add `ResbodyNode#exceptions` and `ResbodyNode#branch_index`. ([@fatkodima][]) * [#89](https://github.com/rubocop-hq/rubocop-ast/pull/89): Support right hand assignment for Ruby 2.8 (3.0) parser. ([@koic][]) * [#93](https://github.com/rubocop-hq/rubocop-ast/pull/93): Add `Node#{left|right}_sibling{s}` ([@marcandre][]) +* [#99](https://github.com/rubocop-hq/rubocop-ast/pull/99): Add `ConstNode` and some helper methods. ([@marcandre][]) ### Changes diff --git a/lib/rubocop/ast.rb b/lib/rubocop/ast.rb index 153ada376..49c84a08e 100644 --- a/lib/rubocop/ast.rb +++ b/lib/rubocop/ast.rb @@ -29,6 +29,7 @@ require_relative 'ast/node/case_match_node' require_relative 'ast/node/case_node' require_relative 'ast/node/class_node' +require_relative 'ast/node/const_node' require_relative 'ast/node/def_node' require_relative 'ast/node/defined_node' require_relative 'ast/node/ensure_node' diff --git a/lib/rubocop/ast/builder.rb b/lib/rubocop/ast/builder.rb index c80c29519..afbd15022 100644 --- a/lib/rubocop/ast/builder.rb +++ b/lib/rubocop/ast/builder.rb @@ -27,6 +27,7 @@ class Builder < Parser::Builders::Default case_match: CaseMatchNode, case: CaseNode, class: ClassNode, + const: ConstNode, def: DefNode, defined?: DefinedNode, defs: DefNode, diff --git a/lib/rubocop/ast/node/const_node.rb b/lib/rubocop/ast/node/const_node.rb new file mode 100644 index 000000000..12f52e01d --- /dev/null +++ b/lib/rubocop/ast/node/const_node.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +module RuboCop + module AST + # A node extension for `const` nodes. + class ConstNode < Node + # The `send` node associated with this block. + # + # @return [Node, nil] the node associated with the scope (e.g. cbase, const, ...) + def namespace + children[0] + end + + # @return [Symbol] the demodulized name of the constant: "::Foo::Bar" => :Bar + def short_name + children[1] + end + + # The body of this block. + # + # @return [Boolean] if the constant is a Module / Class, according to the standard convention. + # Note: some classes might have uppercase in which case this method + # returns false + def module_name? + short_name.match?(/[[:lower:]]/) + end + alias class_name? module_name? + + # @return [Boolean] if the constant starts with `::` (aka s(:cbase)) + def absolute? + each_path.first.cbase_type? + end + + # @return [Boolean] if the constant does not start with `::` (aka s(:cbase)) + def relative? + !absolute? + end + + # Yield nodes for the namespace + # + # For `::Foo::Bar::BAZ` => yields: + # s(:cbase), then + # s(:const, :Foo), then + # s(:const, s(:const, :Foo), :Bar) + def each_path(&block) + return to_enum(__method__) unless block_given? + + descendants = [] + last = self + loop do + last = last.children.first + break if last.nil? + + descendants << last + break unless last.const_type? + end + descendants.reverse_each(&block) + + self + end + end + end +end diff --git a/spec/rubocop/ast/const_node_spec.rb b/spec/rubocop/ast/const_node_spec.rb new file mode 100644 index 000000000..8a53db0f9 --- /dev/null +++ b/spec/rubocop/ast/const_node_spec.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +RSpec.describe RuboCop::AST::ConstNode do + let(:ast) { parse_source(source).ast } + let(:const_node) { ast } + let(:source) { '::Foo::Bar::BAZ' } + + describe '#namespace' do + it { expect(const_node.namespace.source).to eq '::Foo::Bar' } + end + + describe '#short_name' do + it { expect(const_node.short_name).to eq :BAZ } + end + + describe '#module_name?' do + it { expect(const_node.module_name?).to eq false } + + context 'with a constant with a lowercase letter' do + let(:source) { '::Foo::Bar' } + + it { expect(const_node.module_name?).to eq true } + end + end + + describe '#absolute?' do + it { expect(const_node.absolute?).to eq true } + + context 'with a constant not starting with ::' do + let(:source) { 'Foo::Bar::BAZ' } + + it { expect(const_node.absolute?).to eq false } + end + end + + describe '#each_path' do + let(:source) { 'var = ::Foo::Bar::BAZ' } + let(:const_node) { ast.children.last } + + it 'yields all parts of the namespace' do + expect(const_node.each_path.map(&:type)).to eq %i[cbase const const] + expect(const_node.each_path.to_a.last(2).map(&:short_name)).to eq %i[Foo Bar] + end + end +end