utils: add AST helper functions for editing formulae

This commit is contained in:
Seeker 2020-12-18 17:24:10 -08:00
parent c8210ac3c7
commit aaf7bc2bc5
3 changed files with 177 additions and 69 deletions

View 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

View 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

View File

@ -2,6 +2,7 @@
# frozen_string_literal: true # frozen_string_literal: true
require "tab" require "tab"
require "utils/ast"
module Utils module Utils
# Helper functions for bottles. # Helper functions for bottles.
@ -76,75 +77,10 @@ module Utils
end end
def add_bottle_stanza!(formula_contents, bottle_output) def add_bottle_stanza!(formula_contents, bottle_output)
Homebrew.install_bundler_gems! Utils::AST.add_formula_stanza!(formula_contents,
require "rubocop-ast" name: :bottle,
type: :block_call,
ruby_version = Version.new(HOMEBREW_REQUIRED_RUBY_VERSION).major_minor.to_f text: "\n#{bottle_output.chomp}")
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
end end
end end