|
| 1 | +# frozen_string_literal: true |
| 2 | + |
| 3 | +require 'rbs' |
| 4 | + |
| 5 | +## |
| 6 | +# Parse RBS signature files as first-class RDoc input. |
| 7 | + |
| 8 | +class RDoc::Parser::RBS < RDoc::Parser |
| 9 | + RBS_FILE_EXTENSION = /\.rbs$/ |
| 10 | + |
| 11 | + parse_files_matching RBS_FILE_EXTENSION |
| 12 | + |
| 13 | + def scan |
| 14 | + _, _, decls = ::RBS::Parser.parse_signature(@content) |
| 15 | + decls.each do |decl| |
| 16 | + parse_decl decl, @top_level |
| 17 | + end |
| 18 | + @top_level |
| 19 | + end |
| 20 | + |
| 21 | + private |
| 22 | + |
| 23 | + def record_object_location(object, location) |
| 24 | + object.line = location.start_line if location |
| 25 | + |
| 26 | + if RDoc::ClassModule === object |
| 27 | + @top_level.add_to_classes_or_modules object unless |
| 28 | + @top_level.classes_or_modules.include? object |
| 29 | + end |
| 30 | + |
| 31 | + object.record_location @top_level |
| 32 | + object |
| 33 | + end |
| 34 | + |
| 35 | + def rdoc_comment_for(decl) |
| 36 | + rbs_comment = decl.comment if decl.respond_to?(:comment) |
| 37 | + return unless rbs_comment |
| 38 | + |
| 39 | + # TODO: Run RBS comments through RDoc's directive preprocessor so |
| 40 | + # directives like :nodoc: affect the documented object. |
| 41 | + comment = RDoc::Comment.new rbs_comment.string, @top_level |
| 42 | + comment.format = 'markdown' |
| 43 | + comment |
| 44 | + end |
| 45 | + |
| 46 | + def local_module_name(type_name, context) |
| 47 | + name = type_name.to_s |
| 48 | + return name if name.start_with?('::') |
| 49 | + |
| 50 | + namespace_names = context == @top_level ? [] : context.full_name.split('::') |
| 51 | + |
| 52 | + namespace_names.length.downto(1) do |length| |
| 53 | + qualified_name = namespace_names.take(length).join('::') |
| 54 | + if module_name = @top_level.find_module_named("#{qualified_name}::#{name}") |
| 55 | + return module_name.full_name |
| 56 | + end |
| 57 | + end |
| 58 | + |
| 59 | + name |
| 60 | + end |
| 61 | + |
| 62 | + def merge_documentation(object, comment, type_signature_lines) |
| 63 | + if comment |
| 64 | + object.comment = if object.comment.empty? |
| 65 | + comment |
| 66 | + else |
| 67 | + merge_comments object, comment |
| 68 | + end |
| 69 | + end |
| 70 | + |
| 71 | + # TODO: Track RBS-owned documentation overlays so incremental reparsing can |
| 72 | + # replace stale comments and signatures from the previous RBS parse. |
| 73 | + object.type_signature_lines ||= type_signature_lines |
| 74 | + end |
| 75 | + |
| 76 | + def merge_comments(object, comment) |
| 77 | + document = RDoc::Markup::Document.new |
| 78 | + document.concat object.parse(object.comment).parts |
| 79 | + document << RDoc::Markup::Rule.new(1) |
| 80 | + document.concat comment.parse.parts |
| 81 | + |
| 82 | + # Keep this text separator in sync with the Rule node above. |
| 83 | + merged_comment = RDoc::Comment.new "#{object.comment}\n---\n#{comment}", comment.location |
| 84 | + merged_comment.format = 'markdown' |
| 85 | + merged_comment.document = document |
| 86 | + merged_comment |
| 87 | + end |
| 88 | + |
| 89 | + def attr_rw_matches?(existing_rw, new_rw) |
| 90 | + existing_rw.each_char.any? { |rw| new_rw.include? rw } |
| 91 | + end |
| 92 | + |
| 93 | + def merge_attribute_methods(context, name, rw, singleton, comment, type_signature_lines) |
| 94 | + method_names = [] |
| 95 | + method_names << name if rw.include?('R') |
| 96 | + method_names << "#{name}=" if rw.include?('W') |
| 97 | + |
| 98 | + methods = method_names.map { |method_name| context.find_method(method_name, singleton) } |
| 99 | + methods.compact.each do |method| |
| 100 | + merge_documentation method, comment, type_signature_lines |
| 101 | + end |
| 102 | + |
| 103 | + methods.any? |
| 104 | + end |
| 105 | + |
| 106 | + def rdoc_method_name(decl) |
| 107 | + rbs_constructor_decl?(decl) ? 'new' : decl.name.to_s |
| 108 | + end |
| 109 | + |
| 110 | + def rdoc_method_singleton?(decl) |
| 111 | + # TODO: RBS `self?` methods are :singleton_instance and should add both a |
| 112 | + # singleton method and a private instance method. |
| 113 | + rbs_constructor_decl?(decl) || decl.singleton? |
| 114 | + end |
| 115 | + |
| 116 | + def rdoc_method_visibility(decl) |
| 117 | + rbs_constructor_decl?(decl) ? :public : decl.visibility |
| 118 | + end |
| 119 | + |
| 120 | + def rbs_constructor_decl?(decl) |
| 121 | + decl.kind == :instance && decl.name == :initialize |
| 122 | + end |
| 123 | + |
| 124 | + def parse_attr_decl(decl, context) |
| 125 | + rw = case decl |
| 126 | + when ::RBS::AST::Members::AttrReader |
| 127 | + 'R' |
| 128 | + when ::RBS::AST::Members::AttrWriter |
| 129 | + 'W' |
| 130 | + when ::RBS::AST::Members::AttrAccessor |
| 131 | + 'RW' |
| 132 | + end |
| 133 | + |
| 134 | + comment = rdoc_comment_for decl |
| 135 | + type_signature_lines = [decl.type.to_s] |
| 136 | + name = decl.name.to_s |
| 137 | + singleton = decl.kind == :singleton |
| 138 | + if attribute = context.find_attribute(name, singleton) |
| 139 | + merge_documentation attribute, comment, type_signature_lines if |
| 140 | + attr_rw_matches? attribute.rw, rw |
| 141 | + return |
| 142 | + end |
| 143 | + |
| 144 | + if merge_attribute_methods(context, name, rw, singleton, comment, type_signature_lines) |
| 145 | + return |
| 146 | + end |
| 147 | + |
| 148 | + attribute = RDoc::Attr.new( |
| 149 | + name, |
| 150 | + rw, |
| 151 | + comment, |
| 152 | + singleton: singleton |
| 153 | + ) |
| 154 | + record_object_location attribute, decl.location |
| 155 | + attribute.type_signature_lines = type_signature_lines |
| 156 | + context.add_attribute attribute |
| 157 | + attribute.visibility = decl.visibility if decl.visibility |
| 158 | + end |
| 159 | + |
| 160 | + def parse_class_decl(decl, context) |
| 161 | + owner, name = context.find_or_create_constant_owner_for_path decl.name |
| 162 | + superclass = decl.super_class&.name&.to_s || '::Object' |
| 163 | + klass = owner.add_class RDoc::NormalClass, name, superclass |
| 164 | + record_object_location klass, decl.location |
| 165 | + comment = rdoc_comment_for decl |
| 166 | + klass.add_comment comment, @top_level if comment |
| 167 | + |
| 168 | + decl.members.each { |member| parse_decl member, klass } |
| 169 | + end |
| 170 | + |
| 171 | + def parse_constant_decl(decl, context) |
| 172 | + constant = RDoc::Constant.new decl.name.to_s, decl.type.to_s, |
| 173 | + rdoc_comment_for(decl) |
| 174 | + record_object_location constant, decl.location |
| 175 | + context.add_constant constant |
| 176 | + end |
| 177 | + |
| 178 | + def parse_decl(decl, context) |
| 179 | + case decl |
| 180 | + when ::RBS::AST::Declarations::Class |
| 181 | + parse_class_decl decl, context |
| 182 | + when ::RBS::AST::Declarations::Module, ::RBS::AST::Declarations::Interface |
| 183 | + parse_module_decl decl, context |
| 184 | + when ::RBS::AST::Declarations::ClassAlias, |
| 185 | + ::RBS::AST::Declarations::ModuleAlias |
| 186 | + # TODO: Add RBS class and module aliases to the RDoc store. |
| 187 | + nil |
| 188 | + else |
| 189 | + parse_member_decl decl, context |
| 190 | + end |
| 191 | + end |
| 192 | + |
| 193 | + def parse_extend_decl(decl, context) |
| 194 | + extend_decl = RDoc::Extend.new local_module_name(decl.name, context), |
| 195 | + rdoc_comment_for(decl) |
| 196 | + record_object_location extend_decl, decl.location |
| 197 | + context.add_extend extend_decl |
| 198 | + end |
| 199 | + |
| 200 | + def parse_include_decl(decl, context) |
| 201 | + include_decl = RDoc::Include.new local_module_name(decl.name, context), |
| 202 | + rdoc_comment_for(decl) |
| 203 | + record_object_location include_decl, decl.location |
| 204 | + context.add_include include_decl |
| 205 | + end |
| 206 | + |
| 207 | + def parse_member_decl(decl, context) |
| 208 | + case decl |
| 209 | + when ::RBS::AST::Declarations::Constant |
| 210 | + parse_constant_decl decl, context |
| 211 | + when ::RBS::AST::Members::MethodDefinition |
| 212 | + parse_method_decl decl, context |
| 213 | + when ::RBS::AST::Members::Alias |
| 214 | + parse_method_alias_decl decl, context |
| 215 | + when ::RBS::AST::Members::AttrReader, |
| 216 | + ::RBS::AST::Members::AttrWriter, |
| 217 | + ::RBS::AST::Members::AttrAccessor |
| 218 | + parse_attr_decl decl, context |
| 219 | + when ::RBS::AST::Members::Include |
| 220 | + parse_include_decl decl, context |
| 221 | + when ::RBS::AST::Members::Extend |
| 222 | + parse_extend_decl decl, context |
| 223 | + when ::RBS::AST::Members::Private, |
| 224 | + ::RBS::AST::Members::Public |
| 225 | + # TODO: Track standalone RBS visibility members. |
| 226 | + nil |
| 227 | + end |
| 228 | + end |
| 229 | + |
| 230 | + def parse_method_alias_decl(decl, context) |
| 231 | + alias_def = RDoc::Alias.new( |
| 232 | + decl.old_name.to_s, |
| 233 | + decl.new_name.to_s, |
| 234 | + rdoc_comment_for(decl), |
| 235 | + singleton: decl.kind == :singleton |
| 236 | + ) |
| 237 | + record_object_location alias_def, decl.location |
| 238 | + context.add_alias alias_def |
| 239 | + end |
| 240 | + |
| 241 | + def parse_method_decl(decl, context) |
| 242 | + comment = rdoc_comment_for decl |
| 243 | + type_signature_lines = decl.overloads.map { |overload| overload.method_type.to_s } |
| 244 | + method_name = rdoc_method_name(decl) |
| 245 | + singleton = rdoc_method_singleton?(decl) |
| 246 | + visibility = rdoc_method_visibility(decl) |
| 247 | + |
| 248 | + if method = context.find_method(method_name, singleton) |
| 249 | + merge_documentation method, comment, type_signature_lines |
| 250 | + return |
| 251 | + end |
| 252 | + |
| 253 | + method = RDoc::AnyMethod.new method_name, singleton: singleton |
| 254 | + record_object_location method, decl.location |
| 255 | + method.type_signature_lines = type_signature_lines |
| 256 | + |
| 257 | + if loc = decl.location |
| 258 | + method.start_collecting_tokens :ruby |
| 259 | + method.add_token line_no: loc.start_line, char_no: 1, text: loc.source |
| 260 | + end |
| 261 | + |
| 262 | + method.comment = comment if comment |
| 263 | + context.add_method method |
| 264 | + method.visibility = visibility if visibility |
| 265 | + end |
| 266 | + |
| 267 | + def parse_module_decl(decl, context) |
| 268 | + mod = context.add_module RDoc::NormalModule, decl.name.to_s |
| 269 | + record_object_location mod, decl.location |
| 270 | + comment = rdoc_comment_for decl |
| 271 | + mod.add_comment comment, @top_level if comment |
| 272 | + |
| 273 | + decl.members.each { |member| parse_decl member, mod } |
| 274 | + end |
| 275 | +end |
0 commit comments