diff --git a/lib/rubocop/ast.rb b/lib/rubocop/ast.rb index 41eed81cf..17e3eb96e 100644 --- a/lib/rubocop/ast.rb +++ b/lib/rubocop/ast.rb @@ -3,6 +3,7 @@ require 'parser' require 'forwardable' +require_relative 'ast/tuple' require_relative 'ast/node_pattern' require_relative 'ast/sexp' require_relative 'ast/node' diff --git a/lib/rubocop/ast/node.rb b/lib/rubocop/ast/node.rb index 483ffc13f..9bc2a6ad9 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 = Tuple %i[== === != <= >= > <] + + TRUTHY_LITERALS = Tuple %i[str dstr xstr int float sym dsym array + hash regexp true irange erange complex + rational regopt] + FALSEY_LITERALS = Tuple %i[false nil] + LITERALS = Tuple(TRUTHY_LITERALS + FALSEY_LITERALS) + COMPOSITE_LITERALS = Tuple %i[dstr xstr dsym array hash irange + erange regexp] + BASIC_LITERALS = Tuple(LITERALS - COMPOSITE_LITERALS) + MUTABLE_LITERALS = Tuple %i[str dstr xstr array hash + regexp irange erange] + IMMUTABLE_LITERALS = Tuple(LITERALS - MUTABLE_LITERALS) + + EQUALS_ASSIGNMENTS = Tuple %i[lvasgn ivasgn cvasgn gvasgn + casgn masgn] + SHORTHAND_ASSIGNMENTS = Tuple %i[op_asgn or_asgn and_asgn] + ASSIGNMENTS = Tuple(EQUALS_ASSIGNMENTS + SHORTHAND_ASSIGNMENTS) + + BASIC_CONDITIONALS = Tuple %i[if while until] + CONDITIONALS = Tuple[*BASIC_CONDITIONALS, :case] + VARIABLES = Tuple %i[ivar gvar cvar lvar] + REFERENCES = Tuple %i[nth_ref back_ref] + KEYWORDS = Tuple %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 = Tuple %i[and or] + SPECIAL_KEYWORDS = Tuple %w[__FILE__ __LINE__ __ENCODING__] + ARGUMENT_TYPES = Tuple %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..fff821b9e 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 = Tuple %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..2fd3e219b 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 + Tuple(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 bf2bd3b4f..6b9c63867 100644 --- a/lib/rubocop/ast/node/mixin/method_dispatch_node.rb +++ b/lib/rubocop/ast/node/mixin/method_dispatch_node.rb @@ -8,8 +8,8 @@ module MethodDispatchNode extend NodePattern::Macros include MethodIdentifierPredicates - ARITHMETIC_OPERATORS = %i[+ - * / % **].freeze - SPECIAL_MODIFIERS = %w[private protected].freeze + ARITHMETIC_OPERATORS = Tuple %i[+ - * / % **] + SPECIAL_MODIFIERS = Tuple %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 7ddca670a..db595acfa 100644 --- a/lib/rubocop/ast/node/mixin/method_identifier_predicates.rb +++ b/lib/rubocop/ast/node/mixin/method_identifier_predicates.rb @@ -7,14 +7,14 @@ module AST # # @note this mixin expects `#method_name` and `#receiver` to be implemented module MethodIdentifierPredicates - 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].freeze + ENUMERATOR_METHODS = Tuple %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] # http://phrogz.net/programmingruby/language.html#table_18.4 - OPERATOR_METHODS = %i[| ^ & <=> == === =~ > >= < <= << >> + - * / - % ** ~ +@ -@ !@ ~@ [] []= ! != !~ `].freeze + OPERATOR_METHODS = Tuple %i[| ^ & <=> == === =~ > >= < <= << >> + - * / + % ** ~ +@ -@ !@ ~@ [] []= ! != !~ `] # Checks whether the method name matches the argument. # diff --git a/lib/rubocop/ast/traversal.rb b/lib/rubocop/ast/traversal.rb index 91de20a3b..7c86b6a7c 100644 --- a/lib/rubocop/ast/traversal.rb +++ b/lib/rubocop/ast/traversal.rb @@ -15,27 +15,27 @@ 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 lambda redo retry - forward_args forwarded_args - match_var match_nil_pattern empty_else].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].freeze - SECOND_CHILD_ONLY = %i[lvasgn ivasgn cvasgn gvasgn optarg kwarg - kwoptarg].freeze + NO_CHILD_NODES = Tuple %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 lambda redo retry + forward_args forwarded_args + match_var match_nil_pattern empty_else] + ONE_CHILD_NODE = Tuple %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 = Tuple %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] + SECOND_CHILD_ONLY = Tuple %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/lib/rubocop/ast/tuple.rb b/lib/rubocop/ast/tuple.rb new file mode 100644 index 000000000..bba2d1a58 --- /dev/null +++ b/lib/rubocop/ast/tuple.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +module RuboCop + module AST + # Tuple represents a frozen and indexed Array + # It's meant to be used to store arrays of constants in an array + # but with faster lookup via `include?` + # Like `Set`, the case equality `===` is an alias for `include?` + # + # FOO = Tuple[:hello, :world] + # FOO.include?(:hello) # => true, quickly + # + # case bar + # when FOO # Note: no splat + # # decided quickly + # # ... + class Tuple < ::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 Tuple + def to_a + self + end + + def include?(value) + @to_set.include?(value) + end + + alias === include? + end + end +end + +def Tuple(list) # rubocop:disable Naming/MethodName + RuboCop::AST::Tuple.new(list) +end diff --git a/spec/rubocop/ast/tuple_spec.rb b/spec/rubocop/ast/tuple_spec.rb new file mode 100644 index 000000000..1f04ae23b --- /dev/null +++ b/spec/rubocop/ast/tuple_spec.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +RSpec.describe RuboCop::AST::Tuple do + shared_examples 'a tuple' do + it { is_expected.to be_frozen } + it { expect(tuple.include?(:included)).to be true } + it { expect(tuple.include?(:not_included)).to be false } + it { is_expected.to eq tuple.dup } + + describe '#to_a' do + subject { tuple.to_a } + + it { is_expected.to equal tuple.to_a } + it { is_expected.to be_frozen } + it { is_expected.to include :included } + end + + describe '#to_set' do + subject { tuple.to_set } + + it { is_expected.to equal tuple.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(:tuple) { described_class.new(values) } + + it_behaves_like 'a tuple' + + 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(tuple.freeze).to equal tuple + end + + it 'has the right case equality' do + expect(tuple).to be === :included # rubocop:disable Style/CaseEquality + end + end + + describe '.[]' do + subject(:tuple) { described_class[*values] } + + it_behaves_like 'a tuple' + end + + describe '()' do + subject(:tuple) { Tuple values } + + it_behaves_like 'a tuple' + end +end