diff --git a/.yardopts b/.yardopts index df787d05..484443cf 100644 --- a/.yardopts +++ b/.yardopts @@ -1,3 +1,4 @@ +--main README.md --private --plugin sorbet lib/**/*.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index 2202fe93..f28a9251 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### New features +* [#49](https://github.com/dduugg/yard-sorbet/issues/49) Apply `@abstract` tags to `abstact!`/`interface!` modules * [#43](https://github.com/dduugg/yard-sorbet/issues/43) Add `T::Enum` support ### Bug fixes diff --git a/README.md b/README.md index bb57b010..df15bfc4 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ A YARD [plugin](https://rubydoc.info/gems/yard/file/docs/GettingStarted.md#Plugi - Translates `sig` type signatures into corresponding YARD tags - Generates method definitions from `T::Struct` fields - Generates constant definitions from `T::Enum` enums +- Modules marked `abstract!` or `interface!` are tagged `@abstract` ## Install diff --git a/lib/yard-sorbet/handlers.rb b/lib/yard-sorbet/handlers.rb index 94b671e6..ba7e71ea 100644 --- a/lib/yard-sorbet/handlers.rb +++ b/lib/yard-sorbet/handlers.rb @@ -5,6 +5,7 @@ # @see https://rubydoc.info/gems/yard/YARD/Handlers/Base YARD Base Handler documentation module YARDSorbet::Handlers; end +require_relative 'handlers/abstract_dsl_handler' require_relative 'handlers/enums_handler' require_relative 'handlers/sig_handler' require_relative 'handlers/struct_handler' diff --git a/lib/yard-sorbet/handlers/abstract_dsl_handler.rb b/lib/yard-sorbet/handlers/abstract_dsl_handler.rb new file mode 100644 index 00000000..6df63e6d --- /dev/null +++ b/lib/yard-sorbet/handlers/abstract_dsl_handler.rb @@ -0,0 +1,26 @@ +# typed: strict +# frozen_string_literal: true + +# Apllies an +@abstract+ tag to +abstract!+/+interface!+ modules (if not alerady present). +class YARDSorbet::Handlers::AbstractDSLHandler < YARD::Handlers::Ruby::Base + extend T::Sig + + handles method_call(:abstract!), method_call(:interface!) + namespace_only + + # The text accompanying the `@abstract` tag. + # @see https://github.com/lsegal/yard/blob/main/templates/default/docstring/html/abstract.erb + # The `@abstract` tag template + TAG_TEXT = 'Subclasses must implement the `abstract` methods below.' + # Extra text for class namespaces + CLASS_TAG_TEXT = T.let("This class cannont be directly instantiated. #{TAG_TEXT}", String) + + sig { void } + def process + return if namespace.has_tag?(:abstract) + + text = namespace.is_a?(YARD::CodeObjects::ClassObject) ? CLASS_TAG_TEXT : TAG_TEXT + tag = YARD::Tags::Tag.new(:abstract, text) + namespace.add_tag(tag) + end +end diff --git a/lib/yard-sorbet/handlers/sig_handler.rb b/lib/yard-sorbet/handlers/sig_handler.rb index 9fb39e91..ad60999b 100644 --- a/lib/yard-sorbet/handlers/sig_handler.rb +++ b/lib/yard-sorbet/handlers/sig_handler.rb @@ -15,7 +15,9 @@ class ParsedSig < T::Struct prop :return, T.nilable(T::Array[String]) end + # Skip these node types when parsing `sig` params PARAM_EXCLUDES = T.let(%i[array call hash].freeze, T::Array[Symbol]) + # Skip these node types when parsing `sig`s SIG_EXCLUDES = T.let(%i[array hash].freeze, T::Array[Symbol]) private_constant :ParsedSig, :PARAM_EXCLUDES, :SIG_EXCLUDES diff --git a/lib/yard-sorbet/sig_to_yard.rb b/lib/yard-sorbet/sig_to_yard.rb index 133da98b..e8a27650 100644 --- a/lib/yard-sorbet/sig_to_yard.rb +++ b/lib/yard-sorbet/sig_to_yard.rb @@ -5,6 +5,7 @@ module YARDSorbet::SigToYARD extend T::Sig + # Ruby 2.5 parsed call nodes slightly differently IS_LEGACY_RUBY_VERSION = T.let(RUBY_VERSION.start_with?('2.5.'), T::Boolean) private_constant :IS_LEGACY_RUBY_VERSION diff --git a/spec/data/abstract_dsl_handler.rb b/spec/data/abstract_dsl_handler.rb new file mode 100644 index 00000000..3f0374da --- /dev/null +++ b/spec/data/abstract_dsl_handler.rb @@ -0,0 +1,35 @@ +# typed: true +# frozen_string_literal: true + +# An abstract class +# @note this class is abstract +class MyAbstractClass + extend T::Helpers + extend T::Sig + + abstract! + + sig { abstract.void } + def abstract_method; end +end + + +module MyInterface + extend T::Helpers + extend T::Sig + + interface! + sig { abstract.returns(T::Boolean) } + def ibool; end +end + + +# @abstract Existing abstract tag +module MyTaggedAbstractModule + extend T::Helpers + extend T::Sig + + abstract! + sig { abstract.returns(T::Boolean) } + def ibool; end +end diff --git a/spec/data/abstract_handler.rb b/spec/data/abstract_handler.rb new file mode 100644 index 00000000..d810fa74 --- /dev/null +++ b/spec/data/abstract_handler.rb @@ -0,0 +1,34 @@ +# typed: true +# frozen_string_literal: true + + +module MyInterface + extend T::Helpers + extend T::Sig + + interface! + sig { abstract.returns(T::Boolean) } + def ibool; end +end + +# An abstract class +# @note this class is abstract +class MyAbstractClass + extend T::Helpers + extend T::Sig + + abstract! + + sig { abstract.void } + def abstract_method; end +end + +# @abstract Existing abstract tag +module MyTaggedAbstractModule + extend T::Helpers + extend T::Sig + + abstract! + sig { abstract.returns(T::Boolean) } + def ibool; end +end diff --git a/spec/yard_sorbet/handlers/abstract_dsl_handler_spec.rb b/spec/yard_sorbet/handlers/abstract_dsl_handler_spec.rb new file mode 100644 index 00000000..c4a82a23 --- /dev/null +++ b/spec/yard_sorbet/handlers/abstract_dsl_handler_spec.rb @@ -0,0 +1,41 @@ +# typed: false +# frozen_string_literal: true + +require 'yard' + +RSpec.describe YARDSorbet::Handlers::AbstractDSLHandler do + path = File.join( + File.expand_path('../../data', __dir__), + 'abstract_dsl_handler.rb' + ) + + before do + YARD::Registry.clear + YARD::Parser::SourceParser.parse(path) + end + + describe 'modules with abstract!/interface! declarations' do + it('apply @abstract tags') do + node = YARD::Registry.at('MyInterface') + expect(node.tags.size).to eq(1) + expect(node.has_tag?(:abstract)).to be(true) + expect(node.tags.first.text).to eq(YARDSorbet::Handlers::AbstractDSLHandler::TAG_TEXT) + end + + it('apply class text to abstract classes') do + node = YARD::Registry.at('MyAbstractClass') + expect(node.docstring).to eq('An abstract class') + expect(node.tags.size).to eq(2) + expect(node.has_tag?(:abstract)).to be(true) + abstract_tag = node.tags.find { |tag| tag.tag_name == 'abstract' } + expect(abstract_tag.text).to eq(YARDSorbet::Handlers::AbstractDSLHandler::CLASS_TAG_TEXT) + end + + it('keep existing @abstract tags') do + node = YARD::Registry.at('MyTaggedAbstractModule') + expect(node.has_tag?(:abstract)).to be(true) + abstract_tag = node.tags.find { |tag| tag.tag_name == 'abstract' } + expect(abstract_tag.text).to eq('Existing abstract tag') + end + end +end