From f07a5e25781afb330909d78e1e9c3ca4d59a5648 Mon Sep 17 00:00:00 2001 From: Douglas Eichelberger Date: Wed, 6 Oct 2021 13:09:29 -0700 Subject: [PATCH] Add `mixes_in_class_methods` support --- CHANGELOG.md | 4 ++ README.md | 1 + lib/yard-sorbet/handlers.rb | 2 + lib/yard-sorbet/handlers/include_handler.rb | 37 ++++++++++++++ .../mixes_in_class_methods_handler.rb | 22 ++++++++ spec/data/include_handler.rb | 51 +++++++++++++++++++ .../handlers/include_handler_spec.rb | 28 ++++++++++ 7 files changed, 145 insertions(+) create mode 100644 lib/yard-sorbet/handlers/include_handler.rb create mode 100644 lib/yard-sorbet/handlers/mixes_in_class_methods_handler.rb create mode 100644 spec/data/include_handler.rb create mode 100644 spec/yard_sorbet/handlers/include_handler_spec.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ed14c9c..64f586c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## main + +* [#77](https://github.com/dduugg/yard-sorbet/pull/77) Add `mixes_in_class_methods` support + ## 0.5.3 (2021-08-01) ### Bug fixes diff --git a/README.md b/README.md index f665a1b2..223bdd10 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ A YARD [plugin](https://rubydoc.info/gems/yard/file/docs/GettingStarted.md#Plugi - Generates method definitions from `T::Struct` fields - Generates constant definitions from `T::Enum` enums - Modules marked `abstract!` or `interface!` are tagged `@abstract` +- Modules using `mixes_in_class_methods` will attach class methods ## Usage diff --git a/lib/yard-sorbet/handlers.rb b/lib/yard-sorbet/handlers.rb index 1486d56b..6c250f39 100644 --- a/lib/yard-sorbet/handlers.rb +++ b/lib/yard-sorbet/handlers.rb @@ -9,6 +9,8 @@ module Handlers; end require_relative 'handlers/abstract_dsl_handler' require_relative 'handlers/enums_handler' +require_relative 'handlers/include_handler' +require_relative 'handlers/mixes_in_class_methods_handler' require_relative 'handlers/sig_handler' require_relative 'handlers/struct_class_handler' require_relative 'handlers/struct_prop_handler' diff --git a/lib/yard-sorbet/handlers/include_handler.rb b/lib/yard-sorbet/handlers/include_handler.rb new file mode 100644 index 00000000..86d83f5f --- /dev/null +++ b/lib/yard-sorbet/handlers/include_handler.rb @@ -0,0 +1,37 @@ +# typed: strict +# frozen_string_literal: true + +module YARDSorbet + module Handlers + # Extends any modules included via `mixes_in_class_methods` + # @see https://sorbet.org/docs/abstract#interfaces-and-the-included-hook + # Sorbet `mixes_in_class_methods` documentation + class IncludeHandler < YARD::Handlers::Ruby::Base + extend T::Sig + + handles method_call(:include) + namespace_only + + sig { void } + def process + return unless extra_state.mix_in_class_methods + + statement.parameters(false).each do |mixin| + obj = YARD::CodeObjects::Proxy.new(namespace, mixin.source) + class_methods_namespace = extra_state.mix_in_class_methods[obj.to_s] + next unless class_methods_namespace + + included_in.mixins(:class) << YARD::CodeObjects::Proxy.new(obj, class_methods_namespace) + end + end + + private + + # @return the namespace object that is including the module + sig { returns(YARD::CodeObjects::NamespaceObject) } + def included_in + statement.namespace ? YARD::CodeObjects::Proxy.new(namespace, statement.namespace.source) : namespace + end + end + end +end diff --git a/lib/yard-sorbet/handlers/mixes_in_class_methods_handler.rb b/lib/yard-sorbet/handlers/mixes_in_class_methods_handler.rb new file mode 100644 index 00000000..a9ed10fc --- /dev/null +++ b/lib/yard-sorbet/handlers/mixes_in_class_methods_handler.rb @@ -0,0 +1,22 @@ +# typed: strict +# frozen_string_literal: true + +module YARDSorbet + module Handlers + # Tracks modules that invoke `mixes_in_class_methods` for use in {IncludeHandler} + # @see https://sorbet.org/docs/abstract#interfaces-and-the-included-hook + # Sorbet `mixes_in_class_methods` documentation + class MixesInClassMethodsHandler < YARD::Handlers::Ruby::Base + extend T::Sig + + handles method_call(:mixes_in_class_methods) + namespace_only + + sig { void } + def process + extra_state.mix_in_class_methods ||= {} + extra_state.mix_in_class_methods[namespace.to_s] = statement.parameters(false)[0].source + end + end + end +end diff --git a/spec/data/include_handler.rb b/spec/data/include_handler.rb new file mode 100644 index 00000000..7a543745 --- /dev/null +++ b/spec/data/include_handler.rb @@ -0,0 +1,51 @@ +# typed: true +module M + extend T::Helpers + interface! + + module ClassMethods + extend T::Sig + extend T::Helpers + abstract! + + sig {void} + def foo + bar + end + + sig {abstract.void} + def bar; end + end + + mixes_in_class_methods(ClassMethods) +end + +class A # error: Missing definition for abstract method + include M + + extend T::Sig + + sig {override.void} + def self.bar; end +end + +# Sorbet knows that `foo` is a class method on `A` +A.foo + +module Receiver; end + +Receiver.include(M) + +module OuterModule + module InnerModule + extend T::Helpers + module ClassMethods + def foo; end + end + mixes_in_class_methods(ClassMethods) + end + + class InnerClass + include InnerModule + end +end diff --git a/spec/yard_sorbet/handlers/include_handler_spec.rb b/spec/yard_sorbet/handlers/include_handler_spec.rb new file mode 100644 index 00000000..a01adc8e --- /dev/null +++ b/spec/yard_sorbet/handlers/include_handler_spec.rb @@ -0,0 +1,28 @@ +# typed: strict +# frozen_string_literal: true + +RSpec.describe YARDSorbet::Handlers::IncludeHandler do + path = File.join(File.expand_path('../../data', __dir__), 'include_handler.rb') + + before do + YARD::Registry.clear + YARD::Parser::SourceParser.parse(path) + end + + describe 'including a module with `mixes_in_class_methods`' do + it 'adds the class method namespace to `class_mixins`' do + node = YARD::Registry.at('A') + expect(node.class_mixins.map(&:to_s)).to include('M::ClassMethods') + end + + it 'attches class method namespace to explicit receiver' do + node = YARD::Registry.at('Receiver') + expect(node.class_mixins.map(&:to_s)).to include('M::ClassMethods') + end + + it 'resolves full class method namespace' do + node = YARD::Registry.at('OuterModule::InnerClass') + expect(node.class_mixins.map(&:to_s)).to include('OuterModule::InnerModule::ClassMethods') + end + end +end