From a1efaf1864049977d7d90f32e5ff1e897cf57d96 Mon Sep 17 00:00:00 2001 From: Markus Reiter Date: Sat, 29 Apr 2023 23:39:26 +0200 Subject: [PATCH] Merge `HeadVersion` and `NullVersion` into `Version`. --- Library/Homebrew/language/python.rb | 4 +- Library/Homebrew/os/linux.rb | 5 +- Library/Homebrew/os/mac/version.rb | 36 ++++- Library/Homebrew/resource.rb | 1 + Library/Homebrew/tab.rb | 4 +- Library/Homebrew/test/os/mac/version_spec.rb | 7 +- Library/Homebrew/version.rb | 151 ++++++++++++++----- Library/Homebrew/version/head.rb | 33 ---- Library/Homebrew/version/null.rb | 104 ------------- 9 files changed, 151 insertions(+), 194 deletions(-) delete mode 100644 Library/Homebrew/version/head.rb delete mode 100644 Library/Homebrew/version/null.rb diff --git a/Library/Homebrew/language/python.rb b/Library/Homebrew/language/python.rb index aac47e201c..093103f448 100644 --- a/Library/Homebrew/language/python.rb +++ b/Library/Homebrew/language/python.rb @@ -7,10 +7,10 @@ module Language # @api public module Python def self.major_minor_version(python) - version = /\d\.\d+/.match `#{python} --version 2>&1` + version = `#{python} --version 2>&1`.chomp[/(\d\.\d+)/, 1] return unless version - Version.create(version.to_s) + Version.new(version) end def self.homebrew_site_packages(python = "python3.7") diff --git a/Library/Homebrew/os/linux.rb b/Library/Homebrew/os/linux.rb index 15d096bd97..9273a6b5f7 100644 --- a/Library/Homebrew/os/linux.rb +++ b/Library/Homebrew/os/linux.rb @@ -1,6 +1,7 @@ # typed: true # frozen_string_literal: true +require "os/mac/version" require "utils" module OS @@ -53,11 +54,11 @@ module OS raise "Loaded OS::Linux on generic OS!" if ENV["HOMEBREW_TEST_GENERIC_OS"] def self.version - ::Version::NULL + Version::NULL end def self.full_version - ::Version::NULL + Version::NULL end def self.languages diff --git a/Library/Homebrew/os/mac/version.rb b/Library/Homebrew/os/mac/version.rb index 6dcda2f571..37681b4d2b 100644 --- a/Library/Homebrew/os/mac/version.rb +++ b/Library/Homebrew/os/mac/version.rb @@ -30,18 +30,29 @@ module OS sig { override.params(other: T.untyped).returns(T.nilable(Integer)) } def <=>(other) - @comparison_cache.fetch(other) do + return @comparison_cache[other] if @comparison_cache.key?(other) + + result = case other + when Symbol if MacOSVersions::SYMBOLS.key?(other) && to_sym == other 0 else v = MacOSVersions::SYMBOLS.fetch(other) { other.to_s } - @comparison_cache[other] = super(::Version.new(v)) + super(v) end + else + super end + + @comparison_cache[other] = result unless frozen? + + result end sig { returns(T.self_type) } def strip_patch + return self if null? + # Big Sur is 11.x but Catalina is 10.15.x. if T.must(major) >= 11 self.class.new(major.to_s) @@ -52,12 +63,24 @@ module OS sig { returns(Symbol) } def to_sym - @to_sym ||= MacOSVersions::SYMBOLS.invert.fetch(strip_patch.to_s, :dunno) + return @sym if defined?(@sym) + + sym = MacOSVersions::SYMBOLS.invert.fetch(strip_patch.to_s, :dunno) + + @sym = sym unless frozen? + + sym end sig { returns(String) } def pretty_name - @pretty_name ||= to_sym.to_s.split("_").map(&:capitalize).join(" ").freeze + return @pretty_name if defined?(@pretty_name) + + pretty_name = to_sym.to_s.split("_").map(&:capitalize).join(" ").freeze + + @pretty_name = pretty_name unless frozen? + + pretty_name end sig { returns(T::Boolean) } @@ -78,6 +101,8 @@ module OS # For {OS::Mac::Version} compatibility. sig { returns(T::Boolean) } def requires_nehalem_cpu? + return false if null? + unless Hardware::CPU.intel? raise "Unexpected architecture: #{Hardware::CPU.arch}. This only works with Intel architecture." end @@ -90,6 +115,9 @@ module OS alias requires_sse41? requires_nehalem_cpu? alias requires_sse42? requires_nehalem_cpu? alias requires_popcnt? requires_nehalem_cpu? + + # Represents the absence of a version. + NULL = Version.new("10.0").tap { |v| v.instance_variable_set(:@version, nil) }.freeze end end end diff --git a/Library/Homebrew/resource.rb b/Library/Homebrew/resource.rb index 73fc5b4a26..92497a99c4 100644 --- a/Library/Homebrew/resource.rb +++ b/Library/Homebrew/resource.rb @@ -190,6 +190,7 @@ class Resource < Downloadable @download_strategy = @url.download_strategy end + sig { params(val: T.nilable(T.any(String, Version))).returns(T.nilable(Version)) } def version(val = nil) return super() if val.nil? diff --git a/Library/Homebrew/tab.rb b/Library/Homebrew/tab.rb index a5ba02d5ea..3258c4e34c 100644 --- a/Library/Homebrew/tab.rb +++ b/Library/Homebrew/tab.rb @@ -315,11 +315,11 @@ class Tab end def stable_version - Version.create(versions["stable"]) if versions["stable"] + versions["stable"]&.then(&Version.method(:new)) end def head_version - Version.create(versions["head"]) if versions["head"] + versions["head"]&.then(&Version.method(:new)) end def version_scheme diff --git a/Library/Homebrew/test/os/mac/version_spec.rb b/Library/Homebrew/test/os/mac/version_spec.rb index 20c7734c05..9792051d5b 100644 --- a/Library/Homebrew/test/os/mac/version_spec.rb +++ b/Library/Homebrew/test/os/mac/version_spec.rb @@ -15,16 +15,11 @@ describe OS::Mac::Version do expect(version).to be < :catalina end - specify "comparison with Fixnum" do + specify "comparison with Integer" do expect(version).to be > 10 expect(version).to be < 11 end - specify "comparison with Float" do - expect(version).to be > 10.13 - expect(version).to be < 10.15 - end - specify "comparison with String" do expect(version).to be > "10.3" expect(version).to be == "10.14" diff --git a/Library/Homebrew/version.rb b/Library/Homebrew/version.rb index af12755d83..6b1b7d3fe6 100644 --- a/Library/Homebrew/version.rb +++ b/Library/Homebrew/version.rb @@ -2,8 +2,6 @@ # frozen_string_literal: true require "pkg_version" -require "version/head" -require "version/null" require "version/parser" # A formula's version. @@ -343,37 +341,25 @@ class Version parse(specs.fetch(:tag, url), detected_from_url: true) end + # TODO: `odeprecate` this and just use `Version.new`. sig { params(val: String).returns(Version) } def self.create(val) - raise TypeError, "Version value must be a string; got a #{val.class} (#{val})" unless val.respond_to?(:to_str) - - if val.to_str.start_with?("HEAD") - HeadVersion.new(val) - else - Version.new(val) - end + new(val) end - sig { params(spec: T.any(String, Pathname), detected_from_url: T::Boolean).returns(Version) } + sig { params(spec: T.any(String, Pathname), detected_from_url: T::Boolean).returns(T.attached_class) } def self.parse(spec, detected_from_url: false) - version = _parse(spec, detected_from_url: detected_from_url) - version.nil? ? NULL : new(version, detected_from_url: detected_from_url) - end - - sig { params(spec: T.any(String, Pathname), detected_from_url: T::Boolean).returns(T.nilable(String)) } - def self._parse(spec, detected_from_url:) spec = CGI.unescape(spec.to_s) if detected_from_url - spec = Pathname.new(spec) unless spec.is_a? Pathname + spec = Pathname(spec) VERSION_PARSERS.each do |parser| version = parser.parse(spec) - return version if version.present? + return new(version, detected_from_url: detected_from_url) if version.present? end - nil + NULL end - private_class_method :_parse NUMERIC_WITH_OPTIONAL_DOTS = /(?:\d+(?:\.\d+)*)/.source.freeze private_constant :NUMERIC_WITH_OPTIONAL_DOTS @@ -517,14 +503,36 @@ class Version @detected_from_url end + HEAD_VERSION_REGEX = /\AHEAD(?:-(?.*))?\Z/.freeze + private_constant :HEAD_VERSION_REGEX + + # Check if this is a HEAD version. sig { returns(T::Boolean) } def head? - false + version&.match?(HEAD_VERSION_REGEX) || false + end + + # Return the commit for a HEAD version. + sig { returns(T.nilable(String)) } + def commit + version&.match(HEAD_VERSION_REGEX)&.[](:commit) + end + + # Update the commit for a HEAD version. + sig { params(commit: T.nilable(String)).void } + def update_commit(commit) + raise ArgumentError, "Cannot update commit for non-HEAD version." unless head? + + @version = if commit + "HEAD-#{commit}" + else + "HEAD" + end end sig { returns(T::Boolean) } def null? - false + @version.nil? end sig { params(comparator: String, other: Version).returns(T::Boolean) } @@ -542,16 +550,47 @@ class Version sig { params(other: T.untyped).returns(T.nilable(Integer)) } def <=>(other) - # Needed to retain API compatibility with older string comparisons - # for compiler versions, etc. - other = Version.new(other) if other.is_a? String - # Used by the *_build_version comparisons, which formerly returned Fixnum - other = Version.new(other.to_s) if other.is_a? Integer - return 1 if other.nil? - return 1 if other.respond_to?(:null?) && other.null? + other = case other + when String + if other.blank? + # Cannot compare `NULL` to empty string. + return if null? + + return 1 + end + + # Needed to retain API compatibility with older string comparisons for compiler versions, etc. + Version.new(other) + when Integer + # Used by the `*_build_version` comparisons, which formerly returned an integer. + Version.new(other.to_s) + when Token + if other.null? + # Cannot compare `NULL` to `NULL`. + return if null? + + return 1 + end + + Version.new(other.to_s) + when Version + if other.null? + # Cannot compare `NULL` to `NULL`. + return if null? + + return 1 + end + + other + when nil + return 1 + else + return + end + + # All `other.null?` cases are handled at this point. + return -1 if null? - other = Version.new(other.to_s) if other.is_a? Token - return unless other.is_a?(Version) return 0 if version == other.version return 1 if head? && !other.head? return -1 if !head? && other.head? @@ -585,41 +624,64 @@ class Version 0 end + + sig { override.params(other: T.untyped).returns(T::Boolean) } + def ==(other) + # Makes sure that the same instance of Version::NULL + # will never equal itself; normally Comparable#== + # will return true for this regardless of the return + # value of #<=> + return false if null? + + super + end alias eql? == # @api public sig { returns(T.nilable(Token)) } def major + return NULL_TOKEN if null? + tokens.first end # @api public sig { returns(T.nilable(Token)) } def minor + return NULL_TOKEN if null? + tokens.second end # @api public sig { returns(T.nilable(Token)) } def patch + return NULL_TOKEN if null? + tokens.third end # @api public sig { returns(T.self_type) } def major_minor - self.class.new([major, minor].compact.join(".")) + return self if null? + + major_minor = T.must(tokens[0..1]) + major_minor.empty? ? NULL : self.class.new(major_minor.join(".")) end # @api public sig { returns(T.self_type) } def major_minor_patch - self.class.new([major, minor, patch].compact.join(".")) + return self if null? + + major_minor_patch = T.must(tokens[0..2]) + major_minor_patch.empty? ? NULL : self.class.new(major_minor_patch.join(".")) end sig { returns(T::Boolean) } def empty? - version.empty? + version&.empty? || false end sig { returns(Integer) } @@ -629,6 +691,8 @@ class Version sig { returns(Float) } def to_f + return Float::NAN if null? + version.to_f end @@ -639,10 +703,17 @@ class Version sig { returns(String) } def to_s - version.dup + version.to_s end alias to_str to_s + sig { returns(String) } + def inspect + return "#" if null? + + super + end + sig { returns(T.self_type) } def freeze tokens # Determine and store tokens before freezing @@ -651,12 +722,12 @@ class Version protected - sig { returns(String) } + sig { returns(T.nilable(String)) } attr_reader :version sig { returns(T::Array[Token]) } def tokens - @tokens ||= tokenize + @tokens ||= version&.scan(SCAN_PATTERN)&.map { |token| Token.create(T.cast(token, String)) } || [] end private @@ -666,8 +737,6 @@ class Version (first > second) ? first : second end - sig { returns(T::Array[Token]) } - def tokenize - version.scan(SCAN_PATTERN).map { |token| Token.create(T.cast(token, String)) } - end + # Represents the absence of a version. + NULL = Version.new("NULL").tap { |v| v.instance_variable_set(:@version, nil) }.freeze end diff --git a/Library/Homebrew/version/head.rb b/Library/Homebrew/version/head.rb deleted file mode 100644 index 0edeb47470..0000000000 --- a/Library/Homebrew/version/head.rb +++ /dev/null @@ -1,33 +0,0 @@ -# typed: true -# frozen_string_literal: true - -class Version - # A formula's HEAD version. - # @see https://docs.brew.sh/Formula-Cookbook#unstable-versions-head Unstable versions (head) - # - # @api private - class HeadVersion < Version - sig { returns(T.nilable(String)) } - attr_reader :commit - - def initialize(*) - super - @commit = @version[/^HEAD-(.+)$/, 1] - end - - sig { params(commit: T.nilable(String)).void } - def update_commit(commit) - @commit = commit - @version = if commit - "HEAD-#{commit}" - else - "HEAD" - end - end - - sig { returns(T::Boolean) } - def head? - true - end - end -end diff --git a/Library/Homebrew/version/null.rb b/Library/Homebrew/version/null.rb deleted file mode 100644 index 80fa6f686c..0000000000 --- a/Library/Homebrew/version/null.rb +++ /dev/null @@ -1,104 +0,0 @@ -# typed: true -# frozen_string_literal: true - -require "singleton" - -class Version - # A pseudo-version representing the absence of a version. - # - # @api private - class NullVersion < Version - include Comparable - include Singleton - - sig { override.params(_other: T.untyped).returns(Integer) } - def <=>(_other) - -1 - end - - sig { override.params(_other: T.untyped).returns(T::Boolean) } - def eql?(_other) - # Makes sure that the same instance of Version::NULL - # will never equal itself; normally Comparable#== - # will return true for this regardless of the return - # value of #<=> - false - end - - sig { override.returns(T::Boolean) } - def detected_from_url? - false - end - - sig { override.returns(T::Boolean) } - def head? - false - end - - sig { override.returns(T::Boolean) } - def null? - true - end - - # For {OS::Mac::Version} compatibility. - sig { returns(T::Boolean) } - def requires_nehalem_cpu? - false - end - alias requires_sse4? requires_nehalem_cpu? - alias requires_sse41? requires_nehalem_cpu? - alias requires_sse42? requires_nehalem_cpu? - alias requires_popcnt? requires_nehalem_cpu? - alias outdated_release? requires_nehalem_cpu? - - sig { override.returns(Token) } - def major - NULL_TOKEN - end - - sig { override.returns(Token) } - def minor - NULL_TOKEN - end - - sig { override.returns(Token) } - def patch - NULL_TOKEN - end - - sig { override.returns(T.self_type) } - def major_minor - self - end - - sig { override.returns(T.self_type) } - def major_minor_patch - self - end - - sig { override.returns(Float) } - def to_f - Float::NAN - end - - sig { override.returns(Integer) } - def to_i - 0 - end - - sig { override.returns(String) } - def to_s - "" - end - alias to_str to_s - - sig { override.returns(String) } - def inspect - "#" - end - end - private_constant :NullVersion - - # Represents the absence of a version. - NULL = NullVersion.instance.freeze -end