# typed: strict # frozen_string_literal: true module RuboCop module Cop module Packs # This cop states that public API should live on class methods, which are more easily statically analyzable, # searchable, and typically hold less state. # # Options: # # * `AcceptableParentClasses`: A list of classes that, if inherited from, non-class methods are permitted (useful when value objects are a part of your public API) # # @example # # # bad # # packs/foo/app/public/foo.rb # module Foo # def blah # end # end # # # good # # packs/foo/app/public/foo.rb # module Foo # def self.blah # end # end # class ClassMethodsAsPublicApis < Base extend T::Sig sig { returns(T::Boolean) } def support_autocorrect? false end sig { params(node: T.untyped).void } def on_def(node) # This cop only applies for ruby files in `app/public` return if !processed_source.file_path.include?('app/public') # Looked at https://www.rubydoc.info/gems/rubocop/RuboCop/Cop/Lint/MissingSuper source code as inspiration for htis part. class_node = node.each_ancestor(:class).first module_node = node.each_ancestor(:module).first parent_class = class_node&.parent_class || module_node&.parent acceptable_parent_classes = cop_config['AcceptableParentClasses'] || [] # Used this PR as inspiration to check if we're within a `class << self` block uses_implicit_static_methods = node.each_ancestor(:sclass).first&.identifier&.source == 'self' class_is_allowed_to_have_instance_methods = acceptable_parent_classes.include?(parent_class&.const_name) return if uses_implicit_static_methods || class_is_allowed_to_have_instance_methods add_offense( node.source_range, message: format( "Public API method must be a class method (e.g. `self.#{node.method_name}(...)`)" ) ) end end end end end