Skip to content

Commit

Permalink
Add Lint/ConstantReassignment cop
Browse files Browse the repository at this point in the history
  • Loading branch information
lovro-bikic committed Dec 20, 2024
1 parent 29d3f14 commit 04dd3c5
Show file tree
Hide file tree
Showing 5 changed files with 420 additions and 0 deletions.
1 change: 1 addition & 0 deletions changelog/new_lint_constant_reassignment_cop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* [#13612](https://github.com/rubocop/rubocop/pull/13612): Create new cop `Lint/ConstantReassignment`. ([@lovro-bikic][])
5 changes: 5 additions & 0 deletions config/default.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1665,6 +1665,11 @@ Lint/ConstantOverwrittenInRescue:
Enabled: pending
VersionAdded: '1.31'

Lint/ConstantReassignment:
Description: 'Checks for constant reassignments.'
Enabled: pending
VersionAdded: '<<next>>'

Lint/ConstantResolution:
Description: 'Check that constants are fully qualified with `::`.'
Enabled: false
Expand Down
1 change: 1 addition & 0 deletions lib/rubocop.rb
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,7 @@
require_relative 'rubocop/cop/lint/circular_argument_reference'
require_relative 'rubocop/cop/lint/constant_definition_in_block'
require_relative 'rubocop/cop/lint/constant_overwritten_in_rescue'
require_relative 'rubocop/cop/lint/constant_reassignment'
require_relative 'rubocop/cop/lint/constant_resolution'
require_relative 'rubocop/cop/lint/debugger'
require_relative 'rubocop/cop/lint/deprecated_class_methods'
Expand Down
124 changes: 124 additions & 0 deletions lib/rubocop/cop/lint/constant_reassignment.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
# frozen_string_literal: true

module RuboCop
module Cop
module Lint
# Checks for constant reassignments.
#
# Emulates Ruby's runtime warning "already initialized constant X"
# when a constant is reassigned in the same file and namespace using the
# `NAME = value` syntax.
#
# The cop cannot catch all offenses, like, for example, when a constant
# is reassigned in another file, or when using metaprogramming (`Module#const_set`).
#
# The cop also tracks constant removal using `Module#remove_const` with a symbol
# argument.
#
# @example
# # bad
# X = :foo
# X = :bar
#
# # bad
# class A
# X = :foo
# X = :bar
# end
#
# # bad
# module A
# X = :foo
# X = :bar
# end
#
# # good - keep only one assignment
# X = :bar
#
# class A
# X = :bar
# end
#
# module A
# X = :bar
# end
#
# # good - use OR assignment
# X = :foo
# X ||= :bar
#
# # good - remove the assigned constant first
# class A
# X = :foo
# remove_const :X
# X = :bar
# end
#
class ConstantReassignment < Base
MSG = 'Constant `%<constant>s` is already assigned in this namespace.'

RESTRICT_ON_SEND = %i[remove_const].freeze

# @!method remove_sym_const(node)
def_node_matcher :remove_sym_const, <<~PATTERN
(send _ :remove_const
(sym $_))
PATTERN

def on_casgn(node)
return if no_reassignment?(node)
return if constant_names.add?(fully_qualified_constant_name(node))

add_offense(node, message: format(MSG, constant: node.name))
end

def on_send(node)
constant = remove_sym_const(node)

namespaces = ancestor_namespaces(node)

return if namespaces.none?

constant_names.delete(fully_qualified_name_for(namespaces, constant))
end

private

def no_reassignment?(node)
node.parent&.or_asgn_type?
end

def fully_qualified_constant_name(node)
if node.absolute?
namespace = node.namespace.const_type? ? node.namespace.source : nil

"#{namespace}::#{node.name}"
else
constant_namespaces = ancestor_namespaces(node) + constant_namespaces(node)

fully_qualified_name_for(constant_namespaces, node.name)
end
end

def fully_qualified_name_for(namespaces, constant)
['', *namespaces, constant].join('::')
end

def constant_namespaces(node)
node.each_path.map(&:short_name)
end

def ancestor_namespaces(node)
node
.each_ancestor(:class, :module)
.map { |ancestor| ancestor.identifier.short_name }
.reverse
end

def constant_names
@constant_names ||= Set.new
end
end
end
end
end
Loading

0 comments on commit 04dd3c5

Please sign in to comment.