Merge pull request #10064 from SeekingMeaning/ast
utils: add AST helper functions for editing formulae
This commit is contained in:
		
						commit
						943ee39981
					
				@ -3,6 +3,7 @@
 | 
			
		||||
 | 
			
		||||
require "formula"
 | 
			
		||||
require "cli/parser"
 | 
			
		||||
require "utils/ast"
 | 
			
		||||
 | 
			
		||||
module Homebrew
 | 
			
		||||
  extend T::Sig
 | 
			
		||||
@ -36,56 +37,25 @@ module Homebrew
 | 
			
		||||
 | 
			
		||||
    args.named.to_formulae.each do |formula|
 | 
			
		||||
      current_revision = formula.revision
 | 
			
		||||
 | 
			
		||||
      if current_revision.zero?
 | 
			
		||||
        formula_spec = formula.stable
 | 
			
		||||
        hash_type, old_hash = if (checksum = formula_spec.checksum)
 | 
			
		||||
          [checksum.hash_type, checksum.hexdigest]
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
        old = if formula.license
 | 
			
		||||
          license_string = case formula.license
 | 
			
		||||
          when String
 | 
			
		||||
            "\"#{formula.license}\""
 | 
			
		||||
          when Symbol
 | 
			
		||||
            ":#{formula.license}"
 | 
			
		||||
          else
 | 
			
		||||
            formula.license.to_s.gsub(/:(\w+)=>/, '\1: ')                   # Change `:any_of=>` to `any_of: `
 | 
			
		||||
                   .tr("{}", "")                                            # Remove braces
 | 
			
		||||
                   .gsub(/=>with: "([a-zA-Z0-9-]+)"/, ' => { with: "\1" }') # Add braces and spacing around exceptions
 | 
			
		||||
          end
 | 
			
		||||
          # insert replacement revision after license
 | 
			
		||||
          <<~EOS
 | 
			
		||||
            license #{license_string}
 | 
			
		||||
          EOS
 | 
			
		||||
        elsif formula.path.read.include?("stable do\n")
 | 
			
		||||
          # insert replacement revision after homepage
 | 
			
		||||
          <<~EOS
 | 
			
		||||
            homepage "#{formula.homepage}"
 | 
			
		||||
          EOS
 | 
			
		||||
        elsif hash_type
 | 
			
		||||
          # insert replacement revision after hash
 | 
			
		||||
          <<~EOS
 | 
			
		||||
            #{hash_type} "#{old_hash}"
 | 
			
		||||
          EOS
 | 
			
		||||
        else
 | 
			
		||||
          # insert replacement revision after :revision
 | 
			
		||||
          <<~EOS
 | 
			
		||||
            revision: "#{formula_spec.specs[:revision]}"
 | 
			
		||||
          EOS
 | 
			
		||||
        end
 | 
			
		||||
        replacement = "#{old}  revision 1\n"
 | 
			
		||||
 | 
			
		||||
      else
 | 
			
		||||
        old = "revision #{current_revision}"
 | 
			
		||||
        replacement = "revision #{current_revision+1}"
 | 
			
		||||
      end
 | 
			
		||||
      text = "revision #{current_revision+1}"
 | 
			
		||||
 | 
			
		||||
      if args.dry_run?
 | 
			
		||||
        ohai "replace #{old.inspect} with #{replacement.inspect}" unless args.quiet?
 | 
			
		||||
        unless args.quiet?
 | 
			
		||||
          if current_revision.zero?
 | 
			
		||||
            ohai "add #{text.inspect}"
 | 
			
		||||
          else
 | 
			
		||||
            old = "revision #{current_revision}"
 | 
			
		||||
            ohai "replace #{old.inspect} with #{text.inspect}"
 | 
			
		||||
          end
 | 
			
		||||
        end
 | 
			
		||||
      else
 | 
			
		||||
        Utils::Inreplace.inreplace(formula.path) do |s|
 | 
			
		||||
          s.gsub!(old, replacement)
 | 
			
		||||
          s = s.inreplace_string
 | 
			
		||||
          if current_revision.zero?
 | 
			
		||||
            Utils::AST.add_formula_stanza!(s, name: :revision, text: text)
 | 
			
		||||
          else
 | 
			
		||||
            Utils::AST.replace_formula_stanza!(s, name: :revision, replacement: text)
 | 
			
		||||
          end
 | 
			
		||||
        end
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										50
									
								
								Library/Homebrew/test/utils/ast_spec.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								Library/Homebrew/test/utils/ast_spec.rb
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,50 @@
 | 
			
		||||
# typed: false
 | 
			
		||||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
require "utils/ast"
 | 
			
		||||
 | 
			
		||||
describe Utils::AST do
 | 
			
		||||
  let(:initial_formula) do
 | 
			
		||||
    <<~RUBY
 | 
			
		||||
      class Foo < Formula
 | 
			
		||||
        url "https://brew.sh/foo-1.0.tar.gz"
 | 
			
		||||
        license all_of: [
 | 
			
		||||
          :public_domain,
 | 
			
		||||
          "MIT",
 | 
			
		||||
          "GPL-3.0-or-later" => { with: "Autoconf-exception-3.0" },
 | 
			
		||||
        ]
 | 
			
		||||
      end
 | 
			
		||||
    RUBY
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  describe ".replace_formula_stanza!" do
 | 
			
		||||
    it "replaces the specified stanza in a formula" do
 | 
			
		||||
      contents = initial_formula.dup
 | 
			
		||||
      described_class.replace_formula_stanza! contents, name: :license, replacement: "license :public_domain"
 | 
			
		||||
      expect(contents).to eq <<~RUBY
 | 
			
		||||
        class Foo < Formula
 | 
			
		||||
          url "https://brew.sh/foo-1.0.tar.gz"
 | 
			
		||||
          license :public_domain
 | 
			
		||||
        end
 | 
			
		||||
      RUBY
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  describe ".add_formula_stanza!" do
 | 
			
		||||
    it "adds the specified stanza to a formula" do
 | 
			
		||||
      contents = initial_formula.dup
 | 
			
		||||
      described_class.add_formula_stanza! contents, name: :revision, text: "revision 1"
 | 
			
		||||
      expect(contents).to eq <<~RUBY
 | 
			
		||||
        class Foo < Formula
 | 
			
		||||
          url "https://brew.sh/foo-1.0.tar.gz"
 | 
			
		||||
          license all_of: [
 | 
			
		||||
            :public_domain,
 | 
			
		||||
            "MIT",
 | 
			
		||||
            "GPL-3.0-or-later" => { with: "Autoconf-exception-3.0" },
 | 
			
		||||
          ]
 | 
			
		||||
          revision 1
 | 
			
		||||
        end
 | 
			
		||||
      RUBY
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
							
								
								
									
										122
									
								
								Library/Homebrew/utils/ast.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										122
									
								
								Library/Homebrew/utils/ast.rb
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,122 @@
 | 
			
		||||
# typed: true
 | 
			
		||||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
module Utils
 | 
			
		||||
  # Helper functions for editing Ruby files.
 | 
			
		||||
  #
 | 
			
		||||
  # @api private
 | 
			
		||||
  module AST
 | 
			
		||||
    class << self
 | 
			
		||||
      extend T::Sig
 | 
			
		||||
 | 
			
		||||
      def replace_formula_stanza!(formula_contents, name:, replacement:, type: nil)
 | 
			
		||||
        processed_source, body_node = process_formula(formula_contents)
 | 
			
		||||
        children = body_node.begin_type? ? body_node.children.compact : [body_node]
 | 
			
		||||
        stanza_node = children.find { |child| call_node_match?(child, name: name, type: type) }
 | 
			
		||||
        raise "Could not find #{name} stanza!" if stanza_node.nil?
 | 
			
		||||
 | 
			
		||||
        tree_rewriter = Parser::Source::TreeRewriter.new(processed_source.buffer)
 | 
			
		||||
        tree_rewriter.replace(stanza_node.source_range, replacement.strip)
 | 
			
		||||
        formula_contents.replace(tree_rewriter.process)
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      def add_formula_stanza!(formula_contents, name:, text:, type: nil)
 | 
			
		||||
        processed_source, body_node = process_formula(formula_contents)
 | 
			
		||||
 | 
			
		||||
        preceding_component = if body_node.begin_type?
 | 
			
		||||
          body_node.children.compact.reduce do |previous_child, current_child|
 | 
			
		||||
            if formula_component_before_target?(current_child,
 | 
			
		||||
                                                target_name: name,
 | 
			
		||||
                                                target_type: type)
 | 
			
		||||
              next current_child
 | 
			
		||||
            else
 | 
			
		||||
              break previous_child
 | 
			
		||||
            end
 | 
			
		||||
          end
 | 
			
		||||
        else
 | 
			
		||||
          body_node
 | 
			
		||||
        end
 | 
			
		||||
        preceding_component = preceding_component.last_argument if preceding_component.send_type?
 | 
			
		||||
 | 
			
		||||
        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
 | 
			
		||||
 | 
			
		||||
        tree_rewriter = Parser::Source::TreeRewriter.new(processed_source.buffer)
 | 
			
		||||
        tree_rewriter.insert_after(preceding_expr, "\n#{text.match?(/\A\s+/) ? text : text.indent(2)}")
 | 
			
		||||
        formula_contents.replace(tree_rewriter.process)
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      private
 | 
			
		||||
 | 
			
		||||
      def process_formula(formula_contents)
 | 
			
		||||
        Homebrew.install_bundler_gems!
 | 
			
		||||
        require "rubocop-ast"
 | 
			
		||||
 | 
			
		||||
        ruby_version = Version.new(HOMEBREW_REQUIRED_RUBY_VERSION).major_minor.to_f
 | 
			
		||||
        processed_source = RuboCop::AST::ProcessedSource.new(formula_contents, ruby_version)
 | 
			
		||||
        root_node = processed_source.ast
 | 
			
		||||
 | 
			
		||||
        class_node = if root_node.class_type?
 | 
			
		||||
          root_node
 | 
			
		||||
        elsif root_node.begin_type?
 | 
			
		||||
          root_node.children.find { |n| n.class_type? && n.parent_class&.const_name == "Formula" }
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
        raise "Could not find formula class!" if class_node.nil?
 | 
			
		||||
 | 
			
		||||
        body_node = class_node.body
 | 
			
		||||
        raise "Formula class is empty!" if body_node.nil?
 | 
			
		||||
 | 
			
		||||
        [processed_source, body_node]
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      def formula_component_before_target?(node, target_name:, target_type: nil)
 | 
			
		||||
        require "rubocops/components_order"
 | 
			
		||||
 | 
			
		||||
        RuboCop::Cop::FormulaAudit::ComponentsOrder::COMPONENT_PRECEDENCE_LIST.each do |components|
 | 
			
		||||
          return false if components.any? do |component|
 | 
			
		||||
            component_match?(component_name: component[:name],
 | 
			
		||||
                             component_type: component[:type],
 | 
			
		||||
                             target_name:    target_name,
 | 
			
		||||
                             target_type:    target_type)
 | 
			
		||||
          end
 | 
			
		||||
          return true if components.any? do |component|
 | 
			
		||||
            call_node_match?(node, name: component[:name], type: component[:type])
 | 
			
		||||
          end
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
        false
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      def component_match?(component_name:, component_type:, target_name:, target_type: nil)
 | 
			
		||||
        component_name == target_name && (target_type.nil? || component_type == target_type)
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      def call_node_match?(node, name:, type: nil)
 | 
			
		||||
        node_type = if node.send_type?
 | 
			
		||||
          :method_call
 | 
			
		||||
        elsif node.block_type?
 | 
			
		||||
          :block_call
 | 
			
		||||
        end
 | 
			
		||||
        return false if node_type.nil?
 | 
			
		||||
 | 
			
		||||
        component_match?(component_name: T.unsafe(node).method_name,
 | 
			
		||||
                         component_type: node_type,
 | 
			
		||||
                         target_name:    name,
 | 
			
		||||
                         target_type:    type)
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
@ -2,6 +2,7 @@
 | 
			
		||||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
require "tab"
 | 
			
		||||
require "utils/ast"
 | 
			
		||||
 | 
			
		||||
module Utils
 | 
			
		||||
  # Helper functions for bottles.
 | 
			
		||||
@ -76,75 +77,10 @@ module Utils
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      def add_bottle_stanza!(formula_contents, bottle_output)
 | 
			
		||||
        Homebrew.install_bundler_gems!
 | 
			
		||||
        require "rubocop-ast"
 | 
			
		||||
 | 
			
		||||
        ruby_version = Version.new(HOMEBREW_REQUIRED_RUBY_VERSION).major_minor.to_f
 | 
			
		||||
        processed_source = RuboCop::AST::ProcessedSource.new(formula_contents, ruby_version)
 | 
			
		||||
        root_node = processed_source.ast
 | 
			
		||||
 | 
			
		||||
        class_node = if root_node.class_type?
 | 
			
		||||
          root_node
 | 
			
		||||
        elsif root_node.begin_type?
 | 
			
		||||
          root_node.children.find do |n|
 | 
			
		||||
            n.class_type? && n.parent_class&.const_name == "Formula"
 | 
			
		||||
          end
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
        odie "Could not find formula class!" if class_node.nil?
 | 
			
		||||
 | 
			
		||||
        body_node = class_node.body
 | 
			
		||||
        odie "Formula class is empty!" if body_node.nil?
 | 
			
		||||
 | 
			
		||||
        node_before_bottle = if body_node.begin_type?
 | 
			
		||||
          body_node.children.compact.reduce do |previous_child, current_child|
 | 
			
		||||
            break previous_child unless component_before_bottle_block? current_child
 | 
			
		||||
 | 
			
		||||
            current_child
 | 
			
		||||
          end
 | 
			
		||||
        else
 | 
			
		||||
          body_node
 | 
			
		||||
        end
 | 
			
		||||
        node_before_bottle = node_before_bottle.last_argument if node_before_bottle.send_type?
 | 
			
		||||
 | 
			
		||||
        expr_before_bottle = node_before_bottle.location.expression
 | 
			
		||||
        processed_source.comments.each do |comment|
 | 
			
		||||
          comment_expr = comment.location.expression
 | 
			
		||||
          distance = comment_expr.first_line - expr_before_bottle.first_line
 | 
			
		||||
          case distance
 | 
			
		||||
          when 0
 | 
			
		||||
            if comment_expr.last_line > expr_before_bottle.last_line ||
 | 
			
		||||
               comment_expr.end_pos > expr_before_bottle.end_pos
 | 
			
		||||
              expr_before_bottle = comment_expr
 | 
			
		||||
            end
 | 
			
		||||
          when 1
 | 
			
		||||
            expr_before_bottle = comment_expr
 | 
			
		||||
          end
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
        tree_rewriter = Parser::Source::TreeRewriter.new(processed_source.buffer)
 | 
			
		||||
        tree_rewriter.insert_after(expr_before_bottle, "\n\n#{bottle_output.chomp}")
 | 
			
		||||
        formula_contents.replace(tree_rewriter.process)
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      private
 | 
			
		||||
 | 
			
		||||
      def component_before_bottle_block?(node)
 | 
			
		||||
        require "rubocops/components_order"
 | 
			
		||||
 | 
			
		||||
        RuboCop::Cop::FormulaAudit::ComponentsOrder::COMPONENT_PRECEDENCE_LIST.each do |components|
 | 
			
		||||
          components.each do |component|
 | 
			
		||||
            return false if component[:name] == :bottle && component[:type] == :block_call
 | 
			
		||||
 | 
			
		||||
            case component[:type]
 | 
			
		||||
            when :method_call
 | 
			
		||||
              return true if node.send_type? && node.method_name == component[:name]
 | 
			
		||||
            when :block_call
 | 
			
		||||
              return true if node.block_type? && node.method_name == component[:name]
 | 
			
		||||
            end
 | 
			
		||||
          end
 | 
			
		||||
        end
 | 
			
		||||
        false
 | 
			
		||||
        Utils::AST.add_formula_stanza!(formula_contents,
 | 
			
		||||
                                       name: :bottle,
 | 
			
		||||
                                       type: :block_call,
 | 
			
		||||
                                       text: "\n#{bottle_output.chomp}")
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user