diff --git a/lib/rubocop/ast.rb b/lib/rubocop/ast.rb index d9bf7efbf..e2cd0b585 100644 --- a/lib/rubocop/ast.rb +++ b/lib/rubocop/ast.rb @@ -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' diff --git a/lib/rubocop/ast/fast_array.rb b/lib/rubocop/ast/fast_array.rb new file mode 100644 index 000000000..50974f6ca --- /dev/null +++ b/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 diff --git a/lib/rubocop/ast/node.rb b/lib/rubocop/ast/node.rb index e23b0b665..754afd3cf 100644 --- a/lib/rubocop/ast/node.rb +++ b/lib/rubocop/ast/node.rb @@ -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 = {}) diff --git a/lib/rubocop/ast/node/block_node.rb b/lib/rubocop/ast/node/block_node.rb index 1af323acd..7160b1ea8 100644 --- a/lib/rubocop/ast/node/block_node.rb +++ b/lib/rubocop/ast/node/block_node.rb @@ -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. # diff --git a/lib/rubocop/ast/node/mixin/collection_node.rb b/lib/rubocop/ast/node/mixin/collection_node.rb index acb12de7a..f2077dfa3 100644 --- a/lib/rubocop/ast/node/mixin/collection_node.rb +++ b/lib/rubocop/ast/node/mixin/collection_node.rb @@ -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 diff --git a/lib/rubocop/ast/node/mixin/method_dispatch_node.rb b/lib/rubocop/ast/node/mixin/method_dispatch_node.rb index 88d1587c2..bd81d5e08 100644 --- a/lib/rubocop/ast/node/mixin/method_dispatch_node.rb +++ b/lib/rubocop/ast/node/mixin/method_dispatch_node.rb @@ -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. # diff --git a/lib/rubocop/ast/node/mixin/method_identifier_predicates.rb b/lib/rubocop/ast/node/mixin/method_identifier_predicates.rb index 060d2b3fb..f8383eaf7 100644 --- a/lib/rubocop/ast/node/mixin/method_identifier_predicates.rb +++ b/lib/rubocop/ast/node/mixin/method_identifier_predicates.rb @@ -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 + 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].to_set.freeze + 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? @@ -36,9 +36,9 @@ 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 @@ -46,9 +46,9 @@ module MethodIdentifierPredicates # rubocop:disable Metrics/ModuleLength 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 @@ -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. # diff --git a/lib/rubocop/ast/traversal.rb b/lib/rubocop/ast/traversal.rb index 37dc4cc54..2141a2d1f 100644 --- a/lib/rubocop/ast/traversal.rb +++ b/lib/rubocop/ast/traversal.rb @@ -15,19 +15,19 @@ def walk(node) nil end - NO_CHILD_NODES = %i[true false nil int float complex + 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__].freeze - ONE_CHILD_NODE = %i[splat kwsplat block_pass not break next + 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].freeze - MANY_CHILD_NODES = %i[dstr dsym xstr regexp array hash pair + 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 @@ -35,9 +35,9 @@ def walk(node) 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 + 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__) diff --git a/spec/rubocop/ast/fast_array_spec.rb b/spec/rubocop/ast/fast_array_spec.rb new file mode 100644 index 000000000..92d9cfb25 --- /dev/null +++ b/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