@@ -117,6 +117,7 @@ def delete_constant_assignment(context)
117117 Prism ::ConstantPathAndWriteNode , Prism ::ConstantPathOrWriteNode
118118 # Nesting node is an assign, it means only one constant is assigned on the line
119119 # so we can remove the whole assign
120+ remove_constant_visibility_call ( context )
120121 delete_node_and_comments_and_sigs ( context )
121122 return
122123 end
@@ -127,6 +128,7 @@ def delete_constant_assignment(context)
127128 if parent_node . is_a? ( Prism ::ConstantWriteNode )
128129 # Nesting node is an assign, it means only one constant is assigned on the line
129130 # so we can remove the whole assign
131+ remove_constant_visibility_call ( parent_context )
130132 delete_node_and_comments_and_sigs ( parent_context )
131133 return
132134 elsif parent_node . is_a? ( Prism ::MultiWriteNode ) && parent_node . lefts . size == 1
@@ -190,6 +192,64 @@ def delete_constant_assignment(context)
190192 end
191193 end
192194
195+ # A dead constant is often followed by a `private_constant`/`public_constant` call naming it.
196+ # That call references the now-removed constant (a load-time `NameError` if left behind), so
197+ # remove the reference too: delete the whole call when the constant is its only argument, or
198+ # drop just that symbol when the call lists several constants.
199+ #: (NodeContext context) -> void
200+ def remove_constant_visibility_call ( context )
201+ node = context . node
202+ return unless node . is_a? ( Prism ::ConstantWriteNode )
203+
204+ name = node . name
205+ call = context . next_nodes . find { |sibling | constant_visibility_call? ( sibling , name ) }
206+ return unless call . is_a? ( Prism ::CallNode )
207+
208+ call_context = NodeContext . new ( @old_source , @node_context . comments , call , context . nesting )
209+ arguments = call . arguments &.arguments #: Array[Prism::Node]?
210+ if arguments && arguments . size > 1
211+ delete_symbol_argument ( call_context , name )
212+ else
213+ delete_node_and_comments_and_sigs ( call_context )
214+ end
215+ end
216+
217+ # Whether `node` is a bare `private_constant`/`public_constant` call listing `name`.
218+ #: (Prism::Node node, Symbol name) -> bool
219+ def constant_visibility_call? ( node , name )
220+ return false unless node . is_a? ( Prism ::CallNode )
221+ return false unless node . receiver . nil?
222+ return false unless node . name == :private_constant || node . name == :public_constant
223+
224+ arguments = node . arguments &.arguments
225+ return false unless arguments
226+
227+ arguments . any? { |argument | argument . is_a? ( Prism ::SymbolNode ) && argument . value == name . to_s }
228+ end
229+
230+ # Drop the `:name` symbol from a `private_constant`/`public_constant` call that lists several
231+ # constants, keeping the call and the other names intact.
232+ #: (NodeContext context, Symbol name) -> void
233+ def delete_symbol_argument ( context , name )
234+ arguments = T . cast ( context . node , Prism ::CallNode ) . arguments &.arguments
235+ return unless arguments
236+
237+ index = arguments . index { |argument | argument . is_a? ( Prism ::SymbolNode ) && argument . value == name . to_s }
238+ return unless index
239+
240+ argument = arguments . fetch ( index )
241+ prev_argument = arguments [ index - 1 ] if index . positive?
242+ next_argument = arguments [ index + 1 ]
243+
244+ if prev_argument && next_argument
245+ replace_chars ( prev_argument . location . end_offset , next_argument . location . start_offset , ", " )
246+ elsif prev_argument
247+ delete_chars ( prev_argument . location . end_offset , argument . location . end_offset )
248+ elsif next_argument
249+ delete_chars ( argument . location . start_offset , next_argument . location . start_offset )
250+ end
251+ end
252+
193253 #: (NodeContext context) -> void
194254 def delete_attr_accessor ( context )
195255 args_context = context . parent_context
0 commit comments