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