diff --git a/Library/Homebrew/cask/cask_loader.rb b/Library/Homebrew/cask/cask_loader.rb index 591daa9e47..a261671240 100644 --- a/Library/Homebrew/cask/cask_loader.rb +++ b/Library/Homebrew/cask/cask_loader.rb @@ -18,11 +18,6 @@ module Cask extend T::Helpers interface! - sig { overridable.params(_ref: T.any(String, Pathname, Cask, URI::Generic)).returns(T::Boolean) } - def self.can_load?(_ref) - false - end - sig { abstract.params(config: Config).returns(Cask) } def load(config:); end end @@ -55,8 +50,7 @@ module Cask # Loads a cask from a string. class FromContentLoader < AbstractContentLoader - sig { params(ref: T.any(String, Pathname, Cask, URI::Generic)).returns(T::Boolean) } - def self.can_load?(ref) + def self.try_new(ref, warn: false) return false unless ref.respond_to?(:to_str) content = T.unsafe(ref).to_str @@ -69,14 +63,16 @@ module Cask /\A\s*cask(?:#{curly.source}|#{do_end.source})\s*\Z/m end - content.match?(@regex) + return unless content.match?(@regex) + + new(content) end sig { params(content: String, tap: Tap).void } def initialize(content, tap: T.unsafe(nil)) super() - @content = content.force_encoding("UTF-8") + @content = content.dup.force_encoding("UTF-8") @tap = tap end @@ -89,13 +85,20 @@ module Cask # Loads a cask from a path. class FromPathLoader < AbstractContentLoader - sig { params(ref: T.any(String, Pathname, Cask, URI::Generic)).returns(T::Boolean) } - def self.can_load?(ref) + sig { + params(ref: T.any(String, Pathname, Cask, URI::Generic), warn: T::Boolean) + .returns(T.nilable(T.attached_class)) + } + def self.try_new(ref, warn: false) ref = Pathname(ref) if ref.is_a?(String) - return false unless ref.is_a?(Pathname) + return unless ref.is_a?(Pathname) path = ref - %w[.rb .json].include?(path.extname) && path.expand_path.exist? + + return if %w[.rb .json].exclude?(path.extname) + return unless path.expand_path.exist? + + new(path) end attr_reader :token, :path @@ -146,8 +149,11 @@ module Cask # Loads a cask from a URI. class FromURILoader < FromPathLoader - sig { params(ref: T.any(String, Pathname, Cask, URI::Generic)).returns(T::Boolean) } - def self.can_load?(ref) + sig { + params(ref: T.any(String, Pathname, Cask, URI::Generic), warn: T::Boolean) + .returns(T.nilable(T.attached_class)) + } + def self.try_new(ref, warn: false) # Cache compiled regex @uri_regex ||= begin uri_regex = ::URI::DEFAULT_PARSER.make_regexp @@ -155,12 +161,12 @@ module Cask end uri = ref.to_s - return false unless uri.match?(@uri_regex) + return unless uri.match?(@uri_regex) uri = URI(uri) - return false unless uri.path + return unless uri.path - true + new(uri) end attr_reader :url @@ -187,9 +193,14 @@ module Cask # Loads a cask from a tap path. class FromTapPathLoader < FromPathLoader - sig { params(ref: T.any(String, Pathname, Cask, URI::Generic)).returns(T::Boolean) } - def self.can_load?(ref) - super && !Tap.from_path(ref).nil? + sig { + params(ref: T.any(String, Pathname, Cask, URI::Generic), warn: T::Boolean) + .returns(T.nilable(T.attached_class)) + } + def self.try_new(ref, warn: false) + return unless (loader = super) + + loader unless Tap.from_path(ref).nil? end sig { params(path: T.any(Pathname, String)).void } @@ -201,9 +212,17 @@ module Cask # Loads a cask from a specific tap. class FromTapLoader < FromTapPathLoader - sig { params(ref: T.any(String, Pathname, Cask, URI::Generic)).returns(T::Boolean) } - def self.can_load?(ref) - ref.to_s.match?(HOMEBREW_TAP_CASK_REGEX) + sig { + params(ref: T.any(String, Pathname, Cask, URI::Generic), warn: T::Boolean) + .returns(T.nilable(T.attached_class)) + } + def self.try_new(ref, warn: false) + ref = ref.to_s + + return unless ref.match?(HOMEBREW_TAP_CASK_REGEX) + + token, tap, = CaskLoader.tap_cask_token_type(ref, warn: warn) + new("#{tap}/#{token}") end sig { params(tapped_name: String).void } @@ -223,13 +242,11 @@ module Cask # Loads a cask from the default tap path. class FromDefaultTapPathLoader < FromTapPathLoader - sig { params(ref: T.any(String, Pathname, Cask, URI::Generic)).returns(T::Boolean) } - def self.can_load?(ref) - super CaskLoader.default_path(ref) - end - - sig { params(ref: String).void } - def initialize(ref) + sig { + params(ref: T.any(String, Pathname, Cask, URI::Generic), warn: T::Boolean) + .returns(T.nilable(T.attached_class)) + } + def self.try_new(ref, warn: false) super CaskLoader.default_path(ref) end end @@ -238,9 +255,14 @@ module Cask class FromInstanceLoader include ILoader - sig { params(ref: T.any(String, Pathname, Cask, URI::Generic)).returns(T::Boolean) } - def self.can_load?(ref) - ref.is_a?(Cask) + sig { + params(ref: T.any(String, Pathname, Cask, URI::Generic), warn: T::Boolean) + .returns(T.nilable(T.attached_class)) + } + def self.try_new(ref, warn: false) + return unless ref.is_a?(Cask) + + new(ref) end sig { params(cask: Cask).void } @@ -262,15 +284,24 @@ module Cask sig { returns(T.nilable(Hash)) } attr_reader :from_json - sig { params(ref: T.any(String, Pathname, Cask, URI::Generic)).returns(T::Boolean) } - def self.can_load?(ref) - return false if Homebrew::EnvConfig.no_install_from_api? - return false unless ref.is_a?(String) + sig { + params(ref: T.any(String, Pathname, Cask, URI::Generic), warn: T::Boolean) + .returns(T.nilable(T.attached_class)) + } + def self.try_new(ref, warn: false) + return if Homebrew::EnvConfig.no_install_from_api? + return unless ref.is_a?(String) - return false unless (match = ref.match(HOMEBREW_MAIN_TAP_CASK_REGEX)) + return unless (match = ref.match(HOMEBREW_MAIN_TAP_CASK_REGEX)) token = match[:token] - Homebrew::API::Cask.all_casks.key?(token) + + return unless Homebrew::API::Cask.all_casks.key?(token) + + ref = "#{CoreCaskTap.instance}/#{token}" + + token, tap, = CaskLoader.tap_cask_token_type(ref, warn: warn) + new("#{tap}/#{token}") end sig { params(token: String, from_json: Hash).void } @@ -429,11 +460,41 @@ module Cask end end + # Loader which tries loading casks from tap paths, failing + # if the same token exists in multiple taps. + class FromAmbiguousTapPathLoader < FromTapPathLoader + def self.try_new(ref, warn: false) + case (possible_tap_casks = CaskLoader.tap_paths(ref, warn: warn)).count + when 1 + new(possible_tap_casks.first) + when 2..Float::INFINITY + loaders = possible_tap_casks.map(&FromTapPathLoader.method(:new)) + raise TapCaskAmbiguityError.new(ref, loaders) + end + end + end + + # Loader which loads a cask from the installed cask file. + class FromInstalledPathLoader < FromPathLoader + def self.try_new(ref, warn: false) + possible_installed_cask = Cask.new(ref) + return unless (installed_caskfile = possible_installed_cask.installed_caskfile) + + new(installed_caskfile) + end + end + # Pseudo-loader which raises an error when trying to load the corresponding cask. class NullLoader < FromPathLoader - sig { params(_ref: T.any(String, Pathname, Cask, URI::Generic)).returns(T::Boolean) } - def self.can_load?(_ref) - true + sig { + params(ref: T.any(String, Pathname, Cask, URI::Generic), warn: T::Boolean) + .returns(T.nilable(T.attached_class)) + } + def self.try_new(ref, warn: false) + return if ref.is_a?(Cask) + return if ref.is_a?(URI::Generic) + + new(ref) end sig { params(ref: T.any(String, Pathname)).void } @@ -492,35 +553,14 @@ module Cask FromTapPathLoader, FromPathLoader, FromDefaultTapPathLoader, + FromAmbiguousTapPathLoader, + FromInstalledPathLoader, + NullLoader, ].each do |loader_class| - next unless loader_class.can_load?(ref) - - $stderr.puts "#{$PROGRAM_NAME} (#{loader_class}): loading #{ref}" if debug? - - if [FromAPILoader, FromTapLoader].include?(loader_class) - ref = "#{CoreCaskTap.instance}/#{ref}" if CoreCaskTap.instance.cask_renames.key?(ref) - token, tap, = tap_cask_token_type(ref, warn: warn) - loader_class = T.cast(loader_class, T.any(T.class_of(FromAPILoader), T.class_of(FromTapLoader))) - return loader_class.new("#{tap}/#{token}") + if (loader = loader_class.try_new(ref, warn: warn)) + return loader end - - return loader_class.new(ref) end - - case (possible_tap_casks = tap_paths(ref, warn: warn)).count - when 1 - return FromTapPathLoader.new(possible_tap_casks.first) - when 2..Float::INFINITY - loaders = possible_tap_casks.map(&FromTapPathLoader.method(:new)) - raise TapCaskAmbiguityError.new(ref, loaders) - end - - possible_installed_cask = Cask.new(ref) - if (installed_caskfile = possible_installed_cask.installed_caskfile) - return FromPathLoader.new(installed_caskfile) - end - - NullLoader.new(ref) end def self.default_path(token) diff --git a/Library/Homebrew/test/cask/cask_loader/from_api_loader_spec.rb b/Library/Homebrew/test/cask/cask_loader/from_api_loader_spec.rb index deccbb9780..a7ec38d858 100644 --- a/Library/Homebrew/test/cask/cask_loader/from_api_loader_spec.rb +++ b/Library/Homebrew/test/cask/cask_loader/from_api_loader_spec.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true describe Cask::CaskLoader::FromAPILoader, :cask do - alias_matcher :be_able_to_load, :be_can_load - shared_context "with API setup" do |new_token| let(:token) { new_token } let(:cask_from_source) { Cask::CaskLoader.load(token) } @@ -30,7 +28,7 @@ describe Cask::CaskLoader::FromAPILoader, :cask do end it "returns false" do - expect(described_class).not_to be_able_to_load(token) + expect(described_class.try_new(token)).to be_nil end end @@ -39,16 +37,16 @@ describe Cask::CaskLoader::FromAPILoader, :cask do ENV.delete("HOMEBREW_NO_INSTALL_FROM_API") end - it "returns true for valid token" do - expect(described_class).to be_able_to_load(token) + it "returns a loader for valid token" do + expect(described_class.try_new(token)).not_to be_nil end - it "returns true for valid full name" do - expect(described_class).to be_able_to_load("homebrew/cask/#{token}") + it "returns a loader for valid full name" do + expect(described_class.try_new("homebrew/cask/#{token}")).not_to be_nil end - it "returns false for full name with invalid tap" do - expect(described_class).not_to be_able_to_load("homebrew/cask-versions/#{token}") + it "returns nil for full name with invalid tap" do + expect(described_class.try_new("homebrew/cask-versions/#{token}")).to be_nil end end end diff --git a/Library/Homebrew/test/cask/cask_loader/from_content_loader_spec.rb b/Library/Homebrew/test/cask/cask_loader/from_content_loader_spec.rb index 41836b9a1c..e4b5ce2efe 100644 --- a/Library/Homebrew/test/cask/cask_loader/from_content_loader_spec.rb +++ b/Library/Homebrew/test/cask/cask_loader/from_content_loader_spec.rb @@ -1,57 +1,55 @@ # frozen_string_literal: true describe Cask::CaskLoader::FromContentLoader do - alias_matcher :be_able_to_load, :be_can_load - - describe "::can_load?" do - it "returns true for Casks specified with `cask \"token\" do … end`" do - expect(described_class).to be_able_to_load <<~RUBY + describe "::try_new" do + it "returns a loader for Casks specified with `cask \"token\" do … end`" do + expect(described_class.try_new(<<~RUBY)).not_to be_nil cask "token" do end RUBY end - it "returns true for Casks specified with `cask \"token\" do; end`" do - expect(described_class).to be_able_to_load <<~RUBY + it "returns a loader for Casks specified with `cask \"token\" do; end`" do + expect(described_class.try_new(<<~RUBY)).not_to be_nil cask "token" do; end RUBY end - it "returns true for Casks specified with `cask 'token' do … end`" do - expect(described_class).to be_able_to_load <<~RUBY + it "returns a loader for Casks specified with `cask 'token' do … end`" do + expect(described_class.try_new(<<~RUBY)).not_to be_nil cask 'token' do end RUBY end - it "returns true for Casks specified with `cask 'token' do; end`" do - expect(described_class).to be_able_to_load <<~RUBY + it "returns a loader for Casks specified with `cask 'token' do; end`" do + expect(described_class.try_new(<<~RUBY)).not_to be_nil cask 'token' do; end RUBY end - it "returns true for Casks specified with `cask(\"token\") { … }`" do - expect(described_class).to be_able_to_load <<~RUBY + it "returns a loader for Casks specified with `cask(\"token\") { … }`" do + expect(described_class.try_new(<<~RUBY)).not_to be_nil cask("token") { } RUBY end - it "returns true for Casks specified with `cask(\"token\") {}`" do - expect(described_class).to be_able_to_load <<~RUBY + it "returns a loader for Casks specified with `cask(\"token\") {}`" do + expect(described_class.try_new(<<~RUBY)).not_to be_nil cask("token") {} RUBY end - it "returns true for Casks specified with `cask('token') { … }`" do - expect(described_class).to be_able_to_load <<~RUBY + it "returns a loader for Casks specified with `cask('token') { … }`" do + expect(described_class.try_new(<<~RUBY)).not_to be_nil cask('token') { } RUBY end - it "returns true for Casks specified with `cask('token') {}`" do - expect(described_class).to be_able_to_load <<~RUBY + it "returns a loader for Casks specified with `cask('token') {}`" do + expect(described_class.try_new(<<~RUBY)).not_to be_nil cask('token') {} RUBY end diff --git a/Library/Homebrew/test/cask/cask_loader/from_uri_loader_spec.rb b/Library/Homebrew/test/cask/cask_loader/from_uri_loader_spec.rb index cb8e021523..fb838bac44 100644 --- a/Library/Homebrew/test/cask/cask_loader/from_uri_loader_spec.rb +++ b/Library/Homebrew/test/cask/cask_loader/from_uri_loader_spec.rb @@ -1,19 +1,17 @@ # frozen_string_literal: true describe Cask::CaskLoader::FromURILoader do - alias_matcher :be_able_to_load, :be_can_load - - describe "::can_load?" do - it "returns true when given an URI" do - expect(described_class).to be_able_to_load(URI("https://brew.sh/")) + describe "::try_new" do + it "returns a loader when given an URI" do + expect(described_class.try_new(URI("https://brew.sh/"))).not_to be_nil end - it "returns true when given a String which can be parsed to a URI" do - expect(described_class).to be_able_to_load("https://brew.sh/") + it "returns a loader when given a string which can be parsed to a URI" do + expect(described_class.try_new("https://brew.sh/")).not_to be_nil end - it "returns false when given a String with Cask contents containing a URL" do - expect(described_class).not_to be_able_to_load <<~RUBY + it "returns nil when given a string with Cask contents containing a URL" do + expect(described_class.try_new(<<~RUBY)).to be_nil cask 'token' do url 'https://brew.sh/' end