197 lines
		
	
	
		
			7.8 KiB
		
	
	
	
		
			Ruby
		
	
	
	
	
	
		
		
			
		
	
	
			197 lines
		
	
	
		
			7.8 KiB
		
	
	
	
		
			Ruby
		
	
	
	
	
	
|   | # typed: false | ||
|  | # frozen_string_literal: true | ||
|  | 
 | ||
|  | require "macos_versions" | ||
|  | require "rubocops/shared/helper_functions" | ||
|  | 
 | ||
|  | module RuboCop | ||
|  |   module Cop | ||
|  |     # This module performs common checks on `on_{system}` blocks in both formulae and casks. | ||
|  |     # | ||
|  |     # @api private | ||
|  |     module OnSystemConditionalsHelper | ||
|  |       extend NodePattern::Macros | ||
|  |       include HelperFunctions | ||
|  | 
 | ||
|  |       ARCH_OPTIONS = [:arm, :intel].freeze | ||
|  |       BASE_OS_OPTIONS = [:macos, :linux].freeze | ||
|  |       MACOS_VERSION_OPTIONS = MacOSVersions::SYMBOLS.keys.freeze | ||
|  |       ON_SYSTEM_OPTIONS = [*ARCH_OPTIONS, *BASE_OS_OPTIONS, *MACOS_VERSION_OPTIONS, :system].freeze | ||
|  | 
 | ||
|  |       MACOS_VERSION_CONDITIONALS = { | ||
|  |         "==" => nil, | ||
|  |         "<=" => :or_older, | ||
|  |         ">=" => :or_newer, | ||
|  |       }.freeze | ||
|  | 
 | ||
|  |       def audit_on_system_blocks(body_node, parent_name) | ||
|  |         parent_string = if body_node.def_type? | ||
|  |           "def #{parent_name}" | ||
|  |         else | ||
|  |           "#{parent_name} do" | ||
|  |         end | ||
|  | 
 | ||
|  |         ON_SYSTEM_OPTIONS.each do |on_system_option| | ||
|  |           on_system_method = :"on_#{on_system_option}" | ||
|  |           if_statement_string = if ARCH_OPTIONS.include?(on_system_option) | ||
|  |             "if Hardware::CPU.#{on_system_option}?" | ||
|  |           elsif BASE_OS_OPTIONS.include?(on_system_option) | ||
|  |             "if OS.#{(on_system_option == :macos) ? "mac" : "linux"}?" | ||
|  |           elsif on_system_option == :system | ||
|  |             "if OS.linux? || MacOS.version" | ||
|  |           else | ||
|  |             "if MacOS.version" | ||
|  |           end | ||
|  | 
 | ||
|  |           find_every_method_call_by_name(body_node, on_system_method).each do |on_system_node| | ||
|  |             if_conditional = "" | ||
|  |             if MACOS_VERSION_OPTIONS.include? on_system_option | ||
|  |               on_macos_version_method_call(on_system_node, on_method: on_system_method) do |on_method_parameters| | ||
|  |                 if on_method_parameters.empty? | ||
|  |                   if_conditional = " == :#{on_system_option}" | ||
|  |                 else | ||
|  |                   if_condition_operator = MACOS_VERSION_CONDITIONALS.key(on_method_parameters.first) | ||
|  |                   if_conditional = " #{if_condition_operator} :#{on_system_option}" | ||
|  |                 end | ||
|  |               end | ||
|  |             elsif on_system_option == :system | ||
|  |               on_system_method_call(on_system_node) do |macos_symbol| | ||
|  |                 base_os, condition = macos_symbol.to_s.split(/_(?=or_)/).map(&:to_sym) | ||
|  |                 if_condition_operator = MACOS_VERSION_CONDITIONALS.key(condition) | ||
|  |                 if_conditional = " #{if_condition_operator} :#{base_os}" | ||
|  |               end | ||
|  |             end | ||
|  | 
 | ||
|  |             offending_node(on_system_node) | ||
|  |             problem "Don't use `#{on_system_node.source}` in `#{parent_string}`, " \ | ||
|  |                     "use `#{if_statement_string}#{if_conditional}` instead." do |corrector| | ||
|  |               block_node = offending_node.parent | ||
|  |               next if block_node.type != :block | ||
|  | 
 | ||
|  |               # TODO: could fix corrector to handle this but punting for now. | ||
|  |               next if block_node.single_line? | ||
|  | 
 | ||
|  |               source_range = offending_node.source_range.join(offending_node.parent.loc.begin) | ||
|  |               corrector.replace(source_range, "#{if_statement_string}#{if_conditional}") | ||
|  |             end | ||
|  |           end | ||
|  |         end | ||
|  |       end | ||
|  | 
 | ||
|  |       def audit_arch_conditionals(body_node, allowed_methods: [], allowed_blocks: []) | ||
|  |         ARCH_OPTIONS.each do |arch_option| | ||
|  |           else_method = (arch_option == :arm) ? :on_intel : :on_arm | ||
|  |           if_arch_node_search(body_node, arch: :"#{arch_option}?") do |if_node, else_node| | ||
|  |             next if if_node_is_allowed?(if_node, allowed_methods: allowed_methods, allowed_blocks: allowed_blocks) | ||
|  | 
 | ||
|  |             if_statement_problem(if_node, "if Hardware::CPU.#{arch_option}?", "on_#{arch_option}", | ||
|  |                                  else_method: else_method, else_node: else_node) | ||
|  |           end | ||
|  |         end | ||
|  |       end | ||
|  | 
 | ||
|  |       def audit_base_os_conditionals(body_node, allowed_methods: [], allowed_blocks: []) | ||
|  |         BASE_OS_OPTIONS.each do |base_os_option| | ||
|  |           os_method, else_method = if base_os_option == :macos | ||
|  |             [:mac?, :on_linux] | ||
|  |           else | ||
|  |             [:linux?, :on_macos] | ||
|  |           end | ||
|  |           if_base_os_node_search(body_node, base_os: os_method) do |if_node, else_node| | ||
|  |             next if if_node_is_allowed?(if_node, allowed_methods: allowed_methods, allowed_blocks: allowed_blocks) | ||
|  | 
 | ||
|  |             if_statement_problem(if_node, "if OS.#{os_method}", "on_#{base_os_option}", | ||
|  |                                  else_method: else_method, else_node: else_node) | ||
|  |           end | ||
|  |         end | ||
|  |       end | ||
|  | 
 | ||
|  |       def audit_macos_version_conditionals(body_node, allowed_methods: [], allowed_blocks: [], | ||
|  |                                            recommend_on_system: true) | ||
|  |         MACOS_VERSION_OPTIONS.each do |macos_version_option| | ||
|  |           if_macos_version_node_search(body_node, os_version: macos_version_option) do |if_node, operator, else_node| | ||
|  |             next if if_node_is_allowed?(if_node, allowed_methods: allowed_methods, allowed_blocks: allowed_blocks) | ||
|  | 
 | ||
|  |             autocorrect = else_node.blank? && MACOS_VERSION_CONDITIONALS.key?(operator.to_s) | ||
|  |             on_system_method_string = if recommend_on_system && operator == :< | ||
|  |               "on_system" | ||
|  |             elsif recommend_on_system && operator == :<= | ||
|  |               "on_system :linux, macos: :#{macos_version_option}_or_older" | ||
|  |             elsif operator != :== && MACOS_VERSION_CONDITIONALS.key?(operator.to_s) | ||
|  |               "on_#{macos_version_option} :#{MACOS_VERSION_CONDITIONALS[operator.to_s]}" | ||
|  |             else | ||
|  |               "on_#{macos_version_option}" | ||
|  |             end | ||
|  | 
 | ||
|  |             if_statement_problem(if_node, "if MacOS.version #{operator} :#{macos_version_option}", | ||
|  |                                  on_system_method_string, autocorrect: autocorrect) | ||
|  |           end | ||
|  |         end | ||
|  |       end | ||
|  | 
 | ||
|  |       private | ||
|  | 
 | ||
|  |       def if_statement_problem(if_node, if_statement_string, on_system_method_string, | ||
|  |                                else_method: nil, else_node: nil, autocorrect: true) | ||
|  |         offending_node(if_node) | ||
|  |         problem "Don't use `#{if_statement_string}`, " \ | ||
|  |                 "use `#{on_system_method_string} do` instead." do |corrector| | ||
|  |           next unless autocorrect | ||
|  |           # TODO: could fix corrector to handle this but punting for now. | ||
|  |           next if if_node.unless? | ||
|  | 
 | ||
|  |           if else_method.present? && else_node.present? | ||
|  |             corrector.replace(if_node.source_range, | ||
|  |                               "#{on_system_method_string} do\n#{if_node.body.source}\nend\n" \ | ||
|  |                               "#{else_method} do\n#{else_node.source}\nend") | ||
|  |           else | ||
|  |             corrector.replace(if_node.source_range, "#{on_system_method_string} do\n#{if_node.body.source}\nend") | ||
|  |           end | ||
|  |         end | ||
|  |       end | ||
|  | 
 | ||
|  |       def if_node_is_allowed?(if_node, allowed_methods: [], allowed_blocks: []) | ||
|  |         # TODO: check to see if it's legal | ||
|  |         valid = false | ||
|  |         if_node.each_ancestor do |ancestor| | ||
|  |           valid_method_names = case ancestor.type | ||
|  |           when :def | ||
|  |             allowed_methods | ||
|  |           when :block | ||
|  |             allowed_blocks | ||
|  |           else | ||
|  |             next | ||
|  |           end | ||
|  |           next unless valid_method_names.include?(ancestor.method_name) | ||
|  | 
 | ||
|  |           valid = true | ||
|  |           break | ||
|  |         end | ||
|  |         return true if valid | ||
|  | 
 | ||
|  |         false | ||
|  |       end | ||
|  | 
 | ||
|  |       def_node_matcher :on_macos_version_method_call, <<~PATTERN | ||
|  |         (send nil? %on_method (sym ${:or_newer :or_older})?) | ||
|  |       PATTERN | ||
|  | 
 | ||
|  |       def_node_matcher :on_system_method_call, <<~PATTERN | ||
|  |         (send nil? :on_system (sym :linux) (hash (pair (sym :macos) (sym $_)))) | ||
|  |       PATTERN | ||
|  | 
 | ||
|  |       def_node_search :if_arch_node_search, <<~PATTERN | ||
|  |         $(if (send (const (const nil? :Hardware) :CPU) %arch) _ $_) | ||
|  |       PATTERN | ||
|  | 
 | ||
|  |       def_node_search :if_base_os_node_search, <<~PATTERN | ||
|  |         $(if (send (const nil? :OS) %base_os) _ $_) | ||
|  |       PATTERN | ||
|  | 
 | ||
|  |       def_node_search :if_macos_version_node_search, <<~PATTERN | ||
|  |         $(if (send (send (const nil? :MacOS) :version) ${:== :<= :< :>= :> :!=} (sym %os_version)) _ $_) | ||
|  |       PATTERN | ||
|  |     end | ||
|  |   end | ||
|  | end |