Skip to content

Commit

Permalink
Merge pull request #15223 from issyl0/rubocop-cask-stanza-order-in-on…
Browse files Browse the repository at this point in the history
…-blocks

rubocops/cask: Check for correct stanza order within `on_*` blocks
  • Loading branch information
reitermarkus authored May 8, 2023
2 parents 498ce8d + 1df501b commit c0f8068
Show file tree
Hide file tree
Showing 10 changed files with 285 additions and 255 deletions.
68 changes: 38 additions & 30 deletions Library/Homebrew/rubocops/cask/ast/cask_block.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,27 +6,57 @@
module RuboCop
module Cask
module AST
# This class wraps the AST block node that represents the entire cask
# definition. It includes various helper methods to aid cops in their
# analysis.
class CaskBlock
extend Forwardable
class StanzaBlock
extend T::Helpers

sig { returns(RuboCop::AST::BlockNode) }
attr_reader :block_node

sig { returns(T::Array[Parser::Source::Comment]) }
attr_reader :comments

sig { params(block_node: RuboCop::AST::BlockNode, comments: T::Array[Parser::Source::Comment]).void }
def initialize(block_node, comments)
@block_node = block_node
@comments = comments
end

attr_reader :block_node, :comments
sig { returns(T::Array[Stanza]) }
def stanzas
return [] unless (block_body = block_node.block_body)

# If a block only contains one stanza, it is that stanza's direct parent, otherwise
# stanzas are grouped in a nested block and the block is that nested block's parent.
is_stanza = if block_body.begin_block?
->(node) { node.parent.parent == block_node }
else
->(node) { node.parent == block_node }
end

@stanzas ||= block_body.each_node
.select(&:stanza?)
.select(&is_stanza)
.map { |node| Stanza.new(node, comments) }
end
end

# This class wraps the AST block node that represents the entire cask
# definition. It includes various helper methods to aid cops in their
# analysis.
class CaskBlock < StanzaBlock
extend Forwardable

alias cask_node block_node
def cask_node
block_node
end

def_delegator :cask_node, :block_body, :cask_body

def header
@header ||= CaskHeader.new(cask_node.method_node)
@header ||= CaskHeader.new(block_node.method_node)
end

# TODO: Use `StanzaBlock#stanzas` for all cops, where possible.
def stanzas
return [] unless cask_body

Expand All @@ -46,28 +76,6 @@ def toplevel_stanzas

@toplevel_stanzas ||= stanzas.select(&is_toplevel_stanza)
end

def sorted_toplevel_stanzas
@sorted_toplevel_stanzas ||= sort_stanzas(toplevel_stanzas)
end

private

def sort_stanzas(stanzas)
stanzas.sort do |s1, s2|
i1 = stanza_order_index(s1)
i2 = stanza_order_index(s2)
if i1 == i2 || i1.blank? || i2.blank?
i1 = stanzas.index(s1)
i2 = stanzas.index(s2)
end
i1 - i2
end
end

def stanza_order_index(stanza)
Constants::STANZA_ORDER.index(stanza.stanza_name)
end
end
end
end
Expand Down
6 changes: 5 additions & 1 deletion Library/Homebrew/rubocops/cask/ast/stanza.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ def initialize(method_node, all_comments)

def_delegator :stanza_node, :parent, :parent_node
def_delegator :stanza_node, :arch_variable?
def_delegator :stanza_node, :on_system_block?

def source_range
stanza_node.location_expression
Expand All @@ -48,6 +49,10 @@ def stanza_group
Constants::STANZA_GROUP_HASH[stanza_name]
end

def stanza_index
Constants::STANZA_ORDER.index(stanza_name)
end

def same_group?(other)
stanza_group == other.stanza_group
end
Expand All @@ -65,7 +70,6 @@ def comments_hash
def ==(other)
self.class == other.class && stanza_node == other.stanza_node
end

alias eql? ==

Constants::STANZA_ORDER.each do |stanza_name|
Expand Down
9 changes: 8 additions & 1 deletion Library/Homebrew/rubocops/cask/extend/node.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,18 @@ class Node
def_node_matcher :key_node, "{(pair $_ _) (hash (pair $_ _) ...)}"
def_node_matcher :val_node, "{(pair _ $_) (hash (pair _ $_) ...)}"

def_node_matcher :cask_block?, "(block (send nil? :cask _) args ...)"
def_node_matcher :cask_block?, "(block (send nil? :cask ...) args ...)"
def_node_matcher :on_system_block?,
"(block (send nil? {#{ON_SYSTEM_METHODS.map(&:inspect).join(" ")}} ...) args ...)"
def_node_matcher :arch_variable?, "(lvasgn _ (send nil? :on_arch_conditional ...))"

def_node_matcher :begin_block?, "(begin ...)"

sig { returns(T::Boolean) }
def cask_on_system_block?
(on_system_block? && each_ancestor.any?(&:cask_block?)) || false
end

def stanza?
return true if arch_variable?

Expand Down
36 changes: 30 additions & 6 deletions Library/Homebrew/rubocops/cask/mixin/cask_help.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,46 @@ module Cop
module Cask
# Common functionality for cops checking casks.
module CaskHelp
extend T::Helpers
prepend CommentsHelp

abstract!

sig { abstract.params(cask_block: RuboCop::Cask::AST::CaskBlock).void }
sig { overridable.params(cask_block: RuboCop::Cask::AST::CaskBlock).void }
def on_cask(cask_block); end

sig { overridable.params(cask_stanza_block: RuboCop::Cask::AST::StanzaBlock).void }
def on_cask_stanza_block(cask_stanza_block); end

# FIXME: Workaround until https://github.com/rubocop/rubocop/pull/11858 is released.
def find_end_line(node)
return node.loc.end.line if node.block_type? || node.numblock_type?

super
end

sig { params(block_node: RuboCop::AST::BlockNode).void }
def on_block(block_node)
super if defined? super
return unless respond_to?(:on_cask)

return if !block_node.cask_block? && !block_node.cask_on_system_block?

comments = comments_in_range(block_node).to_a
stanza_block = RuboCop::Cask::AST::StanzaBlock.new(block_node, comments)
on_cask_stanza_block(stanza_block)

return unless block_node.cask_block?

comments = processed_source.comments
cask_block = RuboCop::Cask::AST::CaskBlock.new(block_node, comments)
on_cask(cask_block)
end

def on_system_methods(cask_stanzas)
cask_stanzas.select(&:on_system_block?)
end

def inner_stanzas(block_node, comments)
block_contents = block_node.child_nodes.select(&:begin_type?)
inner_nodes = block_contents.map(&:child_nodes).flatten.select(&:send_type?)
inner_nodes.map { |n| RuboCop::Cask::AST::Stanza.new(n, comments) }
end
end
end
end
Expand Down
10 changes: 3 additions & 7 deletions Library/Homebrew/rubocops/cask/no_overrides.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ module Cask
class NoOverrides < Base
include CaskHelp

ON_SYSTEM_METHODS = RuboCop::Cask::Constants::ON_SYSTEM_METHODS
# These stanzas can be overridden by `on_*` blocks, so take them into account.
# TODO: Update this list if new stanzas are added to `Cask::DSL` that call `set_unique_stanza`.
OVERRIDEABLE_METHODS = [
Expand All @@ -22,8 +21,7 @@ class NoOverrides < Base
def on_cask(cask_block)
cask_stanzas = cask_block.toplevel_stanzas

# Skip if there are no `on_*` blocks.
return if (on_blocks = cask_stanzas.select { |s| ON_SYSTEM_METHODS.include?(s.stanza_name) }).none?
return if (on_blocks = on_system_methods(cask_stanzas)).none?

stanzas_in_blocks = on_system_stanzas(on_blocks)

Expand All @@ -40,9 +38,7 @@ def on_cask(cask_block)
def on_system_stanzas(on_system)
names = Set.new
method_nodes = on_system.map(&:method_node)
method_nodes.each do |node|
next unless node.block_type?

method_nodes.select(&:block_type?).each do |node|
node.child_nodes.each do |child|
child.each_node(:send) do |send_node|
# Skip (nested) livecheck blocks as its `url` is different to a download `url`.
Expand All @@ -51,7 +47,7 @@ def on_system_stanzas(on_system)
if send_node.ancestors.drop_while { |a| !a.begin_type? }.any? { |a| a.dstr_type? || a.regexp_type? }
next
end
next if ON_SYSTEM_METHODS.include?(send_node.method_name)
next if RuboCop::Cask::Constants::ON_SYSTEM_METHODS.include?(send_node.method_name)

names.add(send_node.method_name)
end
Expand Down
17 changes: 5 additions & 12 deletions Library/Homebrew/rubocops/cask/stanza_grouping.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,14 @@
module RuboCop
module Cop
module Cask
# This cop checks that a cask's stanzas are grouped correctly.
# This cop checks that a cask's stanzas are grouped correctly, including nested within `on_*` blocks.
# @see https://docs.brew.sh/Cask-Cookbook#stanza-order
class StanzaGrouping < Base
extend Forwardable
extend AutoCorrector
include CaskHelp
include RangeHelp

ON_SYSTEM_METHODS = RuboCop::Cask::Constants::ON_SYSTEM_METHODS
MISSING_LINE_MSG = "stanza groups should be separated by a single empty line"
EXTRA_LINE_MSG = "stanzas within the same group should have no lines between them"

Expand All @@ -24,17 +23,11 @@ def on_cask(cask_block)
cask_stanzas = cask_block.toplevel_stanzas
add_offenses(cask_stanzas)

# If present, check grouping of stanzas within `on_*` blocks.
return if (on_blocks = cask_stanzas.select { |s| ON_SYSTEM_METHODS.include?(s.stanza_name) }).none?
return if (on_blocks = on_system_methods(cask_stanzas)).none?

on_blocks.map(&:method_node).each do |on_block|
next unless on_block.block_type?

block_contents = on_block.child_nodes.select(&:begin_type?)
inner_nodes = block_contents.map(&:child_nodes).flatten.select(&:send_type?)
inner_stanzas = inner_nodes.map { |node| RuboCop::Cask::AST::Stanza.new(node, processed_source.comments) }

add_offenses(inner_stanzas)
on_blocks.map(&:method_node).select(&:block_type?).each do |on_block|
stanzas = inner_stanzas(on_block, processed_source.comments)
add_offenses(stanzas)
end
end

Expand Down
66 changes: 44 additions & 22 deletions Library/Homebrew/rubocops/cask/stanza_order.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,46 +6,68 @@
module RuboCop
module Cop
module Cask
# This cop checks that a cask's stanzas are ordered correctly.
# This cop checks that a cask's stanzas are ordered correctly, including nested within `on_*` blocks.
# @see https://docs.brew.sh/Cask-Cookbook#stanza-order
class StanzaOrder < Base
include IgnoredNode
extend Forwardable
extend AutoCorrector
include CaskHelp

MESSAGE = "`%<stanza>s` stanza out of order"

def on_cask(cask_block)
@cask_block = cask_block
add_offenses
end
def on_cask_stanza_block(stanza_block)
stanzas = stanza_block.stanzas
ordered_stanzas = sort_stanzas(stanzas)

private
return if stanzas == ordered_stanzas

attr_reader :cask_block
stanzas.zip(ordered_stanzas).each do |stanza_before, stanza_after|
next if stanza_before == stanza_after

def_delegators :cask_block, :cask_node, :toplevel_stanzas,
:sorted_toplevel_stanzas
add_offense(
stanza_before.method_node,
message: format(MESSAGE, stanza: stanza_before.stanza_name),
) do |corrector|
next if part_of_ignored_node?(stanza_before.method_node)

def add_offenses
offending_stanzas.each do |stanza|
message = format(MESSAGE, stanza: stanza.stanza_name)
add_offense(stanza.source_range_with_comments, message: message) do |corrector|
correct_stanza_index = toplevel_stanzas.index(stanza)
correct_stanza = sorted_toplevel_stanzas[correct_stanza_index]
corrector.replace(stanza.source_range_with_comments,
correct_stanza.source_with_comments)
corrector.replace(
stanza_before.source_range_with_comments,
stanza_after.source_with_comments,
)

# Ignore node so that nested content is not auto-corrected and clobbered.
ignore_node(stanza_before.method_node)
end
end
end

def offending_stanzas
stanza_pairs = toplevel_stanzas.zip(sorted_toplevel_stanzas)
stanza_pairs.each_with_object([]) do |stanza_pair, offending_stanzas|
stanza, sorted_stanza = *stanza_pair
offending_stanzas << stanza if stanza != sorted_stanza
def on_new_investigation
super

ignored_nodes.clear
end

private

def sort_stanzas(stanzas)
stanzas.sort do |stanza1, stanza2|
i1 = stanza1.stanza_index
i2 = stanza2.stanza_index

if i1 == i2
i1 = stanzas.index(stanza1)
i2 = stanzas.index(stanza2)
end

i1 - i2
end
end

def stanza_order_index(stanza)
stanza_name = stanza.respond_to?(:method_name) ? stanza.method_name : stanza.stanza_name
RuboCop::Cask::Constants::STANZA_ORDER.index(stanza_name)
end
end
end
end
Expand Down
Loading

0 comments on commit c0f8068

Please sign in to comment.