diff --git a/lib/rubocop/ast.rb b/lib/rubocop/ast.rb index d9bf7efbf..6c6eb2962 100644 --- a/lib/rubocop/ast.rb +++ b/lib/rubocop/ast.rb @@ -4,6 +4,7 @@ require 'forwardable' require 'set' +require_relative 'ast/auto_const_to_set' require_relative 'ast/node_pattern' require_relative 'ast/sexp' require_relative 'ast/node' diff --git a/lib/rubocop/ast/auto_const_to_set.rb b/lib/rubocop/ast/auto_const_to_set.rb new file mode 100644 index 000000000..9cbd52ee4 --- /dev/null +++ b/lib/rubocop/ast/auto_const_to_set.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module RuboCop + module AST + # If a module extends this, then `SOME_CONSTANT_SET` will be a set created + # automatically from `SOME_CONSTANT` + # + # class Foo + # extend AutoConstToSet + # + # WORDS = %w[hello world].freeze + # end + # + # Foo::WORDS_SET # => Set['hello', 'world'] + module AutoConstToSet + def const_missing(name) + return super unless name =~ /(?.*)_SET/ + + array = const_get(Regexp.last_match(:array_name)) + raise TypeError, "Already a set!" if array.is_a?(Set) + const_set(name, array.to_set.freeze) + end + end + end +end diff --git a/lib/rubocop/ast/node.rb b/lib/rubocop/ast/node.rb index 0ca94ed1e..bbe2eec3d 100644 --- a/lib/rubocop/ast/node.rb +++ b/lib/rubocop/ast/node.rb @@ -21,6 +21,7 @@ module AST class Node < Parser::AST::Node # rubocop:disable Metrics/ClassLength include RuboCop::AST::Sexp extend NodePattern::Macros + extend AutoConstToSet # <=> isn't included here, because it doesn't return a boolean. COMPARISON_OPERATORS = %i[== === != <= >= > <].freeze @@ -360,27 +361,27 @@ def empty_source? PATTERN def literal? - LITERALS.include?(type) + LITERALS_SET.include?(type) end def basic_literal? - BASIC_LITERALS.include?(type) + BASIC_LITERALS_SET.include?(type) end def truthy_literal? - TRUTHY_LITERALS.include?(type) + TRUTHY_LITERALS_SET.include?(type) end def falsey_literal? - FALSEY_LITERALS.include?(type) + FALSEY_LITERALS_SET.include?(type) end def mutable_literal? - MUTABLE_LITERALS.include?(type) + MUTABLE_LITERALS_SET.include?(type) end def immutable_literal? - IMMUTABLE_LITERALS.include?(type) + IMMUTABLE_LITERALS_SET.include?(type) end %i[literal basic_literal].each do |kind| @@ -401,55 +402,55 @@ def immutable_literal? end def variable? - VARIABLES.include?(type) + VARIABLES_SET.include?(type) end def reference? - REFERENCES.include?(type) + REFERENCES_SET.include?(type) end def equals_asgn? - EQUALS_ASSIGNMENTS.include?(type) + EQUALS_ASSIGNMENTS_SET.include?(type) end def shorthand_asgn? - SHORTHAND_ASSIGNMENTS.include?(type) + SHORTHAND_ASSIGNMENTS_SET.include?(type) end def assignment? - ASSIGNMENTS.include?(type) + ASSIGNMENTS_SET.include?(type) end def basic_conditional? - BASIC_CONDITIONALS.include?(type) + BASIC_CONDITIONALS_SET.include?(type) end def conditional? - CONDITIONALS.include?(type) + CONDITIONALS_SET.include?(type) end def post_condition_loop? - POST_CONDITION_LOOP_TYPES.include?(type) + POST_CONDITION_LOOP_TYPES_SET.include?(type) end # Note: `loop { }` is a normal method call and thus not a loop keyword. def loop_keyword? - LOOP_TYPES.include?(type) + LOOP_TYPES_SET.include?(type) end def keyword? return true if special_keyword? || send_type? && prefix_not? - return false unless KEYWORDS.include?(type) + return false unless KEYWORDS_SET.include?(type) - !OPERATOR_KEYWORDS.include?(type) || loc.operator.is?(type.to_s) + !OPERATOR_KEYWORDS_SET.include?(type) || loc.operator.is?(type.to_s) end def special_keyword? - SPECIAL_KEYWORDS.include?(source) + SPECIAL_KEYWORDS_SET.include?(source) end def operator_keyword? - OPERATOR_KEYWORDS.include?(type) + OPERATOR_KEYWORDS_SET.include?(type) end def parenthesized_call? @@ -469,7 +470,7 @@ def argument? end def argument_type? - ARGUMENT_TYPES.include?(type) + ARGUMENT_TYPES_SET.include?(type) end def boolean_type? diff --git a/lib/rubocop/ast/node/mixin/method_dispatch_node.rb b/lib/rubocop/ast/node/mixin/method_dispatch_node.rb index 88d1587c2..408bc2c00 100644 --- a/lib/rubocop/ast/node/mixin/method_dispatch_node.rb +++ b/lib/rubocop/ast/node/mixin/method_dispatch_node.rb @@ -8,6 +8,7 @@ module AST module MethodDispatchNode extend NodePattern::Macros include MethodIdentifierPredicates + extend AutoConstToSet ARITHMETIC_OPERATORS = %i[+ - * / % **].freeze SPECIAL_MODIFIERS = %w[private protected].freeze @@ -167,7 +168,7 @@ def block_literal? # @return [Boolean] whether the dispatched method is an arithmetic # operation def arithmetic_operation? - ARITHMETIC_OPERATORS.include?(method_name) + ARITHMETIC_OPERATORS_SET.include?(method_name) end # Checks if this node is part of a chain of `def` modifiers. diff --git a/lib/rubocop/ast/node/mixin/method_identifier_predicates.rb b/lib/rubocop/ast/node/mixin/method_identifier_predicates.rb index 060d2b3fb..5e684ea1d 100644 --- a/lib/rubocop/ast/node/mixin/method_identifier_predicates.rb +++ b/lib/rubocop/ast/node/mixin/method_identifier_predicates.rb @@ -7,6 +7,8 @@ module AST # # @note this mixin expects `#method_name` and `#receiver` to be implemented module MethodIdentifierPredicates # rubocop:disable Metrics/ModuleLength + extend AutoConstToSet + ENUMERATOR_METHODS = %i[collect collect_concat detect downto each find find_all find_index inject loop map! map reduce reject reject! reverse_each select @@ -75,49 +77,49 @@ def method?(name) # # @return [Boolean] whether the method is an operator def operator_method? - OPERATOR_METHODS.include?(method_name) + OPERATOR_METHODS_SET.include?(method_name) end # Checks whether the method is a nonmutating binary operator method. # # @return [Boolean] whether the method is a nonmutating binary operator method def nonmutating_binary_operator_method? - NONMUTATING_BINARY_OPERATOR_METHODS.include?(method_name) + NONMUTATING_BINARY_OPERATOR_METHODS_SET.include?(method_name) end # Checks whether the method is a nonmutating unary operator method. # # @return [Boolean] whether the method is a nonmutating unary operator method def nonmutating_unary_operator_method? - NONMUTATING_UNARY_OPERATOR_METHODS.include?(method_name) + NONMUTATING_UNARY_OPERATOR_METHODS_SET.include?(method_name) end # Checks whether the method is a nonmutating operator method. # # @return [Boolean] whether the method is a nonmutating operator method def nonmutating_operator_method? - NONMUTATING_OPERATOR_METHODS.include?(method_name) + NONMUTATING_OPERATOR_METHODS_SET.include?(method_name) end # Checks whether the method is a nonmutating Array method. # # @return [Boolean] whether the method is a nonmutating Array method def nonmutating_array_method? - NONMUTATING_ARRAY_METHODS.include?(method_name) + NONMUTATING_ARRAY_METHODS_SET.include?(method_name) end # Checks whether the method is a nonmutating Hash method. # # @return [Boolean] whether the method is a nonmutating Hash method def nonmutating_hash_method? - NONMUTATING_HASH_METHODS.include?(method_name) + NONMUTATING_HASH_METHODS_SET.include?(method_name) end # Checks whether the method is a nonmutating String method. # # @return [Boolean] whether the method is a nonmutating String method def nonmutating_string_method? - NONMUTATING_STRING_METHODS.include?(method_name) + NONMUTATING_STRING_METHODS_SET.include?(method_name) end # Checks whether the method is a comparison method. @@ -138,7 +140,7 @@ def assignment_method? # # @return [Boolean] whether the method is an enumerator def enumerator_method? - ENUMERATOR_METHODS.include?(method_name) || + ENUMERATOR_METHODS_SET.include?(method_name) || method_name.to_s.start_with?('each_') end @@ -146,7 +148,7 @@ def enumerator_method? # # @return [Boolean] whether the method is an Enumerable method def enumerable_method? - ENUMERABLE_METHODS.include?(method_name) + ENUMERABLE_METHODS_SET.include?(method_name) end # Checks whether the method is a predicate method. diff --git a/spec/rubocop/ast/auto_const_to_set_spec.rb b/spec/rubocop/ast/auto_const_to_set_spec.rb new file mode 100644 index 000000000..24a4196da --- /dev/null +++ b/spec/rubocop/ast/auto_const_to_set_spec.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +RSpec.describe RuboCop::AST::AutoConstToSet do + let(:mod) do + Module.new do + extend RuboCop::AST::AutoConstToSet + + WORDS = %w[hello world].freeze + end + end + + it 'automatically creates set variants for array constants' do + expect(mod.constants).not_to include :WORDS_SET + expect(mod::WORDS_SET).to eq Set['hello', 'world'] + expect { mod::WORDS_SET_SET }.to raise_error(TypeError) + end +end