Skip to content

Commit

Permalink
[fixes rubocop#22] Introduce Tuple, a frozen Array/Set
Browse files Browse the repository at this point in the history
  • Loading branch information
marcandre committed Jun 10, 2020
1 parent f496d2b commit b6fc172
Show file tree
Hide file tree
Showing 9 changed files with 162 additions and 62 deletions.
1 change: 1 addition & 0 deletions lib/rubocop/ast.rb
Expand Up @@ -3,6 +3,7 @@
require 'parser'
require 'forwardable'

require_relative 'ast/tuple'
require_relative 'ast/node_pattern'
require_relative 'ast/sexp'
require_relative 'ast/node'
Expand Down
62 changes: 31 additions & 31 deletions lib/rubocop/ast/node.rb
Expand Up @@ -23,37 +23,37 @@ class Node < Parser::AST::Node # rubocop:disable Metrics/ClassLength
extend NodePattern::Macros

# <=> isn't included here, because it doesn't return a boolean.
COMPARISON_OPERATORS = %i[== === != <= >= > <].freeze

TRUTHY_LITERALS = %i[str dstr xstr int float sym dsym array
hash regexp true irange erange complex
rational regopt].freeze
FALSEY_LITERALS = %i[false nil].freeze
LITERALS = (TRUTHY_LITERALS + FALSEY_LITERALS).freeze
COMPOSITE_LITERALS = %i[dstr xstr dsym array hash irange
erange regexp].freeze
BASIC_LITERALS = (LITERALS - COMPOSITE_LITERALS).freeze
MUTABLE_LITERALS = %i[str dstr xstr array hash
regexp irange erange].freeze
IMMUTABLE_LITERALS = (LITERALS - MUTABLE_LITERALS).freeze

EQUALS_ASSIGNMENTS = %i[lvasgn ivasgn cvasgn gvasgn
casgn masgn].freeze
SHORTHAND_ASSIGNMENTS = %i[op_asgn or_asgn and_asgn].freeze
ASSIGNMENTS = (EQUALS_ASSIGNMENTS + SHORTHAND_ASSIGNMENTS).freeze

BASIC_CONDITIONALS = %i[if while until].freeze
CONDITIONALS = [*BASIC_CONDITIONALS, :case].freeze
VARIABLES = %i[ivar gvar cvar lvar].freeze
REFERENCES = %i[nth_ref back_ref].freeze
KEYWORDS = %i[alias and break case class def defs defined?
kwbegin do else ensure for if module next
not or postexe redo rescue retry return self
super zsuper then undef until when while
yield].freeze
OPERATOR_KEYWORDS = %i[and or].freeze
SPECIAL_KEYWORDS = %w[__FILE__ __LINE__ __ENCODING__].freeze
ARGUMENT_TYPES = %i[arg optarg restarg kwarg kwoptarg kwrestarg blockarg].freeze
COMPARISON_OPERATORS = Tuple %i[== === != <= >= > <]

TRUTHY_LITERALS = Tuple %i[str dstr xstr int float sym dsym array
hash regexp true irange erange complex
rational regopt]
FALSEY_LITERALS = Tuple %i[false nil]
LITERALS = Tuple(TRUTHY_LITERALS + FALSEY_LITERALS)
COMPOSITE_LITERALS = Tuple %i[dstr xstr dsym array hash irange
erange regexp]
BASIC_LITERALS = Tuple(LITERALS - COMPOSITE_LITERALS)
MUTABLE_LITERALS = Tuple %i[str dstr xstr array hash
regexp irange erange]
IMMUTABLE_LITERALS = Tuple(LITERALS - MUTABLE_LITERALS)

EQUALS_ASSIGNMENTS = Tuple %i[lvasgn ivasgn cvasgn gvasgn
casgn masgn]
SHORTHAND_ASSIGNMENTS = Tuple %i[op_asgn or_asgn and_asgn]
ASSIGNMENTS = Tuple(EQUALS_ASSIGNMENTS + SHORTHAND_ASSIGNMENTS)

BASIC_CONDITIONALS = Tuple %i[if while until]
CONDITIONALS = Tuple[*BASIC_CONDITIONALS, :case]
VARIABLES = Tuple %i[ivar gvar cvar lvar]
REFERENCES = Tuple %i[nth_ref back_ref]
KEYWORDS = Tuple %i[alias and break case class def defs defined?
kwbegin do else ensure for if module next
not or postexe redo rescue retry return self
super zsuper then undef until when while
yield]
OPERATOR_KEYWORDS = Tuple %i[and or]
SPECIAL_KEYWORDS = Tuple %w[__FILE__ __LINE__ __ENCODING__]
ARGUMENT_TYPES = Tuple %i[arg optarg restarg kwarg kwoptarg kwrestarg blockarg]

# @see https://www.rubydoc.info/gems/ast/AST/Node:initialize
def initialize(type, children = [], properties = {})
Expand Down
2 changes: 1 addition & 1 deletion lib/rubocop/ast/node/block_node.rb
Expand Up @@ -11,7 +11,7 @@ module AST
class BlockNode < Node
include MethodIdentifierPredicates

VOID_CONTEXT_METHODS = %i[each tap].freeze
VOID_CONTEXT_METHODS = Tuple %i[each tap]

# The `send` node associated with this block.
#
Expand Down
2 changes: 1 addition & 1 deletion lib/rubocop/ast/node/mixin/collection_node.rb
Expand Up @@ -7,7 +7,7 @@ module CollectionNode
extend Forwardable

ARRAY_METHODS =
(Array.instance_methods - Object.instance_methods - [:to_a]).freeze
Tuple(Array.instance_methods - Object.instance_methods - [:to_a])

def_delegators :to_a, *ARRAY_METHODS
end
Expand Down
4 changes: 2 additions & 2 deletions lib/rubocop/ast/node/mixin/method_dispatch_node.rb
Expand Up @@ -8,8 +8,8 @@ module MethodDispatchNode
extend NodePattern::Macros
include MethodIdentifierPredicates

ARITHMETIC_OPERATORS = %i[+ - * / % **].freeze
SPECIAL_MODIFIERS = %w[private protected].freeze
ARITHMETIC_OPERATORS = Tuple %i[+ - * / % **]
SPECIAL_MODIFIERS = Tuple %w[private protected]

# The receiving node of the method dispatch.
#
Expand Down
12 changes: 6 additions & 6 deletions lib/rubocop/ast/node/mixin/method_identifier_predicates.rb
Expand Up @@ -7,14 +7,14 @@ module AST
#
# @note this mixin expects `#method_name` and `#receiver` to be implemented
module MethodIdentifierPredicates
ENUMERATOR_METHODS = %i[collect collect_concat detect downto each
find find_all find_index inject loop map!
map reduce reject reject! reverse_each select
select! times upto].freeze
ENUMERATOR_METHODS = Tuple %i[collect collect_concat detect downto each
find find_all find_index inject loop map!
map reduce reject reject! reverse_each select
select! times upto]

# http://phrogz.net/programmingruby/language.html#table_18.4
OPERATOR_METHODS = %i[| ^ & <=> == === =~ > >= < <= << >> + - * /
% ** ~ +@ -@ !@ ~@ [] []= ! != !~ `].freeze
OPERATOR_METHODS = Tuple %i[| ^ & <=> == === =~ > >= < <= << >> + - * /
% ** ~ +@ -@ !@ ~@ [] []= ! != !~ `]

# Checks whether the method name matches the argument.
#
Expand Down
42 changes: 21 additions & 21 deletions lib/rubocop/ast/traversal.rb
Expand Up @@ -15,27 +15,27 @@ def walk(node)
nil
end

NO_CHILD_NODES = %i[true false nil int float complex
rational str sym regopt self lvar
ivar cvar gvar nth_ref back_ref cbase
arg restarg blockarg shadowarg
kwrestarg zsuper lambda redo retry
forward_args forwarded_args
match_var match_nil_pattern empty_else].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
match_with_trailing_comma].freeze
MANY_CHILD_NODES = %i[dstr dsym xstr regexp array hash pair
mlhs masgn or_asgn and_asgn
undef alias args super yield or and
while_post until_post iflipflop eflipflop
match_with_lvasgn begin kwbegin return
in_match match_alt
match_as array_pattern array_pattern_with_tail
hash_pattern const_pattern].freeze
SECOND_CHILD_ONLY = %i[lvasgn ivasgn cvasgn gvasgn optarg kwarg
kwoptarg].freeze
NO_CHILD_NODES = Tuple %i[true false nil int float complex
rational str sym regopt self lvar
ivar cvar gvar nth_ref back_ref cbase
arg restarg blockarg shadowarg
kwrestarg zsuper lambda redo retry
forward_args forwarded_args
match_var match_nil_pattern empty_else]
ONE_CHILD_NODE = Tuple %i[splat kwsplat block_pass not break next
preexe postexe match_current_line defined?
arg_expr pin match_rest if_guard unless_guard
match_with_trailing_comma]
MANY_CHILD_NODES = Tuple %i[dstr dsym xstr regexp array hash pair
mlhs masgn or_asgn and_asgn
undef alias args super yield or and
while_post until_post iflipflop eflipflop
match_with_lvasgn begin kwbegin return
in_match match_alt
match_as array_pattern array_pattern_with_tail
hash_pattern const_pattern]
SECOND_CHILD_ONLY = Tuple %i[lvasgn ivasgn cvasgn gvasgn optarg kwarg
kwoptarg]

NO_CHILD_NODES.each do |type|
module_eval("def on_#{type}(node); end", __FILE__, __LINE__)
Expand Down
40 changes: 40 additions & 0 deletions lib/rubocop/ast/tuple.rb
@@ -0,0 +1,40 @@
# frozen_string_literal: true

module RuboCop
module AST
# A frozen Array/Set
#
class Tuple < ::Array
extend Forwardable
attr_reader :to_set

def initialize(ary)
raise ArgumentError, 'Must be initialized with an array' unless ary.is_a?(Array)

super
freeze
end

def self.[](*values)
new(values)
end

def freeze
@to_set ||= Set.new(self).freeze
super
end

def to_a
self
end

def_delegators :@to_set, :include?

alias === include?
end
end
end

def Tuple(list) # rubocop:disable Naming/MethodName
RuboCop::AST::Tuple.new(list)
end
59 changes: 59 additions & 0 deletions spec/rubocop/ast/tuple_spec.rb
@@ -0,0 +1,59 @@
# frozen_string_literal: true

RSpec.describe RuboCop::AST::Tuple do
shared_examples 'a tuple' do
it { is_expected.to be_frozen }
it { expect(tuple.include?(:included)).to be true }
it { expect(tuple.include?(:not_included)).to be false }
it { is_expected.to eq tuple.dup }

describe '#to_a' do
subject { tuple.to_a }

it { is_expected.to equal tuple.to_a }
it { is_expected.to be_frozen }
it { is_expected.to include :included }
end

describe '#to_set' do
subject { tuple.to_set }

it { is_expected.to equal tuple.to_set }
it { is_expected.to be_frozen }
it { is_expected.to be >= Set[:included] }
end
end

let(:values) { %i[included also_included] }

describe '.new' do
subject(:tuple) { described_class.new(values) }

it_behaves_like 'a tuple'

it 'enforces a single array argument' do
expect { described_class.new }.to raise_error ArgumentError
expect { described_class.new(5) }.to raise_error ArgumentError
end

it 'has freeze return self' do
expect(tuple.freeze).to equal tuple
end

it 'has the right case equality' do
is_expected.to be === :included
end
end

describe '.[]' do
subject(:tuple) { described_class[*values] }

it_behaves_like 'a tuple'
end

describe '()' do
subject(:tuple) { Tuple values }

it_behaves_like 'a tuple'
end
end

0 comments on commit b6fc172

Please sign in to comment.