Skip to content

Commit 6b2ee9f

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

10 files changed

+198
-76
lines changed

.rubocop_todo.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ Metrics/MethodLength:
3232
# Offense count: 1
3333
# Configuration parameters: CountComments.
3434
Metrics/ModuleLength:
35-
Max: 101
35+
Max: 102
3636

3737
# Offense count: 1
3838
# Configuration parameters: ExpectMatchingDefinition, Regex, IgnoreExecutableScripts, AllowedAcronyms.

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

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
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+
# Defines a function `FastArray(list)` similar to `Array(list)`
18+
module Tool
19+
def FastArray(list) # rubocop:disable Naming/MethodName
20+
RuboCop::AST::FastArray.new(list)
21+
end
22+
end
23+
24+
attr_reader :to_set
25+
26+
def initialize(ary)
27+
raise ArgumentError, 'Must be initialized with an array' unless ary.is_a?(Array)
28+
29+
super
30+
freeze
31+
end
32+
33+
def self.[](*values)
34+
new(values)
35+
end
36+
37+
def freeze
38+
@to_set ||= Set.new(self).freeze
39+
super
40+
end
41+
42+
# Return self, not a newly allocated FastArray
43+
def to_a
44+
self
45+
end
46+
47+
def include?(value)
48+
@to_set.include?(value)
49+
end
50+
51+
alias === include?
52+
end
53+
end
54+
end

lib/rubocop/ast/node.rb

+32-31
Original file line numberDiff line numberDiff line change
@@ -21,39 +21,40 @@ module AST
2121
class Node < Parser::AST::Node # rubocop:disable Metrics/ClassLength
2222
include RuboCop::AST::Sexp
2323
extend NodePattern::Macros
24+
extend FastArray::Tool
2425

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

5859
# @see https://www.rubydoc.info/gems/ast/AST/Node:initialize
5960
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+
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

+3-2
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@ module AST
88
module MethodDispatchNode
99
extend NodePattern::Macros
1010
include MethodIdentifierPredicates
11+
extend FastArray::Tool
1112

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

1516
# The receiving node of the method dispatch.
1617
#

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

+19-17
Original file line numberDiff line numberDiff line change
@@ -7,23 +7,25 @@ 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+
extend FastArray::Tool
1411

15-
ENUMERABLE_METHODS = (Enumerable.instance_methods + [:each]).to_set.freeze
12+
ENUMERATOR_METHODS = FastArray %i[collect collect_concat detect downto each
13+
find find_all find_index inject loop map!
14+
map reduce reject reject! reverse_each select
15+
select! times upto]
16+
17+
ENUMERABLE_METHODS = FastArray(Enumerable.instance_methods + [:each])
1618

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

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
23+
NONMUTATING_BINARY_OPERATOR_METHODS = FastArray %i[* / % + - == === != < > <= >= <=>]
24+
NONMUTATING_UNARY_OPERATOR_METHODS = FastArray %i[+@ -@ ~ !]
25+
NONMUTATING_OPERATOR_METHODS = FastArray(NONMUTATING_BINARY_OPERATOR_METHODS +
26+
NONMUTATING_UNARY_OPERATOR_METHODS)
2527

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

41-
NONMUTATING_HASH_METHODS = %i[
43+
NONMUTATING_HASH_METHODS = FastArray %i[
4244
any? assoc compact dig each each_key each_pair
4345
each_value empty? eql? fetch fetch_values filter
4446
flatten has_key? has_value? hash include? inspect
4547
invert key key? keys? length member? merge rassoc
4648
rehash reject select size slice to_a to_h to_hash
4749
to_proc to_s transform_keys transform_values value?
4850
values values_at
49-
].to_set.freeze
51+
]
5052

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

6668
# Checks whether the method name matches the argument.
6769
#

lib/rubocop/ast/traversal.rb

+25-23
Original file line numberDiff line numberDiff line change
@@ -8,36 +8,38 @@ module AST
88
# Override methods to perform custom processing. Remember to call `super`
99
# if you want to recursively process descendant nodes.
1010
module Traversal
11+
extend FastArray::Tool
12+
1113
def walk(node)
1214
return if node.nil?
1315

1416
send(:"on_#{node.type}", node)
1517
nil
1618
end
1719

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

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

spec/rubocop/ast/fast_array_spec.rb

+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
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+
before { extend RuboCop::AST::FastArray::Tool }
58+
59+
it_behaves_like 'a fast_array'
60+
end
61+
end

0 commit comments

Comments
 (0)