From aaf7bc2bc5533f2ab177374193ad260f97b79985 Mon Sep 17 00:00:00 2001 From: Seeker Date: Fri, 18 Dec 2020 17:24:10 -0800 Subject: [PATCH 1/2] utils: add AST helper functions for editing formulae --- Library/Homebrew/test/utils/ast_spec.rb | 50 ++++++++++ Library/Homebrew/utils/ast.rb | 122 ++++++++++++++++++++++++ Library/Homebrew/utils/bottles.rb | 74 +------------- 3 files changed, 177 insertions(+), 69 deletions(-) create mode 100644 Library/Homebrew/test/utils/ast_spec.rb create mode 100644 Library/Homebrew/utils/ast.rb diff --git a/Library/Homebrew/test/utils/ast_spec.rb b/Library/Homebrew/test/utils/ast_spec.rb new file mode 100644 index 0000000000..cdf05a7032 --- /dev/null +++ b/Library/Homebrew/test/utils/ast_spec.rb @@ -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 diff --git a/Library/Homebrew/utils/ast.rb b/Library/Homebrew/utils/ast.rb new file mode 100644 index 0000000000..fcc718c7ec --- /dev/null +++ b/Library/Homebrew/utils/ast.rb @@ -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 diff --git a/Library/Homebrew/utils/bottles.rb b/Library/Homebrew/utils/bottles.rb index ea5377f418..a825f46fd8 100644 --- a/Library/Homebrew/utils/bottles.rb +++ b/Library/Homebrew/utils/bottles.rb @@ -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 From 4520c05957fff176b2364ef6092a01924b6c94cf Mon Sep 17 00:00:00 2001 From: Seeker Date: Fri, 18 Dec 2020 17:24:40 -0800 Subject: [PATCH 2/2] bump-revision: use Utils::AST --- Library/Homebrew/dev-cmd/bump-revision.rb | 62 ++++++----------------- 1 file changed, 16 insertions(+), 46 deletions(-) diff --git a/Library/Homebrew/dev-cmd/bump-revision.rb b/Library/Homebrew/dev-cmd/bump-revision.rb index 18d2a94379..fac6c1461b 100644 --- a/Library/Homebrew/dev-cmd/bump-revision.rb +++ b/Library/Homebrew/dev-cmd/bump-revision.rb @@ -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