|
| 1 | +# typed: strict |
| 2 | +# frozen_string_literal: true |
| 3 | + |
| 4 | +module Tapioca |
| 5 | + module Gem |
| 6 | + module Listeners |
| 7 | + class Documentation < Base |
| 8 | + IGNORED_COMMENTS = [ |
| 9 | + ":doc:", |
| 10 | + ":nodoc:", |
| 11 | + "typed:", |
| 12 | + "frozen_string_literal:", |
| 13 | + "encoding:", |
| 14 | + "warn_indent:", |
| 15 | + "shareable_constant_value:", |
| 16 | + "rubocop:", |
| 17 | + "@requires_ancestor:", |
| 18 | + ] #: Array[String] |
| 19 | + |
| 20 | + #: (Pipeline pipeline, Rubydex::Graph gem_graph) -> void |
| 21 | + def initialize(pipeline, gem_graph) |
| 22 | + super(pipeline) |
| 23 | + |
| 24 | + @gem_graph = gem_graph |
| 25 | + end |
| 26 | + |
| 27 | + private |
| 28 | + |
| 29 | + #: (String line) -> bool |
| 30 | + def rbs_comment?(line) |
| 31 | + line.start_with?(": ", "| ") |
| 32 | + end |
| 33 | + |
| 34 | + # @override |
| 35 | + #: (ConstNodeAdded event) -> void |
| 36 | + def on_const(event) |
| 37 | + event.node.comments = documentation_comments(event.symbol) |
| 38 | + end |
| 39 | + |
| 40 | + # @override |
| 41 | + #: (ScopeNodeAdded event) -> void |
| 42 | + def on_scope(event) |
| 43 | + event.node.comments = documentation_comments(event.symbol) |
| 44 | + end |
| 45 | + |
| 46 | + # @override |
| 47 | + #: (MethodNodeAdded event) -> void |
| 48 | + def on_method(event) |
| 49 | + name = if event.constant.singleton_class? |
| 50 | + "#{event.symbol}::<#{event.symbol.split("::").last}>##{event.node.name}()" |
| 51 | + else |
| 52 | + "#{event.symbol}##{event.node.name}()" |
| 53 | + end |
| 54 | + event.node.comments = documentation_comments(name, sigs: event.node.sigs) |
| 55 | + end |
| 56 | + |
| 57 | + #: (String name, ?sigs: Array[RBI::Sig]) -> Array[RBI::Comment] |
| 58 | + def documentation_comments(name, sigs: []) |
| 59 | + declaration = @gem_graph[name] |
| 60 | + # For attr_writer methods (name ending in =), fall back to reader docs |
| 61 | + if declaration.nil? && name.end_with?("=()") |
| 62 | + declaration = @gem_graph[name.delete_suffix("=()") + "()"] |
| 63 | + end |
| 64 | + # For singleton methods (Class::<Class>#method()), fall back to instance method docs. |
| 65 | + # This handles module_function and extend self methods which Rubydex indexes |
| 66 | + # only under the instance method name. |
| 67 | + if declaration.nil? && name.include?("::<") |
| 68 | + declaration = @gem_graph[name.sub(/::<[^>]+>#/, "#")] |
| 69 | + end |
| 70 | + return [] unless declaration |
| 71 | + |
| 72 | + comments = declaration.definitions.flat_map(&:comments) |
| 73 | + comments.uniq! |
| 74 | + return [] if comments.empty? |
| 75 | + |
| 76 | + lines = comments |
| 77 | + .map { |comment| comment.string.gsub(/^#+ ?/, "") } |
| 78 | + .reject { |line| IGNORED_COMMENTS.any? { |comment| line.include?(comment) } || rbs_comment?(line) } |
| 79 | + |
| 80 | + # Strip leading and trailing blank lines, matching YARD's behavior |
| 81 | + lines = lines.drop_while(&:empty?).reverse.drop_while(&:empty?).reverse |
| 82 | + |
| 83 | + lines.map! { |line| RBI::Comment.new(line) } |
| 84 | + end |
| 85 | + |
| 86 | + # @override |
| 87 | + #: (NodeAdded event) -> bool |
| 88 | + def ignore?(event) |
| 89 | + event.is_a?(Tapioca::Gem::ForeignScopeNodeAdded) |
| 90 | + end |
| 91 | + end |
| 92 | + end |
| 93 | + end |
| 94 | +end |
0 commit comments