Skip to content

Commit

Permalink
[fixes rubocop#22] Introduce Tuple, a frozen Array with fast include?
Browse files Browse the repository at this point in the history
  • Loading branch information
marcandre committed Jun 24, 2020
1 parent c2bc840 commit b03f186
Show file tree
Hide file tree
Showing 9 changed files with 186 additions and 75 deletions.
1 change: 1 addition & 0 deletions lib/rubocop/ast.rb
Expand Up @@ -4,6 +4,7 @@
require 'forwardable'
require 'set'

require_relative 'ast/fast_array'
require_relative 'ast/node_pattern'
require_relative 'ast/sexp'
require_relative 'ast/node'
Expand Down
51 changes: 51 additions & 0 deletions lib/rubocop/ast/fast_array.rb
@@ -0,0 +1,51 @@
# frozen_string_literal: true

module RuboCop
module AST
# FastArray represents a frozen `Array` with fast lookup
# using `include?`.
# Like `Set`, the case equality `===` is an alias for `include?`
#
# FOO = FastArray[:hello, :world]
# FOO.include?(:hello) # => true, quickly
#
# case bar
# when FOO # Note: no splat
# # decided quickly
# # ...
class FastArray < ::Array
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

# Return self, not a newly allocated FastArray
def to_a
self
end

def include?(value)
@to_set.include?(value)
end

alias === include?
end
end
end

def FastArray(list) # rubocop:disable Naming/MethodName
RuboCop::AST::FastArray.new(list)
end
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 = FastArray %i[== === != <= >= > <]

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

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

BASIC_CONDITIONALS = FastArray %i[if while until]
CONDITIONALS = FastArray[*BASIC_CONDITIONALS, :case]
VARIABLES = FastArray %i[ivar gvar cvar lvar]
REFERENCES = FastArray %i[nth_ref back_ref]
KEYWORDS = FastArray %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 = FastArray %i[and or]
SPECIAL_KEYWORDS = FastArray %w[__FILE__ __LINE__ __ENCODING__]
ARGUMENT_TYPES = FastArray %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 = FastArray %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
FastArray(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 @@ -9,8 +9,8 @@ module MethodDispatchNode
extend NodePattern::Macros
include MethodIdentifierPredicates

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

# The receiving node of the method dispatch.
#
Expand Down
34 changes: 17 additions & 17 deletions lib/rubocop/ast/node/mixin/method_identifier_predicates.rb
Expand Up @@ -7,23 +7,23 @@ module AST
#
# @note this mixin expects `#method_name` and `#receiver` to be implemented
module MethodIdentifierPredicates # rubocop:disable Metrics/ModuleLength
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].to_set.freeze
ENUMERATOR_METHODS = FastArray %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]

ENUMERABLE_METHODS = (Enumerable.instance_methods + [:each]).to_set.freeze
ENUMERABLE_METHODS = FastArray(Enumerable.instance_methods + [:each])

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

NONMUTATING_BINARY_OPERATOR_METHODS = %i[* / % + - == === != < > <= >= <=>].to_set.freeze
NONMUTATING_UNARY_OPERATOR_METHODS = %i[+@ -@ ~ !].to_set.freeze
NONMUTATING_OPERATOR_METHODS = (NONMUTATING_BINARY_OPERATOR_METHODS +
NONMUTATING_UNARY_OPERATOR_METHODS).freeze
NONMUTATING_BINARY_OPERATOR_METHODS = FastArray %i[* / % + - == === != < > <= >= <=>]
NONMUTATING_UNARY_OPERATOR_METHODS = FastArray %i[+@ -@ ~ !]
NONMUTATING_OPERATOR_METHODS = FastArray(NONMUTATING_BINARY_OPERATOR_METHODS +
NONMUTATING_UNARY_OPERATOR_METHODS)

NONMUTATING_ARRAY_METHODS = %i[
NONMUTATING_ARRAY_METHODS = FastArray %i[
all? any? assoc at bsearch bsearch_index collect
combination compact count cycle deconstruct difference
dig drop drop_while each each_index empty? eql?
Expand All @@ -36,19 +36,19 @@ module MethodIdentifierPredicates # rubocop:disable Metrics/ModuleLength
size slice sort sum take take_while
to_a to_ary to_h to_s transpose union uniq
values_at zip |
].to_set.freeze
]

NONMUTATING_HASH_METHODS = %i[
NONMUTATING_HASH_METHODS = FastArray %i[
any? assoc compact dig each each_key each_pair
each_value empty? eql? fetch fetch_values filter
flatten has_key? has_value? hash include? inspect
invert key key? keys? length member? merge rassoc
rehash reject select size slice to_a to_h to_hash
to_proc to_s transform_keys transform_values value?
values values_at
].to_set.freeze
]

NONMUTATING_STRING_METHODS = %i[
NONMUTATING_STRING_METHODS = FastArray %i[
ascii_only? b bytes bytesize byteslice capitalize
casecmp casecmp? center chars chomp chop chr codepoints
count crypt delete delete_prefix delete_suffix
Expand All @@ -61,7 +61,7 @@ module MethodIdentifierPredicates # rubocop:disable Metrics/ModuleLength
strip sub succ sum swapcase to_a to_c to_f to_i to_r to_s
to_str to_sym tr tr_s unicode_normalize unicode_normalized?
unpack unpack1 upcase upto valid_encoding?
].to_set.freeze
]

# Checks whether the method name matches the argument.
#
Expand Down
46 changes: 23 additions & 23 deletions lib/rubocop/ast/traversal.rb
Expand Up @@ -15,29 +15,29 @@ 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 redo retry
forward_args forwarded_args
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
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
index indexasgn].freeze
SECOND_CHILD_ONLY = %i[lvasgn ivasgn cvasgn gvasgn optarg kwarg
kwoptarg].freeze
NO_CHILD_NODES = FastArray %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 redo retry
forward_args forwarded_args
match_var match_nil_pattern empty_else
forward_arg lambda procarg0 __ENCODING__]
ONE_CHILD_NODE = FastArray %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 = FastArray %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
index indexasgn]
SECOND_CHILD_ONLY = FastArray %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
59 changes: 59 additions & 0 deletions spec/rubocop/ast/fast_array_spec.rb
@@ -0,0 +1,59 @@
# frozen_string_literal: true

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

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

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

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

it { is_expected.to equal fast_array.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(:fast_array) { described_class.new(values) }

it_behaves_like 'a fast_array'

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(fast_array.freeze).to equal fast_array
end

it 'has the right case equality' do
expect(fast_array).to be === :included # rubocop:disable Style/CaseEquality
end
end

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

it_behaves_like 'a fast_array'
end

describe '()' do
subject(:fast_array) { FastArray values }

it_behaves_like 'a fast_array'
end
end

0 comments on commit b03f186

Please sign in to comment.