utils/ast: add FormulaAST class

This commit is contained in:
Seeker 2021-01-10 09:14:16 -08:00
parent d75e9c99b3
commit 2ebfb4221c
5 changed files with 123 additions and 113 deletions

View File

@ -493,22 +493,22 @@ module Homebrew
require "utils/ast"
path = Pathname.new((HOMEBREW_REPOSITORY/bottle_hash["formula"]["path"]).to_s)
checksums = old_checksums(path, bottle_hash, args: args)
formula = Formulary.factory(path)
formula_ast = Utils::AST::FormulaAST.new(path.read)
checksums = old_checksums(formula, formula_ast, bottle_hash, args: args)
update_or_add = checksums.nil? ? "add" : "update"
checksums&.each(&bottle.method(:sha256))
output = bottle_output(bottle)
puts output
Utils::Inreplace.inreplace(path) do |s|
formula_contents = s.inreplace_string
case update_or_add
when "update"
Utils::AST.replace_bottle_stanza!(formula_contents, output)
when "add"
Utils::AST.add_bottle_stanza!(formula_contents, output)
end
case update_or_add
when "update"
formula_ast.replace_bottle_block(output)
when "add"
formula_ast.add_bottle_block(output)
end
path.atomic_write(formula_ast.process)
unless args.no_commit?
Utils::Git.set_name_email!
@ -566,16 +566,16 @@ module Homebrew
[mismatches, checksums]
end
def old_checksums(formula_path, bottle_hash, args:)
bottle_node = Utils::AST.bottle_block(formula_path.read)
def old_checksums(formula, formula_ast, bottle_hash, args:)
bottle_node = formula_ast.bottle_block
if bottle_node.nil?
odie "--keep-old was passed but there was no existing bottle block!" if args.keep_old?
return
end
return [] unless args.keep_old?
old_keys = Utils::AST.body_children(bottle_node.body).map(&:method_name)
old_bottle_spec = Formulary.factory(formula_path).bottle_specification
old_keys = Utils::AST::FormulaAST.body_children(bottle_node.body).map(&:method_name)
old_bottle_spec = formula.bottle_specification
mismatches, checksums = merge_bottle_spec(old_keys, old_bottle_spec, bottle_hash["bottle"])
if mismatches.present?
odie <<~EOS

View File

@ -52,14 +52,13 @@ module Homebrew
Homebrew.install_bundler_gems!
require "utils/ast"
Utils::Inreplace.inreplace(formula.path) do |s|
s = s.inreplace_string
if current_revision.zero?
Utils::AST.add_formula_stanza!(s, :revision, new_revision)
else
Utils::AST.replace_formula_stanza!(s, :revision, new_revision)
end
formula_ast = Utils::AST::FormulaAST.new(formula.path.read)
if current_revision.zero?
formula_ast.add_stanza(:revision, new_revision)
else
formula_ast.replace_stanza(:revision, new_revision)
end
formula.path.atomic_write(formula_ast.process)
end
message = "#{formula.name}: revision bump #{args.message}"

View File

@ -3,9 +3,9 @@
require "utils/ast"
describe Utils::AST do
let(:initial_formula) do
<<~RUBY
describe Utils::AST::FormulaAST do
subject(:formula_ast) do
described_class.new <<~RUBY
class Foo < Formula
url "https://brew.sh/foo-1.0.tar.gz"
license all_of: [
@ -17,11 +17,10 @@ describe Utils::AST do
RUBY
end
describe ".replace_formula_stanza!" do
describe "#replace_stanza" do
it "replaces the specified stanza in a formula" do
contents = initial_formula.dup
described_class.replace_formula_stanza!(contents, :license, :public_domain)
expect(contents).to eq <<~RUBY
formula_ast.replace_stanza(:license, :public_domain)
expect(formula_ast.process).to eq <<~RUBY
class Foo < Formula
url "https://brew.sh/foo-1.0.tar.gz"
license :public_domain
@ -30,11 +29,10 @@ describe Utils::AST do
end
end
describe ".add_formula_stanza!" do
describe "#add_stanza" do
it "adds the specified stanza to a formula" do
contents = initial_formula.dup
described_class.add_formula_stanza!(contents, :revision, 1)
expect(contents).to eq <<~RUBY
formula_ast.add_stanza(:revision, 1)
expect(formula_ast.process).to eq <<~RUBY
class Foo < Formula
url "https://brew.sh/foo-1.0.tar.gz"
license all_of: [
@ -92,7 +90,7 @@ describe Utils::AST do
end
end
describe ".add_bottle_stanza!" do
describe "#add_bottle_block" do
let(:bottle_output) do
<<~RUBY.chomp.indent(2)
bottle do
@ -102,8 +100,8 @@ describe Utils::AST do
end
context "when `license` is a string" do
let(:formula_contents) do
<<~RUBY.chomp
subject(:formula_ast) do
described_class.new <<~RUBY.chomp
class Foo < Formula
url "https://brew.sh/foo-1.0.tar.gz"
license "MIT"
@ -125,14 +123,14 @@ describe Utils::AST do
end
it "adds `bottle` after `license`" do
described_class.add_bottle_stanza!(formula_contents, bottle_output)
expect(formula_contents).to eq(new_contents)
formula_ast.add_bottle_block(bottle_output)
expect(formula_ast.process).to eq(new_contents)
end
end
context "when `license` is a symbol" do
let(:formula_contents) do
<<~RUBY.chomp
subject(:formula_ast) do
described_class.new <<~RUBY.chomp
class Foo < Formula
url "https://brew.sh/foo-1.0.tar.gz"
license :cannot_represent
@ -154,14 +152,14 @@ describe Utils::AST do
end
it "adds `bottle` after `license`" do
described_class.add_bottle_stanza!(formula_contents, bottle_output)
expect(formula_contents).to eq(new_contents)
formula_ast.add_bottle_block(bottle_output)
expect(formula_ast.process).to eq(new_contents)
end
end
context "when `license` is multiline" do
let(:formula_contents) do
<<~RUBY.chomp
subject(:formula_ast) do
described_class.new <<~RUBY.chomp
class Foo < Formula
url "https://brew.sh/foo-1.0.tar.gz"
license all_of: [
@ -191,14 +189,14 @@ describe Utils::AST do
end
it "adds `bottle` after `license`" do
described_class.add_bottle_stanza!(formula_contents, bottle_output)
expect(formula_contents).to eq(new_contents)
formula_ast.add_bottle_block(bottle_output)
expect(formula_ast.process).to eq(new_contents)
end
end
context "when `head` is a string" do
let(:formula_contents) do
<<~RUBY.chomp
subject(:formula_ast) do
described_class.new <<~RUBY.chomp
class Foo < Formula
url "https://brew.sh/foo-1.0.tar.gz"
head "https://brew.sh/foo.git"
@ -220,14 +218,14 @@ describe Utils::AST do
end
it "adds `bottle` after `head`" do
described_class.add_bottle_stanza!(formula_contents, bottle_output)
expect(formula_contents).to eq(new_contents)
formula_ast.add_bottle_block(bottle_output)
expect(formula_ast.process).to eq(new_contents)
end
end
context "when `head` is a block" do
let(:formula_contents) do
<<~RUBY.chomp
subject(:formula_ast) do
described_class.new <<~RUBY.chomp
class Foo < Formula
url "https://brew.sh/foo-1.0.tar.gz"
@ -255,14 +253,14 @@ describe Utils::AST do
end
it "adds `bottle` before `head`" do
described_class.add_bottle_stanza!(formula_contents, bottle_output)
expect(formula_contents).to eq(new_contents)
formula_ast.add_bottle_block(bottle_output)
expect(formula_ast.process).to eq(new_contents)
end
end
context "when there is a comment on the same line" do
let(:formula_contents) do
<<~RUBY.chomp
subject(:formula_ast) do
described_class.new <<~RUBY.chomp
class Foo < Formula
url "https://brew.sh/foo-1.0.tar.gz" # comment
end
@ -282,14 +280,14 @@ describe Utils::AST do
end
it "adds `bottle` after the comment" do
described_class.add_bottle_stanza!(formula_contents, bottle_output)
expect(formula_contents).to eq(new_contents)
formula_ast.add_bottle_block(bottle_output)
expect(formula_ast.process).to eq(new_contents)
end
end
context "when the next line is a comment" do
let(:formula_contents) do
<<~RUBY.chomp
subject(:formula_ast) do
described_class.new <<~RUBY.chomp
class Foo < Formula
url "https://brew.sh/foo-1.0.tar.gz"
# comment
@ -311,14 +309,14 @@ describe Utils::AST do
end
it "adds `bottle` after the comment" do
described_class.add_bottle_stanza!(formula_contents, bottle_output)
expect(formula_contents).to eq(new_contents)
formula_ast.add_bottle_block(bottle_output)
expect(formula_ast.process).to eq(new_contents)
end
end
context "when the next line is blank and the one after it is a comment" do
let(:formula_contents) do
<<~RUBY.chomp
subject(:formula_ast) do
described_class.new <<~RUBY.chomp
class Foo < Formula
url "https://brew.sh/foo-1.0.tar.gz"
@ -342,8 +340,8 @@ describe Utils::AST do
end
it "adds `bottle` before the comment" do
described_class.add_bottle_stanza!(formula_contents, bottle_output)
expect(formula_contents).to eq(new_contents)
formula_ast.add_bottle_block(bottle_output)
expect(formula_ast.process).to eq(new_contents)
end
end
end

View File

@ -13,12 +13,28 @@ module Utils
SendNode = RuboCop::AST::SendNode
BlockNode = RuboCop::AST::BlockNode
ProcessedSource = RuboCop::AST::ProcessedSource
TreeRewriter = Parser::Source::TreeRewriter
class << self
# Helper class for editing formulae.
#
# @api private
class FormulaAST
extend T::Sig
extend Forwardable
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
sig { params(body_node: Node).returns(T::Array[Node]) }
def body_children(body_node)
def self.body_children(body_node)
if body_node.nil?
[]
elsif body_node.begin_type?
@ -28,56 +44,36 @@ module Utils
end
end
sig { params(formula_contents: String).returns(T.nilable(Node)) }
def bottle_block(formula_contents)
formula_stanza(formula_contents, :bottle, type: :block_call)
sig { returns(T.nilable(Node)) }
def bottle_block
stanza(:bottle, type: :block_call)
end
sig { params(formula_contents: String, name: Symbol, type: T.nilable(Symbol)).returns(T.nilable(Node)) }
def formula_stanza(formula_contents, name, type: nil)
_, children = process_formula(formula_contents)
sig { params(name: Symbol, type: T.nilable(Symbol)).returns(T.nilable(Node)) }
def stanza(name, type: nil)
children.find { |child| call_node_match?(child, name: name, type: type) }
end
sig { params(formula_contents: String, bottle_output: String).void }
def replace_bottle_stanza!(formula_contents, bottle_output)
replace_formula_stanza!(formula_contents, :bottle, bottle_output.chomp, type: :block_call)
sig { params(bottle_output: String).void }
def replace_bottle_block(bottle_output)
replace_stanza(:bottle, bottle_output.chomp, type: :block_call)
end
sig { params(formula_contents: String, bottle_output: String).void }
def add_bottle_stanza!(formula_contents, bottle_output)
add_formula_stanza!(formula_contents, :bottle, "\n#{bottle_output.chomp}", type: :block_call)
sig { params(bottle_output: String).void }
def add_bottle_block(bottle_output)
add_stanza(:bottle, "\n#{bottle_output.chomp}", type: :block_call)
end
sig do
params(
formula_contents: String,
name: Symbol,
replacement: T.any(Numeric, String, Symbol),
type: T.nilable(Symbol),
).void
end
def replace_formula_stanza!(formula_contents, name, replacement, type: nil)
processed_source, children = process_formula(formula_contents)
sig { params(name: Symbol, replacement: T.any(Numeric, String, Symbol), type: T.nilable(Symbol)).void }
def replace_stanza(name, replacement, type: nil)
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, stanza_text(name, replacement, indent: 2).lstrip)
formula_contents.replace(tree_rewriter.process)
tree_rewriter.replace(stanza_node.source_range, self.class.stanza_text(name, replacement, indent: 2).lstrip)
end
sig do
params(
formula_contents: String,
name: Symbol,
value: T.any(Numeric, String, Symbol),
type: T.nilable(Symbol),
).void
end
def add_formula_stanza!(formula_contents, name, value, type: nil)
processed_source, children = process_formula(formula_contents)
sig { params(name: Symbol, value: T.any(Numeric, String, Symbol), type: T.nilable(Symbol)).void }
def add_stanza(name, value, type: nil)
preceding_component = if children.length > 1
children.reduce do |previous_child, current_child|
if formula_component_before_target?(current_child,
@ -108,13 +104,11 @@ module Utils
end
end
tree_rewriter = Parser::Source::TreeRewriter.new(processed_source.buffer)
tree_rewriter.insert_after(preceding_expr, "\n#{stanza_text(name, value, indent: 2)}")
formula_contents.replace(tree_rewriter.process)
tree_rewriter.insert_after(preceding_expr, "\n#{self.class.stanza_text(name, value, indent: 2)}")
end
sig { params(name: Symbol, value: T.any(Numeric, String, Symbol), indent: T.nilable(Integer)).returns(String) }
def stanza_text(name, value, indent: nil)
def self.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
@ -124,19 +118,31 @@ module Utils
text
end
private
sig { params(source: String).returns([ProcessedSource, Node]) }
def process_source(source)
def self.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
sig { params(formula_contents: String).returns([ProcessedSource, T::Array[Node]]) }
def process_formula(formula_contents)
processed_source, root_node = process_source(formula_contents)
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
processed_source, root_node = self.class.process_source(formula_contents)
class_node = if root_node.class_type?
root_node
@ -146,7 +152,7 @@ module Utils
raise "Could not find formula class!" if class_node.nil?
children = body_children(class_node.body)
children = self.class.body_children(class_node.body)
raise "Formula class is empty!" if children.empty?
[processed_source, children]

View File

@ -0,0 +1,7 @@
# typed: strict
module Utils
module AST
include ::Kernel
end
end