 2e5272d04c
			
		
	
	
		2e5272d04c
		
	
	
	
	
		
			
			Do the usual "disable deprecations" and "uncomment pending deprecations" dance and delete/deprecate/disable relevant/related code.
		
			
				
	
	
		
			249 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Ruby
		
	
	
	
	
	
			
		
		
	
	
			249 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Ruby
		
	
	
	
	
	
| # frozen_string_literal: true
 | |
| 
 | |
| require "rubocops/extend/formula"
 | |
| 
 | |
| module RuboCop
 | |
|   module Cop
 | |
|     module FormulaAudit
 | |
|       # This cop checks for correct order of components in Formulae.
 | |
|       #
 | |
|       # - `component_precedence_list` has component hierarchy in a nested list
 | |
|       #   where each sub array contains components' details which are at same precedence level
 | |
|       class ComponentsOrder < FormulaCop
 | |
|         # `aspell`: options and resources should be grouped by language
 | |
|         COMPONENT_ALLOWLIST = %w[
 | |
|           aspell
 | |
|         ].freeze
 | |
| 
 | |
|         def audit_formula(_node, _class_node, _parent_class_node, body_node)
 | |
|           component_precedence_list = [
 | |
|             [{ name: :include,   type: :method_call }],
 | |
|             [{ name: :desc,      type: :method_call }],
 | |
|             [{ name: :homepage,  type: :method_call }],
 | |
|             [{ name: :url,       type: :method_call }],
 | |
|             [{ name: :mirror,    type: :method_call }],
 | |
|             [{ name: :version,   type: :method_call }],
 | |
|             [{ name: :sha256,    type: :method_call }],
 | |
|             [{ name: :license, type: :method_call }],
 | |
|             [{ name: :revision, type: :method_call }],
 | |
|             [{ name: :version_scheme, type: :method_call }],
 | |
|             [{ name: :head,      type: :method_call }],
 | |
|             [{ name: :stable,    type: :block_call }],
 | |
|             [{ name: :livecheck, type: :block_call }],
 | |
|             [{ name: :bottle,    type: :block_call }],
 | |
|             [{ name: :pour_bottle?, type: :block_call }],
 | |
|             [{ name: :head,      type: :block_call }],
 | |
|             [{ name: :bottle,    type: :method_call }],
 | |
|             [{ name: :keg_only,  type: :method_call }],
 | |
|             [{ name: :option,    type: :method_call }],
 | |
|             [{ name: :deprecated_option, type: :method_call }],
 | |
|             [{ name: :depends_on, type: :method_call }],
 | |
|             [{ name: :uses_from_macos, type: :method_call }],
 | |
|             [{ name: :on_macos, type: :block_call }],
 | |
|             [{ name: :on_linux, type: :block_call }],
 | |
|             [{ name: :conflicts_with, type: :method_call }],
 | |
|             [{ name: :skip_clean, type: :method_call }],
 | |
|             [{ name: :cxxstdlib_check, type: :method_call }],
 | |
|             [{ name: :link_overwrite, type: :method_call }],
 | |
|             [{ name: :fails_with, type: :method_call }, { name: :fails_with, type: :block_call }],
 | |
|             [{ name: :go_resource, type: :block_call }, { name: :resource, type: :block_call }],
 | |
|             [{ name: :patch, type: :method_call }, { name: :patch, type: :block_call }],
 | |
|             [{ name: :needs, type: :method_call }],
 | |
|             [{ name: :install, type: :method_definition }],
 | |
|             [{ name: :post_install, type: :method_definition }],
 | |
|             [{ name: :caveats, type: :method_definition }],
 | |
|             [{ name: :plist_options, type: :method_call }, { name: :plist, type: :method_definition }],
 | |
|             [{ name: :test, type: :block_call }],
 | |
|           ]
 | |
| 
 | |
|           @present_components, @offensive_nodes = check_order(component_precedence_list, body_node)
 | |
| 
 | |
|           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 }],
 | |
|           ]
 | |
| 
 | |
|           on_macos_blocks = find_blocks(body_node, :on_macos)
 | |
| 
 | |
|           if on_macos_blocks.length > 1
 | |
|             @offensive_node = on_macos_blocks.second
 | |
|             @offense_source_range = on_macos_blocks.second.source_range
 | |
|             problem "there can only be one `on_macos` block in a formula."
 | |
|           end
 | |
| 
 | |
|           check_on_os_block_content(component_precedence_list, on_macos_blocks.first) if on_macos_blocks.any?
 | |
| 
 | |
|           on_linux_blocks = find_blocks(body_node, :on_linux)
 | |
| 
 | |
|           if on_linux_blocks.length > 1
 | |
|             @offensive_node = on_linux_blocks.second
 | |
|             @offense_source_range = on_linux_blocks.second.source_range
 | |
|             problem "there can only be one `on_linux` block in a formula."
 | |
|           end
 | |
| 
 | |
|           check_on_os_block_content(component_precedence_list, on_linux_blocks.first) if on_linux_blocks.any?
 | |
| 
 | |
|           resource_blocks = find_blocks(body_node, :resource)
 | |
|           resource_blocks.each do |resource_block|
 | |
|             on_macos_blocks = find_blocks(resource_block.body, :on_macos)
 | |
|             on_linux_blocks = find_blocks(resource_block.body, :on_linux)
 | |
| 
 | |
|             if on_macos_blocks.length.zero? && on_linux_blocks.length.zero?
 | |
|               # Found nothing. Try without .body as depending on the code,
 | |
|               # on_macos or on_linux might be in .body or not ...
 | |
|               on_macos_blocks = find_blocks(resource_block, :on_macos)
 | |
|               on_linux_blocks = find_blocks(resource_block, :on_linux)
 | |
| 
 | |
|               next if on_macos_blocks.length.zero? && on_linux_blocks.length.zero?
 | |
|             end
 | |
| 
 | |
|             @offensive_node = resource_block
 | |
|             @offense_source_range = resource_block.source_range
 | |
| 
 | |
|             next if on_macos_blocks.length.zero? && on_linux_blocks.length.zero?
 | |
| 
 | |
|             if on_macos_blocks.length == 1
 | |
|               on_macos_block = on_macos_blocks.first
 | |
|               child_nodes = on_macos_block.body.child_nodes
 | |
|               if child_nodes[0].method_name.to_s != "url" && child_nodes[1].method_name.to_s != "sha256"
 | |
|                 problem "only an url and a sha256 (in the right order) are allowed in a `on_macos` " \
 | |
|                         "block within a resource block."
 | |
|                 next
 | |
|               end
 | |
|             end
 | |
| 
 | |
|             if on_linux_blocks.length == 1
 | |
|               on_linux_block = on_linux_blocks.first
 | |
|               child_nodes = on_linux_block.body.child_nodes
 | |
|               if child_nodes[0].method_name.to_s != "url" && child_nodes[1].method_name.to_s != "sha256"
 | |
|                 problem "only an url and a sha256 (in the right order) are allowed in a `on_linux` " \
 | |
|                         "block within a resource block."
 | |
|               end
 | |
|             end
 | |
| 
 | |
|             if on_macos_blocks.length > 1
 | |
|               problem "there can only be one `on_macos` block in a resource block."
 | |
|               next
 | |
|             end
 | |
| 
 | |
|             if on_linux_blocks.length > 1
 | |
|               problem "there can only be one `on_linux` block in a resource block."
 | |
|               next
 | |
|             end
 | |
|           end
 | |
|         end
 | |
| 
 | |
|         def check_on_os_block_content(component_precedence_list, on_os_block)
 | |
|           _, offensive_node = check_order(component_precedence_list, on_os_block.body)
 | |
|           component_problem(*offensive_node) if offensive_node
 | |
|           on_os_block.body.child_nodes.each do |child|
 | |
|             valid_node = depends_on_node?(child)
 | |
|             # Check for RuboCop::AST::SendNode instances only, as we are checking the
 | |
|             # method_name for patches and resources.
 | |
|             next unless child.instance_of? RuboCop::AST::SendNode
 | |
| 
 | |
|             valid_node ||= child.method_name.to_s == "patch"
 | |
|             valid_node ||= child.method_name.to_s == "resource"
 | |
| 
 | |
|             @offensive_node = on_os_block
 | |
|             @offense_source_range = on_os_block.source_range
 | |
|             unless valid_node
 | |
|               problem "`#{on_os_block.method_name}` can only include `depends_on`, `patch` and `resource` nodes."
 | |
|             end
 | |
|           end
 | |
|         end
 | |
| 
 | |
|         # autocorrect method gets called just after component_problem method call
 | |
|         def autocorrect(_node)
 | |
|           return if @offensive_nodes.nil?
 | |
| 
 | |
|           succeeding_node = @offensive_nodes[0]
 | |
|           preceding_node = @offensive_nodes[1]
 | |
|           lambda do |corrector|
 | |
|             reorder_components(corrector, succeeding_node, preceding_node)
 | |
|           end
 | |
|         end
 | |
| 
 | |
|         # 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.
 | |
|         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)
 | |
| 
 | |
|           # curr_p_idx.positive? means node1 needs to be grouped with its own kind
 | |
|           if curr_p_idx.positive?
 | |
|             node2 = preceding_comp_arr[curr_p_idx - 1]
 | |
|             indentation = " " * (start_column(node2) - line_start_column(node2))
 | |
|             line_breaks = node2.multiline? ? "\n\n" : "\n"
 | |
|             corrector.insert_after(node2.source_range, line_breaks + indentation + node1.source)
 | |
|           else
 | |
|             indentation = " " * (start_column(node2) - line_start_column(node2))
 | |
|             # No line breaks up to version_scheme, order_idx == 8
 | |
|             line_breaks = (order_idx > 8) ? "\n\n" : "\n"
 | |
|             corrector.insert_before(node2.source_range, node1.source + line_breaks + indentation)
 | |
|           end
 | |
|           corrector.remove(range_with_surrounding_space(range: node1.source_range, side: :left))
 | |
|         end
 | |
| 
 | |
|         # Returns precedence index and component's index to properly reorder and group during autocorrect
 | |
|         def get_state(node1)
 | |
|           @present_components.each_with_index do |comp, idx|
 | |
|             return [idx, comp.index(node1), comp] if comp.member?(node1)
 | |
|           end
 | |
|         end
 | |
| 
 | |
|         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
 | |
|           offensive_nodes = nil
 | |
|           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)
 | |
|               return [present_components, offensive_nodes] if offensive_nodes
 | |
|             end
 | |
|           end
 | |
|           nil
 | |
|         end
 | |
| 
 | |
|         # Method to format message for reporting component precedence violations
 | |
|         def component_problem(c1, c2)
 | |
|           return if COMPONENT_ALLOWLIST.include?(@formula_name)
 | |
| 
 | |
|           problem "`#{format_component(c1)}` (line #{line_number(c1)}) " \
 | |
|                   "should be put before `#{format_component(c2)}` " \
 | |
|                   "(line #{line_number(c2)})"
 | |
|         end
 | |
| 
 | |
|         # Node pattern method to match
 | |
|         # `depends_on` variants
 | |
|         def_node_matcher :depends_on_node?, <<~EOS
 | |
|           {(if _ (send nil? :depends_on ...) nil?)
 | |
|            (send nil? :depends_on ...)}
 | |
|         EOS
 | |
|       end
 | |
|     end
 | |
|   end
 | |
| end
 |