diff --git a/changelog/new_add_masgnnode_class_for_masgn_nodes.md b/changelog/new_add_masgnnode_class_for_masgn_nodes.md new file mode 100644 index 000000000..b7d77099f --- /dev/null +++ b/changelog/new_add_masgnnode_class_for_masgn_nodes.md @@ -0,0 +1 @@ +* [#203](https://github.com/rubocop-hq/rubocop-ast/pull/203): Add classes for `masgn` and `mlhs` nodes. ([@dvandersluis][]) diff --git a/docs/modules/ROOT/pages/node_types.adoc b/docs/modules/ROOT/pages/node_types.adoc index 9007663a8..e6c36a291 100644 --- a/docs/modules/ROOT/pages/node_types.adoc +++ b/docs/modules/ROOT/pages/node_types.adoc @@ -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] diff --git a/lib/rubocop/ast.rb b/lib/rubocop/ast.rb index 5ac1a6581..14a608280 100644 --- a/lib/rubocop/ast.rb +++ b/lib/rubocop/ast.rb @@ -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' diff --git a/lib/rubocop/ast/builder.rb b/lib/rubocop/ast/builder.rb index 0ffa82933..59219fded 100644 --- a/lib/rubocop/ast/builder.rb +++ b/lib/rubocop/ast/builder.rb @@ -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, diff --git a/lib/rubocop/ast/node/masgn_node.rb b/lib/rubocop/ast/node/masgn_node.rb new file mode 100644 index 000000000..2f9f399e6 --- /dev/null +++ b/lib/rubocop/ast/node/masgn_node.rb @@ -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] the assignment nodes of the multiple assignment + def assignments + lhs.assignments + end + + # @return [Array] 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] 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 diff --git a/lib/rubocop/ast/node/mlhs_node.rb b/lib/rubocop/ast/node/mlhs_node.rb new file mode 100644 index 000000000..b0adbb5d4 --- /dev/null +++ b/lib/rubocop/ast/node/mlhs_node.rb @@ -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] 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 diff --git a/spec/rubocop/ast/masgn_node_spec.rb b/spec/rubocop/ast/masgn_node_spec.rb new file mode 100644 index 000000000..82efe9675 --- /dev/null +++ b/spec/rubocop/ast/masgn_node_spec.rb @@ -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 diff --git a/spec/rubocop/ast/mlhs_node_spec.rb b/spec/rubocop/ast/mlhs_node_spec.rb new file mode 100644 index 000000000..1f89f3874 --- /dev/null +++ b/spec/rubocop/ast/mlhs_node_spec.rb @@ -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