diff --git a/.github/workflows/rubocop.yml b/.github/workflows/rubocop.yml index f93c92642..99773e194 100644 --- a/.github/workflows/rubocop.yml +++ b/.github/workflows/rubocop.yml @@ -20,12 +20,14 @@ jobs: ruby: [ 2.4, 2.5, 2.6, 2.7, head ] rubocop: [ master ] coverage: [ null ] + modern: [ null ] title: [ null ] include: - { os: windows, rubocop: master, ruby: mingw } - { rubocop: '0.84.0', ruby: 2.4, os: ubuntu } - { rubocop: '0.84.0', ruby: head, os: ubuntu } - { rubocop: '0.84.0', ruby: 2.4, os: ubuntu, coverage: true, title: 'Cov' } + - { rubocop: master, ruby: 2.7, os: ubuntu, modern: true, title: 'Modern' } steps: - name: windows misc @@ -62,6 +64,9 @@ jobs: with: coverageCommand: bundle exec rake spec debug: true + - name: Set modernize mode + if: matrix.modern == true + run: echo '::set-env name=MODERNIZE::true' - name: spec if: matrix.coverage != true run: bundle exec rake spec diff --git a/README.md b/README.md index d217c0911..8ac7b2086 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,16 @@ gem 'rubocop-ast' Refer to the documentation of `RuboCop::AST::Node` and [`RuboCop::AST::NodePattern`](docs/modules/ROOT/pages/node_pattern.adoc) +### Parser compatibility switches + +The main `RuboCop` gem uses [legacy AST output from parser](https://github.com/whitequark/parser/#usage). +This gem is meant to be compatible with all settings. For example, to have `-> { ... }` emitted +as `LambdaNode` instead of `SendNode`: + +```ruby +RuboCop::AST::Builder.emit_lambda = true +``` + ## Contributing Checkout the [contribution guidelines](CONTRIBUTING.md). diff --git a/lib/rubocop/ast.rb b/lib/rubocop/ast.rb index 0fb63750e..d9bf7efbf 100644 --- a/lib/rubocop/ast.rb +++ b/lib/rubocop/ast.rb @@ -35,8 +35,11 @@ require_relative 'ast/node/float_node' require_relative 'ast/node/hash_node' require_relative 'ast/node/if_node' +require_relative 'ast/node/index_node' +require_relative 'ast/node/indexasgn_node' require_relative 'ast/node/int_node' require_relative 'ast/node/keyword_splat_node' +require_relative 'ast/node/lambda_node' require_relative 'ast/node/module_node' require_relative 'ast/node/or_node' require_relative 'ast/node/pair_node' diff --git a/lib/rubocop/ast/builder.rb b/lib/rubocop/ast/builder.rb index 84ef5abcb..f360d8731 100644 --- a/lib/rubocop/ast/builder.rb +++ b/lib/rubocop/ast/builder.rb @@ -35,9 +35,12 @@ class Builder < Parser::Builders::Default hash: HashNode, if: IfNode, int: IntNode, + index: IndexNode, + indexasgn: IndexasgnNode, irange: RangeNode, erange: RangeNode, kwsplat: KeywordSplatNode, + lambda: LambdaNode, module: ModuleNode, or: OrNode, pair: PairNode, diff --git a/lib/rubocop/ast/node/def_node.rb b/lib/rubocop/ast/node/def_node.rb index ee2d75615..7e37047d7 100644 --- a/lib/rubocop/ast/node/def_node.rb +++ b/lib/rubocop/ast/node/def_node.rb @@ -24,7 +24,7 @@ def void_context? # # @return [Boolean] whether the `def` node uses argument forwarding def argument_forwarding? - arguments.any?(&:forward_args_type?) + arguments.any?(&:forward_args_type?) || arguments.any?(&:forward_arg_type?) end # The name of the defined method as a symbol. diff --git a/lib/rubocop/ast/node/forward_args_node.rb b/lib/rubocop/ast/node/forward_args_node.rb index 8a42434c0..af939967a 100644 --- a/lib/rubocop/ast/node/forward_args_node.rb +++ b/lib/rubocop/ast/node/forward_args_node.rb @@ -5,6 +5,21 @@ module AST # A node extension for `forward-args` nodes. This will be used in place # of a plain node when the builder constructs the AST, making its methods # available to all `forward-args` nodes within RuboCop. + # + # Not used with modern emitters: + # + # $ ruby-parse -e "def foo(...); end" + # (def :foo + # (args + # (forward-arg)) nil) + # $ ruby-parse --legacy -e "->(foo) { bar }" + # (def :foo + # (forward-args) nil) + # + # Note the extra 's' with legacy form. + # + # The main RuboCop runs in legacy mode; this node is only used + # if user `AST::Builder.modernize` or `AST::Builder.emit_lambda=true` class ForwardArgsNode < Node include CollectionNode diff --git a/lib/rubocop/ast/node/index_node.rb b/lib/rubocop/ast/node/index_node.rb new file mode 100644 index 000000000..06ab86bc5 --- /dev/null +++ b/lib/rubocop/ast/node/index_node.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +module RuboCop + module AST + # Used for modern support only! + # Not as thoroughly tested as legacy equivalent + # + # $ ruby-parse -e "foo[:bar]" + # (index + # (send nil :foo) + # (sym :bar)) + # $ ruby-parse --legacy -e "foo[:bar]" + # (send + # (send nil :foo) :[] + # (sym :bar)) + # + # The main RuboCop runs in legacy mode; this node is only used + # if user `AST::Builder.modernize` or `AST::Builder.emit_index=true` + class IndexNode < Node + include ParameterizedNode + include MethodDispatchNode + + # For similarity with legacy mode + def attribute_accessor? + false + end + + # For similarity with legacy mode + def assignment_method? + false + end + + # For similarity with legacy mode + def method_name + :[] + end + + # An array containing the arguments of the dispatched method. + # + # @return [Array] the arguments of the dispatched method + def arguments + node_parts[1..-1] + end + end + end +end diff --git a/lib/rubocop/ast/node/indexasgn_node.rb b/lib/rubocop/ast/node/indexasgn_node.rb new file mode 100644 index 000000000..3643362a2 --- /dev/null +++ b/lib/rubocop/ast/node/indexasgn_node.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +module RuboCop + module AST + # Used for modern support only! + # Not as thoroughly tested as legacy equivalent + # + # $ ruby-parse -e "foo[:bar] = :baz" + # (indexasgn + # (send nil :foo) + # (sym :bar) + # (sym :baz)) + # $ ruby-parse --legacy -e "foo[:bar] = :baz" + # (send + # (send nil :foo) :[]= + # (sym :bar) + # (sym :baz)) + # + # The main RuboCop runs in legacy mode; this node is only used + # if user `AST::Builder.modernize` or `AST::Builder.emit_index=true` + class IndexasgnNode < Node + include ParameterizedNode + include MethodDispatchNode + + # For similarity with legacy mode + def attribute_accessor? + false + end + + # For similarity with legacy mode + def assignment_method? + true + end + + # For similarity with legacy mode + def method_name + :[]= + end + + # An array containing the arguments of the dispatched method. + # + # @return [Array] the arguments of the dispatched method + def arguments + node_parts[1..-1] + end + end + end +end diff --git a/lib/rubocop/ast/node/lambda_node.rb b/lib/rubocop/ast/node/lambda_node.rb new file mode 100644 index 000000000..b6d697cb0 --- /dev/null +++ b/lib/rubocop/ast/node/lambda_node.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +module RuboCop + module AST + # Used for modern support only: + # Not as thoroughly tested as legacy equivalent + # + # $ ruby-parse -e "->(foo) { bar }" + # (block + # (lambda) + # (args + # (arg :foo)) + # (send nil :bar)) + # $ ruby-parse --legacy -e "->(foo) { bar }" + # (block + # (send nil :lambda) + # (args + # (arg :foo)) + # (send nil :bar)) + # + # The main RuboCop runs in legacy mode; this node is only used + # if user `AST::Builder.modernize` or `AST::Builder.emit_lambda=true` + class LambdaNode < Node + include ParameterizedNode + include MethodDispatchNode + + # For similarity with legacy mode + def lambda? + true + end + + # For similarity with legacy mode + def lambda_literal? + true + end + + # For similarity with legacy mode + def attribute_accessor? + false + end + + # For similarity with legacy mode + def assignment_method? + false + end + + # For similarity with legacy mode + def method_name + :lambda + end + + # For similarity with legacy mode + def arguments + [] + end + end + end +end diff --git a/lib/rubocop/ast/node/mixin/method_dispatch_node.rb b/lib/rubocop/ast/node/mixin/method_dispatch_node.rb index bf2bd3b4f..88d1587c2 100644 --- a/lib/rubocop/ast/node/mixin/method_dispatch_node.rb +++ b/lib/rubocop/ast/node/mixin/method_dispatch_node.rb @@ -3,7 +3,8 @@ module RuboCop module AST # Common functionality for nodes that are a kind of method dispatch: - # `send`, `csend`, `super`, `zsuper`, `yield`, `defined?` + # `send`, `csend`, `super`, `zsuper`, `yield`, `defined?`, + # and (modern only): `index`, `indexasgn`, `lambda` module MethodDispatchNode extend NodePattern::Macros include MethodIdentifierPredicates diff --git a/lib/rubocop/ast/node/mixin/parameterized_node.rb b/lib/rubocop/ast/node/mixin/parameterized_node.rb index 9b26aca7e..7d2c3820d 100644 --- a/lib/rubocop/ast/node/mixin/parameterized_node.rb +++ b/lib/rubocop/ast/node/mixin/parameterized_node.rb @@ -4,6 +4,7 @@ module RuboCop module AST # Common functionality for nodes that are parameterized: # `send`, `super`, `zsuper`, `def`, `defs` + # and (modern only): `index`, `indexasgn`, `lambda` module ParameterizedNode # Checks whether this node's arguments are wrapped in parentheses. # diff --git a/lib/rubocop/ast/traversal.rb b/lib/rubocop/ast/traversal.rb index 91de20a3b..37dc4cc54 100644 --- a/lib/rubocop/ast/traversal.rb +++ b/lib/rubocop/ast/traversal.rb @@ -19,9 +19,10 @@ def walk(node) rational str sym regopt self lvar ivar cvar gvar nth_ref back_ref cbase arg restarg blockarg shadowarg - kwrestarg zsuper lambda redo retry + kwrestarg zsuper redo retry forward_args forwarded_args - match_var match_nil_pattern empty_else].freeze + match_var match_nil_pattern empty_else + forward_arg lambda procarg0 __ENCODING__].freeze ONE_CHILD_NODE = %i[splat kwsplat block_pass not break next preexe postexe match_current_line defined? arg_expr pin match_rest if_guard unless_guard @@ -33,7 +34,8 @@ def walk(node) match_with_lvasgn begin kwbegin return in_match match_alt match_as array_pattern array_pattern_with_tail - hash_pattern const_pattern].freeze + hash_pattern const_pattern + index indexasgn].freeze SECOND_CHILD_ONLY = %i[lvasgn ivasgn cvasgn gvasgn optarg kwarg kwoptarg].freeze diff --git a/spec/rubocop/ast/forward_args_node_spec.rb b/spec/rubocop/ast/forward_args_node_spec.rb index ef27a906b..ada0fcc29 100644 --- a/spec/rubocop/ast/forward_args_node_spec.rb +++ b/spec/rubocop/ast/forward_args_node_spec.rb @@ -2,18 +2,21 @@ RSpec.describe RuboCop::AST::ForwardArgsNode do let(:args_node) { parse_source(source).ast.arguments } + let(:source) { 'def foo(...); end' } context 'when using Ruby 2.7 or newer', :ruby27 do - describe '.new' do - let(:source) { 'def foo(...); end' } + if RuboCop::AST::Builder.emit_forward_arg + describe '#to_a' do + it { expect(args_node.to_a).to contain_exactly(be_forward_arg_type) } + end + else + describe '.new' do + it { expect(args_node.is_a?(described_class)).to be(true) } + end - it { expect(args_node.is_a?(described_class)).to be(true) } - end - - describe '#to_a' do - let(:source) { 'def foo(...); end' } - - it { expect(args_node.to_a).to contain_exactly(args_node) } + describe '#to_a' do + it { expect(args_node.to_a).to contain_exactly(args_node) } + end end end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 39ddef2da..919b63dc3 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -8,6 +8,7 @@ end require 'rubocop-ast' +RuboCop::AST::Builder.modernize if ENV['MODERNIZE'] RSpec.shared_context 'ruby 2.3', :ruby23 do let(:ruby_version) { 2.3 }