From 5cd306f40e5d5ba4dacf78c698354747cdac7825 Mon Sep 17 00:00:00 2001 From: Daniel Vandersluis Date: Mon, 23 Nov 2020 12:18:51 -0500 Subject: [PATCH] Add ArgNode for `arg`, `kwarg`, `optarg`, `restarg`, `kwoptarg`, `kwrestarg`, `blockarg`, `forward_arg` and `shadowarg` types. - Add `ArgsNode#argument_list` to return all descendant argument type nodes. - Added Procarg0Node for modernized compatibility with ArgNode. - Expose `ArgsNode#argument_list` on `BlockNode`. --- ...dd_argnode_for_arg_kwarg_optarg_restarg.md | 1 + changelog/new_add_blocknodeargument_names.md | 2 + docs/modules/ROOT/pages/node_types.adoc | 20 +- lib/rubocop/ast.rb | 2 + lib/rubocop/ast/builder.rb | 10 + lib/rubocop/ast/node.rb | 3 +- lib/rubocop/ast/node/arg_node.rb | 34 ++ lib/rubocop/ast/node/args_node.rb | 10 + lib/rubocop/ast/node/block_node.rb | 13 + lib/rubocop/ast/node/procarg0_node.rb | 17 + spec/rubocop/ast/arg_node_spec.rb | 312 ++++++++++++++++++ spec/rubocop/ast/args_node_spec.rb | 32 ++ spec/rubocop/ast/block_node_spec.rb | 36 ++ spec/rubocop/ast/procarg0_node_spec.rb | 25 ++ 14 files changed, 507 insertions(+), 10 deletions(-) create mode 100644 changelog/change_add_argnode_for_arg_kwarg_optarg_restarg.md create mode 100644 changelog/new_add_blocknodeargument_names.md create mode 100644 lib/rubocop/ast/node/arg_node.rb create mode 100644 lib/rubocop/ast/node/procarg0_node.rb create mode 100644 spec/rubocop/ast/arg_node_spec.rb create mode 100644 spec/rubocop/ast/procarg0_node_spec.rb diff --git a/changelog/change_add_argnode_for_arg_kwarg_optarg_restarg.md b/changelog/change_add_argnode_for_arg_kwarg_optarg_restarg.md new file mode 100644 index 000000000..38f7aa16b --- /dev/null +++ b/changelog/change_add_argnode_for_arg_kwarg_optarg_restarg.md @@ -0,0 +1 @@ +* [#154](https://github.com/rubocop-hq/rubocop-ast/pull/154): Add `BlockNode#argument_list` and `BlockNode#argument_names`. ([@dvandersluis][]) diff --git a/changelog/new_add_blocknodeargument_names.md b/changelog/new_add_blocknodeargument_names.md new file mode 100644 index 000000000..be7e4e954 --- /dev/null +++ b/changelog/new_add_blocknodeargument_names.md @@ -0,0 +1,2 @@ +* [#154](https://github.com/rubocop-hq/rubocop-ast/pull/154): Add `ArgNode` and `Procarg0Node` ("modern" mode), and add `ArgsNode#argument_list` to get only argument type nodes. ([@dvandersluis][]) + diff --git a/docs/modules/ROOT/pages/node_types.adoc b/docs/modules/ROOT/pages/node_types.adoc index fa4507c0e..9e855f1b1 100644 --- a/docs/modules/ROOT/pages/node_types.adoc +++ b/docs/modules/ROOT/pages/node_types.adoc @@ -58,7 +58,7 @@ The following fields are given when relevant to nodes in the source code: |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 -|arg|Required positional argument. Must come inside an `args`.|One child - a symbol, representing the argument name.|def foo(bar)|N/A +|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-hq/rubocop-ast/RuboCop/AST/ArgNode[ArgNode] |args|Argument list. Must come inside a `def`, `defs`, `def_e`, `defs_e` or `block` node.|Children must be `arg`, `optarg`, `restarg`, `blockarg`, `kwarg`, `kwoptarg`, `kwrestarg`, `kwnilarg`, or `forwardarg`.|def whatever(foo, bar=1, baz: 5)|https://rubydoc.info/github/rubocop-hq/rubocop-ast/RuboCop/AST/ArgsNode[ArgsNode] @@ -70,7 +70,7 @@ The following fields are given when relevant to nodes in the source code: |block_pass|Used when passing a block as an argument.|One child, an expression node representing the block to pass.|foo(a, &my_block)|N/A -|blockarg|Reference to block argument from a function definition. Must come inside an `args`.|One child - a symbol, representing the argument name.|def foo(&bar)|N/A +|blockarg|Reference to block argument from a function definition. Must come inside an `args`.|One child - a symbol, representing the argument name.|def foo(&bar)|https://rubydoc.info/github/rubocop-hq/rubocop-ast/RuboCop/AST/ArgNode[ArgNode] |break|break keyword|One child with an expression node for the results to be passed through the break.|break 1|https://rubydoc.info/github/rubocop-hq/rubocop-ast/RuboCop/AST/BreakNode[BreakNode] @@ -114,7 +114,7 @@ The following fields are given when relevant to nodes in the source code: |for|for..in looping condition|Three children. First child is a `lvasgn` or `mlhs` node with the variable(s), second child is an expression node with the array/range to loop over, third child is a body statement.|for a in arr do foo; end|https://rubydoc.info/github/rubocop-hq/rubocop-ast/RuboCop/AST/ForNode[ForNode] -|forward_arg|Forwarding argument, for Ruby 2.8 (when `emit_forward_arg` is true). Must come inside an `args` node.|None|def whatever(foo, ...)|N/A +|forward_arg|Forwarding argument, for Ruby 2.8 (when `emit_forward_arg` is true). Must come inside an `args` node.|None|def whatever(foo, ...)|https://rubydoc.info/github/rubocop-hq/rubocop-ast/RuboCop/AST/ArgNode[ArgNode] |forward_args|Forwarding argument list, for Ruby 2.7 (when `emit_forward_arg` is false). Must come inside a `def`, `defs`, `def_e`, or `defs_e` node.|None|def (foo(...)|https://rubydoc.info/github/rubocop-hq/rubocop-ast/RuboCop/AST/ForwardArgsNode[ForwardArgsNode] @@ -136,17 +136,17 @@ The following fields are given when relevant to nodes in the source code: |irange|Inclusive range literal.|Two children, the start and end nodes (including `nil` for beginless/endless)|1..2|https://rubydoc.info/github/rubocop-hq/rubocop-ast/RuboCop/AST/RangeNode[RangeNode] -|kwarg|Required keyword argument. Must come inside an `args`.|One child - a symbol, representing the argument name.|def foo(bar:)|N/A +|kwarg|Required keyword argument. Must come inside an `args`.|One child - a symbol, representing the argument name.|def foo(bar:)|https://rubydoc.info/github/rubocop-hq/rubocop-ast/RuboCop/AST/ArgNode[ArgNode] |kwbegin|Explicit `begin` block.|Child nodes are body statements.|begin,end|N/A |kwnilarg|Double splat with nil in function definition, used to specify that the function does not accept keyword args. Must come inside an `args`.|None|def foo(**nil)|N/A -|kwoptarg|Optional keyword argument. Must come inside an `args`.|Two children - a symbol, representing the argument name, and an expression node for the value.|def foo(bar: 5)|N/A +|kwoptarg|Optional keyword argument. Must come inside an `args`.|Two children - a symbol, representing the argument name, and an expression node for the value.|def foo(bar: 5)|https://rubydoc.info/github/rubocop-hq/rubocop-ast/RuboCop/AST/ArgNode[ArgNode] |kwsplat|Double splat used for keyword arguments inside a function call (as opposed to a function definition).|One child, an expression.|foo(bar, **kwargs)|https://rubydoc.info/github/rubocop-hq/rubocop-ast/RuboCop/AST/KeywordSplatNode[KeywordSplatNode] -|kwrestargs|Double splat used for keyword arguments inside a function definition (as opposed to a function call). Must come inside an `args`.|One child - a symbol, representing the argument name, if a name is given. If no name given, it has no children..|def foo(**kwargs)|N/A +|kwrestargs|Double splat used for keyword arguments inside a function definition (as opposed to a function call). Must come inside an `args`.|One child - a symbol, representing the argument name, if a name is given. If no name given, it has no children..|def foo(**kwargs)|https://rubydoc.info/github/rubocop-hq/rubocop-ast/RuboCop/AST/ArgNode[ArgNode] |lvar|Local variable access|One child, the variable name|foo|N/A @@ -154,7 +154,7 @@ The following fields are given when relevant to nodes in the source code: |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 -|mlhs|Multiple left-hand side. Only used inside a `masgn`.|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. Represents the left side of a multiple assignment (`a, b` in the example).|a, b = 5, 6|N/A |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-hq/rubocop-ast/RuboCop/AST/ModuleNode[ModuleNode] @@ -168,7 +168,7 @@ The following fields are given when relevant to nodes in the source code: |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 -|opt_arg|Optional positional argument. Must come inside an `args`.|One child - a symbol, representing the argument name.|def foo(bar=1)|N/A +|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-hq/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-hq/rubocop-ast/RuboCop/AST/OrNode[OrNode] @@ -189,7 +189,7 @@ The following fields are given when relevant to nodes in the source code: |rescue|A rescue statement.May be "top-level" or may be nested inside an `ensure` block (if both rescue and ensure are in the block).|First node is a body statement. Last child is the "else" body statement, or `nil`. Remaining children are `resbody` nodes.|begin; rescue Exception, A => bar; 1; end| -|restarg|Positional splat argument. Must come inside an `args`.|One child - a symbol, representing the argument name (if given). If no name given, there are no children.|def foo(*rest)|N/A +|restarg|Positional splat argument. Must come inside an `args`.|One child - a symbol, representing the argument name (if given). If no name given, there are no children.|def foo(*rest)|https://rubydoc.info/github/rubocop-hq/rubocop-ast/RuboCop/AST/ArgNode[ArgNode] |return|Return statement|Zero or one child, an expression node for the value to return.|return|https://rubydoc.info/github/rubocop-hq/rubocop-ast/RuboCop/AST/ReturnNode[ReturnNode] @@ -200,6 +200,8 @@ The following fields are given when relevant to nodes in the source code: |send|Non-safe method invocation (i.e. top-level or using a dot)|First child is the receiver node (e.g. `self`), second child is the method name (e.g. `:foo=`) and the remaining children (if any) are the arguments (expression nodes). a|`foo` or `foo.bar`|https://rubydoc.info/github/rubocop-hq/rubocop-ast/RuboCop/AST/SendNode[SendNode] +|shadowarg|Shadow argument, aka block-local variable. Must come inside an `args`.|One child - a symbol, representing the argument name.|foo { \|a; b\| b }|https://rubydoc.info/github/rubocop-hq/rubocop-ast/RuboCop/AST/ArgNode[ArgNode] + |splat|Array or function argument * operator|One child, an expression.|*foo|N/A |str|Non-interpolated string literal. The heredoc version works very differently from the regular version and the location info is totally separate.|One child, the String content. diff --git a/lib/rubocop/ast.rb b/lib/rubocop/ast.rb index 7a04397db..416bd3342 100644 --- a/lib/rubocop/ast.rb +++ b/lib/rubocop/ast.rb @@ -37,6 +37,7 @@ require_relative 'ast/node/mixin/basic_literal_node' require_relative 'ast/node/alias_node' require_relative 'ast/node/and_node' +require_relative 'ast/node/arg_node' require_relative 'ast/node/args_node' require_relative 'ast/node/array_node' require_relative 'ast/node/block_node' @@ -62,6 +63,7 @@ require_relative 'ast/node/next_node' require_relative 'ast/node/or_node' require_relative 'ast/node/pair_node' +require_relative 'ast/node/procarg0_node' require_relative 'ast/node/range_node' require_relative 'ast/node/regexp_node' require_relative 'ast/node/rescue_node' diff --git a/lib/rubocop/ast/builder.rb b/lib/rubocop/ast/builder.rb index 2e3c662a2..957d7549b 100644 --- a/lib/rubocop/ast/builder.rb +++ b/lib/rubocop/ast/builder.rb @@ -20,6 +20,15 @@ class Builder < Parser::Builders::Default NODE_MAP = { and: AndNode, alias: AliasNode, + arg: ArgNode, + blockarg: ArgNode, + forward_arg: ArgNode, + kwarg: ArgNode, + kwoptarg: ArgNode, + kwrestarg: ArgNode, + optarg: ArgNode, + restarg: ArgNode, + shadowarg: ArgNode, args: ArgsNode, array: ArrayNode, block: BlockNode, @@ -49,6 +58,7 @@ class Builder < Parser::Builders::Default next: NextNode, or: OrNode, pair: PairNode, + procarg0: Procarg0Node, regexp: RegexpNode, rescue: RescueNode, resbody: ResbodyNode, diff --git a/lib/rubocop/ast/node.rb b/lib/rubocop/ast/node.rb index 821b2e6e6..98e2c8aeb 100644 --- a/lib/rubocop/ast/node.rb +++ b/lib/rubocop/ast/node.rb @@ -77,7 +77,8 @@ class Node < Parser::AST::Node # rubocop:disable Metrics/ClassLength # @api private SPECIAL_KEYWORDS = %w[__FILE__ __LINE__ __ENCODING__].to_set.freeze # @api private - ARGUMENT_TYPES = %i[arg optarg restarg kwarg kwoptarg kwrestarg blockarg].to_set.freeze + ARGUMENT_TYPES = %i[arg optarg restarg kwarg kwoptarg kwrestarg + blockarg forward_arg shadowarg].to_set.freeze LITERAL_RECURSIVE_METHODS = (COMPARISON_OPERATORS + %i[* ! <=>]).freeze LITERAL_RECURSIVE_TYPES = (OPERATOR_KEYWORDS + COMPOSITE_LITERALS + %i[begin pair]).freeze diff --git a/lib/rubocop/ast/node/arg_node.rb b/lib/rubocop/ast/node/arg_node.rb new file mode 100644 index 000000000..b9ef2e56c --- /dev/null +++ b/lib/rubocop/ast/node/arg_node.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +module RuboCop + module AST + # A node extension for `arg`, `optarg`, `restarg`, `kwarg`, `kwoptarg`, + # `kwrestarg`, `blockarg`, `shadowarg` and `forward_arg` nodes. + # This will be used in place of a plain node when the builder constructs + # the AST, making its methods available to all `arg` nodes within RuboCop. + class ArgNode < Node + # Returns the name of an argument. + # + # @return [Symbol, nil] the name of the argument + def name + node_parts[0] + end + + # Returns the default value of the argument, if any. + # + # @return [Node, nil] the default value of the argument + def default_value + return unless default? + + node_parts[1] + end + + # Checks whether the argument has a default value + # + # @return [Boolean] whether the argument has a default value + def default? + optarg_type? || kwoptarg_type? + end + end + end +end diff --git a/lib/rubocop/ast/node/args_node.rb b/lib/rubocop/ast/node/args_node.rb index 4e3d7689f..1ccb00a07 100644 --- a/lib/rubocop/ast/node/args_node.rb +++ b/lib/rubocop/ast/node/args_node.rb @@ -24,6 +24,16 @@ class ArgsNode < Node def empty_and_without_delimiters? loc.expression.nil? end + + # Yield each argument from the collection. + # Arguments can be inside `mlhs` nodes in the case of destructuring, so this + # flattens the collection to just `arg`, `optarg`, `restarg`, `kwarg`, + # `kwoptarg`, `kwrestarg`, `blockarg`, `forward_arg` and `shadowarg`. + # + # @return [Array] array of argument nodes. + def argument_list + each_descendant(*ARGUMENT_TYPES).to_a.freeze + end end end end diff --git a/lib/rubocop/ast/node/block_node.rb b/lib/rubocop/ast/node/block_node.rb index a3ad9780b..a17ed7b54 100644 --- a/lib/rubocop/ast/node/block_node.rb +++ b/lib/rubocop/ast/node/block_node.rb @@ -22,6 +22,9 @@ def send_node end # The arguments of this block. + # Note that if the block has destructured arguments, `arguments` will + # return a `mlhs` node, whereas `argument_list` will return only + # actual argument nodes. # # @return [Array] def arguments @@ -32,6 +35,16 @@ def arguments end end + # Returns a collection of all descendants of this node that are + # argument type nodes. See `ArgsNode#argument_list` for details. + # + # @return [Array] + def argument_list + return [].freeze unless arguments? + + arguments.argument_list + end + # The body of this block. # # @return [Node, nil] the body of the `block` node or `nil` diff --git a/lib/rubocop/ast/node/procarg0_node.rb b/lib/rubocop/ast/node/procarg0_node.rb new file mode 100644 index 000000000..72c311e77 --- /dev/null +++ b/lib/rubocop/ast/node/procarg0_node.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module RuboCop + module AST + # A node extension for `procarg0` nodes. + # This will be used in place of a plain node when the builder constructs + # the AST, making its methods available to all `arg` nodes within RuboCop. + class Procarg0Node < ArgNode + # Returns the name of an argument. + # + # @return [Symbol, nil] the name of the argument + def name + node_parts[0].name + end + end + end +end diff --git a/spec/rubocop/ast/arg_node_spec.rb b/spec/rubocop/ast/arg_node_spec.rb new file mode 100644 index 000000000..29f4dd6a7 --- /dev/null +++ b/spec/rubocop/ast/arg_node_spec.rb @@ -0,0 +1,312 @@ +# frozen_string_literal: true + +RSpec.describe RuboCop::AST::ArgNode do + let(:args_node) { parse_source(source).ast.arguments } + let(:arg_node) { args_node.first } + + describe '.new' do + context 'with a method definition' do + let(:source) { 'def foo(x) end' } + + it { expect(arg_node).to be_a(described_class) } + end + + context 'with a block' do + let(:source) { 'foo { |x| bar }' } + + if RuboCop::AST::Builder.emit_procarg0 + it { expect(arg_node).to be_a(RuboCop::AST::Procarg0Node) } + else + it { expect(arg_node).to be_a(described_class) } + end + end + + context 'with a lambda literal' do + let(:source) { '-> (x) { bar }' } + + it { expect(arg_node).to be_a(described_class) } + end + + context 'with a keyword argument' do + let(:source) { 'def foo(x:) end' } + + it { expect(arg_node).to be_a(described_class) } + end + + context 'with an optional argument' do + let(:source) { 'def foo(x = 42) end' } + + it { expect(arg_node).to be_a(described_class) } + end + + context 'with an optional keyword argument' do + let(:source) { 'def foo(x: 42) end' } + + it { expect(arg_node).to be_a(described_class) } + end + + context 'with a splatted argument' do + let(:source) { 'def foo(*x) end' } + + it { expect(arg_node).to be_a(described_class) } + end + + context 'with a double splatted argument' do + let(:source) { 'def foo(**x) end' } + + it { expect(arg_node).to be_a(described_class) } + end + + context 'with a block argument' do + let(:source) { 'def foo(&x) end' } + + it { expect(arg_node).to be_a(described_class) } + end + + context 'with a shadow argument' do + let(:source) { 'foo { |; x| }' } + + it { expect(arg_node).to be_a(described_class) } + end + + context 'with argument forwarding' do + context 'with Ruby >= 2.7', :ruby27 do + let(:source) { 'def foo(...); end' } + + if RuboCop::AST::Builder.emit_forward_arg + it { expect(arg_node).to be_a(described_class) } + else + it { expect(arg_node).to be_forward_args_type } + end + end + + context 'with Ruby >= 3.0', :ruby30 do + let(:source) { 'def foo(x, ...); end' } + let(:arg_node) { args_node.last } + + it { expect(arg_node).to be_a(described_class) } + end + end + end + + describe '#name' do + subject { arg_node.name } + + context 'with a regular argument' do + let(:source) { 'def foo(x) end' } + + it { is_expected.to eq(:x) } + end + + context 'with a block' do + let(:source) { 'foo { |x| x }' } + + it { is_expected.to eq(:x) } + end + + context 'with a keyword argument' do + let(:source) { 'def foo(x:) end' } + + it { is_expected.to eq(:x) } + end + + context 'with an optional argument' do + let(:source) { 'def foo(x = 42) end' } + + it { is_expected.to eq(:x) } + end + + context 'with an optional keyword argument' do + let(:source) { 'def foo(x: 42) end' } + + it { is_expected.to eq(:x) } + end + + context 'with a splatted argument' do + let(:source) { 'def foo(*x) end' } + + it { is_expected.to eq(:x) } + end + + context 'with a nameless splatted argument' do + let(:source) { 'def foo(*) end' } + + it { is_expected.to be_nil } + end + + context 'with a double splatted argument' do + let(:source) { 'def foo(**x) end' } + + it { is_expected.to eq(:x) } + end + + context 'with a nameless double splatted argument' do + let(:source) { 'def foo(**) end' } + + it { is_expected.to be_nil } + end + + context 'with a block argument' do + let(:source) { 'def foo(&x) end' } + + it { is_expected.to eq(:x) } + end + + context 'with a shadow argument' do + let(:source) { 'foo { |; x| x = 5 }' } + + it { is_expected.to eq(:x) } + end + + context 'with argument forwarding' do + context 'with Ruby >= 2.7', :ruby27 do + let(:source) { 'def foo(...); end' } + + it { is_expected.to be_nil } if RuboCop::AST::Builder.emit_forward_arg + end + + context 'with Ruby >= 3.0', :ruby30 do + let(:source) { 'def foo(x, ...); end' } + let(:arg_node) { args_node.last } + + it { is_expected.to be_nil } + end + end + end + + describe '#default_value' do + include AST::Sexp + + subject { arg_node.default_value } + + context 'with a regular argument' do + let(:source) { 'def foo(x) end' } + + it { is_expected.to be_nil } + end + + context 'with a block' do + let(:source) { 'foo { |x| x }' } + + it { is_expected.to be_nil } + end + + context 'with an optional argument' do + let(:source) { 'def foo(x = 42) end' } + + it { is_expected.to eq(s(:int, 42)) } + end + + context 'with an optional keyword argument' do + let(:source) { 'def foo(x: 42) end' } + + it { is_expected.to eq(s(:int, 42)) } + end + + context 'with a splatted argument' do + let(:source) { 'def foo(*x) end' } + + it { is_expected.to be_nil } + end + + context 'with a double splatted argument' do + let(:source) { 'def foo(**x) end' } + + it { is_expected.to be_nil } + end + + context 'with a block argument' do + let(:source) { 'def foo(&x) end' } + + it { is_expected.to be_nil } + end + + context 'with a shadow argument' do + let(:source) { 'foo { |; x| x = 5 }' } + + it { is_expected.to be_nil } + end + + context 'with argument forwarding' do + context 'with Ruby >= 2.7', :ruby27 do + let(:source) { 'def foo(...); end' } + + it { is_expected.to be_nil } if RuboCop::AST::Builder.emit_forward_arg + end + + context 'with Ruby >= 3.0', :ruby30 do + let(:source) { 'def foo(x, ...); end' } + let(:arg_node) { args_node.last } + + it { is_expected.to be_nil } + end + end + end + + describe '#default?' do + subject { arg_node.default? } + + context 'with a regular argument' do + let(:source) { 'def foo(x) end' } + + it { is_expected.to eq(false) } + end + + context 'with a block' do + let(:source) { 'foo { |x| x }' } + + it { is_expected.to eq(false) } + end + + context 'with an optional argument' do + let(:source) { 'def foo(x = 42) end' } + + it { is_expected.to eq(true) } + end + + context 'with an optional keyword argument' do + let(:source) { 'def foo(x: 42) end' } + + it { is_expected.to eq(true) } + end + + context 'with a splatted argument' do + let(:source) { 'def foo(*x) end' } + + it { is_expected.to eq(false) } + end + + context 'with a double splatted argument' do + let(:source) { 'def foo(**x) end' } + + it { is_expected.to eq(false) } + end + + context 'with a block argument' do + let(:source) { 'def foo(&x) end' } + + it { is_expected.to eq(false) } + end + + context 'with a shadow argument' do + let(:source) { 'foo { |; x| x = 5 }' } + + it { is_expected.to eq(false) } + end + + context 'with argument forwarding' do + context 'with Ruby >= 2.7', :ruby27 do + let(:source) { 'def foo(...); end' } + + it { is_expected.to eq(false) } if RuboCop::AST::Builder.emit_forward_arg + end + + context 'with Ruby >= 3.0', :ruby30 do + let(:source) { 'def foo(x, ...); end' } + let(:arg_node) { args_node.last } + + it { is_expected.to eq(false) } + end + end + end +end diff --git a/spec/rubocop/ast/args_node_spec.rb b/spec/rubocop/ast/args_node_spec.rb index aac3ede81..b6ce964ba 100644 --- a/spec/rubocop/ast/args_node_spec.rb +++ b/spec/rubocop/ast/args_node_spec.rb @@ -80,4 +80,36 @@ end end end + + describe '#argument_list' do + include AST::Sexp + + subject { args_node.argument_list } + + let(:source) { 'foo { |a, b = 42, (c, *d), e:, f: 42, **g, &h; i| nil }' } + let(:arguments) do + [ + s(:arg, :a), + s(:optarg, :b, s(:int, 42)), + s(:arg, :c), + s(:restarg, :d), + s(:kwarg, :e), + s(:kwoptarg, :f, s(:int, 42)), + s(:kwrestarg, :g), + s(:blockarg, :h), + s(:shadowarg, :i) + ] + end + + it { is_expected.to eq(arguments) } + + context 'when using Ruby 2.7 or newer', :ruby27 do + context 'with argument forwarding' do + let(:source) { 'def foo(...); end' } + let(:arguments) { [s(:forward_arg)] } + + it { is_expected.to eq(arguments) } if RuboCop::AST::Builder.emit_forward_arg + end + end + end end diff --git a/spec/rubocop/ast/block_node_spec.rb b/spec/rubocop/ast/block_node_spec.rb index c44aa1e5d..66ce0ef69 100644 --- a/spec/rubocop/ast/block_node_spec.rb +++ b/spec/rubocop/ast/block_node_spec.rb @@ -34,6 +34,12 @@ it { expect(block_node.arguments.size).to eq(2) } end + context 'with destructured arguments' do + let(:source) { 'foo { |q, (r, s)| bar(q, r, s) }' } + + it { expect(block_node.arguments.size).to eq(2) } + end + context '>= Ruby 2.7', :ruby27 do context 'using numbered parameters' do let(:source) { 'foo { _1 }' } @@ -43,6 +49,30 @@ end end + describe '#argument_list' do + subject(:argument_list) { block_node.argument_list } + + context 'with no arguments' do + let(:source) { 'foo { bar }' } + + it { is_expected.to be_empty } + end + + context 'all argument types' do + let(:source) { 'foo { |a, b = 42, (c, *d), e:, f: 42, **g, &h| nil }' } + + it { expect(argument_list.size).to eq(8) } + end + + context '>= Ruby 2.7', :ruby27 do + context 'using numbered parameters' do + let(:source) { 'foo { _1 }' } + + it { is_expected.to be_empty } + end + end + end + describe '#arguments?' do context 'with no arguments' do let(:source) { 'foo { bar }' } @@ -68,6 +98,12 @@ it { is_expected.to be_arguments } end + context 'with destructuring arguments' do + let(:source) { 'foo { |(q, r)| bar(q, r) }' } + + it { is_expected.to be_arguments } + end + context '>= Ruby 2.7', :ruby27 do context 'using numbered parameters' do let(:source) { 'foo { _1 }' } diff --git a/spec/rubocop/ast/procarg0_node_spec.rb b/spec/rubocop/ast/procarg0_node_spec.rb new file mode 100644 index 000000000..2bce26866 --- /dev/null +++ b/spec/rubocop/ast/procarg0_node_spec.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +RSpec.describe RuboCop::AST::Procarg0Node, :ruby27 do + let(:procarg0_node) { parse_source(source).ast.arguments.first } + + describe '.new' do + context 'with a block' do + let(:source) { 'foo { |x| x }' } + + if RuboCop::AST::Builder.emit_procarg0 + it { expect(procarg0_node).to be_a(described_class) } + else + it { expect(procarg0_node).to be_a(RuboCop::AST::ArgNode) } + end + end + end + + describe '#name' do + subject { procarg0_node.name } + + let(:source) { 'foo { |x| x }' } + + it { is_expected.to eq(:x) } + end +end