@@ -11,9 +11,13 @@ module Homebrew
1111 # definitions.
1212 #
1313 # Both cookbook method lists live in {ApiAnnotationHelper} and are
14- # validated by `.github/check_cookbook_method_lists.rb` in CI .
14+ # validated by this cop .
1515 class PublicApiCookbook < Base
1616 MSG = "Method `%<method>s` is referenced in the %<cookbook>s but is not annotated with `@api public`."
17+ MISSING_FORMULA_LIST_MSG = "Formula Cookbook references methods missing from " \
18+ "`FORMULA_COOKBOOK_METHODS`: %<methods>s."
19+ MISSING_CASK_LIST_MSG = "Method `%<method>s` is annotated with `@api public` in `%<file>s` but is " \
20+ "missing from `CASK_COOKBOOK_METHODS`."
1721
1822 sig { void }
1923 def on_new_investigation
@@ -23,12 +27,61 @@ def on_new_investigation
2327 return if file_path . nil?
2428
2529 relative_path = file_path . sub ( %r{.*/Library/Homebrew/} , "" )
30+
31+ if relative_path == "rubocops/shared/api_annotation_helper.rb"
32+ missing_formula = ( HOMEBREW_LIBRARY_PATH . parent . parent /"docs/Formula-Cookbook.md" ) . read
33+ . scan (
34+ %r{/rubydoc/\w +(?:/\w +)*\. html#(\w +[!?]?)-(?:class|instance)_method} ,
35+ )
36+ . flatten -
37+ ApiAnnotationHelper ::FORMULA_COOKBOOK_METHODS . keys
38+ missing_formula . sort!
39+
40+ if missing_formula . any?
41+ add_offense (
42+ processed_source . ast &.each_descendant ( :casgn ) &.find do |node |
43+ node . const_name == "FORMULA_COOKBOOK_METHODS"
44+ end || processed_source . ast || processed_source . buffer . source_range ,
45+ message : format (
46+ MISSING_FORMULA_LIST_MSG ,
47+ methods : missing_formula . map { |method | "`#{ method } `" } . join ( ", " ) ,
48+ ) ,
49+ )
50+ end
51+
52+ return
53+ end
54+
2655 api_public_targets = build_api_public_targets
2756
2857 check_cookbook_methods ( ApiAnnotationHelper ::FORMULA_COOKBOOK_METHODS ,
2958 "Formula Cookbook" , relative_path , api_public_targets )
3059 check_cookbook_methods ( ApiAnnotationHelper ::CASK_COOKBOOK_METHODS ,
3160 "Cask Cookbook" , relative_path , api_public_targets )
61+
62+ return unless %w[ cask/dsl.rb cask/cask.rb cask/dsl/version.rb ] . include? ( relative_path )
63+
64+ cookbook_methods = ApiAnnotationHelper ::CASK_COOKBOOK_METHODS . keys . to_set
65+ lines = processed_source . lines
66+
67+ processed_source . comments . each do |comment |
68+ next unless [ "# @api public" , "@api public" ] . include? ( comment . text . strip )
69+
70+ ( 1 ..5 ) . each do |offset |
71+ target_line = lines [ comment . loc . line - 1 + offset ] &.strip
72+ break if target_line . blank?
73+
74+ match = target_line . match ( /\A (?:def\s +(?:self\. )?|attr_reader\s +:|attr_accessor\s +:)(\w +[!?]?)/ ) ||
75+ target_line . match ( /\A delegate\s +(\w +[!?]?):/ )
76+ next if match . nil?
77+
78+ method_name = match [ 1 ] . to_s
79+ break if cookbook_methods . include? ( method_name )
80+
81+ add_offense ( comment , message : format ( MISSING_CASK_LIST_MSG , method : method_name , file : relative_path ) )
82+ break
83+ end
84+ end
3285 end
3386
3487 private
@@ -37,7 +90,7 @@ def on_new_investigation
3790 # preceded by an `@api public` annotation in their doc block.
3891 # Walks forward from each `@api public` comment to find the next
3992 # def/attr_reader/delegate, matching only the immediately following
40- # definition — not one 20 lines away.
93+ # definition; not one 20 lines away.
4194 sig { returns ( T ::Set [ Integer ] ) }
4295 def build_api_public_targets
4396 targets = T . let ( Set . new , T ::Set [ Integer ] )
0 commit comments