-
-
Notifications
You must be signed in to change notification settings - Fork 3.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
29d3f14
commit 7a72616
Showing
5 changed files
with
420 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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][]) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
Oops, something went wrong.