From fac4004aa988dbb2bab751a3402759acbf796317 Mon Sep 17 00:00:00 2001 From: Daniel Vandersluis Date: Wed, 11 Aug 2021 16:45:31 -0400 Subject: [PATCH] Add discrete node classes for assignments. --- .../new_add_discrete_node_classes_for.md | 1 + docs/modules/ROOT/pages/node_types.adoc | 16 ++-- lib/rubocop/ast.rb | 5 + lib/rubocop/ast/builder.rb | 8 ++ lib/rubocop/ast/node/and_asgn_node.rb | 17 ++++ lib/rubocop/ast/node/asgn_node.rb | 24 +++++ lib/rubocop/ast/node/casgn_node.rb | 31 +++++++ lib/rubocop/ast/node/op_asgn_node.rb | 36 ++++++++ lib/rubocop/ast/node/or_asgn_node.rb | 17 ++++ spec/rubocop/ast/and_asgn_node_spec.rb | 36 ++++++++ spec/rubocop/ast/asgn_node_spec.rb | 89 ++++++++++++++++++ spec/rubocop/ast/casgn_node_spec.rb | 55 +++++++++++ spec/rubocop/ast/op_asgn_node_spec.rb | 91 +++++++++++++++++++ spec/rubocop/ast/or_asgn_node_spec.rb | 36 ++++++++ 14 files changed, 454 insertions(+), 8 deletions(-) create mode 100644 changelog/new_add_discrete_node_classes_for.md create mode 100644 lib/rubocop/ast/node/and_asgn_node.rb create mode 100644 lib/rubocop/ast/node/asgn_node.rb create mode 100644 lib/rubocop/ast/node/casgn_node.rb create mode 100644 lib/rubocop/ast/node/op_asgn_node.rb create mode 100644 lib/rubocop/ast/node/or_asgn_node.rb create mode 100644 spec/rubocop/ast/and_asgn_node_spec.rb create mode 100644 spec/rubocop/ast/asgn_node_spec.rb create mode 100644 spec/rubocop/ast/casgn_node_spec.rb create mode 100644 spec/rubocop/ast/op_asgn_node_spec.rb create mode 100644 spec/rubocop/ast/or_asgn_node_spec.rb diff --git a/changelog/new_add_discrete_node_classes_for.md b/changelog/new_add_discrete_node_classes_for.md new file mode 100644 index 000000000..dcb41a7bf --- /dev/null +++ b/changelog/new_add_discrete_node_classes_for.md @@ -0,0 +1 @@ +* [#201](https://github.com/rubocop-hq/rubocop-ast/pull/201): Add discrete node classes for assignments. ([@dvandersluis][]) diff --git a/docs/modules/ROOT/pages/node_types.adoc b/docs/modules/ROOT/pages/node_types.adoc index 6ea8143b5..6b8e444e9 100644 --- a/docs/modules/ROOT/pages/node_types.adoc +++ b/docs/modules/ROOT/pages/node_types.adoc @@ -56,7 +56,7 @@ The following fields are given when relevant to nodes in the source code: |and|And operator|Two children are both expression nodes representing the operands.|a and b && c |https://rubydoc.info/github/rubocop/rubocop-ast/RuboCop/AST/AndNode[AndNode] -|and_asgn|And-assignment (AND the receiver with the argument and assign it back to receiver).|First child must be an assignment node, second child is the expression node.|a &&= b |N/A +|and_asgn|And-assignment (AND the receiver with the argument and assign it back to receiver).|First child must be an assignment node, second child is the expression node.|a &&= b |https://rubydoc.info/github/rubocop/rubocop-ast/RuboCop/AST/AndAsgnNode[AndAsgnNode] |arg|Required positional argument. Must come inside an `args`.|One child - a symbol, representing the argument name.|def foo(bar)|https://rubydoc.info/github/rubocop/rubocop-ast/RuboCop/AST/ArgNode[ArgNode] @@ -76,7 +76,7 @@ The following fields are given when relevant to nodes in the source code: |case|Case statement.|First child is an expression node for the condition to check. Last child is an expression node for the "else" condition. All middle nodes are `when` nodes.|case a; when 1; b; when 2; c; else d; end|https://rubydoc.info/github/rubocop/rubocop-ast/RuboCop/AST/CaseNode[CaseNode] -|casgn|Constant assignment|Three children: the parent object (either an expression, `nil` or `cbase`), the constant name (a symbol), and the expression being assigned.|Foo::Bar = 5|N/A +|casgn|Constant assignment|Three children: the parent object (either an expression, `nil` or `cbase`), the constant name (a symbol), and the expression being assigned.|Foo::Bar = 5|https://rubydoc.info/github/rubocop/rubocop-ast/RuboCop/AST/CasgnNode[CasgnNode] |cbase|Represents the top-module constant (i.e. the '::' before a constant name). Only occurs inside a `const` node.|None|::Foo|N/A @@ -90,7 +90,7 @@ The following fields are given when relevant to nodes in the source code: |cvar|Class variable access|One child, the variable name `:@@cfoo`|@@cfoo|N/A -|cvasgn|Class variable assignment|Two children: the variable name `:@@foo` and the expression being assigned|@@foo = 5|N/A +|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] |def|Instance method definition (full format)|Three children. First child is the name of the method (symbol); second child is `args` or `forward_args` (only if `emit_forward` is false, and it's true by default), and the last child is a body statement.|def foo(some_arg, kwarg: 1); end|https://rubydoc.info/github/rubocop/rubocop-ast/RuboCop/AST/DefNode[DefNode] @@ -122,7 +122,7 @@ The following fields are given when relevant to nodes in the source code: |gvar|Global variable access|One child, the variable name as a symbol `:$foo`|$foo|N/A -|gvasgn|Global variable assignment|Two children, the variable name `:$foo` and the expression being assigned|$foo = 5|N/A +|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] |hash|Hash literal.|`pair` s and/or `kwsplat` s.|{ foo: 'bar' }|https://rubydoc.info/github/rubocop/rubocop-ast/RuboCop/AST/HashNode[HashNode] @@ -132,7 +132,7 @@ The following fields are given when relevant to nodes in the source code: |ivar|Instance variable access|One child, the variable name `:@foo`|@foo|N/A -|ivasgn|Instance variable assignment|Two children, the variable name `:@foo` and the expression being assigned|@foo = 5|N/A +|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] |irange|Inclusive range literal.|Two children, the start and end nodes (including `nil` for beginless/endless)|1..2|https://rubydoc.info/github/rubocop/rubocop-ast/RuboCop/AST/RangeNode[RangeNode] @@ -150,7 +150,7 @@ The following fields are given when relevant to nodes in the source code: |lvar|Local variable access|One child, the variable name|foo|N/A -|lvasgn|Local variable assignment|Two children: The variable name (symbol) and the expression.|a = some_thing|N/A +|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 assigment.|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 @@ -166,13 +166,13 @@ The following fields are given when relevant to nodes in the source code: |numblock|Block that has numbered arguments (`_1`) referenced inside it.|Three children. First child is a `send`/`csend` node representing the way the block is created, second child is an `int` (the number of numeric arguments) and the third child is a body statement.|proc { _1 + _3 }|https://rubydoc.info/github/rubocop/rubocop-ast/RuboCop/AST/BlockNode[BlockNode] -|op_asgn|Operator-assignment - perform an operation and assign the value.|Three children. First child must be an assignment node, second child is the operator (e.g. `:+`) and the third child is the expression node.|a += b|N/A +|op_asgn|Operator-assignment - perform an operation and assign the value.|Three children. First child must be an assignment node, second child is the operator (e.g. `:+`) and the third child is the expression node.|a += b|https://rubydoc.info/github/rubocop/rubocop-ast/RuboCop/AST/OpAsgnNode[OpAsgnNode] |optarg|Optional positional argument. Must come inside an `args`.|One child - a symbol, representing the argument name.|def foo(bar=1)|https://rubydoc.info/github/rubocop/rubocop-ast/RuboCop/AST/ArgNode[ArgNode] |or|Or operator|Two children are both expression nodes representing the operands.|a or b|https://rubydoc.info/github/rubocop/rubocop-ast/RuboCop/AST/OrNode[OrNode] -|or_asgn|Or-assignment (OR the receiver with the argument and assign it back to receiver).|Two children. First child must be an assignment node, second child is the expression node.|a \|\|= b|N/A +|or_asgn|Or-assignment (OR the receiver with the argument and assign it back to receiver).|Two children. First child must be an assignment node, second child is the expression node.|a \|\|= b|https://rubydoc.info/github/rubocop/rubocop-ast/RuboCop/AST/OrAsgnNode[OrAsgnNode] |pair|One entry in a hash. |Two children, the key and value nodes.|1 => 2|https://rubydoc.info/github/rubocop/rubocop-ast/RuboCop/AST/PairNode[PairNode] diff --git a/lib/rubocop/ast.rb b/lib/rubocop/ast.rb index 4f4ab8498..bc7096c66 100644 --- a/lib/rubocop/ast.rb +++ b/lib/rubocop/ast.rb @@ -39,10 +39,12 @@ require_relative 'ast/node/arg_node' require_relative 'ast/node/args_node' require_relative 'ast/node/array_node' +require_relative 'ast/node/asgn_node' require_relative 'ast/node/block_node' require_relative 'ast/node/break_node' require_relative 'ast/node/case_match_node' require_relative 'ast/node/case_node' +require_relative 'ast/node/casgn_node' require_relative 'ast/node/class_node' require_relative 'ast/node/const_node' require_relative 'ast/node/def_node' @@ -61,6 +63,9 @@ require_relative 'ast/node/lambda_node' require_relative 'ast/node/module_node' require_relative 'ast/node/next_node' +require_relative 'ast/node/op_asgn_node' +require_relative 'ast/node/and_asgn_node' +require_relative 'ast/node/or_asgn_node' require_relative 'ast/node/or_node' require_relative 'ast/node/pair_node' require_relative 'ast/node/procarg0_node' diff --git a/lib/rubocop/ast/builder.rb b/lib/rubocop/ast/builder.rb index 067495958..4f3744e61 100644 --- a/lib/rubocop/ast/builder.rb +++ b/lib/rubocop/ast/builder.rb @@ -20,6 +20,7 @@ class Builder < Parser::Builders::Default # @api private NODE_MAP = { and: AndNode, + and_asgn: AndAsgnNode, alias: AliasNode, arg: ArgNode, blockarg: ArgNode, @@ -32,10 +33,15 @@ class Builder < Parser::Builders::Default shadowarg: ArgNode, args: ArgsNode, array: ArrayNode, + lvasgn: AsgnNode, + ivasgn: AsgnNode, + cvasgn: AsgnNode, + gvasgn: AsgnNode, block: BlockNode, numblock: BlockNode, break: BreakNode, case_match: CaseMatchNode, + casgn: CasgnNode, case: CaseNode, class: ClassNode, const: ConstNode, @@ -60,6 +66,8 @@ class Builder < Parser::Builders::Default lambda: LambdaNode, module: ModuleNode, next: NextNode, + op_asgn: OpAsgnNode, + or_asgn: OrAsgnNode, or: OrNode, pair: PairNode, procarg0: Procarg0Node, diff --git a/lib/rubocop/ast/node/and_asgn_node.rb b/lib/rubocop/ast/node/and_asgn_node.rb new file mode 100644 index 000000000..fcef0bc93 --- /dev/null +++ b/lib/rubocop/ast/node/and_asgn_node.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module RuboCop + module AST + # A node extension for `op_asgn` 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 AndAsgnNode < OpAsgnNode + # The operator being used for assignment as a symbol. + # + # @return [Symbol] the assignment operator + def operator + :'&&' + end + end + end +end diff --git a/lib/rubocop/ast/node/asgn_node.rb b/lib/rubocop/ast/node/asgn_node.rb new file mode 100644 index 000000000..9931b9b73 --- /dev/null +++ b/lib/rubocop/ast/node/asgn_node.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module RuboCop + module AST + # A node extension for `lvasgn`, `ivasgn`, `cvasgn`, and `gvasgn` 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 AsgnNode < Node + # The name of the variable being assigned as a symbol. + # + # @return [Symbol] the name of the variable being assigned + def name + node_parts[0] + end + + # The expression being assigned to the variable. + # + # @return [Node] the expression being assigned. + def expression + node_parts[1] + end + end + end +end diff --git a/lib/rubocop/ast/node/casgn_node.rb b/lib/rubocop/ast/node/casgn_node.rb new file mode 100644 index 000000000..50831ce48 --- /dev/null +++ b/lib/rubocop/ast/node/casgn_node.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +module RuboCop + module AST + # A node extension for `casgn` 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 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 + + # 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 + + # The expression being assigned to the variable. + # + # @return [Node] the expression being assigned. + def expression + node_parts[2] + end + end + end +end diff --git a/lib/rubocop/ast/node/op_asgn_node.rb b/lib/rubocop/ast/node/op_asgn_node.rb new file mode 100644 index 000000000..a81a1eb33 --- /dev/null +++ b/lib/rubocop/ast/node/op_asgn_node.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +module RuboCop + module AST + # A node extension for `op_asgn` 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 OpAsgnNode < Node + # @return [AsgnNode] the assignment node + def assignment_node + node_parts[0] + end + + # The name of the variable being assigned as a symbol. + # + # @return [Symbol] the name of the variable being assigned + def name + assignment_node.name + end + + # The operator being used for assignment as a symbol. + # + # @return [Symbol] the assignment operator + def operator + node_parts[1] + end + + # The expression being assigned to the variable. + # + # @return [Node] the expression being assigned. + def expression + node_parts.last + end + end + end +end diff --git a/lib/rubocop/ast/node/or_asgn_node.rb b/lib/rubocop/ast/node/or_asgn_node.rb new file mode 100644 index 000000000..28df6a539 --- /dev/null +++ b/lib/rubocop/ast/node/or_asgn_node.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module RuboCop + module AST + # A node extension for `op_asgn` 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 OrAsgnNode < OpAsgnNode + # The operator being used for assignment as a symbol. + # + # @return [Symbol] the assignment operator + def operator + :'||' + end + end + end +end diff --git a/spec/rubocop/ast/and_asgn_node_spec.rb b/spec/rubocop/ast/and_asgn_node_spec.rb new file mode 100644 index 000000000..cf5771e57 --- /dev/null +++ b/spec/rubocop/ast/and_asgn_node_spec.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +RSpec.describe RuboCop::AST::AndAsgnNode do + let(:or_asgn_node) { parse_source(source).ast } + let(:source) { 'var &&= value' } + + describe '.new' do + it { expect(or_asgn_node).to be_a(described_class) } + end + + describe '#assignment_node' do + subject { or_asgn_node.assignment_node } + + it { is_expected.to be_a(RuboCop::AST::AsgnNode) } + end + + describe '#name' do + subject { or_asgn_node.name } + + it { is_expected.to eq(:var) } + end + + describe '#operator' do + subject { or_asgn_node.operator } + + it { is_expected.to eq(:'&&') } + end + + describe '#expression' do + include AST::Sexp + + subject { or_asgn_node.expression } + + it { is_expected.to eq(s(:send, nil, :value)) } + end +end diff --git a/spec/rubocop/ast/asgn_node_spec.rb b/spec/rubocop/ast/asgn_node_spec.rb new file mode 100644 index 000000000..ef00a7de9 --- /dev/null +++ b/spec/rubocop/ast/asgn_node_spec.rb @@ -0,0 +1,89 @@ +# frozen_string_literal: true + +RSpec.describe RuboCop::AST::AsgnNode do + let(:asgn_node) { parse_source(source).ast } + + describe '.new' do + context 'with a `lvasgn` node' do + let(:source) { 'var = value' } + + it { expect(asgn_node).to be_a(described_class) } + end + + context 'with a `ivasgn` node' do + let(:source) { '@var = value' } + + it { expect(asgn_node).to be_a(described_class) } + end + + context 'with a `cvasgn` node' do + let(:source) { '@@var = value' } + + it { expect(asgn_node).to be_a(described_class) } + end + + context 'with a `gvasgn` node' do + let(:source) { '$var = value' } + + it { expect(asgn_node).to be_a(described_class) } + end + end + + describe '#name' do + subject { asgn_node.name } + + context 'with a `lvasgn` node' do + let(:source) { 'var = value' } + + it { is_expected.to eq(:var) } + end + + context 'with a `ivasgn` node' do + let(:source) { '@var = value' } + + it { is_expected.to eq(:@var) } + end + + context 'with a `cvasgn` node' do + let(:source) { '@@var = value' } + + it { is_expected.to eq(:@@var) } + end + + context 'with a `gvasgn` node' do + let(:source) { '$var = value' } + + it { is_expected.to eq(:$var) } + end + end + + describe '#expression' do + include AST::Sexp + + subject { asgn_node.expression } + + context 'with a `lvasgn` node' do + let(:source) { 'var = value' } + + it { is_expected.to eq(s(:send, nil, :value)) } + end + + context 'with a `ivasgn` node' do + let(:source) { '@var = value' } + + it { is_expected.to eq(s(:send, nil, :value)) } + end + + context 'with a `cvasgn` node' do + let(:source) { '@@var = value' } + + it { is_expected.to eq(s(:send, nil, :value)) } + end + + context 'with a `gvasgn` node' do + let(:source) { '$var = value' } + + it { is_expected.to eq(s(:send, nil, :value)) } + end + end +end diff --git a/spec/rubocop/ast/casgn_node_spec.rb b/spec/rubocop/ast/casgn_node_spec.rb new file mode 100644 index 000000000..acab4d1f3 --- /dev/null +++ b/spec/rubocop/ast/casgn_node_spec.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +RSpec.describe RuboCop::AST::CasgnNode do + let(:casgn_node) { parse_source(source).ast } + + describe '.new' do + context 'with a `casgn` node' do + let(:source) { 'VAR = value' } + + it { expect(casgn_node).to be_a(described_class) } + end + end + + describe '#namespace' do + include AST::Sexp + + subject { casgn_node.namespace } + + context 'when there is no parent' do + let(:source) { 'VAR = value' } + + it { is_expected.to eq(nil) } + end + + context 'when the parent is a `cbase`' do + let(:source) { '::VAR = value' } + + it { is_expected.to eq(s(:cbase)) } + end + + context 'when the parent is a `const`' do + let(:source) { 'FOO::VAR = value' } + + it { is_expected.to eq(s(:const, nil, :FOO)) } + end + end + + describe '#name' do + subject { casgn_node.name } + + let(:source) { 'VAR = value' } + + it { is_expected.to eq(:VAR) } + end + + describe '#expression' do + include AST::Sexp + + subject { casgn_node.expression } + + let(:source) { 'VAR = value' } + + it { is_expected.to eq(s(:send, nil, :value)) } + end +end diff --git a/spec/rubocop/ast/op_asgn_node_spec.rb b/spec/rubocop/ast/op_asgn_node_spec.rb new file mode 100644 index 000000000..29542409d --- /dev/null +++ b/spec/rubocop/ast/op_asgn_node_spec.rb @@ -0,0 +1,91 @@ +# frozen_string_literal: true + +RSpec.describe RuboCop::AST::OpAsgnNode do + let(:op_asgn_node) { parse_source(source).ast } + + describe '.new' do + context 'with an `op_asgn_node` node' do + let(:source) { 'var += value' } + + it { expect(op_asgn_node).to be_a(described_class) } + end + end + + describe '#assignment_node' do + subject { op_asgn_node.assignment_node } + + let(:source) { 'var += value' } + + it { is_expected.to be_a(RuboCop::AST::AsgnNode) } + end + + describe '#name' do + subject { op_asgn_node.name } + + let(:source) { 'var += value' } + + it { is_expected.to eq(:var) } + end + + describe '#operator' do + subject { op_asgn_node.operator } + + context 'with +=' do + let(:source) { 'var += value' } + + it { is_expected.to eq(:+) } + end + + context 'with -=' do + let(:source) { 'var -= value' } + + it { is_expected.to eq(:-) } + end + + context 'with *=' do + let(:source) { 'var *= value' } + + it { is_expected.to eq(:*) } + end + + context 'with /=' do + let(:source) { 'var /= value' } + + it { is_expected.to eq(:/) } + end + + context 'with &=' do + let(:source) { 'var &= value' } + + it { is_expected.to eq(:&) } + end + + context 'with |=' do + let(:source) { 'var |= value' } + + it { is_expected.to eq(:|) } + end + + context 'with %=' do + let(:source) { 'var %= value' } + + it { is_expected.to eq(:%) } + end + + context 'with **=' do + let(:source) { 'var **= value' } + + it { is_expected.to eq(:**) } + end + end + + describe '#expression' do + include AST::Sexp + + subject { op_asgn_node.expression } + + let(:source) { 'var += value' } + + it { is_expected.to eq(s(:send, nil, :value)) } + end +end diff --git a/spec/rubocop/ast/or_asgn_node_spec.rb b/spec/rubocop/ast/or_asgn_node_spec.rb new file mode 100644 index 000000000..6b4560104 --- /dev/null +++ b/spec/rubocop/ast/or_asgn_node_spec.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +RSpec.describe RuboCop::AST::OrAsgnNode do + let(:or_asgn_node) { parse_source(source).ast } + let(:source) { 'var ||= value' } + + describe '.new' do + it { expect(or_asgn_node).to be_a(described_class) } + end + + describe '#assignment_node' do + subject { or_asgn_node.assignment_node } + + it { is_expected.to be_a(RuboCop::AST::AsgnNode) } + end + + describe '#name' do + subject { or_asgn_node.name } + + it { is_expected.to eq(:var) } + end + + describe '#operator' do + subject { or_asgn_node.operator } + + it { is_expected.to eq(:'||') } + end + + describe '#expression' do + include AST::Sexp + + subject { or_asgn_node.expression } + + it { is_expected.to eq(s(:send, nil, :value)) } + end +end