diff --git a/Library/.rubocop.yml b/Library/.rubocop.yml index 358bafc5a7c80a6eba03c2fe39a4b91b118622e4..ca07033f9f7934b42d43588e183d63b3a4c35ec0 100644 --- a/Library/.rubocop.yml +++ b/Library/.rubocop.yml @@ -26,10 +26,6 @@ Cask/Desc: Description: 'Ensure that the desc stanza conforms to various content and style checks.' Enabled: true -Cask/HomepageMatchesUrl: - Description: 'Ensure that the homepage and url match, otherwise add a comment. More info at https://github.com/Homebrew/homebrew-cask/blob/HEAD/doc/cask_language_reference/stanzas/url.md#when-url-and-homepage-hostnames-differ-add-a-comment' - Enabled: true - Cask/HomepageUrlTrailingSlash: Description: 'Ensure that the homepage url has a slash after the domain name.' Enabled: true diff --git a/Library/Homebrew/cask/audit.rb b/Library/Homebrew/cask/audit.rb index 1cc322e50730454122767afa8f93740e48bb3477..22980038aabcf406cbb6045b76be228390bdb9f9 100644 --- a/Library/Homebrew/cask/audit.rb +++ b/Library/Homebrew/cask/audit.rb @@ -52,6 +52,9 @@ module Cask check_sha256 check_desc check_url + check_unnecessary_verified + check_missing_verified + check_no_match check_generic_artifacts check_token_valid check_token_bad_words @@ -345,6 +348,80 @@ module Cask bad_url_format?(/osd/, [%r{\Ahttps?://([^/]+.)?dl\.osdn\.jp/}]) end + def homepage + URI(cask.homepage.to_s).host + end + + def domain + URI(cask.url.to_s).host + end + + def url_match_homepage? + host = cask.url.to_s.downcase + host_uri = URI(host) + host = if host.match?(/:\d/) && host_uri.port != 80 + "#{host_uri.host}:#{host_uri.port}" + else + host_uri.host + end + home = homepage.downcase + if (split_host = host.split(".")).length >= 3 + host = split_host[-2..].join(".") + end + if (split_home = homepage.split(".")).length >= 3 + home = split_home[-2..].join(".") + end + host == home + end + + def strip_url_scheme(url) + url.sub(%r{^.*://(www\.)?}, "") + end + + def url_from_verified + cask.url.verified.sub(%r{^https?://}, "") + end + + def verified_matches_url? + strip_url_scheme(cask.url.to_s).start_with?(url_from_verified) + end + + def verified_present? + cask.url.verified.present? + end + + def url_includes_file? + cask.url.to_s.start_with?("file://") + end + + def check_unnecessary_verified + return unless verified_present? + return unless url_match_homepage? + return unless verified_matches_url? + + add_warning "The URL's #{domain} matches the homepage #{homepage}, " \ + "the `verified` parameter of the `url` stanza is unnecessary. " \ + "See https://github.com/Homebrew/homebrew-cask/blob/master/doc/cask_language_reference/stanzas/url.md#when-url-and-homepage-hostnames-differ-add-verified" + end + + def check_missing_verified + return if url_includes_file? + return if url_match_homepage? + return if verified_present? + + add_warning "#{domain} does not match #{homepage}, a `verified` parameter of the `url` has to be added. " \ + " See https://github.com/Homebrew/homebrew-cask/blob/master/doc/cask_language_reference/stanzas/url.md#when-url-and-homepage-hostnames-differ-add-verified" + end + + def check_no_match + return if url_match_homepage? + return unless verified_present? + return if !url_match_homepage? && verified_matches_url? + + add_warning "#{url_from_verified} does not match #{strip_url_scheme(cask.url.to_s)}. " \ + "See https://github.com/Homebrew/homebrew-cask/blob/master/doc/cask_language_reference/stanzas/url.md#when-url-and-homepage-hostnames-differ-add-verified" + end + def check_generic_artifacts cask.artifacts.select { |a| a.is_a?(Artifact::Artifact) }.each do |artifact| unless artifact.target.absolute? diff --git a/Library/Homebrew/cask/url.rb b/Library/Homebrew/cask/url.rb index 5a6bb0d1c9eaae7283a0beba76dd7c8691c1e49a..7bb6d5adf71e9528f79a67dd8f481c2e7f105348 100644 --- a/Library/Homebrew/cask/url.rb +++ b/Library/Homebrew/cask/url.rb @@ -8,7 +8,7 @@ class URL extend T::Sig attr_reader :uri, :specs, - :using, + :verified, :using, :tag, :branch, :revisions, :revision, :trust_cert, :cookies, :referer, :header, :user_agent, :data @@ -19,6 +19,7 @@ class URL sig do params( uri: T.any(URI::Generic, String), + verified: T.nilable(String), using: T.nilable(Symbol), tag: T.nilable(String), branch: T.nilable(String), @@ -34,6 +35,7 @@ class URL end def initialize( uri, + verified: nil, using: nil, tag: nil, branch: nil, @@ -49,6 +51,7 @@ class URL @uri = URI(uri) specs = {} + specs[:verified] = @verified = verified specs[:using] = @using = using specs[:tag] = @tag = tag specs[:branch] = @branch = branch diff --git a/Library/Homebrew/rubocops/cask/homepage_matches_url.rb b/Library/Homebrew/rubocops/cask/homepage_matches_url.rb deleted file mode 100644 index 83f112b5e8b4f3890a001c40f1801a57a77af942..0000000000000000000000000000000000000000 --- a/Library/Homebrew/rubocops/cask/homepage_matches_url.rb +++ /dev/null @@ -1,180 +0,0 @@ -# typed: false -# frozen_string_literal: true - -require "forwardable" - -module RuboCop - module Cop - module Cask - # This cop checks that a cask's homepage matches the download URL, - # or if it doesn't, checks if a comment in the form - # `# example.com was verified as official when first introduced to the cask` - # is present. - class HomepageMatchesUrl < Cop - extend Forwardable - include CaskHelp - - REFERENCE_URL = - "https://github.com/Homebrew/homebrew-cask/blob/HEAD/doc/" \ - "cask_language_reference/stanzas/url.md#when-url-and-homepage-hostnames-differ-add-a-comment" - - COMMENT_FORMAT = /# [^ ]+ was verified as official when first introduced to the cask/.freeze - - MSG_NO_MATCH = "`%<url>s` does not match `%<full_url>s`" - - MSG_MISSING = ("`%<domain>s` does not match `%<homepage>s`, a comment has to be added " \ - "above the `url` stanza. For details, see " + REFERENCE_URL).freeze - - MSG_WRONG_FORMAT = ("`%<comment>s` does not match the expected comment format. " \ - "For details, see " + REFERENCE_URL).freeze - - MSG_UNNECESSARY = "The URL's domain `%<domain>s` matches the homepage `%<homepage>s`, " \ - "the comment above the `url` stanza is unnecessary" - - def on_cask(cask_block) - @cask_block = cask_block - return unless homepage_stanza - - add_offenses - end - - private - - attr_reader :cask_block - - def_delegators :cask_block, :cask_node, :toplevel_stanzas, - :sorted_toplevel_stanzas - - def add_offenses - toplevel_stanzas.select(&:url?).each do |url| - next if add_offense_unnecessary_comment(url) - next if add_offense_missing_comment(url) - next if add_offense_no_match(url) - next if add_offense_wrong_format(url) - end - end - - def add_offense_unnecessary_comment(stanza) - return unless comment?(stanza) - return unless url_match_homepage?(stanza) - return unless comment_matches_format?(stanza) - return unless comment_matches_url?(stanza) - - comment = comment(stanza).loc.expression - add_offense(comment, - location: comment, - message: format(MSG_UNNECESSARY, domain: domain(stanza), homepage: homepage)) - end - - def add_offense_missing_comment(stanza) - return if url_match_homepage?(stanza) - return if !url_match_homepage?(stanza) && comment?(stanza) - - range = stanza.source_range - url_domain = domain(stanza) - add_offense(range, location: range, message: format(MSG_MISSING, domain: url_domain, homepage: homepage)) - end - - def add_offense_no_match(stanza) - return if url_match_homepage?(stanza) - return unless comment?(stanza) - return if !url_match_homepage?(stanza) && comment_matches_url?(stanza) - - comment = comment(stanza).loc.expression - add_offense(comment, - location: comment, - message: format(MSG_NO_MATCH, url: url_from_comment(stanza), full_url: full_url(stanza))) - end - - def add_offense_wrong_format(stanza) - return if url_match_homepage?(stanza) - return unless comment?(stanza) - return if comment_matches_format?(stanza) - - comment = comment(stanza).loc.expression - add_offense(comment, - location: comment, - message: format(MSG_WRONG_FORMAT, comment: comment(stanza).text)) - end - - def comment?(stanza) - !stanza.comments.empty? - end - - def comment(stanza) - stanza.comments.last - end - - def comment_matches_format?(stanza) - comment(stanza).text =~ COMMENT_FORMAT - end - - def url_from_comment(stanza) - comment(stanza).text - .sub(/[^ ]*# ([^ ]+) .*/, '\1') - end - - def comment_matches_url?(stanza) - full_url(stanza).include?(url_from_comment(stanza)) - end - - def strip_url_scheme(url) - url.sub(%r{^.*://(www\.)?}, "") - end - - def domain(stanza) - strip_url_scheme(extract_url(stanza)).gsub(%r{^([^/]+).*}, '\1') - end - - def extract_url(stanza) - string = stanza.stanza_node.children[2] - return string.str_content if string.str_type? - - string.to_s.gsub(%r{.*"([a-z0-9]+://[^"]+)".*}m, '\1') - end - - def url_match_homepage?(stanza) - host = extract_url(stanza).downcase - host_uri = begin - URI(remove_non_ascii(host)) - rescue URI::InvalidURIError - # Can't check if we can't parse. - nil - end - - return true if host_uri.blank? - - host = if host.match?(/:\d/) && host_uri.port != 80 - "#{host_uri.host}:#{host_uri.port}" - else - host_uri.host - end - home = homepage.downcase - if (split_host = host.split(".")).length >= 3 - host = split_host[-2..].join(".") - end - if (split_home = homepage.split(".")).length >= 3 - home = split_home[-2..].join(".") - end - host == home - end - - def full_url(stanza) - strip_url_scheme(extract_url(stanza)) - end - - def homepage - URI(remove_non_ascii(extract_url(homepage_stanza))).host - end - - def homepage_stanza - toplevel_stanzas.find(&:homepage?) - end - - def remove_non_ascii(string) - string.gsub(/\P{ASCII}/, "") - end - end - end - end -end diff --git a/Library/Homebrew/rubocops/rubocop-cask.rb b/Library/Homebrew/rubocops/rubocop-cask.rb index fbba19daf4a26d1caa5b8aeca435e11bf1e4bb7f..3e0a2c457865b075bffbe90c10aff58c4cdd05b4 100644 --- a/Library/Homebrew/rubocops/rubocop-cask.rb +++ b/Library/Homebrew/rubocops/rubocop-cask.rb @@ -13,7 +13,6 @@ require "rubocops/cask/extend/node" require "rubocops/cask/mixin/cask_help" require "rubocops/cask/mixin/on_homepage_stanza" require "rubocops/cask/desc" -require "rubocops/cask/homepage_matches_url" require "rubocops/cask/homepage_url_trailing_slash" require "rubocops/cask/no_dsl_version" require "rubocops/cask/stanza_order" diff --git a/Library/Homebrew/test/cask/audit_spec.rb b/Library/Homebrew/test/cask/audit_spec.rb index 285c6ec3cc662fb554be1785b589f02484a9df85..9477a16f2b3dd81e5d33868b094691d4abca8918 100644 --- a/Library/Homebrew/test/cask/audit_spec.rb +++ b/Library/Homebrew/test/cask/audit_spec.rb @@ -887,5 +887,80 @@ describe Cask::Audit, :cask do expect(subject).to pass end end + + context "when the url matches the homepage" do + let(:cask_token) { "foo" } + let(:cask) do + tmp_cask cask_token.to_s, <<~RUBY + cask '#{cask_token}' do + version '1.0' + sha256 '8dd95daa037ac02455435446ec7bc737b34567afe9156af7d20b2a83805c1d8a' + url 'https://foo.brew.sh/foo.zip' + name 'Audit' + desc 'Audit Description' + homepage 'https://foo.brew.sh' + app 'Audit.app' + end + RUBY + end + + it { is_expected.to pass } + end + + context "when the url does not match the homepage" do + let(:cask_token) { "foo" } + let(:cask) do + tmp_cask cask_token.to_s, <<~RUBY + cask '#{cask_token}' do + version '1.8.0_72,8.13.0.5' + sha256 '8dd95daa037ac02455435446ec7bc737b34567afe9156af7d20b2a83805c1d8a' + url 'https://brew.sh/foo.zip' + name 'Audit' + desc 'Audit Description' + homepage 'https://foo.example.org' + app 'Audit.app' + end + RUBY + end + + it { is_expected.to warn_with(/a `verified` parameter of the `url` has to be added./) } + end + + context "when the url does not match the homepage with verified" do + let(:cask_token) { "foo" } + let(:cask) do + tmp_cask cask_token.to_s, <<~RUBY + cask '#{cask_token}' do + version '1.8.0_72,8.13.0.5' + sha256 '8dd95daa037ac02455435446ec7bc737b34567afe9156af7d20b2a83805c1d8a' + url 'https://brew.sh/foo.zip', verified: 'brew.sh' + name 'Audit' + desc 'Audit Description' + homepage 'https://foo.example.org' + app 'Audit.app' + end + RUBY + end + + it { is_expected.to pass } + end + + context "when there is no homepage" do + let(:cask_token) { "foo" } + let(:cask) do + tmp_cask cask_token.to_s, <<~RUBY + cask '#{cask_token}' do + version '1.8.0_72,8.13.0.5' + sha256 '8dd95daa037ac02455435446ec7bc737b34567afe9156af7d20b2a83805c1d8a' + url 'https://brew.sh/foo.zip' + name 'Audit' + desc 'Audit Description' + app 'Audit.app' + end + RUBY + end + + it { is_expected.to fail_with(/a homepage stanza is required/) } + end end end diff --git a/Library/Homebrew/test/rubocops/cask/homepage_matches_url_spec.rb b/Library/Homebrew/test/rubocops/cask/homepage_matches_url_spec.rb deleted file mode 100644 index 4ef084f3e1182954717a7293cca35784b7e23e81..0000000000000000000000000000000000000000 --- a/Library/Homebrew/test/rubocops/cask/homepage_matches_url_spec.rb +++ /dev/null @@ -1,216 +0,0 @@ -# typed: false -# frozen_string_literal: true - -require "rubocops/rubocop-cask" -require "test/rubocops/cask/shared_examples/cask_cop" - -describe RuboCop::Cop::Cask::HomepageMatchesUrl do - include CaskCop - - subject(:cop) { described_class.new } - - context "when the url matches the homepage and there is no comment" do - let(:source) do - <<-CASK.undent - cask 'foo' do - url 'https://foo.brew.sh/foo.zip' - homepage 'https://foo.brew.sh' - end - CASK - end - - include_examples "does not report any offenses" - end - - context "when the url matches the homepage and the url stanza has " \ - "a referrer and no interpolation" do - let(:source) do - <<-CASK.undent - cask 'foo' do - url 'https://foo.brew.sh/foo.zip', - referrer: 'https://brew.sh/foo/' - homepage 'https://foo.brew.sh' - end - CASK - end - - include_examples "does not report any offenses" - end - - context "when the url matches the homepage and the url stanza has " \ - "a referrer and interpolation" do - let(:source) do - <<-CASK.undent - cask 'foo' do - version '1.8.0_72,8.13.0.5' - url "https://foo.brew.sh/foo-\#{version.after_comma}-\#{version.minor}.\#{version.patch}.\#{version.before_comma.sub(\%r{.*_}, '')}.zip", - referrer: 'https://brew.sh/foo/' - homepage 'https://foo.brew.sh' - end - CASK - end - - include_examples "does not report any offenses" - end - - context "when the url matches the homepage but there is a comment " \ - "which does not match the url" do - let(:source) do - <<-CASK.undent - cask 'foo' do - # this is just a comment with information - url 'https://brew.sh/foo.zip' - homepage 'https://brew.sh' - end - CASK - end - - include_examples "does not report any offenses" - end - - context "when the url matches the homepage " \ - "but there is a comment matching the url" do - let(:source) do - <<-CASK.undent - cask 'foo' do - # foo.brew.sh was verified as official when first introduced to the cask - url 'https://foo.brew.sh/foo.zip' - homepage 'https://foo.brew.sh' - end - CASK - end - let(:expected_offenses) do - [{ - message: "The URL's domain `foo.brew.sh` matches the homepage " \ - "`foo.brew.sh`, the comment above the `url` stanza is " \ - "unnecessary", - severity: :convention, - line: 2, - column: 2, - source: "# foo.brew.sh was verified as official when " \ - "first introduced to the cask", - }] - end - - include_examples "reports offenses" - end - - context "when the url does not match the homepage" do - context "when there is a comment matching the url " \ - "but not matching the expected format" do - let(:source) do - <<-CASK.undent - cask 'foo' do - # brew.sh was verified as official - url 'https://brew.sh/foo.zip' - homepage 'https://foo.example.org' - end - CASK - end - let(:expected_offenses) do - [{ - message: "`# brew.sh was verified as official` does not " \ - "match the expected comment format. For details, see " \ - "https://github.com/Homebrew/homebrew-cask/blob/HEAD/doc/" \ - "cask_language_reference/stanzas/url.md#when-url-and-homepage-hostnames-differ-add-a-comment", - severity: :convention, - line: 2, - column: 2, - source: "# brew.sh was verified as official", - }] - end - - include_examples "reports offenses" - end - - context "when there is a comment matching the url " \ - "and does not have slashes" do - let(:source) do - <<-CASK.undent - cask 'foo' do - # brew.sh was verified as official when first introduced to the cask - url 'https://brew.sh/foo.zip' - homepage 'https://foo.example.org' - end - CASK - end - - include_examples "does not report any offenses" - end - - context "when there is a comment matching the url and has slashes" do - let(:source) do - <<-CASK.undent - cask 'foo' do - # brew.sh/vendor/app was verified as official when first introduced to the cask - url 'https://downloads.brew.sh/vendor/app/foo.zip' - homepage 'https://vendor.example.org/app/' - end - CASK - end - - include_examples "does not report any offenses" - end - - context "when there is a comment which does not match the url" do - let(:source) do - <<-CASK.undent - cask 'foo' do - # brew.sh was verified as official when first introduced to the cask - url 'https://example.org/foo.zip' - homepage 'https://foo.brew.sh' - end - CASK - end - let(:expected_offenses) do - [{ - message: "`brew.sh` does not match `example.org/foo.zip`", - severity: :convention, - line: 2, - column: 2, - source: "# brew.sh was verified as official when " \ - "first introduced to the cask", - }] - end - - include_examples "reports offenses" - end - - context "when the comment is missing" do - let(:source) do - <<-CASK.undent - cask 'foo' do - url 'https://brew.sh/foo.zip' - homepage 'https://example.org' - end - CASK - end - let(:expected_offenses) do - [{ - message: "`brew.sh` does not match `example.org`, a comment " \ - "has to be added above the `url` stanza. For details, see " \ - "https://github.com/Homebrew/homebrew-cask/blob/HEAD/doc/" \ - "cask_language_reference/stanzas/url.md#when-url-and-homepage-hostnames-differ-add-a-comment", - severity: :convention, - line: 2, - column: 2, - source: "url 'https://brew.sh/foo.zip'", - }] - end - - include_examples "reports offenses" - end - end - - context "when there is no homepage" do - let(:source) do - <<-CASK.undent - cask 'foo' do - url 'https://brew.sh/foo.zip' - end - CASK - end - - include_examples "does not report any offenses" - end -end diff --git a/Library/Homebrew/test/support/fixtures/cask/Casks/generic-artifact-relative-target.rb b/Library/Homebrew/test/support/fixtures/cask/Casks/generic-artifact-relative-target.rb index 576f4de472246719cd7b8f4c9c1bb196c5dd2486..e804ffd270dff6eca168a56e0cbaded97228c632 100644 --- a/Library/Homebrew/test/support/fixtures/cask/Casks/generic-artifact-relative-target.rb +++ b/Library/Homebrew/test/support/fixtures/cask/Casks/generic-artifact-relative-target.rb @@ -1,3 +1,11 @@ cask "generic-artifact-relative-target" do + version "1.2.3" + sha256 "d5b2dfbef7ea28c25f7a77cd7fa14d013d82b626db1d82e00e25822464ba19e2" + + url "file://#{TEST_FIXTURE_DIR}/cask/AppWithBinary.zip" + name "With Binary" + desc "Cask with a binary stanza" + homepage "https://brew.sh/with-binary" + artifact "Caffeine.app", target: "Caffeine.app" end