From 1f334f1cb86448036f23b32444f29bc7e4c7b5cb Mon Sep 17 00:00:00 2001 From: Markus Reiter Date: Thu, 27 Jun 2024 17:26:54 +0200 Subject: [PATCH] Add audit for Ruby files in taps. --- Library/Homebrew/tap_auditor.rb | 84 ++++++++++++++--------- Library/Homebrew/test/tap_auditor_spec.rb | 79 +++++++++++++++++++++ 2 files changed, 131 insertions(+), 32 deletions(-) create mode 100644 Library/Homebrew/test/tap_auditor_spec.rb diff --git a/Library/Homebrew/tap_auditor.rb b/Library/Homebrew/tap_auditor.rb index 33a577101969b..37218b5b8edad 100644 --- a/Library/Homebrew/tap_auditor.rb +++ b/Library/Homebrew/tap_auditor.rb @@ -4,59 +4,67 @@ module Homebrew # Auditor for checking common violations in {Tap}s. class TapAuditor - attr_reader :name, :path, :formula_names, :formula_aliases, :formula_renames, :cask_tokens, - :tap_audit_exceptions, :tap_style_exceptions, :tap_pypi_formula_mappings, :problems + sig { returns(Tap) } + attr_reader :tap + + attr_reader :problems sig { params(tap: Tap, strict: T.nilable(T::Boolean)).void } - def initialize(tap, strict:) - Homebrew.with_no_api_env do - @name = tap.name - @path = tap.path - @tap_audit_exceptions = tap.audit_exceptions - @tap_style_exceptions = tap.style_exceptions - @tap_pypi_formula_mappings = tap.pypi_formula_mappings - @problems = [] - - @cask_tokens = tap.cask_tokens.map do |cask_token| - cask_token.split("/").last - end - @formula_aliases = tap.aliases.map do |formula_alias| - formula_alias.split("/").last - end - @formula_renames = tap.formula_renames - @formula_names = tap.formula_names.map do |formula_name| - formula_name.split("/").last - end - end + def initialize(tap, strict: nil) + @tap = tap + @problems = [] end sig { void } def audit - audit_json_files - audit_tap_formula_lists - audit_aliases_renames_duplicates + Homebrew.with_no_api_env do + audit_json_files + audit_ruby_files + audit_tap_formula_lists + audit_aliases_renames_duplicates + end end sig { void } def audit_json_files - json_patterns = Tap::HOMEBREW_TAP_JSON_FILES.map { |pattern| @path/pattern } + json_patterns = Tap::HOMEBREW_TAP_JSON_FILES.map { |pattern| tap.path/pattern } Pathname.glob(json_patterns).each do |file| JSON.parse file.read rescue JSON::ParserError - problem "#{file.to_s.delete_prefix("#{@path}/")} contains invalid JSON" + problem "#{file.to_s.delete_prefix("#{tap.path}/")} contains invalid JSON" end end + sig { void } + def audit_ruby_files + stray_ruby_files = tap.path.glob("**/*.rb", File::FNM_DOTMATCH) + + stray_ruby_files -= tap.command_files + stray_ruby_files.reject! { _1.ascend.any?(tap.command_dir) } + + # Allow mixed formula/cask taps except for core taps. + stray_ruby_files -= tap.cask_files unless tap.core_tap? + stray_ruby_files -= tap.formula_files unless tap.core_cask_tap? + + return if stray_ruby_files.none? + + problem "Ruby files in wrong location:\n#{stray_ruby_files.map(&:to_s).join("\n")}" + end + sig { void } def audit_tap_formula_lists - check_formula_list_directory "audit_exceptions", @tap_audit_exceptions - check_formula_list_directory "style_exceptions", @tap_style_exceptions - check_formula_list "pypi_formula_mappings", @tap_pypi_formula_mappings + check_formula_list_directory "audit_exceptions", tap.audit_exceptions + check_formula_list_directory "style_exceptions", tap.style_exceptions + check_formula_list "pypi_formula_mappings", tap.pypi_formula_mappings end sig { void } def audit_aliases_renames_duplicates - duplicates = formula_aliases & formula_renames.keys + formula_aliases = tap.aliases.map do |formula_alias| + formula_alias.split("/").last + end + + duplicates = formula_aliases & tap.formula_renames.keys return if duplicates.none? problem "The following should either be an alias or a rename, not both: #{duplicates.to_sentence}" @@ -79,6 +87,18 @@ def check_formula_list(list_file, list) return end + cask_tokens = tap.cask_tokens.map do |cask_token| + cask_token.split("/").last + end + + formula_aliases = tap.aliases.map do |formula_alias| + formula_alias.split("/").last + end + + formula_names = tap.formula_names.map do |formula_name| + formula_name.split("/").last + end + list = list.keys if list.is_a? Hash invalid_formulae_casks = list.select do |formula_or_cask_name| formula_names.exclude?(formula_or_cask_name) && @@ -90,7 +110,7 @@ def check_formula_list(list_file, list) problem <<~EOS #{list_file}.json references - formulae or casks that are not found in the #{@name} tap. + formulae or casks that are not found in the #{tap.name} tap. Invalid formulae or casks: #{invalid_formulae_casks.join(", ")} EOS end diff --git a/Library/Homebrew/test/tap_auditor_spec.rb b/Library/Homebrew/test/tap_auditor_spec.rb new file mode 100644 index 0000000000000..9d97467054bdc --- /dev/null +++ b/Library/Homebrew/test/tap_auditor_spec.rb @@ -0,0 +1,79 @@ +# frozen_string_literal: true + +require "tap_auditor" + +RSpec.describe Homebrew::TapAuditor do + subject(:tap_auditor) { described_class.new(tap) } + + let(:tap) { Tap.fetch("user", "repo") } + let(:ruby_files) { [] } + + before do + tap.path.mkpath + + ruby_files.each do |ruby_file| + ruby_file.dirname.mkpath + FileUtils.touch ruby_file + end + end + + after do + tap.path.dirname.rmtree + end + + context "when Ruby files are in a wrong location" do + let(:ruby_files) do + [ + tap.path/"wrong"/"file.rb", + ] + end + + it "fails" do + tap_auditor.audit + expect(tap_auditor.problems.first[:message]).to match "Ruby files in wrong location" + end + end + + context "when formula files are in homebrew/cask" do + let(:tap) { CoreCaskTap.instance } + let(:ruby_files) do + [ + tap.path/"Formula"/"formula.rb", + ] + end + + it "fails" do + tap_auditor.audit + expect(tap_auditor.problems.first[:message]).to match "Ruby files in wrong location" + end + end + + context "when cask files are in homebrew/core" do + let(:tap) { CoreTap.instance } + let(:ruby_files) do + [ + tap.path/"Casks"/"cask.rb", + ] + end + + it "fails" do + tap_auditor.audit + expect(tap_auditor.problems.first[:message]).to match "Ruby files in wrong location" + end + end + + context "when Ruby files are in correct locations" do + let(:ruby_files) do + [ + tap.path/"cmd"/"cmd.rb", + tap.path/"Formula"/"formula.rb", + tap.path/"Casks"/"cask.rb", + ] + end + + it "passes" do + tap_auditor.audit + expect(tap_auditor.problems).to be_empty + end + end +end