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
@@ -858,42 +858,114 @@ def audit_stable_version
858858 current_version = formula . stable . version
859859 current_version_scheme = formula . version_scheme
860860
861- previous_committed , newest_committed = committed_version_info
861+ previous_version_info , origin_head_version_info = committed_version_info
862862
863- if ! newest_committed [ :version ] . nil? &&
864- current_version < newest_committed [ :version ] &&
865- current_version_scheme == previous_committed [ :version_scheme ]
866- problem "Stable: version should not decrease (from #{ newest_committed [ :version ] } to #{ current_version } )"
863+ if ( origin_head_version = origin_head_version_info [ :version ] ) &&
864+ current_version < origin_head_version &&
865+ current_version_scheme == previous_version_info [ :version_scheme ]
866+ problem "Stable: version should not decrease (from #{ origin_head_version } to #{ current_version } )"
867867 end
868868 end
869869
870870 def audit_revision
871871 new_formula_problem ( "New formulae should not define a revision." ) if @new_formula && !formula . revision . zero?
872872
873873 return unless @git
874- return unless formula . tap # skip formula not from core or any taps
875- return unless formula . tap . git? # git log is required
874+
875+ tap = formula . tap
876+ return if tap . nil?
877+ return unless tap . git?
876878 return if formula . stable . blank?
877879
878880 current_version = formula . stable . version
879881 current_revision = formula . revision
880882
881- previous_committed , newest_committed = committed_version_info
883+ previous_version_info , origin_head_version_info = committed_version_info
884+
885+ previous_version = previous_version_info [ :version ]
886+ previous_revision = previous_version_info [ :revision ]
887+ origin_head_version = origin_head_version_info [ :version ]
888+ origin_head_revision = origin_head_version_info [ :revision ]
882889
883- if ( previous_committed [ :version ] != newest_committed [ :version ] ||
884- current_version != newest_committed [ :version ] ) &&
885- !current_revision . zero? &&
886- current_revision == newest_committed [ :revision ] &&
887- current_revision == previous_committed [ :revision ]
890+ if ( previous_version != origin_head_version || current_version != origin_head_version ) &&
891+ !current_revision . zero? && current_revision == origin_head_revision && current_revision == previous_revision
888892 problem "`revision #{ current_revision } ` should be removed"
889- elsif current_version == previous_committed [ :version ] &&
890- !previous_committed [ :revision ] . nil? &&
891- current_revision < previous_committed [ :revision ]
892- problem "`revision` should not decrease (from #{ previous_committed [ :revision ] } to #{ current_revision } )"
893- elsif newest_committed [ :revision ] &&
894- current_revision > ( newest_committed [ :revision ] + 1 )
893+ elsif current_version == previous_version && previous_revision && current_revision < previous_revision
894+ problem "`revision` should not decrease (from #{ previous_revision } to #{ current_revision } )"
895+ elsif origin_head_revision && current_revision > ( origin_head_revision + 1 )
895896 problem "`revision` should only increment by 1"
896897 end
898+
899+ revision_increment = current_revision - previous_revision . to_i
900+ return if revision_increment != 1
901+
902+ dependency_names = formula . recursive_dependencies . map ( &:name )
903+ return if dependency_names . empty?
904+
905+ changed_dependency_paths = changed_formulae_paths ( tap , only_names : dependency_names )
906+ return if changed_dependency_paths . empty?
907+
908+ missing_compatibility_bumps = changed_dependency_paths . filter_map do |path |
909+ changed_formula = Formulary . factory ( path )
910+ # Each changed dependency must raise its compatibility_version by exactly one.
911+ _ , origin_head_dependency_version_info = committed_version_info ( formula : changed_formula )
912+ previous_compatibility_version = origin_head_dependency_version_info [ :compatibility_version ] || 0
913+ current_compatibility_version = changed_formula . compatibility_version || 0
914+ next if current_compatibility_version == previous_compatibility_version + 1
915+
916+ "#{ changed_formula . name } (#{ previous_compatibility_version } to #{ current_compatibility_version } )"
917+ end
918+ return if missing_compatibility_bumps . empty?
919+
920+ problem "`revision` increased but recursive dependencies must increase `compatibility_version` by 1: " \
921+ "#{ missing_compatibility_bumps . join ( ", " ) } ."
922+ end
923+
924+ def audit_compatibility_version
925+ return unless @git
926+
927+ tap = formula . tap
928+ return if tap . nil?
929+ return unless tap . git?
930+
931+ _ , origin_head_version_info = committed_version_info
932+ return if origin_head_version_info . empty?
933+
934+ previous_compatibility_version = origin_head_version_info [ :compatibility_version ] || 0
935+ current_compatibility_version = formula . compatibility_version || previous_compatibility_version
936+
937+ if current_compatibility_version < previous_compatibility_version
938+ problem "`compatibility_version` should not decrease " \
939+ "from #{ previous_compatibility_version } to #{ current_compatibility_version } "
940+ return
941+ elsif current_compatibility_version > ( previous_compatibility_version + 1 )
942+ problem "`compatibility_version` should only increment by 1"
943+ return
944+ end
945+
946+ compatibility_increment = current_compatibility_version - previous_compatibility_version
947+ return if compatibility_increment . zero?
948+
949+ dependent_revision_bumps = changed_formulae_paths ( tap ) . filter_map do |path |
950+ changed_formula = Formulary . factory ( path )
951+ next if changed_formula . name == formula . name
952+
953+ dependencies = changed_formula . recursive_dependencies . map ( &:name )
954+ # Only formulae that depend (recursively) on the audited formula can justify the bump.
955+ next unless dependencies . include? ( formula . name )
956+
957+ _ , origin_head_dependent_version_info = committed_version_info ( formula : changed_formula )
958+ previous_revision = origin_head_dependent_version_info [ :revision ] || 0
959+ current_revision = changed_formula . revision
960+ next if current_revision != previous_revision + 1
961+
962+ changed_formula . name
963+ end
964+ return if dependent_revision_bumps . any?
965+
966+ problem "`compatibility_version` increased from #{ previous_compatibility_version } to " \
967+ "#{ current_compatibility_version } but no recursive dependent formulae increased " \
968+ "`revision` by 1 in this PR."
897969 end
898970
899971 def audit_version_scheme
@@ -904,14 +976,13 @@ def audit_version_scheme
904976
905977 current_version_scheme = formula . version_scheme
906978
907- previous_committed , = committed_version_info
979+ _ , origin_head_version_info = committed_version_info
980+ previous_version_scheme = origin_head_version_info [ :version_scheme ]
981+ return if previous_version_scheme . nil?
908982
909- return if previous_committed [ :version_scheme ] . nil?
910-
911- if current_version_scheme < previous_committed [ :version_scheme ]
912- problem "`version_scheme` should not decrease (from #{ previous_committed [ :version_scheme ] } " \
913- "to #{ current_version_scheme } )"
914- elsif current_version_scheme > ( previous_committed [ :version_scheme ] + 1 )
983+ if current_version_scheme < previous_version_scheme
984+ problem "`version_scheme` should not decrease (from #{ previous_version_scheme } to #{ current_version_scheme } )"
985+ elsif current_version_scheme > ( previous_version_scheme + 1 )
915986 problem "`version_scheme` should only increment by 1"
916987 end
917988 end
@@ -926,12 +997,13 @@ def audit_unconfirmed_checksum_change
926997 current_checksum = formula . stable . checksum
927998 current_url = formula . stable . url
928999
929- _ , newest_committed = committed_version_info
1000+ _ , origin_head_version_info = committed_version_info
1001+ origin_head_checksum = origin_head_version_info [ :checksum ]
9301002
931- if current_version == newest_committed [ :version ] &&
932- current_url == newest_committed [ :url ] &&
933- current_checksum != newest_committed [ :checksum ] &&
934- current_checksum . present? && newest_committed [ :checksum ] . present?
1003+ if current_version == origin_head_version_info [ :version ] &&
1004+ current_url == origin_head_version_info [ :url ] &&
1005+ current_checksum . present? && origin_head_checksum . present? &&
1006+ current_checksum != origin_head_checksum
9351007 problem (
9361008 "stable sha256 changed without the url/version also changing; " \
9371009 "please create an issue upstream to rule out malicious " \
@@ -1058,12 +1130,47 @@ def linux_only_gcc_dep?(formula)
10581130 true
10591131 end
10601132
1061- def committed_version_info
1062- return [ ] unless @git
1063- return [ ] unless formula . tap # skip formula not from core or any taps
1064- return [ ] unless formula . tap . git? # git log is required
1065- return [ ] if formula . stable . blank?
1066- return [ @previous_committed , @newest_committed ] if @previous_committed . present? || @newest_committed . present?
1133+ sig { params ( tap : Tap , only_names : T ::Array [ String ] ) . returns ( T ::Array [ Pathname ] ) }
1134+ def changed_formulae_paths ( tap , only_names : [ ] . freeze )
1135+ return [ ] unless tap . git?
1136+
1137+ base_ref = "origin/HEAD"
1138+ changed_paths = Utils . safe_popen_read ( Utils ::Git . git , "-C" , tap . path , "diff" , "--name-only" , base_ref )
1139+ . lines
1140+ . filter_map do |line |
1141+ relative_path = line . chomp
1142+ next unless relative_path . end_with? ( ".rb" )
1143+
1144+ absolute_path = tap . path /relative_path
1145+ next unless absolute_path . exist?
1146+ next unless absolute_path . to_s . start_with? ( tap . formula_dir . to_s )
1147+
1148+ absolute_path
1149+ end
1150+ return changed_paths if only_names . blank?
1151+
1152+ expected_paths = only_names . filter_map do |name |
1153+ relative_path = Pathname ( name . to_s . delete_prefix ( "#{ tap . name } /" ) )
1154+ relative_path = relative_path . sub_ext ( ".rb" ) if relative_path . extname . empty?
1155+ absolute_path = tap . formula_dir /relative_path
1156+ absolute_path . expand_path if absolute_path . exist?
1157+ end . map ( &:to_s )
1158+
1159+ changed_paths . select { |path | expected_paths . include? ( path . expand_path . to_s ) }
1160+ end
1161+
1162+ sig { params ( formula : Formula ) . returns ( [ T ::Hash [ Symbol , T . untyped ] , T ::Hash [ Symbol , T . untyped ] ] ) }
1163+ def committed_version_info ( formula : @formula )
1164+ empty_result = [ { } , { } ]
1165+ return empty_result unless @git
1166+ return empty_result unless formula . tap # skip formula not from core or any taps
1167+ return empty_result unless formula . tap . git? # git log is required
1168+ return empty_result if formula . stable . blank?
1169+
1170+ return @committed_version_info_cache [ formula . full_name ] if @committed_version_info_cache . key? ( formula . full_name )
1171+
1172+ previous_version_info = { }
1173+ origin_head_version_info = { }
10671174
10681175 current_version = formula . stable . version
10691176 current_revision = formula . revision
@@ -1075,28 +1182,30 @@ def committed_version_info
10751182 stable = f . stable
10761183 next if stable . blank?
10771184
1078- @previous_committed [ :version ] = stable . version
1079- @previous_committed [ :checksum ] = stable . checksum
1080- @previous_committed [ :version_scheme ] = f . version_scheme
1081- @previous_committed [ :revision ] = f . revision
1082-
1083- @newest_committed [ :version ] ||= @previous_committed [ :version ]
1084- @newest_committed [ :checksum ] ||= @previous_committed [ :checksum ]
1085- @newest_committed [ :revision ] ||= @previous_committed [ :revision ]
1086- @newest_committed [ :url ] ||= stable . url
1185+ previous_version_info [ :version ] = stable . version
1186+ previous_version_info [ :checksum ] = stable . checksum
1187+ previous_version_info [ :revision ] = f . revision
1188+ previous_version_info [ :version_scheme ] = f . version_scheme
1189+ previous_version_info [ :compatibility_version ] = f . compatibility_version
1190+
1191+ origin_head_version_info [ :url ] ||= stable . url
1192+ origin_head_version_info [ :version ] ||= previous_version_info [ :version ]
1193+ origin_head_version_info [ :checksum ] ||= previous_version_info [ :checksum ]
1194+ origin_head_version_info [ :revision ] ||= previous_version_info [ :revision ]
1195+ origin_head_version_info [ :compatibility_version ] ||= previous_version_info [ :compatibility_version ]
10871196 end
10881197 rescue MacOSVersion ::Error
10891198 break
10901199 end
10911200
1092- break if @previous_committed [ :version ] && current_version != @previous_committed [ :version ]
1093- break if @previous_committed [ :revision ] && current_revision != @previous_committed [ :revision ]
1201+ break if previous_version_info [ :version ] && current_version != previous_version_info [ :version ]
1202+ break if previous_version_info [ :revision ] && current_revision != previous_version_info [ :revision ]
10941203 end
10951204
1096- @previous_committed . compact!
1097- @newest_committed . compact!
1205+ previous_version_info . compact!
1206+ origin_head_version_info . compact!
10981207
1099- [ @previous_committed , @newest_committed ]
1208+ @committed_version_info_cache [ formula . full_name ] = [ previous_version_info , origin_head_version_info ]
11001209 end
11011210 end
11021211end
0 commit comments