diff --git a/Library/Homebrew/cask/audit.rb b/Library/Homebrew/cask/audit.rb index 5ab7b8da96..322deebe4f 100644 --- a/Library/Homebrew/cask/audit.rb +++ b/Library/Homebrew/cask/audit.rb @@ -5,6 +5,7 @@ require "cask/denylist" require "cask/download" require "digest" require "livecheck/livecheck" +require "source_location" require "utils/curl" require "utils/git" require "utils/shared_audits" @@ -79,7 +80,13 @@ module Cask !errors? end - sig { params(message: T.nilable(String), location: T.nilable(String), strict_only: T::Boolean).void } + sig { + params( + message: T.nilable(String), + location: T.nilable(Homebrew::SourceLocation), + strict_only: T::Boolean, + ).void + } def add_error(message, location: nil, strict_only: false) # Only raise non-critical audits if the user specified `--strict`. return if strict_only && !@strict @@ -231,7 +238,9 @@ module Cask return unless cask.sha256 return if cask.sha256 == :no_check - add_error "Use `sha256 :no_check` when URL is unversioned." if cask.url&.unversioned? + return unless cask.url&.unversioned? + + add_error "Use `sha256 :no_check` when URL is unversioned." end sig { void } @@ -296,11 +305,11 @@ module Cask when %r{sourceforge.net/(\S+)} return unless online? - add_error "Download is hosted on SourceForge, #{add_livecheck}" + add_error "Download is hosted on SourceForge, #{add_livecheck}", location: cask.url.location when %r{dl.devmate.com/(\S+)} - add_error "Download is hosted on DevMate, #{add_livecheck}" + add_error "Download is hosted on DevMate, #{add_livecheck}", location: cask.url.location when %r{rink.hockeyapp.net/(\S+)} - add_error "Download is hosted on HockeyApp, #{add_livecheck}" + add_error "Download is hosted on HockeyApp, #{add_livecheck}", location: cask.url.location end end @@ -312,9 +321,11 @@ module Cask odebug "Auditing URL format" if bad_sourceforge_url? - add_error "SourceForge URL format incorrect. See #{Formatter.url(SOURCEFORGE_OSDN_REFERENCE_URL)}" + add_error "SourceForge URL format incorrect. See #{Formatter.url(SOURCEFORGE_OSDN_REFERENCE_URL)}", + location: cask.url.location elsif bad_osdn_url? - add_error "OSDN URL format incorrect. See #{Formatter.url(SOURCEFORGE_OSDN_REFERENCE_URL)}" + add_error "OSDN URL format incorrect. See #{Formatter.url(SOURCEFORGE_OSDN_REFERENCE_URL)}", + location: cask.url.location end end @@ -352,7 +363,8 @@ module Cask add_error "Verified URL #{Formatter.url(url_from_verified)} does not match URL " \ "#{Formatter.url(strip_url_scheme(cask.url.to_s))}. " \ - "See #{Formatter.url(VERIFIED_URL_REFERENCE_URL)}" + "See #{Formatter.url(VERIFIED_URL_REFERENCE_URL)}", + location: cask.url.location end sig { void } @@ -434,10 +446,11 @@ module Cask def audit_download return if download.blank? || cask.url.blank? - odebug "Auditing download" - download.fetch - rescue => e - add_error "download not possible: #{e}" + begin + download.fetch + rescue => e + add_error "download not possible: #{e}", location: cask.url.location + end end sig { void } @@ -447,10 +460,9 @@ module Cask return if cask.url.to_s.include? cask.version.csv.second return if cask.version.csv.third.present? && cask.url.to_s.include?(cask.version.csv.third) - add_error( - "Download does not require additional version components. Use `&:short_version` in the livecheck", - strict_only: true, - ) + add_error "Download does not require additional version components. Use `&:short_version` in the livecheck", + location: cask.url.location, + strict_only: true end sig { void } @@ -483,7 +495,7 @@ module Cask next if result.success? - add_error(<<~EOS, strict_only: true) + add_error <<~EOS, location: cask.url.location, strict_only: true Signature verification failed: #{result.merged_output} macOS on ARM requires software to be signed. @@ -580,7 +592,7 @@ module Cask tag = SharedAudits.github_tag_from_url(cask.url) tag ||= cask.version error = SharedAudits.github_release(user, repo, tag, cask: cask) - add_error error if error + add_error error, location: cask.url.location if error end sig { void } @@ -595,7 +607,7 @@ module Cask tag = SharedAudits.gitlab_tag_from_url(cask.url) tag ||= cask.version error = SharedAudits.gitlab_release(user, repo, tag, cask: cask) - add_error error if error + add_error error, location: cask.url.location if error end sig { void } @@ -606,12 +618,10 @@ module Cask user, repo = get_repo_data(%r{https?://github\.com/([^/]+)/([^/]+)/?.*}) if online? return if user.nil? - odebug "Auditing GitHub repo archived" - metadata = SharedAudits.github_repo_data(user, repo) return if metadata.nil? - add_error "GitHub repo is archived" if metadata["archived"] + add_error "GitHub repo is archived", location: cask.url.location if metadata["archived"] end sig { void } @@ -627,7 +637,7 @@ module Cask metadata = SharedAudits.gitlab_repo_data(user, repo) return if metadata.nil? - add_error "GitLab repo is archived" if metadata["archived"] + add_error "GitLab repo is archived", location: cask.url.location if metadata["archived"] end sig { void } @@ -640,7 +650,7 @@ module Cask odebug "Auditing GitHub repo" error = SharedAudits.github(user, repo) - add_error error if error + add_error error, location: cask.url.location if error end sig { void } @@ -653,7 +663,7 @@ module Cask odebug "Auditing GitLab repo" error = SharedAudits.gitlab(user, repo) - add_error error if error + add_error error, location: cask.url.location if error end sig { void } @@ -666,7 +676,7 @@ module Cask odebug "Auditing Bitbucket repo" error = SharedAudits.bitbucket(user, repo) - add_error error if error + add_error error, location: cask.url.location if error end sig { void } @@ -694,6 +704,7 @@ module Cask if cask.url && !cask.url.using validate_url_for_https_availability(cask.url, "binary URL", cask.token, cask.tap, + location: cask.url.location, user_agents: [cask.url.user_agent], referer: cask.url&.referer) end @@ -714,14 +725,15 @@ module Cask # params(url_to_check: T.any(String, URL), url_type: String, cask_token: String, tap: Tap, # options: T.untyped).void # } - def validate_url_for_https_availability(url_to_check, url_type, cask_token, tap, **options) + def validate_url_for_https_availability(url_to_check, url_type, cask_token, tap, location: nil, **options) problem = curl_check_http_content(url_to_check.to_s, url_type, **options) exception = tap&.audit_exception(:secure_connection_audit_skiplist, cask_token, url_to_check.to_s) if problem - add_error problem unless exception + add_error problem, location: location unless exception elsif exception - add_error "#{url_to_check} is in the secure connection audit skiplist but does not need to be skipped" + add_error "#{url_to_check} is in the secure connection audit skiplist but does not need to be skipped", + location: location end end diff --git a/Library/Homebrew/cask/url.rb b/Library/Homebrew/cask/url.rb index 8e20f401c6..c90cfc4898 100644 --- a/Library/Homebrew/cask/url.rb +++ b/Library/Homebrew/cask/url.rb @@ -1,6 +1,8 @@ # typed: true # frozen_string_literal: true +require "source_location" + module Cask # Class corresponding to the `url` stanza. # @@ -222,20 +224,25 @@ module Cask @dsl = dsl end - sig { returns(T.nilable(String)) } - def raw_interpolated_url - return @raw_interpolated_url if defined?(@raw_interpolated_url) - - @raw_interpolated_url = - Pathname(@caller_location.path) - .each_line.drop(@caller_location.lineno - 1) - .first&.then { |line| line[/url\s+"([^"]+)"/, 1] } + sig { returns(Homebrew::SourceLocation) } + def location + Homebrew::SourceLocation.new(@caller_location.lineno, raw_url_line&.index("url")) end - private :raw_interpolated_url + + sig { returns(T.nilable(String)) } + def raw_url_line + return @raw_url_line if defined?(@raw_url_line) + + @raw_url_line = Pathname(@caller_location.path) + .each_line + .drop(@caller_location.lineno - 1) + .first + end + private :raw_url_line sig { params(ignore_major_version: T::Boolean).returns(T::Boolean) } def unversioned?(ignore_major_version: false) - interpolated_url = raw_interpolated_url + interpolated_url = raw_url_line&.then { |line| line[/url\s+"([^"]+)"/, 1] } return false unless interpolated_url diff --git a/Library/Homebrew/source_location.rb b/Library/Homebrew/source_location.rb new file mode 100644 index 0000000000..6b7bc7e5f6 --- /dev/null +++ b/Library/Homebrew/source_location.rb @@ -0,0 +1,26 @@ +# typed: true +# frozen_string_literal: true + +module Homebrew + # A location in source code. + # + # @api private + class SourceLocation + sig { returns(Integer) } + attr_reader :line + + sig { returns(T.nilable(Integer)) } + attr_reader :column + + sig { params(line: Integer, column: T.nilable(Integer)).void } + def initialize(line, column = T.unsafe(nil)) + @line = line + @column = column + end + + sig { returns(String) } + def to_s + "#{line}#{column&.to_s&.prepend(":")}" + end + end +end diff --git a/Library/Homebrew/style.rb b/Library/Homebrew/style.rb index 79d1a56ed2..1e88837036 100644 --- a/Library/Homebrew/style.rb +++ b/Library/Homebrew/style.rb @@ -2,6 +2,7 @@ # frozen_string_literal: true require "shellwords" +require "source_location" module Homebrew # Helper module for running RuboCop. @@ -311,7 +312,8 @@ module Homebrew @message = json["message"] @cop_name = json["cop_name"] @corrected = json["corrected"] - @location = LineLocation.new(json["location"]) + location = json["location"] + @location = SourceLocation.new(location.fetch("line"), location["column"]) end def severity_code @@ -322,20 +324,5 @@ module Homebrew @corrected end end - - # Source location of a style offense. - class LineLocation - attr_reader :line, :column - - def initialize(json) - @line = json["line"] - @column = json["column"] - end - - sig { returns(String) } - def to_s - "#{line}: col #{column}" - end - end end end