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 VarNode class for lvar, ivar, cvar and gvar node types #204

Merged
merged 2 commits into from
Oct 29, 2024
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
1 change: 1 addition & 0 deletions changelog/new_add_varnode_class_for_lvar_ivar_cvar_and.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* [#204](https://github.com/rubocop-hq/rubocop-ast/pull/204): Add `VarNode` class for `lvar`, `ivar`, `cvar` and `gvar` node types. ([@dvandersluis][])
8 changes: 4 additions & 4 deletions docs/modules/ROOT/pages/node_types.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ The following fields are given when relevant to nodes in the source code:

|csend|Null-safe method invocation, i.e. using `&.`|First child is the receiver node (e.g. `self`), second child is the method name (e.g. `:foo=`) and the remaining children (if any) are nodes representing arguments.|foo&.bar|https://rubydoc.info/github/rubocop/rubocop-ast/RuboCop/AST/SendNode[SendNode]

|cvar|Class variable access|One child, the variable name `:@@cfoo`|@@cfoo|N/A
|cvar|Class variable access|One child, the variable name `:@@cfoo`|@@cfoo|https://rubydoc.info/github/rubocop/rubocop-ast/RuboCop/AST/VarNode[VarNode]

|cvasgn|Class variable assignment|Two children: the variable name `:@@foo` and the expression being assigned|@@foo = 5|https://rubydoc.info/github/rubocop/rubocop-ast/RuboCop/AST/AsgnNode[AsgnNode]

Expand Down Expand Up @@ -124,7 +124,7 @@ The following fields are given when relevant to nodes in the source code:

|forwarded_kwrestarg|Forwarding keyword arguments into a method call|None|foo(**)|N/A

|gvar|Global variable access|One child, the variable name as a symbol `:$foo`|$foo|N/A
|gvar|Global variable access|One child, the variable name as a symbol `:$foo`|$foo|https://rubydoc.info/github/rubocop/rubocop-ast/RuboCop/AST/VarNode[VarNode]

|gvasgn|Global variable assignment|Two children, the variable name `:$foo` and the expression being assigned|$foo = 5|https://rubydoc.info/github/rubocop/rubocop-ast/RuboCop/AST/AsgnNode[AsgnNode]

Expand All @@ -134,7 +134,7 @@ The following fields are given when relevant to nodes in the source code:

|int|Integer literal|1, the integer value|-123|https://rubydoc.info/github/rubocop/rubocop-ast/RuboCop/AST/IntNode[IntNode]

|ivar|Instance variable access|One child, the variable name `:@foo`|@foo|N/A
|ivar|Instance variable access|One child, the variable name `:@foo`|@foo|https://rubydoc.info/github/rubocop/rubocop-ast/RuboCop/AST/VarNode[VarNode]

|ivasgn|Instance variable assignment|Two children, the variable name `:@foo` and the expression being assigned|@foo = 5|https://rubydoc.info/github/rubocop/rubocop-ast/RuboCop/AST/AsgnNode[AsgnNode]

Expand All @@ -152,7 +152,7 @@ The following fields are given when relevant to nodes in the source code:

|kwrestargs|Double splat used for keyword arguments inside a function definition (as opposed to a function call). Must come inside an `args`.|One child - a symbol, representing the argument name, if a name is given. If no name given, it has no children..|def foo(**kwargs)|https://rubydoc.info/github/rubocop/rubocop-ast/RuboCop/AST/ArgNode[ArgNode]

|lvar|Local variable access|One child, the variable name|foo|N/A
|lvar|Local variable access|One child, the variable name|foo|https://rubydoc.info/github/rubocop/rubocop-ast/RuboCop/AST/VarNode[VarNode]

|lvasgn|Local variable assignment|Two children: The variable name (symbol) and the expression.|a = some_thing|https://rubydoc.info/github/rubocop/rubocop-ast/RuboCop/AST/AsgnNode[AsgnNode]

Expand Down
2 changes: 2 additions & 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 Expand Up @@ -83,6 +84,7 @@
require_relative 'ast/node/super_node'
require_relative 'ast/node/symbol_node'
require_relative 'ast/node/until_node'
require_relative 'ast/node/var_node'
require_relative 'ast/node/when_node'
require_relative 'ast/node/while_node'
require_relative 'ast/node/yield_node'
Expand Down
4 changes: 4 additions & 0 deletions lib/rubocop/ast/builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,10 @@ class Builder < Parser::Builders::Default
sym: SymbolNode,
until: UntilNode,
until_post: UntilNode,
lvar: VarNode,
ivar: VarNode,
cvar: VarNode,
gvar: VarNode,
when: WhenNode,
while: WhileNode,
while_post: WhileNode,
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
15 changes: 15 additions & 0 deletions lib/rubocop/ast/node/var_node.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# frozen_string_literal: true

module RuboCop
module AST
# A node extension for `lvar`, `ivar`, `cvar` and `gvar` nodes.
# 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 VarNode < Node
# @return [Symbol] The name of the variable.
def name
node_parts[0]
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
59 changes: 59 additions & 0 deletions spec/rubocop/ast/var_node_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# frozen_string_literal: true

RSpec.describe RuboCop::AST::VarNode do
let(:node) { parse_source(source).node }

describe '.new' do
context 'with a `lvar` node' do
let(:source) { 'x = 1; >>x<<' }

it { expect(node).to be_a(described_class) }
end

context 'with an `ivar` node' do
let(:source) { '@x' }

it { expect(node).to be_a(described_class) }
end

context 'with an `cvar` node' do
let(:source) { '@@x' }

it { expect(node).to be_a(described_class) }
end

context 'with an `gvar` node' do
let(:source) { '$x' }

it { expect(node).to be_a(described_class) }
end
end

describe '#name' do
subject { node.name }

context 'with a `lvar` node' do
let(:source) { 'x = 1; >>x<<' }

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

context 'with an `ivar` node' do
let(:source) { '@x' }

it { is_expected.to eq(:@x) }
end

context 'with an `cvar` node' do
let(:source) { '@@x' }

it { is_expected.to eq(:@@x) }
end

context 'with an `gvar` node' do
let(:source) { '$x' }

it { is_expected.to eq(:$x) }
end
end
end