Skip to content

Commit

Permalink
Refactor CasgnNode and ConstNode to extract common functionality …
Browse files Browse the repository at this point in the history
…to `ConstantNode` mixin.
  • Loading branch information
dvandersluis authored and marcandre committed Oct 29, 2024
1 parent b91da40 commit 742450b
Show file tree
Hide file tree
Showing 5 changed files with 137 additions and 64 deletions.
1 change: 1 addition & 0 deletions lib/rubocop/ast.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
require_relative 'ast/node/mixin/binary_operator_node'
require_relative 'ast/node/mixin/collection_node'
require_relative 'ast/node/mixin/conditional_node'
require_relative 'ast/node/mixin/constant_node'
require_relative 'ast/node/mixin/hash_element_node'
require_relative 'ast/node/mixin/method_dispatch_node'
require_relative 'ast/node/mixin/modifier_node'
Expand Down
14 changes: 2 additions & 12 deletions lib/rubocop/ast/node/casgn_node.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,9 @@ module AST
# This will be used in place of a plain node when the builder constructs
# the AST, making its methods available to all assignment nodes within RuboCop.
class CasgnNode < Node
# The namespace of the constant being assigned.
#
# @return [Node, nil] the node associated with the scope (e.g. cbase, const, ...)
def namespace
node_parts[0]
end
include ConstantNode

# The name of the variable being assigned as a symbol.
#
# @return [Symbol] the name of the variable being assigned
def name
node_parts[1]
end
alias name short_name

# The expression being assigned to the variable.
#
Expand Down
53 changes: 1 addition & 52 deletions lib/rubocop/ast/node/const_node.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,58 +4,7 @@ module RuboCop
module AST
# A node extension for `const` nodes.
class ConstNode < Node
# @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

# @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?
return false unless namespace

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

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
include ConstantNode
end
end
end
62 changes: 62 additions & 0 deletions lib/rubocop/ast/node/mixin/constant_node.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# frozen_string_literal: true

module RuboCop
module AST
# Common functionality for nodes that deal with constants:
# `const`, `casgn`.
module ConstantNode
# @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

# @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?
return false unless namespace

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

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
71 changes: 71 additions & 0 deletions spec/rubocop/ast/casgn_node_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,14 @@
it { is_expected.to eq(:VAR) }
end

describe '#short_name' do
subject { casgn_node.short_name }

let(:source) { 'VAR = value' }

it { is_expected.to eq(:VAR) }
end

describe '#expression' do
include AST::Sexp

Expand All @@ -52,4 +60,67 @@

it { is_expected.to eq(s(:send, nil, :value)) }
end

describe '#module_name?' do
context 'with a constant with only uppercase letters' do
let(:source) { 'VAR = value' }

it { expect(casgn_node).not_to be_module_name }
end

context 'with a constant with a lowercase letter' do
let(:source) { '::Foo::Bar = value' }

it { expect(casgn_node).to be_module_name }
end
end

describe '#absolute?' do
context 'with a constant starting with ::' do
let(:source) { '::VAR' }

it { expect(casgn_node).to be_absolute }
end

context 'with a constant not starting with ::' do
let(:source) { 'Foo::Bar::BAZ' }

it { expect(casgn_node).not_to be_absolute }
end

context 'with a non-namespaced constant' do
let(:source) { 'Foo' }

it { expect(casgn_node).not_to be_absolute }
end
end

describe '#relative?' do
context 'with a constant starting with ::' do
let(:source) { '::VAR' }

it { expect(casgn_node).not_to be_relative }
end

context 'with a constant not starting with ::' do
let(:source) { 'Foo::Bar::BAZ' }

it { expect(casgn_node).to be_relative }
end

context 'with a non-namespaced constant' do
let(:source) { 'Foo' }

it { expect(casgn_node).to be_relative }
end
end

describe '#each_path' do
let(:source) { '::Foo::Bar::BAZ = value' }

it 'yields all parts of the namespace' do
expect(casgn_node.each_path.map(&:type)).to eq %i[cbase const const]
expect(casgn_node.each_path.to_a.last(2).map(&:short_name)).to eq %i[Foo Bar]
end
end
end

0 comments on commit 742450b

Please sign in to comment.