| 
									
										
										
										
											2021-01-06 09:11:34 -08:00
										 |  |  | # typed: strict | 
					
						
							| 
									
										
										
										
											2020-12-18 17:24:10 -08:00
										 |  |  | # frozen_string_literal: true | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-01-06 09:11:34 -08:00
										 |  |  | require "ast_constants" | 
					
						
							|  |  |  | require "rubocop-ast" | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-12-18 17:24:10 -08:00
										 |  |  | module Utils | 
					
						
							|  |  |  |   # Helper functions for editing Ruby files. | 
					
						
							|  |  |  |   module AST | 
					
						
							| 
									
										
										
										
											2021-01-06 09:11:34 -08:00
										 |  |  |     Node = RuboCop::AST::Node | 
					
						
							|  |  |  |     SendNode = RuboCop::AST::SendNode | 
					
						
							|  |  |  |     BlockNode = RuboCop::AST::BlockNode | 
					
						
							|  |  |  |     ProcessedSource = RuboCop::AST::ProcessedSource | 
					
						
							| 
									
										
										
										
											2021-01-10 09:14:16 -08:00
										 |  |  |     TreeRewriter = Parser::Source::TreeRewriter | 
					
						
							| 
									
										
										
										
											2021-01-06 09:11:34 -08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-01-11 20:11:08 -08:00
										 |  |  |     module_function | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     sig { params(body_node: Node).returns(T::Array[Node]) } | 
					
						
							|  |  |  |     def body_children(body_node) | 
					
						
							|  |  |  |       if body_node.blank? | 
					
						
							|  |  |  |         [] | 
					
						
							|  |  |  |       elsif body_node.begin_type? | 
					
						
							|  |  |  |         body_node.children.compact | 
					
						
							|  |  |  |       else | 
					
						
							|  |  |  |         [body_node] | 
					
						
							|  |  |  |       end | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     sig { params(name: Symbol, value: T.any(Numeric, String, Symbol), indent: T.nilable(Integer)).returns(String) } | 
					
						
							|  |  |  |     def stanza_text(name, value, indent: nil) | 
					
						
							|  |  |  |       text = if value.is_a?(String) | 
					
						
							|  |  |  |         _, node = process_source(value) | 
					
						
							|  |  |  |         value if (node.is_a?(SendNode) || node.is_a?(BlockNode)) && node.method_name == name | 
					
						
							|  |  |  |       end | 
					
						
							|  |  |  |       text ||= "#{name} #{value.inspect}" | 
					
						
							| 
									
										
										
										
											2024-01-08 13:26:41 -08:00
										 |  |  |       text = text.gsub(/^(?!$)/, " " * indent) if indent && !text.match?(/\A\n* +/) | 
					
						
							| 
									
										
										
										
											2021-01-11 20:11:08 -08:00
										 |  |  |       text | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     sig { params(source: String).returns([ProcessedSource, Node]) } | 
					
						
							|  |  |  |     def process_source(source) | 
					
						
							|  |  |  |       ruby_version = Version.new(HOMEBREW_REQUIRED_RUBY_VERSION).major_minor.to_f | 
					
						
							|  |  |  |       processed_source = ProcessedSource.new(source, ruby_version) | 
					
						
							|  |  |  |       root_node = processed_source.ast | 
					
						
							|  |  |  |       [processed_source, root_node] | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-01-17 22:45:55 -08:00
										 |  |  |     sig { | 
					
						
							| 
									
										
										
										
											2021-01-11 20:11:08 -08:00
										 |  |  |       params( | 
					
						
							|  |  |  |         component_name: Symbol, | 
					
						
							|  |  |  |         component_type: Symbol, | 
					
						
							|  |  |  |         target_name:    Symbol, | 
					
						
							|  |  |  |         target_type:    T.nilable(Symbol), | 
					
						
							|  |  |  |       ).returns(T::Boolean) | 
					
						
							| 
									
										
										
										
											2021-01-17 22:45:55 -08:00
										 |  |  |     } | 
					
						
							| 
									
										
										
										
											2021-01-11 20:11:08 -08:00
										 |  |  |     def component_match?(component_name:, component_type:, target_name:, target_type: nil) | 
					
						
							|  |  |  |       component_name == target_name && (target_type.nil? || component_type == target_type) | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     sig { params(node: Node, name: Symbol, type: T.nilable(Symbol)).returns(T::Boolean) } | 
					
						
							|  |  |  |     def call_node_match?(node, name:, type: nil) | 
					
						
							|  |  |  |       node_type = case node | 
					
						
							|  |  |  |       when SendNode then :method_call | 
					
						
							|  |  |  |       when BlockNode then :block_call | 
					
						
							|  |  |  |       else return false | 
					
						
							|  |  |  |       end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       component_match?(component_name: node.method_name, | 
					
						
							|  |  |  |                        component_type: node_type, | 
					
						
							|  |  |  |                        target_name:    name, | 
					
						
							|  |  |  |                        target_type:    type) | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-01-10 09:14:16 -08:00
										 |  |  |     # Helper class for editing formulae. | 
					
						
							|  |  |  |     class FormulaAST | 
					
						
							|  |  |  |       extend Forwardable | 
					
						
							| 
									
										
										
										
											2021-01-11 20:11:08 -08:00
										 |  |  |       include AST | 
					
						
							| 
									
										
										
										
											2021-01-10 09:14:16 -08:00
										 |  |  | 
 | 
					
						
							|  |  |  |       delegate process: :tree_rewriter | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       sig { params(formula_contents: String).void } | 
					
						
							|  |  |  |       def initialize(formula_contents) | 
					
						
							|  |  |  |         @formula_contents = formula_contents | 
					
						
							|  |  |  |         processed_source, children = process_formula | 
					
						
							|  |  |  |         @processed_source = T.let(processed_source, ProcessedSource) | 
					
						
							|  |  |  |         @children = T.let(children, T::Array[Node]) | 
					
						
							|  |  |  |         @tree_rewriter = T.let(TreeRewriter.new(processed_source.buffer), TreeRewriter) | 
					
						
							|  |  |  |       end | 
					
						
							| 
									
										
										
										
											2020-12-18 17:24:10 -08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-01-10 09:14:16 -08:00
										 |  |  |       sig { returns(T.nilable(Node)) } | 
					
						
							|  |  |  |       def bottle_block | 
					
						
							|  |  |  |         stanza(:bottle, type: :block_call) | 
					
						
							| 
									
										
										
										
											2020-12-31 09:42:14 -08:00
										 |  |  |       end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-01-10 09:14:16 -08:00
										 |  |  |       sig { params(name: Symbol, type: T.nilable(Symbol)).returns(T.nilable(Node)) } | 
					
						
							|  |  |  |       def stanza(name, type: nil) | 
					
						
							| 
									
										
										
										
											2024-03-07 16:20:20 +00:00
										 |  |  |         children.find { |child| call_node_match?(child, name:, type:) } | 
					
						
							| 
									
										
										
										
											2020-12-31 09:42:14 -08:00
										 |  |  |       end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-01-10 09:14:16 -08:00
										 |  |  |       sig { params(bottle_output: String).void } | 
					
						
							|  |  |  |       def replace_bottle_block(bottle_output) | 
					
						
							|  |  |  |         replace_stanza(:bottle, bottle_output.chomp, type: :block_call) | 
					
						
							| 
									
										
										
										
											2020-12-29 03:40:39 -08:00
										 |  |  |       end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-01-10 09:14:16 -08:00
										 |  |  |       sig { params(bottle_output: String).void } | 
					
						
							|  |  |  |       def add_bottle_block(bottle_output) | 
					
						
							|  |  |  |         add_stanza(:bottle, "\n#{bottle_output.chomp}", type: :block_call) | 
					
						
							| 
									
										
										
										
											2020-12-29 03:40:39 -08:00
										 |  |  |       end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-12-22 11:36:06 -08:00
										 |  |  |       sig { params(name: Symbol, type: T.nilable(Symbol)).void } | 
					
						
							|  |  |  |       def remove_stanza(name, type: nil) | 
					
						
							| 
									
										
										
										
											2024-03-07 16:20:20 +00:00
										 |  |  |         stanza_node = stanza(name, type:) | 
					
						
							| 
									
										
										
										
											2021-12-22 11:36:06 -08:00
										 |  |  |         raise "Could not find '#{name}' stanza!" if stanza_node.blank? | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         # stanza is probably followed by a newline character | 
					
						
							|  |  |  |         # try to delete it if so | 
					
						
							|  |  |  |         stanza_range = stanza_node.source_range | 
					
						
							|  |  |  |         trailing_range = stanza_range.with(begin_pos: stanza_range.end_pos, | 
					
						
							|  |  |  |                                            end_pos:   stanza_range.end_pos + 1) | 
					
						
							|  |  |  |         if trailing_range.source.chomp.empty? | 
					
						
							|  |  |  |           stanza_range = stanza_range.adjust(end_pos: 1) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |           # stanza_node is probably indented | 
					
						
							|  |  |  |           # since a trailing newline has been removed, | 
					
						
							|  |  |  |           # try to delete leading whitespace on line | 
					
						
							|  |  |  |           leading_range = stanza_range.with(begin_pos: stanza_range.begin_pos - stanza_range.column, | 
					
						
							|  |  |  |                                             end_pos:   stanza_range.begin_pos) | 
					
						
							|  |  |  |           if leading_range.source.strip.empty? | 
					
						
							|  |  |  |             stanza_range = stanza_range.adjust(begin_pos: -stanza_range.column) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             # if the stanza was preceded by a blank line, it should be removed | 
					
						
							|  |  |  |             # that is, if the two previous characters are newlines, | 
					
						
							|  |  |  |             # then delete one of them | 
					
						
							|  |  |  |             leading_range = stanza_range.with(begin_pos: stanza_range.begin_pos - 2, | 
					
						
							|  |  |  |                                               end_pos:   stanza_range.begin_pos) | 
					
						
							|  |  |  |             stanza_range = stanza_range.adjust(begin_pos: -1) if leading_range.source.chomp.chomp.empty? | 
					
						
							|  |  |  |           end | 
					
						
							|  |  |  |         end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         tree_rewriter.remove(stanza_range) | 
					
						
							|  |  |  |       end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-01-10 09:14:16 -08:00
										 |  |  |       sig { params(name: Symbol, replacement: T.any(Numeric, String, Symbol), type: T.nilable(Symbol)).void } | 
					
						
							|  |  |  |       def replace_stanza(name, replacement, type: nil) | 
					
						
							| 
									
										
										
										
											2024-03-07 16:20:20 +00:00
										 |  |  |         stanza_node = stanza(name, type:) | 
					
						
							| 
									
										
										
										
											2021-01-24 21:55:35 -05:00
										 |  |  |         raise "Could not find '#{name}' stanza!" if stanza_node.blank? | 
					
						
							| 
									
										
										
										
											2020-12-18 17:24:10 -08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-01-11 20:11:08 -08:00
										 |  |  |         tree_rewriter.replace(stanza_node.source_range, stanza_text(name, replacement, indent: 2).lstrip) | 
					
						
							| 
									
										
										
										
											2020-12-18 17:24:10 -08:00
										 |  |  |       end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-01-10 09:14:16 -08:00
										 |  |  |       sig { params(name: Symbol, value: T.any(Numeric, String, Symbol), type: T.nilable(Symbol)).void } | 
					
						
							|  |  |  |       def add_stanza(name, value, type: nil) | 
					
						
							| 
									
										
										
										
											2020-12-31 09:42:14 -08:00
										 |  |  |         preceding_component = if children.length > 1
 | 
					
						
							|  |  |  |           children.reduce do |previous_child, current_child| | 
					
						
							| 
									
										
										
										
											2020-12-18 17:24:10 -08:00
										 |  |  |             if formula_component_before_target?(current_child, | 
					
						
							|  |  |  |                                                 target_name: name, | 
					
						
							|  |  |  |                                                 target_type: type) | 
					
						
							|  |  |  |               next current_child | 
					
						
							|  |  |  |             else | 
					
						
							|  |  |  |               break previous_child | 
					
						
							|  |  |  |             end | 
					
						
							|  |  |  |           end | 
					
						
							|  |  |  |         else | 
					
						
							| 
									
										
										
										
											2020-12-31 09:42:14 -08:00
										 |  |  |           children.first | 
					
						
							| 
									
										
										
										
											2020-12-18 17:24:10 -08:00
										 |  |  |         end | 
					
						
							| 
									
										
										
										
											2021-01-06 09:11:34 -08:00
										 |  |  |         preceding_component = preceding_component.last_argument if preceding_component.is_a?(SendNode) | 
					
						
							| 
									
										
										
										
											2020-12-18 17:24:10 -08:00
										 |  |  | 
 | 
					
						
							|  |  |  |         preceding_expr = preceding_component.location.expression | 
					
						
							|  |  |  |         processed_source.comments.each do |comment| | 
					
						
							|  |  |  |           comment_expr = comment.location.expression | 
					
						
							|  |  |  |           distance = comment_expr.first_line - preceding_expr.first_line | 
					
						
							|  |  |  |           case distance | 
					
						
							|  |  |  |           when 0
 | 
					
						
							|  |  |  |             if comment_expr.last_line > preceding_expr.last_line || | 
					
						
							|  |  |  |                comment_expr.end_pos > preceding_expr.end_pos | 
					
						
							|  |  |  |               preceding_expr = comment_expr | 
					
						
							|  |  |  |             end | 
					
						
							|  |  |  |           when 1
 | 
					
						
							|  |  |  |             preceding_expr = comment_expr | 
					
						
							|  |  |  |           end | 
					
						
							|  |  |  |         end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-01-11 20:11:08 -08:00
										 |  |  |         tree_rewriter.insert_after(preceding_expr, "\n#{stanza_text(name, value, indent: 2)}") | 
					
						
							| 
									
										
										
										
											2021-01-03 21:41:35 -08:00
										 |  |  |       end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-01-10 09:14:16 -08:00
										 |  |  |       private | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       sig { returns(String) } | 
					
						
							|  |  |  |       attr_reader :formula_contents | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       sig { returns(ProcessedSource) } | 
					
						
							|  |  |  |       attr_reader :processed_source | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       sig { returns(T::Array[Node]) } | 
					
						
							|  |  |  |       attr_reader :children | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       sig { returns(TreeRewriter) } | 
					
						
							|  |  |  |       attr_reader :tree_rewriter | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       sig { returns([ProcessedSource, T::Array[Node]]) } | 
					
						
							|  |  |  |       def process_formula | 
					
						
							| 
									
										
										
										
											2021-01-11 20:11:08 -08:00
										 |  |  |         processed_source, root_node = process_source(formula_contents) | 
					
						
							| 
									
										
										
										
											2020-12-18 17:24:10 -08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-01-09 15:11:30 -06:00
										 |  |  |         class_node = root_node if root_node.class_type? | 
					
						
							|  |  |  |         if root_node.begin_type? | 
					
						
							|  |  |  |           nodes = root_node.children.select(&:class_type?) | 
					
						
							|  |  |  |           class_node = if nodes.count > 1
 | 
					
						
							|  |  |  |             nodes.find { |n| n.parent_class&.const_name == "Formula" } | 
					
						
							|  |  |  |           else | 
					
						
							|  |  |  |             nodes.first | 
					
						
							|  |  |  |           end | 
					
						
							| 
									
										
										
										
											2020-12-18 17:24:10 -08:00
										 |  |  |         end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         raise "Could not find formula class!" if class_node.nil? | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-01-11 20:11:08 -08:00
										 |  |  |         children = body_children(class_node.body) | 
					
						
							| 
									
										
										
										
											2020-12-31 09:42:14 -08:00
										 |  |  |         raise "Formula class is empty!" if children.empty? | 
					
						
							| 
									
										
										
										
											2020-12-18 17:24:10 -08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-12-31 09:42:14 -08:00
										 |  |  |         [processed_source, children] | 
					
						
							| 
									
										
										
										
											2020-12-18 17:24:10 -08:00
										 |  |  |       end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-01-06 09:11:34 -08:00
										 |  |  |       sig { params(node: Node, target_name: Symbol, target_type: T.nilable(Symbol)).returns(T::Boolean) } | 
					
						
							| 
									
										
										
										
											2020-12-18 17:24:10 -08:00
										 |  |  |       def formula_component_before_target?(node, target_name:, target_type: nil) | 
					
						
							| 
									
										
										
										
											2021-01-06 09:11:34 -08:00
										 |  |  |         FORMULA_COMPONENT_PRECEDENCE_LIST.each do |components| | 
					
						
							| 
									
										
										
										
											2020-12-18 17:24:10 -08:00
										 |  |  |           return false if components.any? do |component| | 
					
						
							|  |  |  |             component_match?(component_name: component[:name], | 
					
						
							|  |  |  |                              component_type: component[:type], | 
					
						
							| 
									
										
										
										
											2024-03-07 16:20:20 +00:00
										 |  |  |                              target_name:, | 
					
						
							|  |  |  |                              target_type:) | 
					
						
							| 
									
										
										
										
											2020-12-18 17:24:10 -08:00
										 |  |  |           end | 
					
						
							|  |  |  |           return true if components.any? do |component| | 
					
						
							|  |  |  |             call_node_match?(node, name: component[:name], type: component[:type]) | 
					
						
							|  |  |  |           end | 
					
						
							|  |  |  |         end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         false | 
					
						
							|  |  |  |       end | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | end |