Skip to content

Commit

Permalink
Add classes for masgn and mlhs nodes.
Browse files Browse the repository at this point in the history
  • Loading branch information
dvandersluis authored and marcandre committed Oct 29, 2024
1 parent 0b334f8 commit 122ed4b
Show file tree
Hide file tree
Showing 8 changed files with 268 additions and 2 deletions.
1 change: 1 addition & 0 deletions changelog/new_add_masgnnode_class_for_masgn_nodes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* [#203](https://github.com/rubocop-hq/rubocop-ast/pull/203): Add classes for `masgn` and `mlhs` nodes. ([@dvandersluis][])
4 changes: 2 additions & 2 deletions docs/modules/ROOT/pages/node_types.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -156,9 +156,9 @@ The following fields are given when relevant to nodes in the source code:

|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]

|masgn|Multiple assignment.|First set of children are all `mlhs` nodes, and the rest of the children must be expression nodes corresponding to the values in the `mlhs` nodes.|a, b, = [1, 2]|N/A
|masgn|Multiple assignment.|First set of children are all `mlhs` nodes, and the rest of the children must be expression nodes corresponding to the values in the `mlhs` nodes.|a, b, = [1, 2]|a = some_thing|https://rubydoc.info/github/rubocop/rubocop-ast/RuboCop/AST/MasgnNode[MasgnNode]

|mlhs|Multiple left-hand side. Used inside a `masgn` and block argument destructuring.|Children must all be assignment nodes. Represents the left side of a multiple assignment (`a, b` in the example).|a, b = 5, 6|N/A
|mlhs|Multiple left-hand side. Used inside a `masgn` and block argument destructuring.|Children must all be assignment nodes or `send` nodes. Represents the left side of a multiple assignment (`a, b` in the example).|a, b = 5, 6|https://rubydoc.info/github/rubocop/rubocop-ast/RuboCop/AST/MlhsNode[MlhsNode]

|module|Module definition|Two children. First child is a `const` node for the module name. Second child is a body statement.|module Foo < Bar; end|https://rubydoc.info/github/rubocop/rubocop-ast/RuboCop/AST/ModuleNode[ModuleNode]

Expand Down
2 changes: 2 additions & 0 deletions lib/rubocop/ast.rb
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@
require_relative 'ast/node/int_node'
require_relative 'ast/node/keyword_splat_node'
require_relative 'ast/node/lambda_node'
require_relative 'ast/node/masgn_node'
require_relative 'ast/node/mlhs_node'
require_relative 'ast/node/module_node'
require_relative 'ast/node/next_node'
require_relative 'ast/node/op_asgn_node'
Expand Down
2 changes: 2 additions & 0 deletions lib/rubocop/ast/builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ class Builder < Parser::Builders::Default
kwargs: HashNode,
kwsplat: KeywordSplatNode,
lambda: LambdaNode,
masgn: MasgnNode,
mlhs: MlhsNode,
module: ModuleNode,
next: NextNode,
op_asgn: OpAsgnNode,
Expand Down
63 changes: 63 additions & 0 deletions lib/rubocop/ast/node/masgn_node.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# frozen_string_literal: true

module RuboCop
module AST
# A node extension for `masgn` 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 MasgnNode < Node
# @return [MlhsNode] the `mlhs` node
def lhs
# The first child is a `mlhs` node
node_parts[0]
end

# @return [Array<Node>] the assignment nodes of the multiple assignment
def assignments
lhs.assignments
end

# @return [Array<Symbol>] names of all the variables being assigned
def names
assignments.map do |assignment|
if assignment.send_type? || assignment.indexasgn_type?
assignment.method_name
else
assignment.name
end
end
end

# The RHS (right hand side) of the multiple assignment. This returns
# the nodes as parsed: either a single node if the RHS has a single value,
# or an `array` node containing multiple nodes.
#
# NOTE: Due to how parsing works, `expression` will return the same for
# `a, b = x, y` and `a, b = [x, y]`.
#
# @return [Node] the right hand side of a multiple assignment.
def expression
node_parts[1]
end
alias rhs expression

# In contrast to `expression`, `values` always returns a Ruby array
# containing all the nodes being assigned on the RHS.
#
# Literal arrays are considered a singular value; but unlike `expression`,
# implied `array` nodes from assigning multiple values on the RHS are treated
# as separate.
#
# @return [Array<Node>] individual values being assigned on the RHS of the multiple assignment
def values
multiple_rhs? ? expression.children : [expression]
end

private

def multiple_rhs?
expression.array_type? && !expression.bracketed?
end
end
end
end
27 changes: 27 additions & 0 deletions lib/rubocop/ast/node/mlhs_node.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# frozen_string_literal: true

module RuboCop
module AST
# A node extension for `mlhs` 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 MlhsNode < Node
# Returns all the assignment nodes on the left hand side (LHS) of a multiple assignment.
# These are generally assignment nodes (`lvasgn`, `ivasgn`, `cvasgn`, `gvasgn`, `casgn`)
# but can also be `send` nodes in case of `foo.bar, ... =` or `foo[:bar], ... =`.
#
# @return [Array<Node>] the assignment nodes of the multiple assignment LHS
def assignments
child_nodes.flat_map do |node|
if node.splat_type?
node.child_nodes.first
elsif node.mlhs_type?
node.assignments
else
node
end
end
end
end
end
end
84 changes: 84 additions & 0 deletions spec/rubocop/ast/masgn_node_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# frozen_string_literal: true

RSpec.describe RuboCop::AST::MasgnNode do
let(:masgn_node) { parse_source(source).ast }
let(:source) { 'x, y = z' }

describe '.new' do
context 'with a `masgn` node' do
it { expect(masgn_node).to be_a(described_class) }
end
end

describe '#names' do
subject { masgn_node.names }

let(:source) { 'a, @b, @@c, $d, E, *f = z' }

it { is_expected.to eq(%i[a @b @@c $d E f]) }

context 'with nested `mlhs` nodes' do
let(:source) { 'a, (b, c) = z' }

it { is_expected.to eq(%i[a b c]) }
end

context 'with array setter' do
let(:source) { 'a, b[c] = z' }

it { is_expected.to eq(%i[a []=]) }
end

context 'with a method chain' do
let(:source) { 'a, b.c = z' }

it { is_expected.to eq(%i[a c=]) }
end
end

describe '#expression' do
include AST::Sexp

subject { masgn_node.expression }

context 'with a single RHS value' do
it { is_expected.to eq(s(:send, nil, :z)) }
end

context 'with multiple RHS values' do
let(:source) { 'x, y = 1, 2' }

it { is_expected.to eq(s(:array, s(:int, 1), s(:int, 2))) }
end
end

describe '#values' do
include AST::Sexp

subject { masgn_node.values }

context 'when the RHS has a single value' do
let(:source) { 'x, y = z' }

it { is_expected.to eq([s(:send, nil, :z)]) }
end

context 'when the RHS is an array literal' do
let(:source) { 'x, y = [z, a]' }

it { is_expected.to eq([s(:array, s(:send, nil, :z), s(:send, nil, :a))]) }
end

context 'when the RHS has a multiple values' do
let(:source) { 'x, y = u, v' }

it { is_expected.to eq([s(:send, nil, :u), s(:send, nil, :v)]) }
end

context 'when the RHS has a splat' do
let(:source) { 'x, y = *z' }

it { is_expected.to eq([s(:splat, s(:send, nil, :z))]) }
end
end
end
87 changes: 87 additions & 0 deletions spec/rubocop/ast/mlhs_node_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# frozen_string_literal: true

RSpec.describe RuboCop::AST::MlhsNode do
let(:mlhs_node) { parse_source(source).ast.node_parts[0] }

describe '.new' do
context 'with a `masgn` node' do
let(:source) { 'x, y = z' }

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

describe '#assignments' do
include AST::Sexp

subject { mlhs_node.assignments }

context 'with variables' do
let(:source) { 'x, y = z' }

it { is_expected.to eq([s(:lvasgn, :x), s(:lvasgn, :y)]) }
end

context 'with a splat' do
let(:source) { 'x, *y = z' }

it { is_expected.to eq([s(:lvasgn, :x), s(:lvasgn, :y)]) }
end

context 'with nested `mlhs` nodes' do
let(:source) { 'a, (b, c) = z' }

it { is_expected.to eq([s(:lvasgn, :a), s(:lvasgn, :b), s(:lvasgn, :c)]) }
end

context 'with different variable types' do
let(:source) { 'a, @b, @@c, $d, E, *f = z' }
let(:expected_nodes) do
[
s(:lvasgn, :a),
s(:ivasgn, :@b),
s(:cvasgn, :@@c),
s(:gvasgn, :$d),
s(:casgn, nil, :E),
s(:lvasgn, :f)
]
end

it { is_expected.to eq(expected_nodes) }
end

context 'with assignment on RHS' do
let(:source) { 'x, y = 1, z += 2' }

it { is_expected.to eq([s(:lvasgn, :x), s(:lvasgn, :y)]) }
end

context 'with nested assignment on LHS' do
let(:source) { 'a, b[c+=1] = z' }

if RuboCop::AST::Builder.emit_index
let(:expected_nodes) do
[
s(:lvasgn, :a),
s(:indexasgn,
s(:send, nil, :b),
s(:op_asgn,
s(:lvasgn, :c), :+, s(:int, 1)))
]
end
else
let(:expected_nodes) do
[
s(:lvasgn, :a),
s(:send,
s(:send, nil, :b), :[]=,
s(:op_asgn,
s(:lvasgn, :c), :+, s(:int, 1)))
]
end
end

it { is_expected.to eq(expected_nodes) }
end
end
end

0 comments on commit 122ed4b

Please sign in to comment.