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
@@ -877,8 +877,10 @@ 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
@@ -900,6 +902,81 @@ def audit_revision
900902 current_revision > ( newest_committed [ :revision ] + 1 )
901903 problem "`revision` should only increment by 1"
902904 end
905+
906+ revision_increment = if ( previous_revision = previous_committed [ :revision ] )
907+ current_revision - previous_revision
908+ else
909+ current_revision
910+ end
911+ return if revision_increment != 1
912+
913+ dependency_names = formula . recursive_dependencies . map ( &:name )
914+ return if dependency_names . empty?
915+
916+ changed_dependency_paths = changed_formulae_paths ( tap , only_names : dependency_names )
917+ return if changed_dependency_paths . empty?
918+
919+ missing_compatibility_bumps = changed_dependency_paths . filter_map do |path |
920+ changed_formula = Formulary . factory ( path )
921+ # Each changed dependency must raise its compatibility_version by exactly one.
922+ previous_committed_dependency , = committed_version_info ( formula : changed_formula )
923+ previous_compatibility_version = previous_committed_dependency [ :compatibility_version ] || 0
924+ current_compatibility_version = changed_formula . compatibility_version || 0
925+ next if current_compatibility_version == previous_compatibility_version + 1
926+
927+ "#{ changed_formula . name } (#{ previous_compatibility_version } to #{ current_compatibility_version } )"
928+ end
929+ return if missing_compatibility_bumps . empty?
930+
931+ problem "`revision` increased but recursive dependencies must increase `compatibility_version` by 1: " \
932+ "#{ missing_compatibility_bumps . join ( ", " ) } ."
933+ end
934+
935+ def audit_compatibility_version
936+ return unless @git
937+
938+ tap = formula . tap
939+ return if tap . nil?
940+ return unless tap . git?
941+
942+ previous_committed , = committed_version_info
943+ return if previous_committed . empty?
944+
945+ previous_compatibility_version = previous_committed [ :compatibility_version ] || 0
946+ current_compatibility_version = formula . compatibility_version || previous_compatibility_version
947+
948+ if current_compatibility_version < previous_compatibility_version
949+ problem "`compatibility_version` should not decrease " \
950+ "from #{ previous_compatibility_version } to #{ current_compatibility_version } "
951+ return
952+ elsif current_compatibility_version > ( previous_compatibility_version + 1 )
953+ problem "`compatibility_version` should only increment by 1"
954+ return
955+ end
956+
957+ compatibility_increment = current_compatibility_version - previous_compatibility_version
958+ return if compatibility_increment . zero?
959+
960+ dependent_revision_bumps = changed_formulae_paths ( tap ) . filter_map do |path |
961+ changed_formula = Formulary . factory ( path )
962+ next if changed_formula . name == formula . name
963+
964+ dependencies = changed_formula . recursive_dependencies . map ( &:name )
965+ # Only formulae that depend (recursively) on the audited formula can justify the bump.
966+ next unless dependencies . include? ( formula . name )
967+
968+ previous_committed_dependent , = committed_version_info ( formula : changed_formula )
969+ previous_revision = previous_committed_dependent [ :revision ] || 0
970+ current_revision = changed_formula . revision
971+ next if current_revision != previous_revision + 1
972+
973+ changed_formula . name
974+ end
975+ return if dependent_revision_bumps . any?
976+
977+ problem "`compatibility_version` increased from #{ previous_compatibility_version } to " \
978+ "#{ current_compatibility_version } but no recursive dependent formulae increased " \
979+ "`revision` by 1 in this PR."
903980 end
904981
905982 def audit_version_scheme
@@ -1064,12 +1141,57 @@ def linux_only_gcc_dep?(formula)
10641141 true
10651142 end
10661143
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?
1144+ sig { params ( tap : Tap , only_names : T . nilable ( T ::Array [ String ] ) ) . returns ( T ::Array [ Pathname ] ) }
1145+ def changed_formulae_paths ( tap , only_names : nil )
1146+ return [ ] unless tap . git?
1147+
1148+ base_ref = "origin/HEAD"
1149+ changed_paths = Utils . safe_popen_read ( Utils ::Git . git , "-C" , tap . path , "diff" , "--name-only" , base_ref )
1150+ . lines
1151+ . filter_map do |line |
1152+ relative_path = line . chomp
1153+ next unless relative_path . end_with? ( ".rb" )
1154+
1155+ absolute_path = tap . path /relative_path
1156+ next unless absolute_path . exist?
1157+ next unless absolute_path . to_s . start_with? ( tap . formula_dir . to_s )
1158+
1159+ absolute_path
1160+ end
1161+
1162+ return changed_paths if only_names . blank?
1163+
1164+ expected_paths = only_names . filter_map do |name |
1165+ relative_name = name . to_s . delete_prefix ( "#{ tap . name } /" )
1166+ relative_path = Pathname ( relative_name )
1167+ relative_path = relative_path . sub_ext ( ".rb" ) if relative_path . extname . empty?
1168+ absolute_path = tap . formula_dir /relative_path
1169+ absolute_path . expand_path if absolute_path . exist?
1170+ rescue
1171+ nil
1172+ end . map ( &:to_s )
1173+
1174+ return [ ] if expected_paths . empty?
1175+
1176+ changed_paths . select { |path | expected_paths . include? ( path . expand_path . to_s ) }
1177+ end
1178+
1179+ def committed_version_info ( formula : @formula )
1180+ empty_result = [ { } , { } ]
1181+ return empty_result unless @git
1182+ return empty_result unless formula . tap # skip formula not from core or any taps
1183+ return empty_result unless formula . tap . git? # git log is required
1184+ return empty_result if formula . stable . blank?
1185+
1186+ cache_key = if formula . respond_to? ( :full_name )
1187+ formula . full_name
1188+ else
1189+ formula . name
1190+ end
1191+ return @committed_version_info_cache [ cache_key ] if @committed_version_info_cache . key? ( cache_key )
1192+
1193+ previous_committed = { }
1194+ newest_committed = { }
10731195
10741196 current_version = formula . stable . version
10751197 current_revision = formula . revision
@@ -1081,28 +1203,30 @@ def committed_version_info
10811203 stable = f . stable
10821204 next if stable . blank?
10831205
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
1206+ previous_committed [ :version ] = stable . version
1207+ previous_committed [ :checksum ] = stable . checksum
1208+ previous_committed [ :version_scheme ] = f . version_scheme
1209+ previous_committed [ :revision ] = f . revision
1210+ previous_committed [ :compatibility_version ] = f . compatibility_version
1211+
1212+ newest_committed [ :version ] ||= previous_committed [ :version ]
1213+ newest_committed [ :checksum ] ||= previous_committed [ :checksum ]
1214+ newest_committed [ :revision ] ||= previous_committed [ :revision ]
1215+ newest_committed [ :url ] ||= stable . url
1216+ newest_committed [ :compatibility_version ] ||= previous_committed [ :compatibility_version ]
10931217 end
10941218 rescue MacOSVersion ::Error
10951219 break
10961220 end
10971221
1098- break if @ previous_committed[ :version ] && current_version != @ previous_committed[ :version ]
1099- break if @ previous_committed[ :revision ] && current_revision != @ previous_committed[ :revision ]
1222+ break if previous_committed [ :version ] && current_version != previous_committed [ :version ]
1223+ break if previous_committed [ :revision ] && current_revision != previous_committed [ :revision ]
11001224 end
11011225
1102- @ previous_committed. compact!
1103- @ newest_committed. compact!
1226+ previous_committed . compact!
1227+ newest_committed . compact!
11041228
1105- [ @ previous_committed, @ newest_committed]
1229+ @committed_version_info_cache [ cache_key ] = [ previous_committed , newest_committed ]
11061230 end
11071231 end
11081232end
0 commit comments