77require "resource_auditor"
88require "utils/shared_audits"
99require "utils/output"
10+ require "utils/git"
1011
1112module Homebrew
1213 # Auditor for checking common violations in {Formula}e.
@@ -39,8 +40,7 @@ def initialize(formula, options = {})
3940 @spdx_license_data = options [ :spdx_license_data ]
4041 @spdx_exception_data = options [ :spdx_exception_data ]
4142 @tap_audit = options [ :tap_audit ]
42- @previous_committed = { }
43- @newest_committed = { }
43+ @committed_version_info_cache = { }
4444 end
4545
4646 def audit_style
@@ -864,42 +864,114 @@ def audit_stable_version
864864 current_version = formula . stable . version
865865 current_version_scheme = formula . version_scheme
866866
867- previous_committed , newest_committed = committed_version_info
867+ previous_version_info , origin_head_version_info = committed_version_info
868868
869- if ! newest_committed [ :version ] . nil? &&
870- current_version < newest_committed [ :version ] &&
871- current_version_scheme == previous_committed [ :version_scheme ]
872- problem "Stable: version should not decrease (from #{ newest_committed [ :version ] } to #{ current_version } )"
869+ if ( origin_head_version = origin_head_version_info [ :version ] ) &&
870+ current_version < origin_head_version &&
871+ current_version_scheme == previous_version_info [ :version_scheme ]
872+ problem "Stable: version should not decrease (from #{ origin_head_version } to #{ current_version } )"
873873 end
874874 end
875875
876876 def audit_revision
877877 new_formula_problem ( "New formulae should not define a revision." ) if @new_formula && !formula . revision . zero?
878878
879879 return unless @git
880- return unless formula . tap # skip formula not from core or any taps
881- return unless formula . tap . git? # git log is required
880+
881+ tap = formula . tap
882+ return if tap . nil?
883+ return unless tap . git?
882884 return if formula . stable . blank?
883885
884886 current_version = formula . stable . version
885887 current_revision = formula . revision
886888
887- previous_committed , newest_committed = committed_version_info
889+ previous_version_info , origin_head_version_info = committed_version_info
890+
891+ previous_version = previous_version_info [ :version ]
892+ previous_revision = previous_version_info [ :revision ]
893+ origin_head_version = origin_head_version_info [ :version ]
894+ origin_head_revision = origin_head_version_info [ :revision ]
888895
889- if ( previous_committed [ :version ] != newest_committed [ :version ] ||
890- current_version != newest_committed [ :version ] ) &&
891- !current_revision . zero? &&
892- current_revision == newest_committed [ :revision ] &&
893- current_revision == previous_committed [ :revision ]
896+ if ( previous_version != origin_head_version || current_version != origin_head_version ) &&
897+ !current_revision . zero? && current_revision == origin_head_revision && current_revision == previous_revision
894898 problem "`revision #{ current_revision } ` should be removed"
895- elsif current_version == previous_committed [ :version ] &&
896- !previous_committed [ :revision ] . nil? &&
897- current_revision < previous_committed [ :revision ]
898- problem "`revision` should not decrease (from #{ previous_committed [ :revision ] } to #{ current_revision } )"
899- elsif newest_committed [ :revision ] &&
900- current_revision > ( newest_committed [ :revision ] + 1 )
899+ elsif current_version == previous_version && previous_revision && current_revision < previous_revision
900+ problem "`revision` should not decrease (from #{ previous_revision } to #{ current_revision } )"
901+ elsif origin_head_revision && current_revision > ( origin_head_revision + 1 )
901902 problem "`revision` should only increment by 1"
902903 end
904+
905+ revision_increment = current_revision - previous_revision . to_i
906+ return if revision_increment != 1
907+
908+ dependency_names = formula . recursive_dependencies . map ( &:name )
909+ return if dependency_names . empty?
910+
911+ changed_dependency_paths = changed_formulae_paths ( tap , only_names : dependency_names )
912+ return if changed_dependency_paths . empty?
913+
914+ missing_compatibility_bumps = changed_dependency_paths . filter_map do |path |
915+ changed_formula = Formulary . factory ( path )
916+ # Each changed dependency must raise its compatibility_version by exactly one.
917+ _ , origin_head_dependency_version_info = committed_version_info ( formula : changed_formula )
918+ previous_compatibility_version = origin_head_dependency_version_info [ :compatibility_version ] || 0
919+ current_compatibility_version = changed_formula . compatibility_version || 0
920+ next if current_compatibility_version == previous_compatibility_version + 1
921+
922+ "#{ changed_formula . name } (#{ previous_compatibility_version } to #{ current_compatibility_version } )"
923+ end
924+ return if missing_compatibility_bumps . empty?
925+
926+ problem "`revision` increased but recursive dependencies must increase `compatibility_version` by 1: " \
927+ "#{ missing_compatibility_bumps . join ( ", " ) } ."
928+ end
929+
930+ def audit_compatibility_version
931+ return unless @git
932+
933+ tap = formula . tap
934+ return if tap . nil?
935+ return unless tap . git?
936+
937+ previous_version_info , origin_head_version_info = committed_version_info
938+ return if origin_head_version_info . empty?
939+
940+ previous_compatibility_version = previous_version_info [ :compatibility_version ] || 0
941+ current_compatibility_version = formula . compatibility_version || previous_compatibility_version
942+
943+ if current_compatibility_version < previous_compatibility_version
944+ problem "`compatibility_version` should not decrease " \
945+ "from #{ previous_compatibility_version } to #{ current_compatibility_version } "
946+ return
947+ elsif current_compatibility_version > ( previous_compatibility_version + 1 )
948+ problem "`compatibility_version` should only increment by 1"
949+ return
950+ end
951+
952+ compatibility_increment = current_compatibility_version - previous_compatibility_version
953+ return if compatibility_increment . zero?
954+
955+ dependent_revision_bumps = changed_formulae_paths ( tap ) . filter_map do |path |
956+ changed_formula = Formulary . factory ( path )
957+ next if changed_formula . name == formula . name
958+
959+ dependencies = changed_formula . recursive_dependencies . map ( &:name )
960+ # Only formulae that depend (recursively) on the audited formula can justify the bump.
961+ next unless dependencies . include? ( formula . name )
962+
963+ _ , origin_head_dependent_version_info = committed_version_info ( formula : changed_formula )
964+ previous_revision = origin_head_dependent_version_info [ :revision ] || 0
965+ current_revision = changed_formula . revision
966+ next if current_revision != previous_revision + 1
967+
968+ changed_formula . name
969+ end
970+ return if dependent_revision_bumps . any?
971+
972+ problem "`compatibility_version` increased from #{ previous_compatibility_version } to " \
973+ "#{ current_compatibility_version } but no recursive dependent formulae increased " \
974+ "`revision` by 1 in this PR."
903975 end
904976
905977 def audit_version_scheme
@@ -910,14 +982,13 @@ def audit_version_scheme
910982
911983 current_version_scheme = formula . version_scheme
912984
913- previous_committed , = committed_version_info
985+ previous_version_info , = committed_version_info
986+ previous_version_scheme = previous_version_info [ :version_scheme ]
987+ return if previous_version_scheme . nil?
914988
915- return if previous_committed [ :version_scheme ] . nil?
916-
917- if current_version_scheme < previous_committed [ :version_scheme ]
918- problem "`version_scheme` should not decrease (from #{ previous_committed [ :version_scheme ] } " \
919- "to #{ current_version_scheme } )"
920- elsif current_version_scheme > ( previous_committed [ :version_scheme ] + 1 )
989+ if current_version_scheme < previous_version_scheme
990+ problem "`version_scheme` should not decrease (from #{ previous_version_scheme } to #{ current_version_scheme } )"
991+ elsif current_version_scheme > ( previous_version_scheme + 1 )
921992 problem "`version_scheme` should only increment by 1"
922993 end
923994 end
@@ -932,12 +1003,13 @@ def audit_unconfirmed_checksum_change
9321003 current_checksum = formula . stable . checksum
9331004 current_url = formula . stable . url
9341005
935- _ , newest_committed = committed_version_info
1006+ _ , origin_head_version_info = committed_version_info
1007+ origin_head_checksum = origin_head_version_info [ :checksum ]
9361008
937- if current_version == newest_committed [ :version ] &&
938- current_url == newest_committed [ :url ] &&
939- current_checksum != newest_committed [ :checksum ] &&
940- current_checksum . present? && newest_committed [ :checksum ] . present?
1009+ if current_version == origin_head_version_info [ :version ] &&
1010+ current_url == origin_head_version_info [ :url ] &&
1011+ current_checksum . present? && origin_head_checksum . present? &&
1012+ current_checksum != origin_head_checksum
9411013 problem (
9421014 "stable sha256 changed without the url/version also changing; " \
9431015 "please create an issue upstream to rule out malicious " \
@@ -1064,12 +1136,47 @@ def linux_only_gcc_dep?(formula)
10641136 true
10651137 end
10661138
1067- def committed_version_info
1068- return [ ] unless @git
1069- return [ ] unless formula . tap # skip formula not from core or any taps
1070- return [ ] unless formula . tap . git? # git log is required
1071- return [ ] if formula . stable . blank?
1072- return [ @previous_committed , @newest_committed ] if @previous_committed . present? || @newest_committed . present?
1139+ sig { params ( tap : Tap , only_names : T ::Array [ String ] ) . returns ( T ::Array [ Pathname ] ) }
1140+ def changed_formulae_paths ( tap , only_names : [ ] . freeze )
1141+ return [ ] unless tap . git?
1142+
1143+ base_ref = "origin/HEAD"
1144+ changed_paths = Utils . safe_popen_read ( Utils ::Git . git , "-C" , tap . path , "diff" , "--name-only" , base_ref )
1145+ . lines
1146+ . filter_map do |line |
1147+ relative_path = line . chomp
1148+ next unless relative_path . end_with? ( ".rb" )
1149+
1150+ absolute_path = tap . path /relative_path
1151+ next unless absolute_path . exist?
1152+ next unless absolute_path . to_s . start_with? ( tap . formula_dir . to_s )
1153+
1154+ absolute_path
1155+ end
1156+ return changed_paths if only_names . blank?
1157+
1158+ expected_paths = only_names . filter_map do |name |
1159+ relative_path = Pathname ( name . to_s . delete_prefix ( "#{ tap . name } /" ) )
1160+ relative_path = relative_path . sub_ext ( ".rb" ) if relative_path . extname . empty?
1161+ absolute_path = tap . formula_dir /relative_path
1162+ absolute_path . expand_path if absolute_path . exist?
1163+ end . map ( &:to_s )
1164+
1165+ changed_paths . select { |path | expected_paths . include? ( path . expand_path . to_s ) }
1166+ end
1167+
1168+ sig { params ( formula : Formula ) . returns ( [ T ::Hash [ Symbol , T . untyped ] , T ::Hash [ Symbol , T . untyped ] ] ) }
1169+ def committed_version_info ( formula : @formula )
1170+ empty_result = [ { } , { } ]
1171+ return empty_result unless @git
1172+ return empty_result unless formula . tap # skip formula not from core or any taps
1173+ return empty_result unless formula . tap . git? # git log is required
1174+ return empty_result if formula . stable . blank?
1175+
1176+ return @committed_version_info_cache [ formula . full_name ] if @committed_version_info_cache . key? ( formula . full_name )
1177+
1178+ previous_version_info = { }
1179+ origin_head_version_info = { }
10731180
10741181 current_version = formula . stable . version
10751182 current_revision = formula . revision
@@ -1081,28 +1188,30 @@ def committed_version_info
10811188 stable = f . stable
10821189 next if stable . blank?
10831190
1084- @previous_committed [ :version ] = stable . version
1085- @previous_committed [ :checksum ] = stable . checksum
1086- @previous_committed [ :version_scheme ] = f . version_scheme
1087- @previous_committed [ :revision ] = f . revision
1088-
1089- @newest_committed [ :version ] ||= @previous_committed [ :version ]
1090- @newest_committed [ :checksum ] ||= @previous_committed [ :checksum ]
1091- @newest_committed [ :revision ] ||= @previous_committed [ :revision ]
1092- @newest_committed [ :url ] ||= stable . url
1191+ previous_version_info [ :version ] = stable . version
1192+ previous_version_info [ :checksum ] = stable . checksum
1193+ previous_version_info [ :revision ] = f . revision
1194+ previous_version_info [ :version_scheme ] = f . version_scheme
1195+ previous_version_info [ :compatibility_version ] = f . compatibility_version
1196+
1197+ origin_head_version_info [ :url ] ||= stable . url
1198+ origin_head_version_info [ :version ] ||= previous_version_info [ :version ]
1199+ origin_head_version_info [ :checksum ] ||= previous_version_info [ :checksum ]
1200+ origin_head_version_info [ :revision ] ||= previous_version_info [ :revision ]
1201+ origin_head_version_info [ :compatibility_version ] ||= previous_version_info [ :compatibility_version ]
10931202 end
10941203 rescue MacOSVersion ::Error
10951204 break
10961205 end
10971206
1098- break if @previous_committed [ :version ] && current_version != @previous_committed [ :version ]
1099- break if @previous_committed [ :revision ] && current_revision != @previous_committed [ :revision ]
1207+ break if previous_version_info [ :version ] && current_version != previous_version_info [ :version ]
1208+ break if previous_version_info [ :revision ] && current_revision != previous_version_info [ :revision ]
11001209 end
11011210
1102- @previous_committed . compact!
1103- @newest_committed . compact!
1211+ previous_version_info . compact!
1212+ origin_head_version_info . compact!
11041213
1105- [ @previous_committed , @newest_committed ]
1214+ @committed_version_info_cache [ formula . full_name ] = [ previous_version_info , origin_head_version_info ]
11061215 end
11071216 end
11081217end
0 commit comments