| 
									
										
										
										
											2022-08-14 15:39:20 -04:00
										 |  |  | # 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 | 
					
						
							| 
									
										
										
										
											2022-08-25 01:22:41 -04:00
										 |  |  | 
 | 
					
						
							|  |  |  |         [:arch, :arm?, :intel?].each do |method| | 
					
						
							|  |  |  |           hardware_cpu_search(body_node, method: method) do |method_node| | 
					
						
							| 
									
										
										
										
											2022-12-28 16:09:05 -05:00
										 |  |  |             # These should already be caught by `if_arch_node_search` | 
					
						
							|  |  |  |             next if method_node.parent.source.start_with? "if #{method_node.source}" | 
					
						
							| 
									
										
										
										
											2022-08-25 01:22:41 -04:00
										 |  |  |             next if if_node_is_allowed?(method_node, allowed_methods: allowed_methods, allowed_blocks: allowed_blocks) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             offending_node(method_node) | 
					
						
							|  |  |  |             problem "Don't use `#{method_node.source}`, use `on_arm` and `on_intel` blocks instead." | 
					
						
							|  |  |  |           end | 
					
						
							|  |  |  |         end | 
					
						
							| 
									
										
										
										
											2022-08-14 15:39:20 -04:00
										 |  |  |       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 | 
					
						
							| 
									
										
										
										
											2022-08-25 03:41:15 -04:00
										 |  |  | 
 | 
					
						
							|  |  |  |           macos_version_comparison_search(body_node, os_version: macos_version_option) do |method_node| | 
					
						
							| 
									
										
										
										
											2022-12-28 16:09:05 -05:00
										 |  |  |             # These should already be caught by `if_macos_version_node_search` | 
					
						
							|  |  |  |             next if method_node.parent.source.start_with? "if #{method_node.source}" | 
					
						
							| 
									
										
										
										
											2022-08-25 03:41:15 -04:00
										 |  |  |             next if if_node_is_allowed?(method_node, allowed_methods: allowed_methods, allowed_blocks: allowed_blocks) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             offending_node(method_node) | 
					
						
							|  |  |  |             problem "Don't use `#{method_node.source}`, use `on_{macos_version}` blocks instead." | 
					
						
							|  |  |  |           end | 
					
						
							| 
									
										
										
										
											2022-08-14 15:39:20 -04:00
										 |  |  |         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 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-08-25 01:22:41 -04:00
										 |  |  |       def_node_search :hardware_cpu_search, <<~PATTERN | 
					
						
							|  |  |  |         (send (const (const nil? :Hardware) :CPU) %method) | 
					
						
							|  |  |  |       PATTERN | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-08-25 03:41:15 -04:00
										 |  |  |       def_node_search :macos_version_comparison_search, <<~PATTERN | 
					
						
							|  |  |  |         (send (send (const nil? :MacOS) :version) {:== :<= :< :>= :> :!=} (sym %os_version)) | 
					
						
							|  |  |  |       PATTERN | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-08-14 15:39:20 -04:00
										 |  |  |       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 |