2023-02-14 19:19:37 -08:00
|
|
|
# typed: true
|
2019-04-19 15:38:03 +09:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
2021-01-06 09:11:34 -08:00
|
|
|
require "ast_constants"
|
2023-02-20 10:22:39 -08:00
|
|
|
require "rubocops/extend/formula_cop"
|
2017-04-08 15:10:44 +05:30
|
|
|
|
|
|
|
module RuboCop
|
|
|
|
module Cop
|
2018-04-24 18:27:37 +10:00
|
|
|
module FormulaAudit
|
2020-11-05 17:17:03 -05:00
|
|
|
# This cop checks for correct order of components in formulae.
|
2017-04-08 15:10:44 +05:30
|
|
|
#
|
2018-10-18 21:42:43 -04:00
|
|
|
# - `component_precedence_list` has component hierarchy in a nested list
|
2017-04-08 15:10:44 +05:30
|
|
|
# where each sub array contains components' details which are at same precedence level
|
2023-02-20 18:10:59 -08:00
|
|
|
class ComponentsOrder < FormulaCop
|
2021-01-12 16:36:06 +11:00
|
|
|
extend AutoCorrector
|
|
|
|
|
2024-07-07 15:18:29 -04:00
|
|
|
sig { override.params(formula_nodes: FormulaNodes).void }
|
|
|
|
def audit_formula(formula_nodes)
|
|
|
|
return if (body_node = formula_nodes.body_node).nil?
|
2022-11-05 04:17:50 +00:00
|
|
|
|
2021-01-06 09:11:34 -08:00
|
|
|
@present_components, @offensive_nodes = check_order(FORMULA_COMPONENT_PRECEDENCE_LIST, body_node)
|
2020-04-23 11:32:23 +02:00
|
|
|
|
|
|
|
component_problem @offensive_nodes[0], @offensive_nodes[1] if @offensive_nodes
|
|
|
|
|
|
|
|
component_precedence_list = [
|
|
|
|
[{ name: :depends_on, type: :method_call }],
|
|
|
|
[{ name: :resource, type: :block_call }],
|
|
|
|
[{ name: :patch, type: :method_call }, { name: :patch, type: :block_call }],
|
|
|
|
]
|
|
|
|
|
2023-03-26 02:15:50 +01:00
|
|
|
head_blocks = find_blocks(body_node, :head)
|
|
|
|
head_blocks.each do |head_block|
|
|
|
|
check_block_component_order(FORMULA_COMPONENT_PRECEDENCE_LIST, head_block)
|
|
|
|
end
|
|
|
|
|
2022-06-30 13:36:16 -04:00
|
|
|
on_system_methods.each do |on_method|
|
|
|
|
on_method_blocks = find_blocks(body_node, on_method)
|
|
|
|
next if on_method_blocks.empty?
|
2018-09-17 02:45:00 +02:00
|
|
|
|
2022-06-30 13:36:16 -04:00
|
|
|
if on_method_blocks.length > 1
|
|
|
|
@offensive_node = on_method_blocks.second
|
|
|
|
problem "there can only be one `#{on_method}` block in a formula."
|
|
|
|
end
|
2020-04-23 11:32:23 +02:00
|
|
|
|
2022-06-30 13:36:16 -04:00
|
|
|
check_on_system_block_content(component_precedence_list, on_method_blocks.first)
|
2020-04-23 11:32:23 +02:00
|
|
|
end
|
|
|
|
|
|
|
|
resource_blocks = find_blocks(body_node, :resource)
|
|
|
|
resource_blocks.each do |resource_block|
|
2023-03-26 02:15:50 +01:00
|
|
|
check_block_component_order(FORMULA_COMPONENT_PRECEDENCE_LIST, resource_block)
|
|
|
|
|
2022-06-30 13:36:16 -04:00
|
|
|
on_system_blocks = {}
|
2018-09-17 02:45:00 +02:00
|
|
|
|
2022-06-30 13:36:16 -04:00
|
|
|
on_system_methods.each do |on_method|
|
|
|
|
on_system_blocks[on_method] = find_blocks(resource_block.body, on_method)
|
|
|
|
end
|
2020-04-23 11:32:23 +02:00
|
|
|
|
2022-06-30 13:36:16 -04:00
|
|
|
if on_system_blocks.empty?
|
|
|
|
# Found nothing. Try without .body as depending on the code,
|
|
|
|
# on_{system} might be in .body or not ...
|
|
|
|
on_system_methods.each do |on_method|
|
|
|
|
on_system_blocks[on_method] = find_blocks(resource_block, on_method)
|
|
|
|
end
|
2020-04-23 11:32:23 +02:00
|
|
|
end
|
2022-06-30 13:36:16 -04:00
|
|
|
next if on_system_blocks.empty?
|
2020-04-23 11:32:23 +02:00
|
|
|
|
|
|
|
@offensive_node = resource_block
|
|
|
|
|
2023-02-14 19:19:37 -08:00
|
|
|
on_system_bodies = T.let([], T::Array[[RuboCop::AST::BlockNode, RuboCop::AST::Node]])
|
2020-12-23 15:00:28 -08:00
|
|
|
|
2022-06-30 13:36:16 -04:00
|
|
|
on_system_blocks.each_value do |blocks|
|
|
|
|
blocks.each do |on_system_block|
|
|
|
|
on_system_body = on_system_block.body
|
|
|
|
branches = on_system_body.if_type? ? on_system_body.branches : [on_system_body]
|
|
|
|
on_system_bodies += branches.map { |branch| [on_system_block, branch] }
|
|
|
|
end
|
2020-04-23 11:32:23 +02:00
|
|
|
end
|
|
|
|
|
2023-02-14 19:19:37 -08:00
|
|
|
message = T.let(nil, T.nilable(String))
|
2020-12-23 15:00:28 -08:00
|
|
|
allowed_methods = [
|
|
|
|
[:url, :sha256],
|
2021-01-14 17:04:40 +11:00
|
|
|
[:url, :mirror, :sha256],
|
2020-12-23 15:00:28 -08:00
|
|
|
[:url, :version, :sha256],
|
2021-01-14 17:04:40 +11:00
|
|
|
[:url, :mirror, :version, :sha256],
|
2020-12-23 15:00:28 -08:00
|
|
|
]
|
2021-01-13 23:18:32 -08:00
|
|
|
minimum_methods = allowed_methods.first.map { |m| "`#{m}`" }.to_sentence
|
|
|
|
maximum_methods = allowed_methods.last.map { |m| "`#{m}`" }.to_sentence
|
2020-12-23 15:00:28 -08:00
|
|
|
|
2022-06-30 13:36:16 -04:00
|
|
|
on_system_bodies.each do |on_system_block, on_system_body|
|
|
|
|
method_name = on_system_block.method_name
|
|
|
|
child_nodes = on_system_body.begin_type? ? on_system_body.child_nodes : [on_system_body]
|
|
|
|
if child_nodes.all? { |n| n.send_type? || n.block_type? || n.lvasgn_type? }
|
2024-02-22 23:29:55 +00:00
|
|
|
method_names = child_nodes.filter_map do |node|
|
2022-06-30 13:36:16 -04:00
|
|
|
next if node.lvasgn_type?
|
|
|
|
next if node.method_name == :patch
|
|
|
|
next if on_system_methods.include? node.method_name
|
|
|
|
|
|
|
|
node.method_name
|
2024-02-22 23:29:55 +00:00
|
|
|
end
|
2022-06-30 13:36:16 -04:00
|
|
|
next if method_names.empty? || allowed_methods.include?(method_names)
|
2020-06-30 23:08:43 +02:00
|
|
|
end
|
2022-06-30 13:36:16 -04:00
|
|
|
offending_node(on_system_block)
|
2022-06-28 10:09:59 +01:00
|
|
|
message = "`#{method_name}` blocks within `resource` blocks must contain at least " \
|
2021-01-13 23:18:32 -08:00
|
|
|
"#{minimum_methods} and at most #{maximum_methods} (in order)."
|
2020-12-23 15:00:28 -08:00
|
|
|
break
|
|
|
|
end
|
|
|
|
|
2023-02-14 19:19:37 -08:00
|
|
|
if message
|
2020-12-23 15:00:28 -08:00
|
|
|
problem message
|
|
|
|
next
|
2020-04-23 11:32:23 +02:00
|
|
|
end
|
|
|
|
|
2022-06-30 13:36:16 -04:00
|
|
|
on_system_blocks.each do |on_method, blocks|
|
|
|
|
if blocks.length > 1
|
|
|
|
problem "there can only be one `#{on_method}` block in a resource block."
|
|
|
|
next
|
|
|
|
end
|
2020-04-23 11:32:23 +02:00
|
|
|
end
|
2017-04-08 15:10:44 +05:30
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2023-03-26 02:15:50 +01:00
|
|
|
def check_block_component_order(component_precedence_list, block)
|
|
|
|
@present_components, offensive_node = check_order(component_precedence_list, block.body)
|
|
|
|
component_problem(*offensive_node) if offensive_node
|
|
|
|
end
|
|
|
|
|
2022-06-30 13:36:16 -04:00
|
|
|
def check_on_system_block_content(component_precedence_list, on_system_block)
|
2023-04-07 21:35:38 +01:00
|
|
|
if on_system_block.body.block_type? && !on_system_methods.include?(on_system_block.body.method_name)
|
2022-07-16 01:59:59 +02:00
|
|
|
offending_node(on_system_block)
|
|
|
|
problem "Nest `#{on_system_block.method_name}` blocks inside `#{on_system_block.body.method_name}` " \
|
|
|
|
"blocks when there is only one inner block." do |corrector|
|
|
|
|
original_source = on_system_block.source.split("\n")
|
|
|
|
new_source = [original_source.second, original_source.first, *original_source.drop(2)]
|
|
|
|
corrector.replace(on_system_block.source_range, new_source.join("\n"))
|
|
|
|
end
|
|
|
|
end
|
2022-06-30 13:36:16 -04:00
|
|
|
on_system_allowed_methods = %w[
|
2022-10-17 13:22:34 -04:00
|
|
|
livecheck
|
|
|
|
keg_only
|
2021-07-04 18:34:03 +02:00
|
|
|
disable!
|
2022-10-17 13:22:34 -04:00
|
|
|
deprecate!
|
|
|
|
depends_on
|
2021-07-04 18:34:03 +02:00
|
|
|
conflicts_with
|
2022-06-30 13:36:16 -04:00
|
|
|
fails_with
|
2022-10-17 13:22:34 -04:00
|
|
|
resource
|
|
|
|
patch
|
2021-07-04 18:34:03 +02:00
|
|
|
]
|
2022-06-30 13:36:16 -04:00
|
|
|
on_system_allowed_methods += on_system_methods.map(&:to_s)
|
|
|
|
_, offensive_node = check_order(component_precedence_list, on_system_block.body)
|
2020-04-23 11:32:23 +02:00
|
|
|
component_problem(*offensive_node) if offensive_node
|
2022-06-30 13:36:16 -04:00
|
|
|
child_nodes = on_system_block.body.begin_type? ? on_system_block.body.child_nodes : [on_system_block.body]
|
2020-12-08 14:33:37 -08:00
|
|
|
child_nodes.each do |child|
|
2020-04-23 11:32:23 +02:00
|
|
|
valid_node = depends_on_node?(child)
|
2020-12-08 14:33:37 -08:00
|
|
|
# Check for RuboCop::AST::SendNode and RuboCop::AST::BlockNode instances
|
|
|
|
# only, as we are checking the method_name for `patch`, `resource`, etc.
|
|
|
|
method_type = child.send_type? || child.block_type?
|
|
|
|
next unless method_type
|
2018-04-24 18:27:37 +10:00
|
|
|
|
2022-06-30 13:36:16 -04:00
|
|
|
valid_node ||= on_system_allowed_methods.include? child.method_name.to_s
|
2018-09-17 02:45:00 +02:00
|
|
|
|
2020-12-09 08:55:51 -08:00
|
|
|
@offensive_node = child
|
|
|
|
next if valid_node
|
|
|
|
|
2022-06-30 13:36:16 -04:00
|
|
|
problem "`#{on_system_block.method_name}` cannot include `#{child.method_name}`. " \
|
|
|
|
"Only #{on_system_allowed_methods.map { |m| "`#{m}`" }.to_sentence} are allowed."
|
2020-04-23 11:32:23 +02:00
|
|
|
end
|
2017-04-08 15:10:44 +05:30
|
|
|
end
|
2017-05-08 18:47:50 +05:30
|
|
|
|
2018-10-18 21:42:43 -04:00
|
|
|
# Reorder two nodes in the source, using the corrector instance in autocorrect method.
|
|
|
|
# Components of same type are grouped together when rewriting the source.
|
|
|
|
# Linebreaks are introduced if components are of two different methods/blocks/multilines.
|
2017-05-08 18:47:50 +05:30
|
|
|
def reorder_components(corrector, node1, node2)
|
|
|
|
# order_idx : node1's index in component_precedence_list
|
|
|
|
# curr_p_idx: node1's index in preceding_comp_arr
|
|
|
|
# preceding_comp_arr: array containing components of same type
|
|
|
|
order_idx, curr_p_idx, preceding_comp_arr = get_state(node1)
|
|
|
|
|
2017-09-24 20:12:58 +01:00
|
|
|
# curr_p_idx.positive? means node1 needs to be grouped with its own kind
|
|
|
|
if curr_p_idx.positive?
|
2017-06-01 16:06:51 +02:00
|
|
|
node2 = preceding_comp_arr[curr_p_idx - 1]
|
2017-05-08 18:47:50 +05:30
|
|
|
indentation = " " * (start_column(node2) - line_start_column(node2))
|
|
|
|
line_breaks = node2.multiline? ? "\n\n" : "\n"
|
2017-06-01 16:06:51 +02:00
|
|
|
corrector.insert_after(node2.source_range, line_breaks + indentation + node1.source)
|
2017-05-08 18:47:50 +05:30
|
|
|
else
|
|
|
|
indentation = " " * (start_column(node2) - line_start_column(node2))
|
2019-08-19 14:27:29 +10:00
|
|
|
# No line breaks up to version_scheme, order_idx == 8
|
2017-06-01 16:06:51 +02:00
|
|
|
line_breaks = (order_idx > 8) ? "\n\n" : "\n"
|
|
|
|
corrector.insert_before(node2.source_range, node1.source + line_breaks + indentation)
|
2017-05-08 18:47:50 +05:30
|
|
|
end
|
2018-01-07 15:40:42 +00:00
|
|
|
corrector.remove(range_with_surrounding_space(range: node1.source_range, side: :left))
|
2017-05-08 18:47:50 +05:30
|
|
|
end
|
|
|
|
|
2020-11-05 17:17:03 -05:00
|
|
|
# Returns precedence index and component's index to properly reorder and group during autocorrect.
|
2017-05-08 18:47:50 +05:30
|
|
|
def get_state(node1)
|
|
|
|
@present_components.each_with_index do |comp, idx|
|
|
|
|
return [idx, comp.index(node1), comp] if comp.member?(node1)
|
|
|
|
end
|
|
|
|
end
|
2020-04-23 11:32:23 +02:00
|
|
|
|
|
|
|
def check_order(component_precedence_list, body_node)
|
|
|
|
present_components = component_precedence_list.map do |components|
|
|
|
|
components.flat_map do |component|
|
|
|
|
case component[:type]
|
|
|
|
when :method_call
|
|
|
|
find_method_calls_by_name(body_node, component[:name]).to_a
|
|
|
|
when :block_call
|
|
|
|
find_blocks(body_node, component[:name]).to_a
|
|
|
|
when :method_definition
|
|
|
|
find_method_def(body_node, component[:name])
|
|
|
|
end
|
|
|
|
end.compact
|
|
|
|
end
|
|
|
|
|
|
|
|
# Check if each present_components is above rest of the present_components
|
2023-02-14 19:19:37 -08:00
|
|
|
offensive_nodes = T.let(nil, T.nilable(T::Array[RuboCop::AST::Node]))
|
2020-04-23 11:32:23 +02:00
|
|
|
present_components.take(present_components.size - 1).each_with_index do |preceding_component, p_idx|
|
|
|
|
next if preceding_component.empty?
|
|
|
|
|
|
|
|
present_components.drop(p_idx + 1).each do |succeeding_component|
|
|
|
|
next if succeeding_component.empty?
|
|
|
|
|
|
|
|
offensive_nodes = check_precedence(preceding_component, succeeding_component)
|
2020-06-30 08:36:11 +01:00
|
|
|
return [present_components, offensive_nodes] if offensive_nodes
|
2020-04-23 11:32:23 +02:00
|
|
|
end
|
|
|
|
end
|
2020-06-30 08:36:11 +01:00
|
|
|
nil
|
2020-04-23 11:32:23 +02:00
|
|
|
end
|
|
|
|
|
2021-01-12 16:36:06 +11:00
|
|
|
# Method to report and correct component precedence violations.
|
2023-03-09 21:54:37 +00:00
|
|
|
def component_problem(component1, component2)
|
2020-11-27 12:29:38 -05:00
|
|
|
return if tap_style_exception? :components_order_exceptions
|
2020-04-23 11:32:23 +02:00
|
|
|
|
2023-03-09 21:54:37 +00:00
|
|
|
problem "`#{format_component(component1)}` (line #{line_number(component1)}) " \
|
|
|
|
"should be put before `#{format_component(component2)}` " \
|
|
|
|
"(line #{line_number(component2)})" do |corrector|
|
|
|
|
reorder_components(corrector, component1, component2)
|
2021-01-12 16:36:06 +11:00
|
|
|
end
|
2020-04-23 11:32:23 +02:00
|
|
|
end
|
|
|
|
|
|
|
|
# Node pattern method to match
|
2020-11-05 17:17:03 -05:00
|
|
|
# `depends_on` variants.
|
2020-04-23 11:32:23 +02:00
|
|
|
def_node_matcher :depends_on_node?, <<~EOS
|
|
|
|
{(if _ (send nil? :depends_on ...) nil?)
|
|
|
|
(send nil? :depends_on ...)}
|
|
|
|
EOS
|
2017-04-08 15:10:44 +05:30
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|