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 "formula"
|
||||||
require "cli/parser"
|
require "cli/parser"
|
||||||
|
require "utils/ast"
|
||||||
|
|
||||||
module Homebrew
|
module Homebrew
|
||||||
extend T::Sig
|
extend T::Sig
|
||||||
@ -36,56 +37,25 @@ module Homebrew
|
|||||||
|
|
||||||
args.named.to_formulae.each do |formula|
|
args.named.to_formulae.each do |formula|
|
||||||
current_revision = formula.revision
|
current_revision = formula.revision
|
||||||
|
text = "revision #{current_revision+1}"
|
||||||
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
|
|
||||||
|
|
||||||
if args.dry_run?
|
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
|
else
|
||||||
Utils::Inreplace.inreplace(formula.path) do |s|
|
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
|
||||||
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
|
# 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
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user