Skip to content

Commit

Permalink
[Fixes rubocop#22] AutoConstToSet builds Set variants of constants au…
Browse files Browse the repository at this point in the history
…tomatically
  • Loading branch information
marcandre committed Jul 7, 2020
1 parent c787af3 commit cd39721
Show file tree
Hide file tree
Showing 6 changed files with 93 additions and 38 deletions.
1 change: 1 addition & 0 deletions lib/rubocop/ast.rb
Expand Up @@ -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'
Expand Down
26 changes: 26 additions & 0 deletions 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'].freeze
module AutoConstToSet
def const_missing(name)
return super unless name =~ /(?<array_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
41 changes: 21 additions & 20 deletions lib/rubocop/ast/node.rb
Expand Up @@ -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
Expand Down Expand Up @@ -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|
Expand All @@ -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?
Expand All @@ -469,7 +470,7 @@ def argument?
end

def argument_type?
ARGUMENT_TYPES.include?(type)
ARGUMENT_TYPES_SET.include?(type)
end

def boolean_type?
Expand Down
3 changes: 2 additions & 1 deletion lib/rubocop/ast/node/mixin/method_dispatch_node.rb
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
36 changes: 19 additions & 17 deletions lib/rubocop/ast/node/mixin/method_identifier_predicates.rb
Expand Up @@ -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

Expand All @@ -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
Expand All @@ -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
Expand All @@ -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.
#
Expand All @@ -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.
Expand All @@ -138,15 +140,15 @@ 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

# Checks whether the method is an Enumerable 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.
Expand Down
24 changes: 24 additions & 0 deletions 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

0 comments on commit cd39721

Please sign in to comment.