Skip to content

Commit e5117a8

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

9 files changed

+167
-62
lines changed

lib/rubocop/ast.rb

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
require 'parser'
44
require 'forwardable'
55

6+
require_relative 'ast/tuple'
67
require_relative 'ast/node_pattern'
78
require_relative 'ast/sexp'
89
require_relative 'ast/node'

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 = Tuple %i[== === != <= >= > <]
27+
28+
TRUTHY_LITERALS = Tuple %i[str dstr xstr int float sym dsym array
29+
hash regexp true irange erange complex
30+
rational regopt]
31+
FALSEY_LITERALS = Tuple %i[false nil]
32+
LITERALS = Tuple(TRUTHY_LITERALS + FALSEY_LITERALS)
33+
COMPOSITE_LITERALS = Tuple %i[dstr xstr dsym array hash irange
34+
erange regexp]
35+
BASIC_LITERALS = Tuple(LITERALS - COMPOSITE_LITERALS)
36+
MUTABLE_LITERALS = Tuple %i[str dstr xstr array hash
37+
regexp irange erange]
38+
IMMUTABLE_LITERALS = Tuple(LITERALS - MUTABLE_LITERALS)
39+
40+
EQUALS_ASSIGNMENTS = Tuple %i[lvasgn ivasgn cvasgn gvasgn
41+
casgn masgn]
42+
SHORTHAND_ASSIGNMENTS = Tuple %i[op_asgn or_asgn and_asgn]
43+
ASSIGNMENTS = Tuple(EQUALS_ASSIGNMENTS + SHORTHAND_ASSIGNMENTS)
44+
45+
BASIC_CONDITIONALS = Tuple %i[if while until]
46+
CONDITIONALS = Tuple[*BASIC_CONDITIONALS, :case]
47+
VARIABLES = Tuple %i[ivar gvar cvar lvar]
48+
REFERENCES = Tuple %i[nth_ref back_ref]
49+
KEYWORDS = Tuple %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 = Tuple %i[and or]
55+
SPECIAL_KEYWORDS = Tuple %w[__FILE__ __LINE__ __ENCODING__]
56+
ARGUMENT_TYPES = Tuple %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 = Tuple %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+
Tuple(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
@@ -8,8 +8,8 @@ module MethodDispatchNode
88
extend NodePattern::Macros
99
include MethodIdentifierPredicates
1010

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

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

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

+6-6
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,14 @@ module AST
77
#
88
# @note this mixin expects `#method_name` and `#receiver` to be implemented
99
module MethodIdentifierPredicates
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].freeze
10+
ENUMERATOR_METHODS = Tuple %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

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

1919
# Checks whether the method name matches the argument.
2020
#

lib/rubocop/ast/traversal.rb

+21-21
Original file line numberDiff line numberDiff line change
@@ -15,27 +15,27 @@ 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 lambda redo retry
23-
forward_args forwarded_args
24-
match_var match_nil_pattern empty_else].freeze
25-
ONE_CHILD_NODE = %i[splat kwsplat block_pass not break next
26-
preexe postexe match_current_line defined?
27-
arg_expr pin match_rest if_guard unless_guard
28-
match_with_trailing_comma].freeze
29-
MANY_CHILD_NODES = %i[dstr dsym xstr regexp array hash pair
30-
mlhs masgn or_asgn and_asgn
31-
undef alias args super yield or and
32-
while_post until_post iflipflop eflipflop
33-
match_with_lvasgn begin kwbegin return
34-
in_match match_alt
35-
match_as array_pattern array_pattern_with_tail
36-
hash_pattern const_pattern].freeze
37-
SECOND_CHILD_ONLY = %i[lvasgn ivasgn cvasgn gvasgn optarg kwarg
38-
kwoptarg].freeze
18+
NO_CHILD_NODES = Tuple %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 lambda redo retry
23+
forward_args forwarded_args
24+
match_var match_nil_pattern empty_else]
25+
ONE_CHILD_NODE = Tuple %i[splat kwsplat block_pass not break next
26+
preexe postexe match_current_line defined?
27+
arg_expr pin match_rest if_guard unless_guard
28+
match_with_trailing_comma]
29+
MANY_CHILD_NODES = Tuple %i[dstr dsym xstr regexp array hash pair
30+
mlhs masgn or_asgn and_asgn
31+
undef alias args super yield or and
32+
while_post until_post iflipflop eflipflop
33+
match_with_lvasgn begin kwbegin return
34+
in_match match_alt
35+
match_as array_pattern array_pattern_with_tail
36+
hash_pattern const_pattern]
37+
SECOND_CHILD_ONLY = Tuple %i[lvasgn ivasgn cvasgn gvasgn optarg kwarg
38+
kwoptarg]
3939

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

lib/rubocop/ast/tuple.rb

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# frozen_string_literal: true
2+
3+
module RuboCop
4+
module AST
5+
# Tuple represents a frozen and indexed Array
6+
# It's meant to be used to store arrays of constants in an array
7+
# but with faster lookup via `include?`
8+
# Like `Set`, the case equality `===` is an alias for `include?`
9+
#
10+
class Tuple < ::Array
11+
attr_reader :to_set
12+
13+
def initialize(ary)
14+
raise ArgumentError, 'Must be initialized with an array' unless ary.is_a?(Array)
15+
16+
super
17+
freeze
18+
end
19+
20+
def self.[](*values)
21+
new(values)
22+
end
23+
24+
def freeze
25+
@to_set ||= Set.new(self).freeze
26+
super
27+
end
28+
29+
# Return self, not a newly allocated Tuple
30+
def to_a
31+
self
32+
end
33+
34+
def include?(value)
35+
@to_set.include?(value)
36+
end
37+
38+
alias === include?
39+
end
40+
end
41+
end
42+
43+
def Tuple(list) # rubocop:disable Naming/MethodName
44+
RuboCop::AST::Tuple.new(list)
45+
end

spec/rubocop/ast/tuple_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::Tuple do
4+
shared_examples 'a tuple' do
5+
it { is_expected.to be_frozen }
6+
it { expect(tuple.include?(:included)).to be true }
7+
it { expect(tuple.include?(:not_included)).to be false }
8+
it { is_expected.to eq tuple.dup }
9+
10+
describe '#to_a' do
11+
subject { tuple.to_a }
12+
13+
it { is_expected.to equal tuple.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 { tuple.to_set }
20+
21+
it { is_expected.to equal tuple.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(:tuple) { described_class.new(values) }
31+
32+
it_behaves_like 'a tuple'
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(tuple.freeze).to equal tuple
41+
end
42+
43+
it 'has the right case equality' do
44+
expect(tuple).to be === :included # rubocop:disable Style/CaseEquality
45+
end
46+
end
47+
48+
describe '.[]' do
49+
subject(:tuple) { described_class[*values] }
50+
51+
it_behaves_like 'a tuple'
52+
end
53+
54+
describe '()' do
55+
subject(:tuple) { Tuple values }
56+
57+
it_behaves_like 'a tuple'
58+
end
59+
end

0 commit comments

Comments
 (0)