Skip to content

Commit 760158b

Browse files
authored
Merge pull request #21130 from toobuntu/feature/cask-dsl/quit-signal-on-upgrade
cask uninstall: opt-in quit/signal on upgrade/reinstall
2 parents ab36c88 + 1086987 commit 760158b

File tree

10 files changed

+544
-20
lines changed

10 files changed

+544
-20
lines changed

Library/Homebrew/cask/artifact/abstract_uninstall.rb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,18 @@ class AbstractUninstall < AbstractArtifact
2929
:rmdir,
3030
].freeze
3131

32+
METADATA_KEYS = [
33+
:on_upgrade,
34+
].freeze
35+
3236
def self.from_args(cask, **directives)
3337
new(cask, **directives)
3438
end
3539

3640
attr_reader :directives
3741

3842
def initialize(cask, **directives)
39-
directives.assert_valid_keys(*ORDERED_DIRECTIVES)
43+
directives.assert_valid_keys(*ORDERED_DIRECTIVES, *METADATA_KEYS)
4044

4145
super
4246
directives[:signal] = Array(directives[:signal]).flatten.each_slice(2).to_a

Library/Homebrew/cask/artifact/uninstall.rb

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,30 @@ module Artifact
99
class Uninstall < AbstractUninstall
1010
UPGRADE_REINSTALL_SKIP_DIRECTIVES = [:quit, :signal].freeze
1111

12-
def uninstall_phase(upgrade: false, reinstall: false, **options)
12+
def uninstall_phase(**options)
13+
upgrade = options.fetch(:upgrade, false)
14+
reinstall = options.fetch(:reinstall, false)
15+
16+
raw_on_upgrade = directives[:on_upgrade]
17+
on_upgrade_syms =
18+
case raw_on_upgrade
19+
when Symbol
20+
[raw_on_upgrade]
21+
when Array
22+
raw_on_upgrade.map(&:to_sym)
23+
else
24+
[]
25+
end
26+
on_upgrade_set = on_upgrade_syms.to_set
27+
1328
filtered_directives = ORDERED_DIRECTIVES.filter do |directive_sym|
1429
next false if directive_sym == :rmdir
1530

16-
next false if (upgrade || reinstall) && UPGRADE_REINSTALL_SKIP_DIRECTIVES.include?(directive_sym)
31+
if (upgrade || reinstall) &&
32+
UPGRADE_REINSTALL_SKIP_DIRECTIVES.include?(directive_sym) &&
33+
on_upgrade_set.exclude?(directive_sym)
34+
next false
35+
end
1736

1837
true
1938
end

Library/Homebrew/rubocops/cask/uninstall_methods_order.rb

Lines changed: 153 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,28 +6,63 @@
66
module 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

Comments
 (0)