mirror of
https://github.com/Homebrew/brew.git
synced 2025-07-14 16:09:03 +08:00

- 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 ```
117 lines
3.8 KiB
Ruby
117 lines
3.8 KiB
Ruby
# typed: true # rubocop:todo Sorbet/StrictSigil
|
|
# frozen_string_literal: true
|
|
|
|
require "system_command"
|
|
|
|
module Homebrew
|
|
# Representation of a macOS bundle version, commonly found in `Info.plist` files.
|
|
class BundleVersion
|
|
include Comparable
|
|
|
|
extend SystemCommand::Mixin
|
|
|
|
sig { params(info_plist_path: Pathname).returns(T.nilable(T.attached_class)) }
|
|
def self.from_info_plist(info_plist_path)
|
|
plist = system_command!("plutil", args: ["-convert", "xml1", "-o", "-", info_plist_path]).plist
|
|
from_info_plist_content(plist)
|
|
end
|
|
|
|
sig { params(plist: T::Hash[String, T.untyped]).returns(T.nilable(T.attached_class)) }
|
|
def self.from_info_plist_content(plist)
|
|
short_version = plist["CFBundleShortVersionString"].presence
|
|
version = plist["CFBundleVersion"].presence
|
|
|
|
new(short_version, version) if short_version || version
|
|
end
|
|
|
|
sig { params(package_info_path: Pathname).returns(T.nilable(T.attached_class)) }
|
|
def self.from_package_info(package_info_path)
|
|
require "rexml/document"
|
|
|
|
xml = REXML::Document.new(package_info_path.read)
|
|
|
|
bundle_version_bundle = xml.get_elements("//pkg-info//bundle-version//bundle").first
|
|
bundle_id = bundle_version_bundle["id"] if bundle_version_bundle
|
|
return if bundle_id.blank?
|
|
|
|
bundle = xml.get_elements("//pkg-info//bundle").find { |b| b["id"] == bundle_id }
|
|
return unless bundle
|
|
|
|
short_version = bundle["CFBundleShortVersionString"]
|
|
version = bundle["CFBundleVersion"]
|
|
|
|
new(short_version, version) if short_version || version
|
|
end
|
|
|
|
sig { returns(T.nilable(String)) }
|
|
attr_reader :short_version, :version
|
|
|
|
sig { params(short_version: T.nilable(String), version: T.nilable(String)).void }
|
|
def initialize(short_version, version)
|
|
# Remove version from short version, if present.
|
|
short_version = short_version&.sub(/\s*\(#{Regexp.escape(version)}\)\Z/, "") if version
|
|
|
|
@short_version = short_version.presence
|
|
@version = version.presence
|
|
|
|
return if @short_version || @version
|
|
|
|
raise ArgumentError, "`short_version` and `version` cannot both be `nil` or empty"
|
|
end
|
|
|
|
def <=>(other)
|
|
return super unless instance_of?(other.class)
|
|
|
|
make_version = ->(v) { v ? Version.new(v) : Version::NULL }
|
|
|
|
version = self.version.then(&make_version)
|
|
other_version = other.version.then(&make_version)
|
|
|
|
difference = version <=> other_version
|
|
|
|
# If `version` is equal or cannot be compared, compare `short_version` instead.
|
|
if difference.nil? || difference.zero?
|
|
short_version = self.short_version.then(&make_version)
|
|
other_short_version = other.short_version.then(&make_version)
|
|
|
|
return short_version <=> other_short_version
|
|
end
|
|
|
|
difference
|
|
end
|
|
|
|
def ==(other)
|
|
instance_of?(other.class) && short_version == other.short_version && version == other.version
|
|
end
|
|
alias eql? ==
|
|
|
|
# Create a nicely formatted version (on a best effort basis).
|
|
sig { returns(String) }
|
|
def nice_version
|
|
nice_parts.join(",")
|
|
end
|
|
|
|
sig { returns(T::Array[String]) }
|
|
def nice_parts
|
|
short_version = self.short_version
|
|
version = self.version
|
|
|
|
return [T.must(short_version)] if short_version == version
|
|
|
|
if short_version && version
|
|
return [version] if version.match?(/\A\d+(\.\d+)+\Z/) && version.start_with?("#{short_version}.")
|
|
return [short_version] if short_version.match?(/\A\d+(\.\d+)+\Z/) && short_version.start_with?("#{version}.")
|
|
|
|
if short_version.match?(/\A\d+(\.\d+)*\Z/) && version.match?(/\A\d+\Z/)
|
|
return [short_version] if short_version.start_with?("#{version}.") || short_version.end_with?(".#{version}")
|
|
|
|
return [short_version, version]
|
|
end
|
|
end
|
|
|
|
[short_version, version].compact
|
|
end
|
|
private :nice_parts
|
|
end
|
|
end
|