2023-02-14 19:19:37 -08:00
|
|
|
# typed: true
|
2019-04-19 15:38:03 +09:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
2023-02-20 10:22:39 -08:00
|
|
|
require "rubocops/extend/formula_cop"
|
2018-02-19 23:40:07 +05:30
|
|
|
|
|
|
|
module RuboCop
|
|
|
|
module Cop
|
2018-09-08 15:46:56 +10:00
|
|
|
module FormulaAudit
|
2020-11-05 17:17:03 -05:00
|
|
|
# This cop checks for correct order of `depends_on` in formulae.
|
2018-02-19 23:40:07 +05:30
|
|
|
#
|
|
|
|
# precedence order:
|
2020-11-05 17:17:03 -05:00
|
|
|
# build-time > test > normal > recommended > optional
|
2023-02-20 18:10:59 -08:00
|
|
|
class DependencyOrder < FormulaCop
|
2021-01-12 14:52:47 +11:00
|
|
|
extend AutoCorrector
|
|
|
|
|
2018-02-19 23:40:07 +05:30
|
|
|
def audit_formula(_node, _class_node, _parent_class_node, body_node)
|
|
|
|
check_dependency_nodes_order(body_node)
|
2020-01-12 10:59:22 -08:00
|
|
|
check_uses_from_macos_nodes_order(body_node)
|
2022-08-02 21:56:45 -07:00
|
|
|
([:head, :stable] + on_system_methods).each do |block_name|
|
2018-02-19 23:40:07 +05:30
|
|
|
block = find_block(body_node, block_name)
|
|
|
|
next unless block
|
2018-09-17 02:45:00 +02:00
|
|
|
|
2018-02-19 23:40:07 +05:30
|
|
|
check_dependency_nodes_order(block.body)
|
2020-01-12 10:59:22 -08:00
|
|
|
check_uses_from_macos_nodes_order(block.body)
|
2018-02-19 23:40:07 +05:30
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2020-01-12 10:59:22 -08:00
|
|
|
def check_uses_from_macos_nodes_order(parent_node)
|
|
|
|
return if parent_node.nil?
|
|
|
|
|
|
|
|
dependency_nodes = parent_node.each_child_node.select { |x| uses_from_macos_node?(x) }
|
|
|
|
ensure_dependency_order(dependency_nodes)
|
|
|
|
end
|
|
|
|
|
2018-02-19 23:40:07 +05:30
|
|
|
def check_dependency_nodes_order(parent_node)
|
|
|
|
return if parent_node.nil?
|
2018-09-17 02:45:00 +02:00
|
|
|
|
2020-01-12 10:59:22 -08:00
|
|
|
dependency_nodes = parent_node.each_child_node.select { |x| depends_on_node?(x) }
|
|
|
|
ensure_dependency_order(dependency_nodes)
|
|
|
|
end
|
|
|
|
|
|
|
|
def ensure_dependency_order(nodes)
|
|
|
|
ordered = nodes.sort_by { |node| dependency_name(node).downcase }
|
2018-02-19 23:40:07 +05:30
|
|
|
ordered = sort_dependencies_by_type(ordered)
|
|
|
|
sort_conditional_dependencies!(ordered)
|
|
|
|
verify_order_in_source(ordered)
|
|
|
|
end
|
|
|
|
|
|
|
|
# Separate dependencies according to precedence order:
|
2018-03-28 20:54:39 -05:00
|
|
|
# build-time > test > normal > recommended > optional
|
2018-02-19 23:40:07 +05:30
|
|
|
def sort_dependencies_by_type(dependency_nodes)
|
2018-03-28 20:54:39 -05:00
|
|
|
unsorted_deps = dependency_nodes.to_a
|
2018-02-19 23:40:07 +05:30
|
|
|
ordered = []
|
2018-03-28 20:54:39 -05:00
|
|
|
ordered.concat(unsorted_deps.select { |dep| buildtime_dependency? dep })
|
|
|
|
unsorted_deps -= ordered
|
|
|
|
ordered.concat(unsorted_deps.select { |dep| test_dependency? dep })
|
|
|
|
unsorted_deps -= ordered
|
|
|
|
ordered.concat(unsorted_deps.reject { |dep| negate_normal_dependency? dep })
|
|
|
|
unsorted_deps -= ordered
|
|
|
|
ordered.concat(unsorted_deps.select { |dep| recommended_dependency? dep })
|
|
|
|
unsorted_deps -= ordered
|
|
|
|
ordered.concat(unsorted_deps.select { |dep| optional_dependency? dep })
|
2018-02-19 23:40:07 +05:30
|
|
|
end
|
|
|
|
|
|
|
|
# `depends_on :apple if build.with? "foo"` should always be defined
|
2020-11-05 17:17:03 -05:00
|
|
|
# after `depends_on :foo`.
|
|
|
|
# This method reorders the dependencies array according to the above rule.
|
2023-02-14 19:19:37 -08:00
|
|
|
sig { params(ordered: T::Array[RuboCop::AST::Node]).returns(T::Array[RuboCop::AST::Node]) }
|
2018-02-19 23:40:07 +05:30
|
|
|
def sort_conditional_dependencies!(ordered)
|
|
|
|
length = ordered.size
|
|
|
|
idx = 0
|
|
|
|
while idx < length
|
2023-02-14 19:19:37 -08:00
|
|
|
idx1 = T.let(nil, T.nilable(Integer))
|
|
|
|
idx2 = T.let(nil, T.nilable(Integer))
|
2018-02-19 23:40:07 +05:30
|
|
|
ordered.each_with_index do |dep, pos|
|
|
|
|
idx = pos+1
|
|
|
|
match_nodes = build_with_dependency_name(dep)
|
2024-01-26 13:47:59 -08:00
|
|
|
next if match_nodes.blank?
|
2018-09-17 02:45:00 +02:00
|
|
|
|
2018-06-30 15:17:15 +05:30
|
|
|
idx1 = pos
|
2018-02-19 23:40:07 +05:30
|
|
|
ordered.drop(idx1+1).each_with_index do |dep2, pos2|
|
|
|
|
next unless match_nodes.index(dependency_name(dep2))
|
2018-09-17 02:45:00 +02:00
|
|
|
|
2018-02-19 23:40:07 +05:30
|
|
|
idx2 = pos2 if idx2.nil? || pos2 > idx2
|
|
|
|
end
|
|
|
|
break if idx2
|
|
|
|
end
|
2023-02-14 19:19:37 -08:00
|
|
|
insert_after!(ordered, idx1, idx2 + T.must(idx1)) if idx2
|
2018-02-19 23:40:07 +05:30
|
|
|
end
|
|
|
|
ordered
|
|
|
|
end
|
|
|
|
|
2020-11-05 17:17:03 -05:00
|
|
|
# Verify actual order of sorted `depends_on` nodes in source code;
|
|
|
|
# raise RuboCop problem otherwise.
|
2018-02-19 23:40:07 +05:30
|
|
|
def verify_order_in_source(ordered)
|
2021-01-12 14:52:47 +11:00
|
|
|
ordered.each_with_index do |node_1, idx|
|
|
|
|
l1 = line_number(node_1)
|
2023-02-14 19:19:37 -08:00
|
|
|
l2 = T.let(nil, T.nilable(Integer))
|
|
|
|
node_2 = T.let(nil, T.nilable(RuboCop::AST::Node))
|
2021-01-12 14:52:47 +11:00
|
|
|
ordered.drop(idx + 1).each do |test_node|
|
|
|
|
l2 = line_number(test_node)
|
|
|
|
node_2 = test_node if l2 < l1
|
2018-02-19 23:40:07 +05:30
|
|
|
end
|
2021-01-12 14:52:47 +11:00
|
|
|
next unless node_2
|
|
|
|
|
|
|
|
offending_node(node_1)
|
2018-09-17 02:45:00 +02:00
|
|
|
|
2022-06-28 10:09:59 +01:00
|
|
|
problem "dependency \"#{dependency_name(node_1)}\" (line #{l1}) should be put before dependency " \
|
2021-01-12 14:52:47 +11:00
|
|
|
"\"#{dependency_name(node_2)}\" (line #{l2})" do |corrector|
|
|
|
|
indentation = " " * (start_column(node_2) - line_start_column(node_2))
|
|
|
|
line_breaks = "\n"
|
|
|
|
corrector.insert_before(node_2.source_range,
|
|
|
|
node_1.source + line_breaks + indentation)
|
|
|
|
corrector.remove(range_with_surrounding_space(range: node_1.source_range, side: :left))
|
|
|
|
end
|
2018-02-19 23:40:07 +05:30
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
# Node pattern method to match
|
2020-11-05 17:17:03 -05:00
|
|
|
# `depends_on` variants.
|
2018-02-19 23:40:07 +05:30
|
|
|
def_node_matcher :depends_on_node?, <<~EOS
|
|
|
|
{(if _ (send nil? :depends_on ...) nil?)
|
|
|
|
(send nil? :depends_on ...)}
|
|
|
|
EOS
|
|
|
|
|
2020-01-12 10:59:22 -08:00
|
|
|
def_node_matcher :uses_from_macos_node?, <<~EOS
|
|
|
|
{(if _ (send nil? :uses_from_macos ...) nil?)
|
|
|
|
(send nil? :uses_from_macos ...)}
|
|
|
|
EOS
|
|
|
|
|
2018-02-19 23:40:07 +05:30
|
|
|
def_node_search :buildtime_dependency?, "(sym :build)"
|
|
|
|
|
|
|
|
def_node_search :recommended_dependency?, "(sym :recommended)"
|
|
|
|
|
2018-03-19 10:11:08 +00:00
|
|
|
def_node_search :test_dependency?, "(sym :test)"
|
2018-02-19 23:40:07 +05:30
|
|
|
|
|
|
|
def_node_search :optional_dependency?, "(sym :optional)"
|
|
|
|
|
2018-03-19 10:11:08 +00:00
|
|
|
def_node_search :negate_normal_dependency?, "(sym {:build :recommended :test :optional})"
|
2018-02-19 23:40:07 +05:30
|
|
|
|
2020-01-12 10:59:22 -08:00
|
|
|
# Node pattern method to extract `name` in `depends_on :name` or `uses_from_macos :name`
|
2018-02-19 23:40:07 +05:30
|
|
|
def_node_search :dependency_name_node, <<~EOS
|
2020-04-28 14:17:54 +01:00
|
|
|
{(send nil? {:depends_on :uses_from_macos} {(hash (pair $_ _) ...) $({str sym} _) $(const nil? _)} ...)
|
2018-06-11 04:14:13 +02:00
|
|
|
(if _ (send nil? :depends_on {(hash (pair $_ _)) $({str sym} _) $(const nil? _)}) nil?)}
|
2018-02-19 23:40:07 +05:30
|
|
|
EOS
|
|
|
|
|
|
|
|
# Node pattern method to extract `name` in `build.with? :name`
|
|
|
|
def_node_search :build_with_dependency_node, <<~EOS
|
|
|
|
(send (send nil? :build) :with? $({str sym} _))
|
|
|
|
EOS
|
|
|
|
|
|
|
|
def insert_after!(arr, idx1, idx2)
|
|
|
|
arr.insert(idx2+1, arr.delete_at(idx1))
|
|
|
|
end
|
|
|
|
|
|
|
|
def build_with_dependency_name(node)
|
|
|
|
match_nodes = build_with_dependency_node(node)
|
2023-12-14 02:52:30 +00:00
|
|
|
match_nodes = match_nodes.to_a.compact
|
2018-02-19 23:40:07 +05:30
|
|
|
match_nodes.map { |n| string_content(n) } unless match_nodes.empty?
|
|
|
|
end
|
|
|
|
|
|
|
|
def dependency_name(dependency_node)
|
|
|
|
match_node = dependency_name_node(dependency_node).to_a.first
|
|
|
|
string_content(match_node) if match_node
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|