66module RuboCop
77 module Cop
88 module Cask
9- # This cop checks for the correct order of methods within the 'uninstall' and 'zap' stanzas.
9+ # This cop checks for the correct order of methods within the
10+ # 'uninstall' and 'zap' stanzas and validates related metadata.
1011 class UninstallMethodsOrder < Base
1112 extend AutoCorrector
1213 include HelperFunctions
1314
1415 MSG = T . let ( "`%<method>s` method out of order" , String )
1516
17+ # These keys are ignored when checking method order.
18+ # Mirrors AbstractUninstall::METADATA_KEYS.
19+ METADATA_KEYS = T . let (
20+ [ :on_upgrade ] . freeze ,
21+ T ::Array [ Symbol ] ,
22+ )
23+
24+ USELESS_METADATA_MSG = T . let (
25+ "`on_upgrade` has no effect without matching `uninstall quit:` or `uninstall signal:` directives" ,
26+ String ,
27+ )
28+
29+ PARTIAL_METADATA_MSG = T . let (
30+ "`on_upgrade` lists %<symbols>s without matching `uninstall` directives" ,
31+ String ,
32+ )
33+
1634 sig { params ( node : AST ::SendNode ) . void }
1735 def on_send ( node )
1836 return unless [ :zap , :uninstall ] . include? ( node . method_name )
1937
2038 hash_node = node . arguments . first
2139 return if hash_node . nil? || ( !hash_node . is_a? ( AST ::Node ) && !hash_node . hash_type? )
2240
23- method_nodes = hash_node . pairs . map ( &:key )
24- expected_order = method_nodes . sort_by { |method | method_order_index ( method ) }
2541 comments = processed_source . comments
2642
43+ check_ordering ( hash_node , comments )
44+ check_metadata ( hash_node , comments )
45+ end
46+
47+ private
48+
49+ sig {
50+ params (
51+ hash_node : AST ::HashNode ,
52+ comments : T ::Array [ Parser ::Source ::Comment ] ,
53+ ) . void
54+ }
55+ def check_ordering ( hash_node , comments )
56+ method_nodes = hash_node . pairs . map ( &:key ) . reject do |method |
57+ name = method . children . first
58+ METADATA_KEYS . include? ( name )
59+ end
60+
61+ expected_order = method_nodes . sort_by { |method | method_order_index ( method ) }
2762 method_nodes . each_with_index do |method , index |
2863 next if method == expected_order [ index ]
2964
30- report_and_correct_offense ( method , hash_node , expected_order , comments )
65+ report_and_correct_ordering_offense ( method , hash_node , expected_order , comments )
3166 end
3267 end
3368
@@ -37,23 +72,126 @@ def on_send(node)
3772 expected_order : T ::Array [ AST ::Node ] ,
3873 comments : T ::Array [ Parser ::Source ::Comment ] ) . void
3974 }
40- def report_and_correct_offense ( method , hash_node , expected_order , comments )
75+ def report_and_correct_ordering_offense ( method , hash_node , expected_order , comments )
4176 add_offense ( method , message : format ( MSG , method : method . children . first ) ) do |corrector |
77+ ordered_pairs = expected_order . map do |expected_method |
78+ hash_node . pairs . find { |pair | pair . key == expected_method }
79+ end
80+
4281 indentation = " " * ( start_column ( method ) - line_start_column ( method ) )
43- new_code = expected_order . map do |expected_method |
44- method_pair = hash_node . pairs . find { |pair | pair . key == expected_method }
45- source = method_pair . source
46-
47- # Find and attach a comment on the same line as the method_pair, if any
48- inline_comment = comments . find do |comment |
49- comment . location . line == method_pair . loc . line && comment . location . column > method_pair . loc . column
50- end
51- inline_comment ? "#{ source } #{ inline_comment . text } " : source
52- end . join ( ",\n #{ indentation } " )
82+ new_code = build_uninstall_body ( ordered_pairs , comments , indentation )
83+
5384 corrector . replace ( hash_node . source_range , new_code )
5485 end
5586 end
5687
88+ sig {
89+ params (
90+ hash_node : AST ::HashNode ,
91+ comments : T ::Array [ Parser ::Source ::Comment ] ,
92+ ) . void
93+ }
94+ def check_metadata ( hash_node , comments )
95+ on_upgrade_pair = hash_node . pairs . find { |p | p . key . value == :on_upgrade }
96+ return unless on_upgrade_pair
97+
98+ requested = on_upgrade_symbols ( on_upgrade_pair . value )
99+ return report_fully_invalid_metadata ( on_upgrade_pair ) if requested . empty?
100+
101+ available = [ ]
102+ available << :quit if hash_node . pairs . any? { |p | p . key . value == :quit }
103+ available << :signal if hash_node . pairs . any? { |p | p . key . value == :signal }
104+
105+ valid_syms = requested & available
106+ invalid_syms = requested - available
107+
108+ if valid_syms . empty?
109+ remaining_pairs = hash_node . pairs . reject { |p | p == on_upgrade_pair }
110+ report_and_correct_useless_metadata ( hash_node , on_upgrade_pair , remaining_pairs , comments )
111+ elsif invalid_syms . any?
112+ report_partially_invalid_metadata ( on_upgrade_pair . value , invalid_syms )
113+ end
114+ end
115+
116+ sig { params ( on_upgrade_pair : AST ::PairNode ) . void }
117+ def report_fully_invalid_metadata ( on_upgrade_pair )
118+ add_offense ( on_upgrade_pair . value ,
119+ message : "`on_upgrade` value must be :quit, :signal, or an array of those symbols" )
120+ end
121+
122+ sig {
123+ params (
124+ hash_node : AST ::HashNode ,
125+ on_upgrade_pair : AST ::PairNode ,
126+ remaining_pairs : T ::Array [ AST ::PairNode ] ,
127+ comments : T ::Array [ Parser ::Source ::Comment ] ,
128+ ) . void
129+ }
130+ def report_and_correct_useless_metadata (
131+ hash_node ,
132+ on_upgrade_pair ,
133+ remaining_pairs ,
134+ comments
135+ )
136+ if remaining_pairs . empty?
137+ # Only on_upgrade is present: report but do not attempt autocorrect
138+ # to avoid generating an empty uninstall hash or removing the stanza.
139+ add_offense ( on_upgrade_pair . key , message : USELESS_METADATA_MSG )
140+ return
141+ end
142+
143+ add_offense ( on_upgrade_pair . key , message : USELESS_METADATA_MSG ) do |corrector |
144+ first_pair = T . must ( remaining_pairs . first )
145+ indentation = " " * ( start_column ( first_pair . key ) - line_start_column ( first_pair . key ) )
146+
147+ new_code = build_uninstall_body ( remaining_pairs , comments , indentation )
148+ corrector . replace ( hash_node . source_range , new_code )
149+ end
150+ end
151+
152+ sig {
153+ params ( value_node : AST ::Node , invalid_syms : T ::Array [ Symbol ] ) . void
154+ }
155+ def report_partially_invalid_metadata ( value_node , invalid_syms )
156+ symbols_str = invalid_syms . map { |s | ":#{ s } " } . join ( ", " )
157+ add_offense ( value_node ,
158+ message : format ( PARTIAL_METADATA_MSG , symbols : symbols_str ) )
159+ end
160+
161+ sig {
162+ params (
163+ pairs : T ::Array [ AST ::PairNode ] ,
164+ comments : T ::Array [ Parser ::Source ::Comment ] ,
165+ indentation : String ,
166+ ) . returns ( String )
167+ }
168+ def build_uninstall_body ( pairs , comments , indentation )
169+ pairs . map do |pair |
170+ source = pair . source
171+
172+ # Find and attach a comment on the same line as the pair, if any
173+ inline_comment = comments . find do |comment |
174+ comment . location . line == pair . loc . line &&
175+ comment . location . column > pair . loc . column
176+ end
177+
178+ inline_comment ? "#{ source } #{ inline_comment . text } " : source
179+ end . join ( ",\n #{ indentation } " )
180+ end
181+
182+ sig { params ( value_node : AST ::Node ) . returns ( T ::Array [ Symbol ] ) }
183+ def on_upgrade_symbols ( value_node )
184+ if value_node . sym_type?
185+ [ T . cast ( value_node , AST ::SymbolNode ) . value ]
186+ elsif value_node . array_type?
187+ value_node . children . select ( &:sym_type? ) . map do |child |
188+ T . cast ( child , AST ::SymbolNode ) . value
189+ end
190+ else
191+ [ ]
192+ end
193+ end
194+
57195 sig { params ( method_node : AST ::SymbolNode ) . returns ( Integer ) }
58196 def method_order_index ( method_node )
59197 method_name = method_node . children . first
0 commit comments