brew/Library/Homebrew/tap_auditor.rb

112 lines
3.8 KiB
Ruby
Raw Permalink Normal View History

rubocop: Use `Sorbet/StrictSigil` as it's better than comments - Previously I thought that comments were fine to discourage people from wasting their time trying to bump things that used `undef` that Sorbet didn't support. But RuboCop is better at this since it'll complain if the comments are unnecessary. - Suggested in https://github.com/Homebrew/brew/pull/18018#issuecomment-2283369501. - I've gone for a mixture of `rubocop:disable` for the files that can't be `typed: strict` (use of undef, required before everything else, etc) and `rubocop:todo` for everything else that should be tried to make strictly typed. There's no functional difference between the two as `rubocop:todo` is `rubocop:disable` with a different name. - And I entirely disabled the cop for the docs/ directory since `typed: strict` isn't going to gain us anything for some Markdown linting config files. - This means that now it's easier to track what needs to be done rather than relying on checklists of files in our big Sorbet issue: ```shell $ git grep 'typed: true # rubocop:todo Sorbet/StrictSigil' | wc -l 268 ``` - And this is confirmed working for new files: ```shell $ git status On branch use-rubocop-for-sorbet-strict-sigils Untracked files: (use "git add <file>..." to include in what will be committed) Library/Homebrew/bad.rb Library/Homebrew/good.rb nothing added to commit but untracked files present (use "git add" to track) $ brew style Offenses: bad.rb:1:1: C: Sorbet/StrictSigil: Sorbet sigil should be at least strict got true. ^^^^^^^^^^^^^ 1340 files inspected, 1 offense detected ```
2024-08-12 10:30:59 +01:00
# typed: true # rubocop:todo Sorbet/StrictSigil
2020-11-18 10:05:23 +01:00
# frozen_string_literal: true
module Homebrew
# Auditor for checking common violations in {Tap}s.
2020-11-18 10:05:23 +01:00
class TapAuditor
attr_reader :name, :path, :formula_names, :formula_aliases, :formula_renames, :cask_tokens,
formula_auditor: create a versioned formula dependent conflict allowlist We have an audit that checks each formula's dependency tree for multiple versions of the same software. We have an allowlist that allows us to ignore this audit, but this allowlist requires each formula with a conflict in its dependency tree to be listed there. Here, I propose the reverse: if formula `foo` appears in the `versioned_formula_dependent_conflicts_allowlist`, then all its dependents will not fail the versioned dependencies conflict because of a conflict with formula `foo`. I'd like to do this in the case of `python`, where I think the versioned dependencies conflict check hurts us more than helps us. Versioned dependency conflicts are most problematic in the case of libraries with the same install name but incompatible ABIs. This is almost never a problem with Python: almost no formulae link with the Python framework on macOS (in part due to one of our audits that disallows Python framework linkage in Python modules). Moreover, the various Python frameworks that we ship have the version in the install name. The above _might_ be a problem on Linux, since we allow unrestricted linkage with `libpython`. However, we don't even check versioned conflicts on Linux, so we aren't as concerned about this in the first place. This is also a lot more convenient than adding the dependents of some Python formula one by one as they acquire conflicts due to changes in other formulae. I've also amended `tap_auditor` to allow the use of formula aliases in an allowlist, to allow us to add `python` to this allowlist instead of each individual versioned Python formula. See also discussion at Homebrew/homebrew-core#108307.
2022-08-18 15:27:23 +08:00
:tap_audit_exceptions, :tap_style_exceptions, :tap_pypi_formula_mappings, :problems
2020-11-18 10:05:23 +01:00
sig { params(tap: Tap, strict: T.nilable(T::Boolean)).void }
2020-11-18 10:05:23 +01:00
def initialize(tap, strict:)
Homebrew.with_no_api_env do
tap.clear_cache if Homebrew::EnvConfig.automatically_set_no_install_from_api?
@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
@tap_autobump = tap.autobump
@tap_official = tap.official?
@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
2020-11-18 10:05:23 +01:00
end
sig { void }
def audit
audit_json_files
audit_tap_formula_lists
audit_aliases_renames_duplicates
2020-11-18 10:05:23 +01:00
end
sig { void }
def audit_json_files
json_patterns = Tap::HOMEBREW_TAP_JSON_FILES.map { |pattern| @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"
end
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 "formula_renames", @formula_renames.values
check_formula_list ".github/autobump.txt", @tap_autobump unless @tap_official
end
2020-11-18 10:05:23 +01:00
sig { void }
def audit_aliases_renames_duplicates
duplicates = formula_aliases & formula_renames.keys
return if duplicates.none?
problem "The following should either be an alias or a rename, not both: #{duplicates.to_sentence}"
end
sig { params(message: String).void }
def problem(message)
2024-03-07 16:20:20 +00:00
@problems << ({ message:, location: nil, corrected: false })
end
2020-11-18 10:05:23 +01:00
private
2020-11-18 10:05:23 +01:00
sig { params(list_file: String, list: T.untyped).void }
def check_formula_list(list_file, list)
list_file += ".json" if File.extname(list_file).empty?
unless [Hash, Array].include? list.class
problem <<~EOS
#{list_file} should contain a JSON array
of formula names or a JSON object mapping formula names to values
EOS
return
end
2020-11-18 10:05:23 +01:00
2020-12-21 12:53:12 -05:00
list = list.keys if list.is_a? Hash
invalid_formulae_casks = list.select do |formula_or_cask_name|
formula_auditor: create a versioned formula dependent conflict allowlist We have an audit that checks each formula's dependency tree for multiple versions of the same software. We have an allowlist that allows us to ignore this audit, but this allowlist requires each formula with a conflict in its dependency tree to be listed there. Here, I propose the reverse: if formula `foo` appears in the `versioned_formula_dependent_conflicts_allowlist`, then all its dependents will not fail the versioned dependencies conflict because of a conflict with formula `foo`. I'd like to do this in the case of `python`, where I think the versioned dependencies conflict check hurts us more than helps us. Versioned dependency conflicts are most problematic in the case of libraries with the same install name but incompatible ABIs. This is almost never a problem with Python: almost no formulae link with the Python framework on macOS (in part due to one of our audits that disallows Python framework linkage in Python modules). Moreover, the various Python frameworks that we ship have the version in the install name. The above _might_ be a problem on Linux, since we allow unrestricted linkage with `libpython`. However, we don't even check versioned conflicts on Linux, so we aren't as concerned about this in the first place. This is also a lot more convenient than adding the dependents of some Python formula one by one as they acquire conflicts due to changes in other formulae. I've also amended `tap_auditor` to allow the use of formula aliases in an allowlist, to allow us to add `python` to this allowlist instead of each individual versioned Python formula. See also discussion at Homebrew/homebrew-core#108307.
2022-08-18 15:27:23 +08:00
formula_names.exclude?(formula_or_cask_name) &&
formula_aliases.exclude?(formula_or_cask_name) &&
cask_tokens.exclude?(formula_or_cask_name)
2020-11-18 10:05:23 +01:00
end
return if invalid_formulae_casks.empty?
problem <<~EOS
#{list_file} references
2020-12-21 12:53:12 -05:00
formulae or casks that are not found in the #{@name} tap.
Invalid formulae or casks: #{invalid_formulae_casks.join(", ")}
EOS
2020-11-18 10:05:23 +01:00
end
sig { params(directory_name: String, lists: Hash).void }
def check_formula_list_directory(directory_name, lists)
lists.each do |list_name, list|
check_formula_list "#{directory_name}/#{list_name}", list
end
2020-11-18 10:05:23 +01:00
end
end
end