lib/rubocop/cop/betterment/internals_protection.rb
# frozen_string_literal: true module RuboCop module Cop module Betterment class InternalsProtection < Base MSG = <<~END.gsub(/\s+/, " ") Internal constants may only be referenced from code within its containing module. Constants defined within a module's Internals submodule may only be referenced by code in that module, or nested classes and modules (e.g. MyModule::Internals::MyClass may only be referenced from code in MyModule or MyModule::MyPublicClass). END # @!method association_with_class_name(node) def_node_matcher :association_with_class_name, <<-PATTERN (send nil? {:has_many :has_one :belongs_to} ... (hash <(pair (sym :class_name) ${str}) ...>)) PATTERN # @!method rspec_describe(node) def_node_matcher :rspec_describe, <<-PATTERN (block (send (const nil? :RSpec) :describe ${const | str} ...) ...) PATTERN def on_const(node) if node.children[1] == :Internals module_path = const_path(node) ensure_allowed_reference!(node, module_path) end end def on_send(node) class_name_node = association_with_class_name(node) return unless class_name_node full_path = string_path(class_name_node) internals_index = full_path.find_index(:Internals) if internals_index module_path = full_path.take(internals_index) ensure_allowed_reference!(class_name_node, module_path) end end alias on_csend on_send private def ensure_allowed_reference!(node, module_path) return if module_path.empty? unless definition_context_path(node).each_cons(module_path.size).any?(module_path) add_offense(node) end end def const_path(const_node) const_node.each_descendant(:const, :cbase).map { |n| n.children[1] }.reverse end def string_path(string_node) string_node.children[0].split('::').map { |name| name == '' ? nil : name.to_sym } end def definition_context_path(node) rspec_context_path(node) || module_class_definition_context_path(node) end def module_class_definition_context_path(node) node.each_ancestor(:class, :module).flat_map { |anc| anc.children[0].each_node(:const, :cbase).map { |c| c.children[1] } }.push(nil).reverse end def rspec_context_path(node) rspec_described_class = node.each_ancestor(:block).filter_map { |ancestor| rspec_describe(ancestor) }.first case rspec_described_class&.type when :const then const_path(rspec_described_class) when :str then string_path(rspec_described_class) else nil # rubocop:disable Style/EmptyElse end end end end end end