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

View File

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

View File

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

View File

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

View File

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