lib/rubocop/cop/lint/useless_access_modifier.rb
# frozen_string_literal: true
module RuboCop
module Cop
module Lint
# Checks for redundant access modifiers, including those with no
# code, those which are repeated, and leading `public` modifiers in a
# class or module body. Conditionally-defined methods are considered as
# always being defined, and thus access modifiers guarding such methods
# are not redundant.
#
# This cop has `ContextCreatingMethods` option. The default setting value
# is an empty array that means no method is specified.
# This setting is an array of methods which, when called, are known to
# create its own context in the module's current access context.
#
# It also has `MethodCreatingMethods` option. The default setting value
# is an empty array that means no method is specified.
# This setting is an array of methods which, when called, are known to
# create other methods in the module's current access context.
#
# @example
# # bad
# class Foo
# public # this is redundant (default access is public)
#
# def method
# end
# end
#
# # bad
# class Foo
# # The following is redundant (methods defined on the class'
# # singleton class are not affected by the private modifier)
# private
#
# def self.method3
# end
# end
#
# # bad
# class Foo
# protected
#
# define_method(:method2) do
# end
#
# protected # this is redundant (repeated from previous modifier)
#
# [1,2,3].each do |i|
# define_method("foo#{i}") do
# end
# end
# end
#
# # bad
# class Foo
# private # this is redundant (no following methods are defined)
# end
#
# # good
# class Foo
# private # this is not redundant (a method is defined)
#
# def method2
# end
# end
#
# # good
# class Foo
# # The following is not redundant (conditionally defined methods are
# # considered as always defining a method)
# private
#
# if condition?
# def method
# end
# end
# end
#
# # good
# class Foo
# protected # this is not redundant (a method is defined)
#
# define_method(:method2) do
# end
# end
#
# @example ContextCreatingMethods: concerning
# # Lint/UselessAccessModifier:
# # ContextCreatingMethods:
# # - concerning
#
# # good
# require 'active_support/concern'
# class Foo
# concerning :Bar do
# def some_public_method
# end
#
# private
#
# def some_private_method
# end
# end
#
# # this is not redundant because `concerning` created its own context
# private
#
# def some_other_private_method
# end
# end
#
# @example MethodCreatingMethods: delegate
# # Lint/UselessAccessModifier:
# # MethodCreatingMethods:
# # - delegate
#
# # good
# require 'active_support/core_ext/module/delegation'
# class Foo
# # this is not redundant because `delegate` creates methods
# private
#
# delegate :method_a, to: :method_b
# end
class UselessAccessModifier < Base
include RangeHelp
extend AutoCorrector
MSG = 'Useless `%<current>s` access modifier.'
def on_class(node)
check_node(node.body)
end
alias on_module on_class
alias on_sclass on_class
def on_block(node)
return unless eval_call?(node) || included_block?(node)
check_node(node.body)
end
alias on_numblock on_block
private
def autocorrect(corrector, node)
range = range_by_whole_lines(node.source_range, include_final_newline: true)
corrector.remove(range)
end
# @!method static_method_definition?(node)
def_node_matcher :static_method_definition?, <<~PATTERN
{def (send nil? {:attr :attr_reader :attr_writer :attr_accessor} ...)}
PATTERN
# @!method dynamic_method_definition?(node)
def_node_matcher :dynamic_method_definition?, <<~PATTERN
{(send nil? :define_method ...) ({block numblock} (send nil? :define_method ...) ...)}
PATTERN
# @!method class_or_instance_eval?(node)
def_node_matcher :class_or_instance_eval?, <<~PATTERN
({block numblock} (send _ {:class_eval :instance_eval}) ...)
PATTERN
def check_node(node)
return if node.nil?
if node.begin_type?
check_scope(node)
elsif node.send_type? && node.bare_access_modifier?
add_offense(node, message: format(MSG, current: node.method_name)) do |corrector|
autocorrect(corrector, node)
end
end
end
def access_modifier?(node)
node.bare_access_modifier? || node.method?(:private_class_method)
end
def check_scope(node)
cur_vis, unused = check_child_nodes(node, nil, :public)
return unless unused
add_offense(unused, message: format(MSG, current: cur_vis)) do |corrector|
autocorrect(corrector, unused)
end
end
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
def check_child_nodes(node, unused, cur_vis)
node.child_nodes.each do |child|
if child.send_type? && access_modifier?(child)
cur_vis, unused = check_send_node(child, cur_vis, unused)
elsif child.block_type? && included_block?(child)
next
elsif method_definition?(child)
unused = nil
elsif start_of_new_scope?(child)
check_scope(child)
elsif !child.defs_type?
cur_vis, unused = check_child_nodes(child, unused, cur_vis)
end
end
[cur_vis, unused]
end
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
def check_send_node(node, cur_vis, unused)
if node.bare_access_modifier?
check_new_visibility(node, unused, node.method_name, cur_vis)
elsif node.method?(:private_class_method) && !node.arguments?
add_offense(node, message: format(MSG, current: node.method_name)) do |corrector|
autocorrect(corrector, node)
end
[cur_vis, unused]
end
end
def check_new_visibility(node, unused, new_vis, cur_vis)
# does this modifier just repeat the existing visibility?
if new_vis == cur_vis
add_offense(node, message: format(MSG, current: cur_vis)) do |corrector|
autocorrect(corrector, node)
end
else
# was the previous modifier never applied to any defs?
if unused
add_offense(unused, message: format(MSG, current: cur_vis)) do |corrector|
autocorrect(corrector, unused)
end
end
# once we have already warned about a certain modifier, don't
# warn again even if it is never applied to any method defs
unused = node
end
[new_vis, unused]
end
def included_block?(block_node)
active_support_extensions_enabled? && block_node.method?(:included)
end
def method_definition?(child)
static_method_definition?(child) ||
dynamic_method_definition?(child) ||
any_method_definition?(child)
end
def any_method_definition?(child)
cop_config.fetch('MethodCreatingMethods', []).any? do |m|
matcher_name = :"#{m}_method?"
unless respond_to?(matcher_name)
self.class.def_node_matcher matcher_name, <<~PATTERN
{def (send nil? :#{m} ...)}
PATTERN
end
public_send(matcher_name, child)
end
end
def start_of_new_scope?(child)
child.module_type? || child.class_type? || child.sclass_type? || eval_call?(child)
end
def eval_call?(child)
class_or_instance_eval?(child) ||
child.class_constructor? ||
any_context_creating_methods?(child)
end
def any_context_creating_methods?(child)
cop_config.fetch('ContextCreatingMethods', []).any? do |m|
matcher_name = :"#{m}_block?"
unless respond_to?(matcher_name)
self.class.def_node_matcher matcher_name, <<~PATTERN
({block numblock} (send {nil? const} {:#{m}} ...) ...)
PATTERN
end
public_send(matcher_name, child)
end
end
end
end
end
end