From 2ebfb4221cac250ba31c07fadd7aadd1c9c6594b Mon Sep 17 00:00:00 2001 From: Seeker Date: Sun, 10 Jan 2021 09:14:16 -0800 Subject: [PATCH] utils/ast: add `FormulaAST` class --- Library/Homebrew/dev-cmd/bottle.rb | 26 +++--- Library/Homebrew/dev-cmd/bump-revision.rb | 13 ++- Library/Homebrew/test/utils/ast_spec.rb | 86 +++++++++--------- Library/Homebrew/utils/ast.rb | 104 ++++++++++++---------- Library/Homebrew/utils/ast.rbi | 7 ++ 5 files changed, 123 insertions(+), 113 deletions(-) create mode 100644 Library/Homebrew/utils/ast.rbi diff --git a/Library/Homebrew/dev-cmd/bottle.rb b/Library/Homebrew/dev-cmd/bottle.rb index 779911cb75..bd782f6ab1 100644 --- a/Library/Homebrew/dev-cmd/bottle.rb +++ b/Library/Homebrew/dev-cmd/bottle.rb @@ -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 diff --git a/Library/Homebrew/dev-cmd/bump-revision.rb b/Library/Homebrew/dev-cmd/bump-revision.rb index f07a5f9dfc..ba33a6494c 100644 --- a/Library/Homebrew/dev-cmd/bump-revision.rb +++ b/Library/Homebrew/dev-cmd/bump-revision.rb @@ -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}" diff --git a/Library/Homebrew/test/utils/ast_spec.rb b/Library/Homebrew/test/utils/ast_spec.rb index a49031916b..c1f4fe32c1 100644 --- a/Library/Homebrew/test/utils/ast_spec.rb +++ b/Library/Homebrew/test/utils/ast_spec.rb @@ -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 diff --git a/Library/Homebrew/utils/ast.rb b/Library/Homebrew/utils/ast.rb index 5c2bed0446..56c00cab76 100644 --- a/Library/Homebrew/utils/ast.rb +++ b/Library/Homebrew/utils/ast.rb @@ -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] diff --git a/Library/Homebrew/utils/ast.rbi b/Library/Homebrew/utils/ast.rbi new file mode 100644 index 0000000000..e062cde554 --- /dev/null +++ b/Library/Homebrew/utils/ast.rbi @@ -0,0 +1,7 @@ +# typed: strict + +module Utils + module AST + include ::Kernel + end +end