Skip to content

Commit b03f186

Browse files
committed
[fixes rubocop#22] Introduce Tuple, a frozen Array with fast include?
1 parent c2bc840 commit b03f186

9 files changed

+186
-75
lines changed

lib/rubocop/ast.rb

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
require 'forwardable'
55
require 'set'
66

7+
require_relative 'ast/fast_array'
78
require_relative 'ast/node_pattern'
89
require_relative 'ast/sexp'
910
require_relative 'ast/node'

lib/rubocop/ast/fast_array.rb

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# frozen_string_literal: true
2+
3+
module RuboCop
4+
module AST
5+
# FastArray represents a frozen `Array` with fast lookup
6+
# using `include?`.
7+
# Like `Set`, the case equality `===` is an alias for `include?`
8+
#
9+
# FOO = FastArray[:hello, :world]
10+
# FOO.include?(:hello) # => true, quickly
11+
#
12+
# case bar
13+
# when FOO # Note: no splat
14+
# # decided quickly
15+
# # ...
16+
class FastArray < ::Array
17+
attr_reader :to_set
18+
19+
def initialize(ary)
20+
raise ArgumentError, 'Must be initialized with an array' unless ary.is_a?(Array)
21+
22+
super
23+
freeze
24+
end
25+
26+
def self.[](*values)
27+
new(values)
28+
end
29+
30+
def freeze
31+
@to_set ||= Set.new(self).freeze
32+
super
33+
end
34+
35+
# Return self, not a newly allocated FastArray
36+
def to_a
37+
self
38+
end
39+
40+
def include?(value)
41+
@to_set.include?(value)
42+
end
43+
44+
alias === include?
45+
end
46+
end
47+
end
48+
49+
def FastArray(list) # rubocop:disable Naming/MethodName
50+
RuboCop::AST::FastArray.new(list)
51+
end

lib/rubocop/ast/node.rb

+31-31
Original file line numberDiff line numberDiff line change
@@ -23,37 +23,37 @@ class Node < Parser::AST::Node # rubocop:disable Metrics/ClassLength
2323
extend NodePattern::Macros
2424

2525
# <=> isn't included here, because it doesn't return a boolean.
26-
COMPARISON_OPERATORS = %i[== === != <= >= > <].freeze
27-
28-
TRUTHY_LITERALS = %i[str dstr xstr int float sym dsym array
29-
hash regexp true irange erange complex
30-
rational regopt].freeze
31-
FALSEY_LITERALS = %i[false nil].freeze
32-
LITERALS = (TRUTHY_LITERALS + FALSEY_LITERALS).freeze
33-
COMPOSITE_LITERALS = %i[dstr xstr dsym array hash irange
34-
erange regexp].freeze
35-
BASIC_LITERALS = (LITERALS - COMPOSITE_LITERALS).freeze
36-
MUTABLE_LITERALS = %i[str dstr xstr array hash
37-
regexp irange erange].freeze
38-
IMMUTABLE_LITERALS = (LITERALS - MUTABLE_LITERALS).freeze
39-
40-
EQUALS_ASSIGNMENTS = %i[lvasgn ivasgn cvasgn gvasgn
41-
casgn masgn].freeze
42-
SHORTHAND_ASSIGNMENTS = %i[op_asgn or_asgn and_asgn].freeze
43-
ASSIGNMENTS = (EQUALS_ASSIGNMENTS + SHORTHAND_ASSIGNMENTS).freeze
44-
45-
BASIC_CONDITIONALS = %i[if while until].freeze
46-
CONDITIONALS = [*BASIC_CONDITIONALS, :case].freeze
47-
VARIABLES = %i[ivar gvar cvar lvar].freeze
48-
REFERENCES = %i[nth_ref back_ref].freeze
49-
KEYWORDS = %i[alias and break case class def defs defined?
50-
kwbegin do else ensure for if module next
51-
not or postexe redo rescue retry return self
52-
super zsuper then undef until when while
53-
yield].freeze
54-
OPERATOR_KEYWORDS = %i[and or].freeze
55-
SPECIAL_KEYWORDS = %w[__FILE__ __LINE__ __ENCODING__].freeze
56-
ARGUMENT_TYPES = %i[arg optarg restarg kwarg kwoptarg kwrestarg blockarg].freeze
26+
COMPARISON_OPERATORS = FastArray %i[== === != <= >= > <]
27+
28+
TRUTHY_LITERALS = FastArray %i[str dstr xstr int float sym dsym array
29+
hash regexp true irange erange complex
30+
rational regopt]
31+
FALSEY_LITERALS = FastArray %i[false nil]
32+
LITERALS = FastArray(TRUTHY_LITERALS + FALSEY_LITERALS)
33+
COMPOSITE_LITERALS = FastArray %i[dstr xstr dsym array hash irange
34+
erange regexp]
35+
BASIC_LITERALS = FastArray(LITERALS - COMPOSITE_LITERALS)
36+
MUTABLE_LITERALS = FastArray %i[str dstr xstr array hash
37+
regexp irange erange]
38+
IMMUTABLE_LITERALS = FastArray(LITERALS - MUTABLE_LITERALS)
39+
40+
EQUALS_ASSIGNMENTS = FastArray %i[lvasgn ivasgn cvasgn gvasgn
41+
casgn masgn]
42+
SHORTHAND_ASSIGNMENTS = FastArray %i[op_asgn or_asgn and_asgn]
43+
ASSIGNMENTS = FastArray(EQUALS_ASSIGNMENTS + SHORTHAND_ASSIGNMENTS)
44+
45+
BASIC_CONDITIONALS = FastArray %i[if while until]
46+
CONDITIONALS = FastArray[*BASIC_CONDITIONALS, :case]
47+
VARIABLES = FastArray %i[ivar gvar cvar lvar]
48+
REFERENCES = FastArray %i[nth_ref back_ref]
49+
KEYWORDS = FastArray %i[alias and break case class def defs defined?
50+
kwbegin do else ensure for if module next
51+
not or postexe redo rescue retry return self
52+
super zsuper then undef until when while
53+
yield]
54+
OPERATOR_KEYWORDS = FastArray %i[and or]
55+
SPECIAL_KEYWORDS = FastArray %w[__FILE__ __LINE__ __ENCODING__]
56+
ARGUMENT_TYPES = FastArray %i[arg optarg restarg kwarg kwoptarg kwrestarg blockarg]
5757

5858
# @see https://www.rubydoc.info/gems/ast/AST/Node:initialize
5959
def initialize(type, children = [], properties = {})

lib/rubocop/ast/node/block_node.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ module AST
1111
class BlockNode < Node
1212
include MethodIdentifierPredicates
1313

14-
VOID_CONTEXT_METHODS = %i[each tap].freeze
14+
VOID_CONTEXT_METHODS = FastArray %i[each tap]
1515

1616
# The `send` node associated with this block.
1717
#

lib/rubocop/ast/node/mixin/collection_node.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ module CollectionNode
77
extend Forwardable
88

99
ARRAY_METHODS =
10-
(Array.instance_methods - Object.instance_methods - [:to_a]).freeze
10+
FastArray(Array.instance_methods - Object.instance_methods - [:to_a])
1111

1212
def_delegators :to_a, *ARRAY_METHODS
1313
end

lib/rubocop/ast/node/mixin/method_dispatch_node.rb

+2-2
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ module MethodDispatchNode
99
extend NodePattern::Macros
1010
include MethodIdentifierPredicates
1111

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

1515
# The receiving node of the method dispatch.
1616
#

lib/rubocop/ast/node/mixin/method_identifier_predicates.rb

+17-17
Original file line numberDiff line numberDiff line change
@@ -7,23 +7,23 @@ module AST
77
#
88
# @note this mixin expects `#method_name` and `#receiver` to be implemented
99
module MethodIdentifierPredicates # rubocop:disable Metrics/ModuleLength
10-
ENUMERATOR_METHODS = %i[collect collect_concat detect downto each
11-
find find_all find_index inject loop map!
12-
map reduce reject reject! reverse_each select
13-
select! times upto].to_set.freeze
10+
ENUMERATOR_METHODS = FastArray %i[collect collect_concat detect downto each
11+
find find_all find_index inject loop map!
12+
map reduce reject reject! reverse_each select
13+
select! times upto]
1414

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

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

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

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

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

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

6666
# Checks whether the method name matches the argument.
6767
#

lib/rubocop/ast/traversal.rb

+23-23
Original file line numberDiff line numberDiff line change
@@ -15,29 +15,29 @@ def walk(node)
1515
nil
1616
end
1717

18-
NO_CHILD_NODES = %i[true false nil int float complex
19-
rational str sym regopt self lvar
20-
ivar cvar gvar nth_ref back_ref cbase
21-
arg restarg blockarg shadowarg
22-
kwrestarg zsuper redo retry
23-
forward_args forwarded_args
24-
match_var match_nil_pattern empty_else
25-
forward_arg lambda procarg0 __ENCODING__].freeze
26-
ONE_CHILD_NODE = %i[splat kwsplat block_pass not break next
27-
preexe postexe match_current_line defined?
28-
arg_expr pin match_rest if_guard unless_guard
29-
match_with_trailing_comma].freeze
30-
MANY_CHILD_NODES = %i[dstr dsym xstr regexp array hash pair
31-
mlhs masgn or_asgn and_asgn
32-
undef alias args super yield or and
33-
while_post until_post iflipflop eflipflop
34-
match_with_lvasgn begin kwbegin return
35-
in_match match_alt
36-
match_as array_pattern array_pattern_with_tail
37-
hash_pattern const_pattern
38-
index indexasgn].freeze
39-
SECOND_CHILD_ONLY = %i[lvasgn ivasgn cvasgn gvasgn optarg kwarg
40-
kwoptarg].freeze
18+
NO_CHILD_NODES = FastArray %i[true false nil int float complex
19+
rational str sym regopt self lvar
20+
ivar cvar gvar nth_ref back_ref cbase
21+
arg restarg blockarg shadowarg
22+
kwrestarg zsuper redo retry
23+
forward_args forwarded_args
24+
match_var match_nil_pattern empty_else
25+
forward_arg lambda procarg0 __ENCODING__]
26+
ONE_CHILD_NODE = FastArray %i[splat kwsplat block_pass not break next
27+
preexe postexe match_current_line defined?
28+
arg_expr pin match_rest if_guard unless_guard
29+
match_with_trailing_comma]
30+
MANY_CHILD_NODES = FastArray %i[dstr dsym xstr regexp array hash pair
31+
mlhs masgn or_asgn and_asgn
32+
undef alias args super yield or and
33+
while_post until_post iflipflop eflipflop
34+
match_with_lvasgn begin kwbegin return
35+
in_match match_alt
36+
match_as array_pattern array_pattern_with_tail
37+
hash_pattern const_pattern
38+
index indexasgn]
39+
SECOND_CHILD_ONLY = FastArray %i[lvasgn ivasgn cvasgn gvasgn optarg kwarg
40+
kwoptarg]
4141

4242
NO_CHILD_NODES.each do |type|
4343
module_eval("def on_#{type}(node); end", __FILE__, __LINE__)

spec/rubocop/ast/fast_array_spec.rb

+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# frozen_string_literal: true
2+
3+
RSpec.describe RuboCop::AST::FastArray do
4+
shared_examples 'a fast_array' do
5+
it { is_expected.to be_frozen }
6+
it { expect(fast_array.include?(:included)).to be true }
7+
it { expect(fast_array.include?(:not_included)).to be false }
8+
it { is_expected.to eq fast_array.dup }
9+
10+
describe '#to_a' do
11+
subject { fast_array.to_a }
12+
13+
it { is_expected.to equal fast_array.to_a }
14+
it { is_expected.to be_frozen }
15+
it { is_expected.to include :included }
16+
end
17+
18+
describe '#to_set' do
19+
subject { fast_array.to_set }
20+
21+
it { is_expected.to equal fast_array.to_set }
22+
it { is_expected.to be_frozen }
23+
it { is_expected.to be >= Set[:included] }
24+
end
25+
end
26+
27+
let(:values) { %i[included also_included] }
28+
29+
describe '.new' do
30+
subject(:fast_array) { described_class.new(values) }
31+
32+
it_behaves_like 'a fast_array'
33+
34+
it 'enforces a single array argument' do
35+
expect { described_class.new }.to raise_error ArgumentError
36+
expect { described_class.new(5) }.to raise_error ArgumentError
37+
end
38+
39+
it 'has freeze return self' do
40+
expect(fast_array.freeze).to equal fast_array
41+
end
42+
43+
it 'has the right case equality' do
44+
expect(fast_array).to be === :included # rubocop:disable Style/CaseEquality
45+
end
46+
end
47+
48+
describe '.[]' do
49+
subject(:fast_array) { described_class[*values] }
50+
51+
it_behaves_like 'a fast_array'
52+
end
53+
54+
describe '()' do
55+
subject(:fast_array) { FastArray values }
56+
57+
it_behaves_like 'a fast_array'
58+
end
59+
end

0 commit comments

Comments
 (0)