From 4ee442b997591f4062cb46f7662bebd0d14037ac Mon Sep 17 00:00:00 2001 From: Marc-Andre Lafortune Date: Tue, 7 Jul 2020 17:30:38 -0400 Subject: [PATCH] [Fixes #22] AutoConstToSet builds Set variants of constants automatically --- lib/rubocop/ast.rb | 1 + lib/rubocop/ast/auto_const_to_set.rb | 26 ++++++++++++ lib/rubocop/ast/node.rb | 41 ++++++++++--------- .../ast/node/mixin/method_dispatch_node.rb | 3 +- .../mixin/method_identifier_predicates.rb | 36 ++++++++-------- spec/rubocop/ast/auto_const_to_set_spec.rb | 24 +++++++++++ 6 files changed, 93 insertions(+), 38 deletions(-) create mode 100644 lib/rubocop/ast/auto_const_to_set.rb create mode 100644 spec/rubocop/ast/auto_const_to_set_spec.rb 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..2d1895678 --- /dev/null +++ b/lib/rubocop/ast/auto_const_to_set.rb @@ -0,0 +1,26 @@ +# 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..6ff99b932 100644 --- a/lib/rubocop/ast/node/mixin/method_identifier_predicates.rb +++ b/lib/rubocop/ast/node/mixin/method_identifier_predicates.rb @@ -7,19 +7,21 @@ 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 - select! times upto].to_set.freeze + select! times upto].freeze - ENUMERABLE_METHODS = (Enumerable.instance_methods + [:each]).to_set.freeze + ENUMERABLE_METHODS = (Enumerable.instance_methods + [:each]).freeze # http://phrogz.net/programmingruby/language.html#table_18.4 OPERATOR_METHODS = %i[| ^ & <=> == === =~ > >= < <= << >> + - * / - % ** ~ +@ -@ !@ ~@ [] []= ! != !~ `].to_set.freeze + % ** ~ +@ -@ !@ ~@ [] []= ! != !~ `].freeze - NONMUTATING_BINARY_OPERATOR_METHODS = %i[* / % + - == === != < > <= >= <=>].to_set.freeze - NONMUTATING_UNARY_OPERATOR_METHODS = %i[+@ -@ ~ !].to_set.freeze + NONMUTATING_BINARY_OPERATOR_METHODS = %i[* / % + - == === != < > <= >= <=>].freeze + NONMUTATING_UNARY_OPERATOR_METHODS = %i[+@ -@ ~ !].freeze NONMUTATING_OPERATOR_METHODS = (NONMUTATING_BINARY_OPERATOR_METHODS + NONMUTATING_UNARY_OPERATOR_METHODS).freeze @@ -36,7 +38,7 @@ 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 + ].freeze NONMUTATING_HASH_METHODS = %i[ any? assoc compact dig each each_key each_pair @@ -46,7 +48,7 @@ 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 + ].freeze NONMUTATING_STRING_METHODS = %i[ ascii_only? b bytes bytesize byteslice capitalize @@ -61,7 +63,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 + ].freeze # Checks whether the method name matches the argument. # @@ -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..5ea1eb8ff --- /dev/null +++ b/spec/rubocop/ast/auto_const_to_set_spec.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +RSpec.describe RuboCop::AST::AutoConstToSet do + let(:mod) do + Module.new do + extend RuboCop::AST::AutoConstToSet + end + end + + before do + stub_const('Mod', mod) + stub_const('Mod::WORDS', %w[hello world].freeze) + 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'] + end + + it 'raises an erreor if constant is already a set' do + stub_const('Mod::WORDS', %w[hello world].to_set.freeze) + expect { mod::WORDS_SET }.to raise_error(TypeError) + end +end