# typed: strict
# frozen_string_literal: true

require "deprecate_disable"
require "formula_versions"
require "formula_name_cask_token_auditor"
require "livecheck/livecheck"
require "resource_auditor"
require "utils"
require "utils/shared_audits"
require "utils/output"
require "utils/git"
require "style"
require "tap_auditor"

module Homebrew
  # Auditor for checking common violations in {Formula}e.
  class FormulaAuditor
    include FormulaCellarChecks
    include Utils::Curl
    include Utils::Output::Mixin

    sig { override.returns(Formula) }
    attr_reader :formula

    sig { returns(String) }
    attr_reader :text

    sig { returns(T::Array[T.any(String, T::Hash[Symbol, T.untyped])]) }
    attr_reader :problems

    sig { returns(T::Array[T.any(String, T::Hash[Symbol, T.untyped])]) }
    attr_reader :new_formula_problems

    sig {
      params(
        formula:             Formula,
        new_formula:         T.nilable(T::Boolean),
        strict:              T.nilable(T::Boolean),
        online:              T.nilable(T::Boolean),
        git:                 T.nilable(T::Boolean),
        display_cop_names:   T.nilable(T::Boolean),
        only:                T.nilable(T::Array[String]),
        except:              T.nilable(T::Array[String]),
        style_offenses:      T.nilable(T::Array[Style::Offense]),
        core_tap:            T.nilable(T::Boolean),
        tap_audit:           T.nilable(T::Boolean),
        spdx_license_data:   T.nilable(T::Hash[String, T.untyped]),
        spdx_exception_data: T.nilable(T::Hash[String, T.untyped]),
      ).void
    }
    def initialize(formula, new_formula: nil, strict: nil, online: nil, git: nil, display_cop_names: nil, only: nil,
                   except: nil, style_offenses: nil, core_tap: nil, tap_audit: nil, spdx_license_data: nil,
                   spdx_exception_data: nil)
      @formula = formula
      @versioned_formula = T.let(formula.versioned_formula?, T::Boolean)
      @new_formula_inclusive = new_formula
      @new_formula = new_formula && !@versioned_formula
      @strict = strict
      @online = online
      @git = git
      @display_cop_names = display_cop_names
      @only = only
      @except = except
      # Accept precomputed style offense results, for efficiency
      @style_offenses = style_offenses
      # Allow the formula tap to be set as homebrew/core, for testing purposes
      @core_tap = T.let(formula.tap&.core_tap? || core_tap || false, T::Boolean)
      @problems = T.let([], T::Array[T.any(String, T::Hash[Symbol, T.untyped])])
      @new_formula_problems = T.let([], T::Array[T.any(String, T::Hash[Symbol, T.untyped])])
      @text = T.let(formula.path.open("rb", &:read), String)
      @specs = T.let(%w[stable head].filter_map { |s| formula.public_send(s) }, T::Array[SoftwareSpec])
      @spdx_license_data = spdx_license_data
      @spdx_exception_data = spdx_exception_data
      @tap_audit = tap_audit
      @committed_version_info_cache = T.let({}, T::Hash[String, T.untyped])
    end

    sig { void }
    def audit_style
      return unless @style_offenses

      @style_offenses.each do |offense|
        cop_name = "#{offense.cop_name}: " if @display_cop_names
        message = "#{cop_name}#{offense.message}"

        problem message, location: offense.location, corrected: offense.corrected?
      end
    end

    sig { void }
    def audit_file
      if formula.core_formula? && @versioned_formula
        unversioned_name = formula.name.gsub(/@.*$/, "")

        # ignore when an unversioned formula doesn't exist after an explicit rename
        return if formula.tap!.formula_renames.key?(unversioned_name)

        # build this ourselves as we want e.g. homebrew/core to be present
        full_name = "#{formula.tap!}/#{unversioned_name}"

        unversioned_formula = begin
          Formulary.factory(full_name).path
        rescue FormulaUnavailableError, TapFormulaAmbiguityError
          Pathname.new formula.path.to_s.gsub(/@.*\.rb$/, ".rb")
        end
        unless unversioned_formula.exist?
          unversioned_name = unversioned_formula.basename(".rb")
          problem "#{formula} is versioned but no #{unversioned_name} formula exists"
        end
      elsif formula.stable? &&
            !@versioned_formula &&
            (versioned_formulae = formula.versioned_formulae - [formula]) &&
            versioned_formulae.present?
        versioned_aliases, unversioned_aliases = formula.aliases.partition { |a| /.@\d/.match?(a) }
        last_alias_version = versioned_formulae.map(&:name).fetch(-1).split("@").fetch(-1)

        alias_name_major = "#{formula.name}@#{formula.version.major}"
        alias_name_major_minor = "#{formula.name}@#{formula.version.major_minor}"
        alias_name = if last_alias_version.split(".").length == 1
          alias_name_major
        else
          alias_name_major_minor
        end
        valid_main_alias_names = [alias_name_major, alias_name_major_minor].uniq

        # Also accept versioned aliases with names of other aliases, but do not require them.
        valid_other_alias_names = unversioned_aliases.flat_map do |name|
          %W[
            #{name}@#{formula.version.major}
            #{name}@#{formula.version.major_minor}
          ].uniq
        end

        unless @core_tap
          [versioned_aliases, valid_main_alias_names, valid_other_alias_names].each do |array|
            array.map! { |a| "#{formula.tap}/#{a}" }
          end
        end

        valid_versioned_aliases = versioned_aliases & valid_main_alias_names
        invalid_versioned_aliases = versioned_aliases - valid_main_alias_names - valid_other_alias_names

        latest_versioned_formula = versioned_formulae.map(&:name).first

        if valid_versioned_aliases.empty? && alias_name != latest_versioned_formula
          if formula.tap
            problem <<~EOS
              Formula has other versions so create a versioned alias:
                cd #{formula.tap!.alias_dir}
                ln -s #{formula.path.to_s.gsub(formula.tap!.path.to_s, "..")} #{alias_name}
            EOS
          else
            problem "Formula has other versions so create an alias named '#{alias_name}'."
          end
        end

        if invalid_versioned_aliases.present?
          problem <<~EOS
            Formula has invalid versioned aliases:
              #{invalid_versioned_aliases.join("\n  ")}
          EOS
        end
      end

      return if !formula.core_formula? || formula.path == formula.tap!.new_formula_path(formula.name)

      problem <<~EOS
        Formula is in wrong path:
          Expected: #{formula.tap!.new_formula_path(formula.name)}
            Actual: #{formula.path}
      EOS
    end

    sig { returns(T::Array[String]) }
    def self.aliases
      # core aliases + tap alias names + tap alias full name
      @aliases ||= T.let(Formula.aliases + Formula.tap_aliases, T.nilable(T::Array[String]))
    end

    sig { void }
    def audit_synced_versions_formulae
      return unless formula.synced_with_other_formulae?

      name = formula.name
      version = formula.version

      formula.tap!.synced_versions_formulae.each do |synced_version_formulae|
        next unless synced_version_formulae.include?(name)

        synced_version_formulae.each do |synced_formula|
          next if synced_formula == name

          if (synced_version = Formulary.factory(synced_formula).version) != version
            problem "Version of #{synced_formula} (#{synced_version}) should match version of #{name} (#{version})"
          end
        end

        break
      end
    end

    sig { void }
    def audit_name
      name = formula.name

      name_auditor = Homebrew::FormulaNameCaskTokenAuditor.new(name)
      if (errors = name_auditor.errors).any?
        problem "Formula name '#{name}' must not contain #{errors.to_sentence(two_words_connector: " or ",
                                                                              last_word_connector: " or ")}."
      end

      return unless @core_tap
      return unless @strict

      problem "'#{name}' is not allowed in homebrew/core." if MissingFormula.disallowed_reason(name)

      if Formula.aliases.include? name
        problem "Formula name conflicts with existing aliases in homebrew/core."
        return
      end

      if (oldname = CoreTap.instance.formula_renames[name])
        problem "'#{name}' is reserved as the old name of #{oldname} in homebrew/core."
        return
      end

      cask_tokens = CoreCaskTap.instance.cask_tokens.presence
      cask_tokens ||= Homebrew::API.cask_tokens

      if cask_tokens.include?(name)
        problem "Formula name conflicts with an existing Homebrew/cask cask's token."
        return
      end

      return if formula.core_formula?
      return unless Formula.core_names.include?(name)

      problem "Formula name conflicts with an existing formula in homebrew/core."
    end

    PERMITTED_LICENSE_MISMATCHES = T.let({
      "AGPL-3.0" => ["AGPL-3.0-only", "AGPL-3.0-or-later"],
      "GPL-2.0"  => ["GPL-2.0-only",  "GPL-2.0-or-later"],
      "GPL-3.0"  => ["GPL-3.0-only",  "GPL-3.0-or-later"],
      "LGPL-2.1" => ["LGPL-2.1-only", "LGPL-2.1-or-later"],
      "LGPL-3.0" => ["LGPL-3.0-only", "LGPL-3.0-or-later"],
    }.freeze, T::Hash[String, T::Array[String]])

    # The following licenses are non-free/open based on multiple sources (e.g. Debian, Fedora, FSF, OSI, ...)
    INCOMPATIBLE_LICENSES = [
      "Aladdin",    # https://www.gnu.org/licenses/license-list.html#Aladdin
      "CPOL-1.02",  # https://www.gnu.org/licenses/license-list.html#cpol
      "gSOAP-1.3b", # https://salsa.debian.org/ellert/gsoap/-/blob/HEAD/debian/copyright
      "JSON",       # https://wiki.debian.org/DFSGLicenses#JSON_evil_license
      "MS-LPL",     # https://github.com/spdx/license-list-XML/issues/1432#issuecomment-1077680709
      "OPL-1.0",    # https://wiki.debian.org/DFSGLicenses#Open_Publication_License_.28OPL.29_v1.0
    ].freeze
    INCOMPATIBLE_LICENSE_PREFIXES = [
      "BUSL",     # https://spdx.org/licenses/BUSL-1.1.html#notes
      "CC-BY-NC", # https://people.debian.org/~bap/dfsg-faq.html#no_commercial
      "Elastic",  # https://www.elastic.co/licensing/elastic-license#Limitations
      "SSPL",     # https://fedoraproject.org/wiki/Licensing/SSPL#License_Notes
    ].freeze

    sig { void }
    def audit_license
      if formula.license.present?
        licenses, exceptions = SPDX.parse_license_expression formula.license

        incompatible_licenses = licenses.select do |license|
          license.to_s.start_with?(*INCOMPATIBLE_LICENSE_PREFIXES) || INCOMPATIBLE_LICENSES.include?(license.to_s)
        end
        if incompatible_licenses.present? && @core_tap
          problem <<~EOS
            Formula #{formula.name} contains incompatible licenses: #{incompatible_licenses}.
            Formulae in homebrew/core must either use a Debian Free Software Guidelines license
            or be released into the public domain: #{Formatter.url("https://docs.brew.sh/Licence-Guidelines")}
          EOS
        end

        non_standard_licenses = licenses.reject { |license| SPDX.valid_license? license }
        if non_standard_licenses.present?
          problem <<~EOS
            Formula #{formula.name} contains non-standard SPDX licenses: #{non_standard_licenses}.
            For a list of valid licenses check: #{Formatter.url("https://spdx.org/licenses/")}
          EOS
        end

        if @strict || @core_tap
          deprecated_licenses = licenses.select do |license|
            SPDX.deprecated_license? license
          end
          if deprecated_licenses.present?
            problem <<~EOS
              Formula #{formula.name} contains deprecated SPDX licenses: #{deprecated_licenses}.
              You may need to add `-only` or `-or-later` for GNU licenses (e.g. `GPL`, `LGPL`, `AGPL`, `GFDL`).
              For a list of valid licenses check: #{Formatter.url("https://spdx.org/licenses/")}
            EOS
          end
        end

        invalid_exceptions = exceptions.reject { |exception| SPDX.valid_license_exception? exception }
        if invalid_exceptions.present?
          problem <<~EOS
            Formula #{formula.name} contains invalid or deprecated SPDX license exceptions: #{invalid_exceptions}.
            For a list of valid license exceptions check:
              #{Formatter.url("https://spdx.org/licenses/exceptions-index.html")}
          EOS
        end

        return unless @online

        user, repo = get_repo_data(%r{https?://github\.com/([^/]+)/([^/]+)/?.*})
        return if user.blank? || repo.blank?

        stable = formula.stable
        raise "Stable is nil for formula #{formula.name}" if stable.nil?

        url = stable.url
        raise "Stable URL is nil for formula #{formula.name}" if url.nil?

        tag = SharedAudits.github_tag_from_url(url)
        tag ||= stable.specs[:tag]
        github_license = GitHub.get_repo_license(user, repo, ref: tag)
        return unless github_license
        return if (licenses + ["NOASSERTION"]).include?(github_license)
        return if PERMITTED_LICENSE_MISMATCHES[github_license]&.intersect?(licenses)
        return if formula.tap&.audit_exception :permitted_formula_license_mismatches, formula.name

        problem "Formula license #{licenses} does not match GitHub license #{Array(github_license)}."

      elsif @core_tap && !formula.disabled?
        problem "Formulae in homebrew/core must specify a license."
      end
    end

    sig { void }
    def audit_deps
      @specs.each do |spec|
        # Check for things we don't like to depend on.
        # We allow non-Homebrew installs whenever possible.
        spec.declared_deps.each do |dep|
          begin
            dep_f = dep.to_formula
          rescue TapFormulaUnavailableError
            # Don't complain about missing cross-tap dependencies
            next
          rescue FormulaUnavailableError
            problem "Can't find dependency '#{dep.name}'."
            next
          rescue TapFormulaAmbiguityError
            problem "Ambiguous dependency '#{dep.name}'."
            next
          end

          if dep_f.oldnames.include?(Utils.name_from_full_name(dep.name))
            problem "Dependency '#{dep.name}' was renamed; use new name '#{dep_f.name}'."
          end

          if @core_tap &&
             @new_formula &&
             !dep.uses_from_macos? &&
             dep_f.keg_only? &&
             dep_f.keg_only_reason.provided_by_macos? &&
             dep_f.keg_only_reason.applicable? &&
             formula.requirements.none?(LinuxRequirement) &&
             !formula.tap&.audit_exception(:provided_by_macos_depends_on_allowlist, dep.name)
            new_formula_problem(
              "Dependency '#{dep.name}' is provided by macOS; " \
              "please replace 'depends_on' with 'uses_from_macos'.",
            )
          end

          dep.options.each do |opt|
            next if @core_tap
            next if dep_f.option_defined?(opt)
            next if dep_f.requirements.find do |r|
              if r.recommended?
                opt.name == "with-#{r.name}"
              elsif r.optional?
                opt.name == "without-#{r.name}"
              end
            end

            problem "Dependency '#{dep}' does not define option: #{opt.name.inspect}"
          end

          problem "Don't use 'git' as a dependency (it's always available)" if @new_formula && dep.name == "git"

          dep.tags.each do |tag|
            if [:run, :linked].include?(tag)
              problem "Dependency '#{dep.name}' is marked as :#{tag}. Remove :#{tag}; it is a no-op."
            elsif tag.is_a?(Symbol) && Dependable::RESERVED_TAGS.exclude?(tag)
              problem "Dependency '#{dep.name}' is marked as :#{tag} which is not a valid tag."
            end
          end

          next unless @core_tap

          if dep_f.tap.nil?
            problem <<~EOS
              Dependency '#{dep.name}' does not exist in any tap.
            EOS
          elsif !dep_f.tap!.core_tap?
            problem <<~EOS
              Dependency '#{dep.name}' is not in homebrew/core. Formulae in homebrew/core
              should not have dependencies in external taps.
            EOS
          end

          if dep_f.deprecated? && !formula.deprecated? && !formula.disabled?
            problem <<~EOS
              Dependency '#{dep.name}' is deprecated but has un-deprecated dependents. Either
              un-deprecate '#{dep.name}' or deprecate it and all of its dependents.
            EOS
          end

          if dep_f.disabled? && !formula.disabled?
            problem <<~EOS
              Dependency '#{dep.name}' is disabled but has un-disabled dependents. Either
              un-disable '#{dep.name}' or disable it and all of its dependents.
            EOS
          end

          # we can only verify the OS requirement of the currently running platform
          os_req = if Homebrew::SimulateSystem.simulating_or_running_on_linux? &&
                      !dep_f.supports_linux? && !spec.depends_on_macos_set_top_level?
            "macOS"
          elsif Homebrew::SimulateSystem.simulating_or_running_on_macos? &&
                !dep_f.supports_macos? && !spec.depends_on_linux_set_top_level?
            "Linux"
          end
          problem <<~EOS if os_req
            Dependency '#{dep.name}' has a #{os_req} requirement. Either move the
            dependency inside an `on_#{os_req.downcase}` block or add `depends_on :#{os_req.downcase}`.
          EOS

          # we want to allow uses_from_macos for aliases but not bare dependencies.
          # we also allow `pkg-config` for backwards compatibility in external taps.
          if self.class.aliases.include?(dep.name) && !dep.uses_from_macos? && (dep.name != "pkg-config" || @core_tap)
            problem "Dependency '#{dep.name}' is an alias; use the canonical name '#{dep.to_formula.full_name}'."
          end

          if dep.tags.include?(:recommended) || dep.tags.include?(:optional)
            problem "Formulae in homebrew/core should not have optional or recommended dependencies"
          end
        end

        next unless @core_tap

        if spec.requirements.map(&:recommended?).any? || spec.requirements.map(&:optional?).any?
          problem "Formulae in homebrew/core should not have optional or recommended requirements"
        end
      end

      return unless @core_tap
      return if formula.tap&.audit_exception :versioned_dependencies_conflicts_allowlist, formula.name

      # The number of conflicts on Linux is absurd.
      # TODO: remove this and check these there too.
      return if Homebrew::SimulateSystem.simulating_or_running_on_linux?

      # Skip the versioned dependencies conflict audit for *-staging branches.
      # This will allow us to migrate dependents of formulae like Python or OpenSSL
      # gradually over separate PRs which target a *-staging branch. See:
      #   https://github.com/Homebrew/homebrew-core/pull/134260
      ignore_formula_conflict, staging_formula =
        if @tap_audit && (github_event_path = ENV.fetch("GITHUB_EVENT_PATH", nil)).present?
          event_payload = JSON.parse(File.read(github_event_path))
          base_info = event_payload.dig("pull_request", "base").to_h # handle `nil`

          # We need to read the head ref from `GITHUB_EVENT_PATH` because
          # `git branch --show-current` returns the default branch on PR branches.
          staging_branch = base_info["ref"]&.end_with?("-staging")
          homebrew_owned_repo = base_info.dig("repo", "owner", "login") == "Homebrew"
          homebrew_core_pr = base_info.dig("repo", "name") == "homebrew-core"
          # Support staging branches named `formula-staging` or `formula@version-staging`.
          base_formula = base_info["ref"]&.split(/-|@/, 2)&.first

          [staging_branch && homebrew_owned_repo && homebrew_core_pr, base_formula]
        end

      recursive_runtime_formulae = formula.runtime_formula_dependencies(undeclared: false)
      version_hash = {}
      version_conflicts = Set.new
      recursive_runtime_formulae.each do |f|
        name = f.name
        unversioned_name = name.split("@").fetch(0)
        next if ignore_formula_conflict && unversioned_name == staging_formula
        # Allow use of the full versioned name (e.g. `python@3.99`) or an unversioned alias (`python`).
        next if formula.tap&.audit_exception :versioned_formula_dependent_conflicts_allowlist, name
        next if formula.tap&.audit_exception :versioned_formula_dependent_conflicts_allowlist, unversioned_name

        version_hash[unversioned_name] ||= Set.new
        version_hash[unversioned_name] << name
        next if version_hash[unversioned_name].length < 2

        version_conflicts += version_hash[unversioned_name]
      end

      return if version_conflicts.empty?

      return if formula.disabled?

      return if formula.deprecated? &&
                formula.deprecation_reason != DeprecateDisable::FORMULA_DEPRECATE_DISABLE_REASONS[:versioned_formula]

      problem <<~EOS
        #{formula.full_name} contains conflicting version recursive dependencies:
          #{version_conflicts.to_a.join ", "}
        View these with `brew deps --tree #{formula.full_name}`.
      EOS
    end

    sig { void }
    def audit_node_modules
      return unless @core_tap

      node_modules = formula.libexec/"lib/node_modules"
      return unless node_modules.directory?

      incompatible_license_packages = %w[
        @anthropic-ai/claude-agent-sdk
        @github/copilot
      ]

      incompatible_license_packages.each do |package|
        # Search for package in all nested node_modules. Also including dot match for .pnpm hoisted packages
        next if node_modules.glob("{**/node_modules/,}#{package}/", File::FNM_DOTMATCH).empty?

        problem <<~EOS
          Formula #{formula.name} uses #{package} which has an incompatible license.
          All installed npm dependencies must satisfy #{Formatter.url("https://docs.brew.sh/Licence-Guidelines")}
        EOS
      end
    end

    sig { void }
    def audit_conflicts
      tap = formula.tap
      formula.conflicts.each do |conflict|
        conflicting_formula = Formulary.factory(conflict.name)
        next if tap != conflicting_formula.tap

        problem "Formula should not conflict with itself" if formula == conflicting_formula

        if T.must(tap).formula_renames.key?(conflict.name) || T.must(tap).aliases.include?(conflict.name)
          problem "Formula conflict should be declared using " \
                  "canonical name (#{conflicting_formula.name}) instead of '#{conflict.name}'"
        end

        reverse_conflict_found = T.let(false, T::Boolean)
        conflicting_formula.conflicts.each do |reverse_conflict|
          reverse_conflict_formula = Formulary.factory(reverse_conflict.name)
          if T.must(tap).formula_renames.key?(reverse_conflict.name) ||
             T.must(tap).aliases.include?(reverse_conflict.name)
            problem "Formula #{conflicting_formula.name} conflict should be declared using " \
                    "canonical name (#{reverse_conflict_formula.name}) instead of '#{reverse_conflict.name}'"
          end

          reverse_conflict_found ||= reverse_conflict_formula == formula
        end
        unless reverse_conflict_found
          problem "Formula #{conflicting_formula.name} should also have a conflict declared with #{formula.name}"
        end
      rescue TapFormulaUnavailableError
        # Don't complain about missing cross-tap conflicts.
        next
      rescue FormulaUnavailableError
        problem "Can't find conflicting formula #{conflict.name.inspect}."
      rescue TapFormulaAmbiguityError
        problem "Ambiguous conflicting formula #{conflict.name.inspect}."
      end
    end

    sig { void }
    def audit_gcc_dependency
      return unless @core_tap
      return unless Homebrew::SimulateSystem.simulating_or_running_on_linux?
      return unless linux_only_gcc_dep?(formula)
      # https://github.com/Homebrew/homebrew-core/pull/171634
      # https://github.com/nghttp2/nghttp2/issues/2194
      return if formula.tap&.audit_exception(:linux_only_gcc_dependency_allowlist, formula.name)

      problem "Formulae in homebrew/core should not have a Linux-only dependency on GCC."
    end

    sig { void }
    def audit_glibc
      return unless @core_tap
      return if formula.name != "glibc"
      # Also allow LINUX_GLIBC_NEXT_CI_VERSION for when we're upgrading.
      return if [OS::LINUX_GLIBC_CI_VERSION, OS::LINUX_GLIBC_NEXT_CI_VERSION].include?(formula.version.to_s)

      problem "The glibc version must be #{OS::LINUX_GLIBC_CI_VERSION}, as needed by our CI on Linux. " \
              "The glibc formula is for users who have a system glibc with a lower version, " \
              "which allows them to use our Linux bottles, which were compiled against system glibc on CI."
    end

    sig { void }
    def audit_relicensed_formulae
      return unless @core_tap

      relicensed_version = formula.tap&.audit_exception :relicensed_formulae_versions, formula.name
      return unless relicensed_version.is_a?(String)
      return if formula.version < Version.new(relicensed_version)

      problem "#{formula.name} was relicensed to a non-open-source license from version #{relicensed_version}. " \
              "It must not be upgraded to version #{relicensed_version} or newer."
    end

    sig { void }
    def audit_versioned_keg_only
      return unless @versioned_formula
      return unless @core_tap

      if formula.keg_only?
        return if formula.keg_only_reason.versioned_formula?
        return if formula.name.start_with?("openssl", "libressl") && formula.keg_only_reason.by_macos?
      end

      return if formula.tap&.audit_exception :versioned_keg_only_allowlist, formula.name

      problem "Versioned formulae in homebrew/core should use `keg_only :versioned_formula`"
    end

    sig { void }
    def audit_homepage
      homepage = formula.homepage

      return if homepage.blank?
      return if SharedAudits.homepage_browsed_recently?(formula.homepage_browsed)

      return unless @online

      return if formula.tap&.audit_exception :cert_error_allowlist, formula.name, homepage

      return unless DevelopmentTools.curl_handles_most_https_certificates?

      # Skip gnu.org and nongnu.org audit on GitHub runners
      # See issue: https://github.com/Homebrew/homebrew-core/issues/206757
      github_runner = GitHub::Actions.env_set? && !ENV["GITHUB_ACTIONS_HOMEBREW_SELF_HOSTED"]
      return if homepage.match?(%r{^https?://www\.(?:non)?gnu\.org/.+}) && github_runner

      use_homebrew_curl = [:stable, :head].any? do |spec_name|
        next false unless (spec = formula.public_send(spec_name))

        spec.using == :homebrew_curl
      end

      if (http_content_problem = curl_check_http_content(
        homepage,
        SharedAudits::URL_TYPE_HOMEPAGE,
        user_agents:       [:browser, :default],
        check_content:     true,
        strict:            @strict || false,
        use_homebrew_curl:,
      ))
        problem http_content_problem
      end
    end

    sig { void }
    def audit_duplicate_formula
      return unless @core_tap
      return unless @new_formula

      # Using internal API here as using `Formulary.factory` is too slow
      return if !@online && !Homebrew::API::Internal.cached_packages_json_file_path.exist?

      formula_url = formula.stable&.url
      return unless formula_url

      formula_hashes = Homebrew::API::Internal.formula_hashes
      duplicate_formula_name = formula_hashes.find do |name, formula_hash|
        formula_hash["stable_url_args"].any?(formula_url) && name != formula.name
      end&.first

      return unless duplicate_formula_name

      new_formula_problem "Possible duplicate, this formula has the same stable URL as `#{duplicate_formula_name}`"
    end

    sig { void }
    def audit_bottle_spec
      # special case: new versioned formulae should be audited
      return unless @new_formula_inclusive
      return unless @core_tap

      return unless formula.bottle_defined?

      new_formula_problem "New formulae in homebrew/core should not have a `bottle do` block"
    end

    sig { void }
    def audit_eol
      return unless @online
      return unless @core_tap

      return if formula.deprecated? || formula.disabled?

      name = if formula.versioned_formula?
        formula.name.split("@").fetch(0)
      else
        formula.name
      end

      return if formula.tap&.audit_exception :eol_date_blocklist, name

      metadata = SharedAudits.eol_data(name, formula.version.major.to_s)
      metadata ||= SharedAudits.eol_data(name, formula.version.major_minor.to_s)

      return if metadata.blank? || (metadata.dig("result", "isEol") != true)

      eol_from = metadata.dig("result", "eolFrom")
      eol_date = Date.parse(eol_from) if eol_from.present?

      message = "Product is EOL"
      message += " since #{eol_date}" if eol_date.present?
      message += ", see #{Formatter.url("https://endoflife.date/#{name}")}"

      problem message
    end

    sig { void }
    def audit_wayback_url
      return unless @core_tap
      return if formula.deprecated? || formula.disabled?

      regex = %r{^https?://web\.archive\.org}
      problem_prefix = "Formula with a Internet Archive Wayback Machine"

      if formula.stable && regex.match?(T.must(formula.stable).url)
        problem "#{problem_prefix} `url` should be deprecated with `:repo_removed`"
      end

      if regex.match?(formula.homepage)
        problem "#{problem_prefix} `homepage` should find an alternative `homepage` or be deprecated."
      end

      return unless formula.head

      return unless regex.match?(T.must(formula.head).url)

      problem "Remove Internet Archive Wayback Machine `head` URL"
    end

    sig { void }
    def audit_github_repository_archived
      return if formula.deprecated? || formula.disabled?

      user, repo = get_repo_data(%r{https?://github\.com/([^/]+)/([^/]+)/?.*}) if @online
      return if user.blank? || repo.nil?

      metadata = SharedAudits.github_repo_data(user, repo)
      return if metadata.nil?

      problem "GitHub repository is archived" if metadata["archived"]
    end

    sig { void }
    def audit_gitlab_repository_archived
      return if formula.deprecated? || formula.disabled?

      user, repo = get_repo_data(%r{https?://gitlab\.com/([^/]+)/([^/]+)/?.*}) if @online
      return if user.blank? || repo.nil?

      metadata = SharedAudits.gitlab_repo_data(user, repo)
      return if metadata.nil?

      problem "GitLab repository is archived" if metadata["archived"]
    end

    sig { void }
    def audit_forgejo_repository_archived
      return if formula.deprecated? || formula.disabled?

      user, repo = get_repo_data(%r{https?://codeberg\.org/([^/]+)/([^/]+)/?.*}) if @online
      return if user.blank? || repo.nil?

      metadata = SharedAudits.forgejo_repo_data(user, repo)
      return if metadata.nil?

      problem "Forgejo repository is archived since #{metadata["archived_at"]}" if metadata["archived"]
    end

    sig { void }
    def audit_github_repository
      user, repo = get_repo_data(%r{https?://github\.com/([^/]+)/([^/]+)/?.*}) if @new_formula

      return if user.blank? || repo.nil?

      self_submission = self_submission?(user)
      warning = SharedAudits.github(user, repo, self_submission:)
      return if warning.nil?

      new_formula_problem warning
    end

    sig { void }
    def audit_gitlab_repository
      user, repo = get_repo_data(%r{https?://gitlab\.com/([^/]+)/([^/]+)/?.*}) if @new_formula
      return if user.blank? || repo.nil?

      self_submission = self_submission?(user)
      warning = SharedAudits.gitlab(user, repo, self_submission:)
      return if warning.nil?

      new_formula_problem warning
    end

    sig { void }
    def audit_bitbucket_repository
      user, repo = get_repo_data(%r{https?://bitbucket\.org/([^/]+)/([^/]+)/?.*}) if @new_formula
      return if user.blank? || repo.nil?

      self_submission = self_submission?(user)
      warning = SharedAudits.bitbucket(user, repo, self_submission:)
      return if warning.nil?

      new_formula_problem warning
    end

    sig { void }
    def audit_forgejo_repository
      user, repo = get_repo_data(%r{https?://codeberg\.org/([^/]+)/([^/]+)/?.*}) if @new_formula
      return if user.blank? || repo.nil?

      self_submission = self_submission?(user)
      warning = SharedAudits.forgejo(user, repo, self_submission:)
      return if warning.nil?

      new_formula_problem warning
    end

    sig { params(regex: Regexp).returns(T.nilable([String, String])) }
    def get_repo_data(regex)
      return unless @core_tap
      return unless @online

      _, user, repo = *regex.match(T.must(formula.stable).url) if formula.stable
      _, user, repo = *regex.match(formula.homepage) unless user
      _, user, repo = *regex.match(T.must(formula.head).url) if !user && formula.head
      return if !user || !repo

      repo.delete_suffix!(".git")

      [user, repo]
    end

    sig { void }
    def audit_specs
      problem "HEAD-only (no stable download)" if head_only?(formula) && @core_tap

      %w[Stable HEAD].each do |name|
        spec_name = name.downcase.to_sym
        next unless (spec = formula.public_send(spec_name))

        except = @except.to_a
        if spec_name == :head &&
           formula.tap&.audit_exception(:head_non_default_branch_allowlist, formula.name, spec.specs[:branch])
          except << "head_branch"
        end

        ra = ResourceAuditor.new(
          spec, spec_name,
          online: @online, strict: @strict, only: @only, core_tap: @core_tap, except:,
          use_homebrew_curl: spec.using == :homebrew_curl
        ).audit
        ra.problems.each do |message|
          problem "#{name}: #{message}"
        end

        spec.resources.each_value do |resource|
          problem "Resource name should be different from the formula name" if resource.name == formula.name

          ra = ResourceAuditor.new(
            resource, spec_name,
            online: @online, strict: @strict, only: @only, except: @except,
            use_homebrew_curl: resource.using == :homebrew_curl
          ).audit
          ra.problems.each do |message|
            problem "#{name} resource #{resource.name.inspect}: #{message}"
          end
        end

        next if spec.patches.empty?
        next if !@new_formula || !@core_tap

        new_formula_problem(
          "Formulae should not require patches to build. " \
          "Patches should be submitted and accepted upstream first.",
        )
      end

      return unless @core_tap

      if formula.head && @versioned_formula &&
         !formula.tap&.audit_exception(:versioned_head_spec_allowlist, formula.name)
        problem "Versioned formulae should not have a `head` spec"
      end

      stable = formula.stable
      return unless stable
      return unless (url = stable.url)

      version = stable.version
      problem "Stable: version (#{version}) is set to a string without a digit" unless /\d/.match?(version.to_s)

      stable_version_string = version.to_s
      if stable_version_string.start_with?("HEAD")
        problem "Stable: non-HEAD version (#{stable_version_string}) should not begin with `HEAD`"
      end

      stable_url_version = Version.parse(url)
      stable_url_minor_version = stable_url_version.minor.to_i

      throttle_rate = formula.livecheck.throttle
      throttle_days = formula.livecheck.throttle_days
      if throttle_days.nil? &&
         !throttle_rate.nil? &&
         !Livecheck.throttle_allows_bump?(formula, stable.version, throttle_rate:, throttle_days:)
        throttle_items = []
        throttle_items << "#{throttle_rate} releases on multiples of #{throttle_rate}" if throttle_rate

        problem "Should only be updated every #{throttle_items.join(" or ")}"
      end

      case url
      when /[\d._-](alpha|beta|rc\d)/
        matched = Regexp.last_match(1)
        version_prefix = stable_version_string.sub(/\d+$/, "")
        return if formula.tap&.audit_exception :unstable_allowlist, formula.name, version_prefix
        return if formula.tap&.audit_exception :unstable_devel_allowlist, formula.name, version_prefix

        problem "Stable: version URLs should not contain `#{matched}`"
      when %r{download\.gnome\.org/sources}, %r{ftp\.gnome\.org/pub/GNOME/sources}i
        version_prefix = stable.version.major_minor
        return if formula.tap&.audit_exception :gnome_devel_allowlist, formula.name, version_prefix
        return if formula.tap&.audit_exception :gnome_devel_allowlist, formula.name, "all"
        return if stable_url_version < Version.new("1.0")
        # All minor versions are stable in the new GNOME version scheme (which starts at version 40.0)
        # https://discourse.gnome.org/t/new-gnome-versioning-scheme/4235
        return if stable_url_version >= Version.new("40.0")
        return if stable_url_minor_version.even?

        problem "Stable: version (#{stable.version}) is a development release"
      when %r{isc.org/isc/bind\d*/}i
        return if stable_url_minor_version.even?

        problem "Stable: version (#{stable.version}) is a development release"

      when %r{https?://gitlab\.com/([\w-]+)/([\w-]+)}
        owner = T.must(Regexp.last_match(1))
        repo = T.must(Regexp.last_match(2))

        tag = SharedAudits.gitlab_tag_from_url(url)
        tag ||= stable.specs[:tag]
        tag ||= stable.version.to_s

        if @online
          error = SharedAudits.gitlab_release(owner, repo, tag, formula:)
          problem error if error
        end
      when %r{^https://github.com/([\w-]+)/([\w-]+)}
        owner = T.must(Regexp.last_match(1))
        repo = T.must(Regexp.last_match(2))
        tag = SharedAudits.github_tag_from_url(url)
        tag ||= formula.stable&.specs&.[](:tag)

        if @online && !tag.nil?
          error = SharedAudits.github_release(owner, repo, tag, formula:)
          problem error if error
        end
      when %r{^https://codeberg\.org/([\w-]+)/([\w-]+)}
        owner = T.must(Regexp.last_match(1))
        repo = T.must(Regexp.last_match(2))
        tag = SharedAudits.forgejo_tag_from_url(url)
        tag ||= formula.stable&.specs&.[](:tag)

        if @online && !tag.nil?
          error = SharedAudits.forgejo_release(owner, repo, tag, formula:)
          problem error if error
        end
      end
    end

    sig { void }
    def audit_stable_version
      return unless @git
      return unless formula.tap # skip formula not from core or any taps
      return unless formula.tap!.git? # git log is required
      return if formula.stable.blank?

      current_version = T.must(formula.stable).version
      current_version_scheme = formula.version_scheme

      previous_version_info, base_ref_version_info = committed_version_info
      return unless (base_ref_version = base_ref_version_info[:version])

      if current_version == base_ref_version && current_version.to_s != base_ref_version.to_s
        problem "Stable: version should not change from #{base_ref_version} to #{current_version}"
      elsif current_version < base_ref_version && current_version_scheme == previous_version_info[:version_scheme]
        problem "Stable: version should not decrease (from #{base_ref_version} to #{current_version})"
      end
    end

    sig { void }
    def audit_revision
      new_formula_problem("New formulae should not define a revision.") if @new_formula && !formula.revision.zero?

      return unless @git

      tap = formula.tap
      return if tap.nil?
      return unless tap.git?
      return if formula.stable.blank?

      current_version = T.must(formula.stable).version
      current_revision = formula.revision

      previous_version_info, base_ref_version_info = committed_version_info

      previous_version = previous_version_info[:version]
      previous_revision = previous_version_info[:revision]
      base_ref_version = base_ref_version_info[:version]
      base_ref_revision = base_ref_version_info[:revision]

      if (previous_version != base_ref_version || current_version != base_ref_version) &&
         !current_revision.zero? && current_revision == base_ref_revision && current_revision == previous_revision
        problem "`revision #{current_revision}` should be removed"
      elsif current_version == previous_version && previous_revision && current_revision < previous_revision
        problem "`revision` should not decrease (from #{previous_revision} to #{current_revision})"
      elsif base_ref_revision && current_revision > (base_ref_revision + 1)
        problem "`revision` should only increment by 1"
      end

      revision_increment = current_revision - previous_revision.to_i
      return if revision_increment != 1

      dependency_names = formula.recursive_dependencies.map(&:name)
      return if dependency_names.empty?

      changed_dependency_paths = changed_formulae_paths(tap, only_names: dependency_names)
      return if changed_dependency_paths.empty?

      missing_compatibility_bumps = changed_dependency_paths.filter_map do |path|
        changed_formula = Formulary.factory(path)
        # Each changed dependency that updates its version must raise its compatibility_version by exactly one.
        _, base_ref_dependency_version_info = committed_version_info(formula: changed_formula)
        previous_dependency_version = base_ref_dependency_version_info[:version]
        current_dependency_version = changed_formula.stable&.version
        if previous_dependency_version.present? && current_dependency_version.present? &&
           current_dependency_version == previous_dependency_version
          next
        end

        previous_compatibility_version = base_ref_dependency_version_info[:compatibility_version] || 0
        current_compatibility_version = changed_formula.compatibility_version || 0
        next if current_compatibility_version == previous_compatibility_version + 1

        expected_compatibility_version = previous_compatibility_version + 1
        "#{changed_formula.name} (#{previous_compatibility_version} to #{expected_compatibility_version})"
      end
      return if missing_compatibility_bumps.empty? || !formula.core_formula?

      problem "`revision` increased but changed recursive dependencies must increase `compatibility_version` by 1 " \
              "in the same PR: #{missing_compatibility_bumps.join(", ")}. " \
              "See #{Formatter.url("https://docs.brew.sh/Formula-Cookbook#compatibility_version")}."
    end

    sig { void }
    def audit_compatibility_version
      return unless @git

      tap = formula.tap
      return if tap.nil?
      return unless tap.git?

      _, base_ref_version_info = committed_version_info
      return if base_ref_version_info.empty?

      previous_compatibility_version = base_ref_version_info[:compatibility_version] || 0
      current_compatibility_version = formula.compatibility_version || previous_compatibility_version

      if current_compatibility_version < previous_compatibility_version
        problem "`compatibility_version` should not decrease " \
                "from #{previous_compatibility_version} to #{current_compatibility_version}"
        return
      elsif current_compatibility_version > (previous_compatibility_version + 1)
        problem "`compatibility_version` should only increment by 1"
        return
      end

      compatibility_increment = current_compatibility_version - previous_compatibility_version
      return if compatibility_increment.zero?
      return unless formula.valid_platform?

      dependent_revision_bumps = changed_formulae_paths(tap).filter_map do |path|
        changed_formula = Formulary.factory(path)
        next if changed_formula.name == formula.name

        dependencies = changed_formula.recursive_dependencies.map(&:name)
        # Only formulae that depend (recursively) on the audited formula can justify the bump.
        next unless dependencies.include?(formula.name)

        _, base_ref_dependent_version_info = committed_version_info(formula: changed_formula)
        previous_revision = base_ref_dependent_version_info[:revision] || 0
        current_revision = changed_formula.revision
        next if current_revision != previous_revision + 1

        changed_formula.name
      end
      return if dependent_revision_bumps.any?

      problem "`compatibility_version` increased from #{previous_compatibility_version} to " \
              "#{current_compatibility_version} but no recursive dependent formulae increased " \
              "`revision` by 1 in this PR. Only bump `compatibility_version` when at least one recursive " \
              "dependent needs a `revision` bump. " \
              "See #{Formatter.url("https://docs.brew.sh/Formula-Cookbook#compatibility_version")}."
    end

    sig { void }
    def audit_version_scheme
      return unless @git
      return unless formula.tap # skip formula not from core or any taps
      return unless formula.tap!.git? # git log is required
      return if formula.stable.blank?

      current_version_scheme = formula.version_scheme

      _, base_ref_version_info = committed_version_info
      previous_version_scheme = base_ref_version_info[:version_scheme]
      return if previous_version_scheme.nil?

      if current_version_scheme < previous_version_scheme
        problem "`version_scheme` should not decrease (from #{previous_version_scheme} to #{current_version_scheme})"
      elsif current_version_scheme > (previous_version_scheme + 1)
        problem "`version_scheme` should only increment by 1"
      end
    end

    sig { void }
    def audit_unconfirmed_checksum_change
      return unless @git
      return unless formula.tap # skip formula not from core or any taps
      return unless formula.tap!.git? # git log is required
      return unless (current_stable = formula.stable)

      current_version = current_stable.version
      current_checksum = current_stable.checksum
      current_url = current_stable.url

      _, base_ref_version_info = committed_version_info
      base_ref_checksum = base_ref_version_info[:checksum]

      if current_version == base_ref_version_info[:version] &&
         current_url == base_ref_version_info[:url] &&
         current_checksum.present? && base_ref_checksum.present? &&
         current_checksum != base_ref_checksum
        problem(
          "stable sha256 changed without the url/version also changing; " \
          "please create an issue upstream to rule out malicious " \
          "circumstances and to find out why the file changed.",
        )
      end
    end

    sig { void }
    def audit_text
      bin_names = Set.new
      bin_names << formula.name
      bin_names += formula.aliases
      [formula.bin, formula.sbin].each do |dir|
        next unless dir.exist?

        bin_names += dir.children.map { |child| child.basename.to_s }
      end
      shell_commands = ["system", "shell_output", "pipe_output"]
      bin_names.each do |name|
        shell_commands.each do |cmd|
          if text.to_s.match?(/test do.*#{cmd}[(\s]+['"]#{Regexp.escape(name)}[\s'"]/m)
            problem %Q(Fully scope test `#{cmd}` calls, e.g.: #{cmd} "\#{bin}/#{name}")
          end
        end
      end
    end

    sig { void }
    def audit_reverse_migration
      # Only enforce for new formula being re-added to core
      return unless @strict
      return unless @core_tap
      return unless formula.tap!.tap_migrations.key?(formula.name)

      problem <<~EOS
        #{formula.name} seems to be listed in tap_migrations.json!
        Please remove #{formula.name} from present tap & tap_migrations.json
        before submitting it to Homebrew/homebrew-#{formula.tap!.repository}.
      EOS
    end

    sig { void }
    def audit_prefix_has_contents
      return unless formula.prefix.directory?
      return unless Keg.new(formula.prefix).empty_installation?

      problem <<~EOS
        The installation seems to be empty. Please ensure the prefix
        is set correctly and expected files are installed.
        The prefix configure/make argument may be case-sensitive.
      EOS
    end

    sig { void }
    def audit_deprecate_disable
      error = SharedAudits.check_deprecate_disable_reason(formula)
      problem error if error
    end

    sig { override.params(output: T.nilable(String)).void }
    def problem_if_output(output)
      problem(output) if output
    end

    sig { void }
    def audit
      only_audits = @only
      except_audits = @except

      methods.map(&:to_s).grep(/^audit_/).each do |audit_method_name|
        name = audit_method_name.delete_prefix("audit_")
        next if only_audits&.exclude?(name)
        next if except_audits&.include?(name)

        send(audit_method_name)
      end
    end

    sig { params(tap: Tap, only_names: T::Array[String]).returns(T::Array[Pathname]) }
    def changed_formulae_paths(tap, only_names: [].freeze)
      return [] unless tap.git?

      base_ref = git_audit_base_ref(tap)
      changed_paths = Utils.safe_popen_read(Utils::Git.git, "-C", tap.path, "diff", "--name-only", base_ref)
                           .lines
                           .filter_map do |line|
        relative_path = line.chomp
        next unless relative_path.end_with?(".rb")

        absolute_path = tap.path/relative_path
        next unless absolute_path.exist?
        next unless absolute_path.to_s.start_with?(tap.formula_dir.to_s)

        absolute_path
      end
      return changed_paths if only_names.blank?

      expected_paths = only_names.filter_map do |name|
        formula_name = name.to_s.delete_prefix("#{tap.name}/")
        formula_name = formula_name.delete_suffix(".rb")
        tap.formula_files_by_name[formula_name]&.expand_path
      end.map(&:to_s)

      changed_paths.select { |path| expected_paths.include?(path.expand_path.to_s) }
    end

    sig { params(formula: Formula).returns([T::Hash[Symbol, T.untyped], T::Hash[Symbol, T.untyped]]) }
    def committed_version_info(formula: @formula)
      empty_result = [{}, {}]
      return empty_result unless @git

      tap = formula.tap
      return empty_result unless tap # skip formula not from core or any taps
      return empty_result unless tap.git? # git log is required
      return empty_result if formula.stable.blank?

      if @committed_version_info_cache.key?(formula.full_name)
        return @committed_version_info_cache.fetch(formula.full_name)
      end

      previous_version_info = {}
      base_ref_version_info = {}

      current_version = formula.stable&.version
      current_revision = formula.revision

      fv = FormulaVersions.new(formula)
      fv.rev_list(git_audit_base_ref(tap)) do |revision, path|
        begin
          fv.formula_at_revision(revision, path) do |f|
            stable = f.stable
            next if stable.blank?

            previous_version_info[:version]  = stable.version
            previous_version_info[:checksum] = stable.checksum
            previous_version_info[:revision] = f.revision
            previous_version_info[:version_scheme] = f.version_scheme
            previous_version_info[:compatibility_version] = f.compatibility_version

            base_ref_version_info[:url] ||= stable.url
            base_ref_version_info[:version]  ||= previous_version_info[:version]
            base_ref_version_info[:checksum] ||= previous_version_info[:checksum]
            base_ref_version_info[:revision] ||= previous_version_info[:revision]
            base_ref_version_info[:version_scheme] ||= previous_version_info[:version_scheme]
            base_ref_version_info[:compatibility_version] ||= previous_version_info[:compatibility_version]
          end
        rescue MacOSVersion::Error, LegacyDSLError
          break
        end

        break if previous_version_info[:version]  && current_version  != previous_version_info[:version]
        break if previous_version_info[:revision] && current_revision != previous_version_info[:revision]
      end

      previous_version_info.compact!
      base_ref_version_info.compact!

      @committed_version_info_cache[formula.full_name] = [previous_version_info, base_ref_version_info]
    end

    private

    sig { params(message: String, location: T.nilable(Homebrew::SourceLocation), corrected: T::Boolean).void }
    def problem(message, location: nil, corrected: false)
      @problems << { message:, location:, corrected: }
    end

    sig { params(message: String, location: T.nilable(Homebrew::SourceLocation), corrected: T::Boolean).void }
    def new_formula_problem(message, location: nil, corrected: false)
      @new_formula_problems << { message:, location:, corrected: }
    end

    sig { params(repo_owner: String).returns(T::Boolean) }
    def self_submission?(repo_owner)
      return false if repo_owner.blank?

      SharedAudits.self_submission_for_repo_owner?(repo_owner)
    end

    sig { params(formula: Formula).returns(T::Boolean) }
    def head_only?(formula)
      !!formula.head && formula.stable.nil?
    end

    sig { params(formula: Formula).returns(T::Boolean) }
    def linux_only_gcc_dep?(formula)
      odie "`#linux_only_gcc_dep?` works only on Linux!" if Homebrew::SimulateSystem.simulating_or_running_on_macos?
      return false if formula.deps.none? { |dep| dep.name == "gcc" && !dep.implicit? }

      variations = formula.to_hash_with_variations["variations"]
      # The formula has no variations, so all OS-version-arch triples depend on GCC.
      return false if variations.blank?

      MacOSVersion::SYMBOLS.keys.product(OnSystem::ARCH_OPTIONS).each do |os, arch|
        bottle_tag = Utils::Bottles::Tag.new(system: os, arch:)
        next unless bottle_tag.valid_combination?

        variation_dependencies = variations.dig(bottle_tag.to_sym, "dependencies")
        # This variation either:
        #   1. does not exist
        #   2. has no variation-specific dependencies
        # In either case, it matches Linux. We must check for `nil` because an empty
        # array indicates that this variation does not depend on GCC.
        return false if variation_dependencies.nil?
        # We found a non-Linux variation that depends on GCC.
        return false if variation_dependencies.include?("gcc")
      end

      true
    end

    sig { params(tap: Tap).returns(String) }
    def git_audit_base_ref(tap)
      @git_audit_base_ref_cache ||= T.let({}, T.nilable(T::Hash[Pathname, T.nilable(String)]))
      @git_audit_base_ref_cache[tap.path] ||= Utils.popen_read(Utils::Git.git, "-C", tap.path, "merge-base",
                                                               "origin/HEAD", "HEAD").chomp.presence
      @git_audit_base_ref_cache[tap.path] ||= "origin/HEAD"
    end
  end
end
