class RDoc::Parser::PrismRuby
Parse and collect document from Ruby source code. RDoc::Parser::PrismRuby is compatible with RDoc::Parser::Ruby and aims to replace it.
Constants
- Nesting
-
Nestinginformation container: ClassModule or TopLevel singleton: true(container is a singleton class) or false nodoc: true(in shallow nodoc) or false state: :startdoc, :stopdoc, :enddoc visibility: :public, :private, :protected block_level: block nesting level within current container. > 0 means in block
Public Class Methods
Source
# File lib/rdoc/parser/prism_ruby.rb, line 26 def initialize(top_level, content, options, stats) super content = handle_tab_width(content) @size = 0 @token_listeners = nil content = RDoc::Encoding.remove_magic_comment content @content = content @markup = @options.markup @track_visibility = :nodoc != @options.visibility @encoding = @options.encoding # Names of constant/class/module marked as nodoc in this file local scope @file_local_nodoc_names = Set.new # Represent module_nesting, visibility, block nesting level and startdoc/stopdoc/enddoc/nodoc for each module_nesting @nestings = [Nesting.new(top_level, false, 0, :public, false, :startdoc)] end
RDoc::Parser::new
Public Instance Methods
Source
# File lib/rdoc/parser/prism_ruby.rb, line 552 def add_alias_method(old_name, new_name, line_no) comment, directives = consecutive_comment(line_no) apply_document_control_directive(directives) if directives handle_code_object_directives(current_container, directives) if directives visibility = current_container.find_method(old_name, singleton?)&.visibility || :public a = RDoc::Alias.new(nil, old_name, new_name, comment, singleton: singleton?) modifier_nodoc = handle_modifier_directive(a, line_no) return unless container_accept_document?(current_container) && !(@track_visibility && modifier_nodoc) a.store = @store a.line = line_no mark_container_documentable(current_container) record_location(a) current_container.add_alias(a) current_container.find_method(new_name, singleton?)&.visibility = visibility end
Handles ‘alias foo bar` and `alias_method :foo, :bar`
Source
# File lib/rdoc/parser/prism_ruby.rb, line 572 def add_attributes(names, rw, line_no) comment, directives = consecutive_comment(line_no) apply_document_control_directive(directives) if directives handle_code_object_directives(current_container, directives) if directives return unless container_accept_document?(current_container) names.each do |symbol| a = RDoc::Attr.new(nil, symbol.to_s, rw, comment, singleton: singleton?) a.store = @store a.line = line_no modifier_nodoc = handle_modifier_directive(a, line_no) next if @track_visibility && modifier_nodoc record_location(a) current_container.add_attribute(a) mark_container_documentable(current_container) a.visibility = current_visibility # should set after adding to container end end
Handles ‘attr :a, :b`, `attr_reader :a, :b`, `attr_writer :a, :b` and `attr_accessor :a, :b`
Source
# File lib/rdoc/parser/prism_ruby.rb, line 771 def add_constant(constant_name, rhs_name, start_line, end_line) comment, directives = consecutive_comment(start_line) apply_document_control_directive(directives) if directives handle_code_object_directives(current_container, directives) if directives owner, name = find_or_create_constant_owner_name(constant_name) return unless owner constant = RDoc::Constant.new(name, rhs_name, comment) constant.store = @store constant.line = start_line constant.parent = owner modifier_nodocs = [ handle_modifier_directive(constant, start_line), handle_modifier_directive(constant, end_line) ].compact if @track_visibility && modifier_nodocs.include?(:nodoc_all) constant.document_self = nil owner.add_constant(constant) elsif @track_visibility && modifier_nodocs.include?(:nodoc) locally_mark_const_name_as_nodoc(constant.full_name) constant.ignore elsif container_accept_document?(owner) mark_container_documentable(owner) record_location(constant) else constant.ignore end owner.add_constant(constant) mod = if rhs_name =~ /^::/ @store.find_class_or_module(rhs_name) else full_name = resolve_constant_path(rhs_name) @store.find_class_or_module(full_name) end if mod a = current_container.add_module_alias(mod, rhs_name, constant, @top_level) a.store = @store a.line = start_line record_location(a) end end
Adds a constant
Source
# File lib/rdoc/parser/prism_ruby.rb, line 626 def add_method(method_name, receiver_name:, receiver_fallback_type:, visibility:, singleton:, params:, calls_super:, block_params:, tokens:, start_line:, args_end_line:, end_line:) receiver = receiver_name ? find_or_create_module_path(receiver_name, receiver_fallback_type) : current_container comment, directives = consecutive_comment(start_line) apply_document_control_directive(directives) if directives handle_code_object_directives(current_container, directives) if directives internal_add_method( method_name, receiver, comment: comment, directives: directives, modifier_comment_lines: [start_line, args_end_line, end_line].uniq, line_no: start_line, visibility: visibility, singleton: singleton, params: params, calls_super: calls_super, block_params: block_params, tokens: tokens ) end
Adds a method defined by def syntax
Source
# File lib/rdoc/parser/prism_ruby.rb, line 818 def add_module_or_class(module_name, start_line, end_line, is_class: false, superclass_name: nil, superclass_expr: nil) comment, directives = consecutive_comment(start_line) apply_document_control_directive(directives) if directives handle_code_object_directives(current_container, directives) if directives owner, name = find_or_create_constant_owner_name(module_name) return unless owner if is_class # RDoc::NormalClass resolves superclass name despite of the lack of module nesting information. # We need to fix it when RDoc::NormalClass resolved to a wrong constant name if superclass_name superclass_full_path = resolve_constant_path(superclass_name) superclass = @store.find_class_or_module(superclass_full_path) if superclass_full_path superclass_full_path ||= superclass_name superclass_full_path = superclass_full_path.sub(/^::/, '') end # add_class should be done after resolving superclass mod = owner.classes_hash[name] unless mod mod = owner.add_class(RDoc::NormalClass, name, superclass_name || superclass_expr || '::Object') mod.ignore end if superclass_name if superclass mod.superclass = superclass elsif (mod.superclass.is_a?(String) || mod.superclass.name == 'Object') && mod.superclass != superclass_full_path mod.superclass = superclass_full_path end end else mod = owner.modules_hash[name] unless mod mod = owner.add_module(RDoc::NormalModule, name) mod.ignore end end mod.store = @store mod.line = start_line modifier_nodocs = [ handle_modifier_directive(mod, start_line), handle_modifier_directive(mod, end_line) ] nodoc = false if @track_visibility && modifier_nodocs.include?(:nodoc_all) mod.document_self = nil nodoc = true elsif @track_visibility && modifier_nodocs.include?(:nodoc) locally_mark_const_name_as_nodoc(mod.full_name) nodoc = true elsif container_accept_document?(owner) && !locally_marked_as_nodoc?(mod) mark_container_documentable(owner) mark_container_documentable(mod) record_location(mod) mod.add_comment(comment, @top_level) if comment end [mod, nodoc] end
Adds module or class
Source
# File lib/rdoc/parser/prism_ruby.rb, line 163 def apply_document_control_directive(directives) directives.each do |key, (value, _loc)| case key when 'startdoc', 'stopdoc' state = key.to_sym if current_nesting.doc_state == state || current_nesting.doc_state == :enddoc warn "Already in :#{state}: state, ignoring" else current_nesting.doc_state = state end when 'enddoc' if current_nesting.doc_state == :enddoc warn "Already in :enddoc: state, ignoring" else current_nesting.doc_state = :enddoc end when 'nodoc' if value == 'all' current_nesting.doc_state = :enddoc current_nesting.nodoc = true # Globally mark container as nodoc current_container.document_self = nil elsif current_nesting.nodoc warn "Already in :nodoc: state, ignoring" elsif current_nesting.doc_state == :enddoc warn "Already in :enddoc: state, ignoring" else # Mark this shallow scope as nodoc: methods and constants are not documented current_nesting.nodoc = true # And mark this scope as enddoc: nested containers are not documented current_nesting.doc_state = :enddoc # Mark container as nodoc in this file. When this container is reopened later, # `nodoc!` will be applied again but `enddoc!` will not be applied. locally_mark_const_name_as_nodoc(current_container.full_name) unless current_container.is_a?(RDoc::TopLevel) end end end end
Apply document control directive such as :startdoc:, :stopdoc: and :enddoc: to the current container
Source
# File lib/rdoc/parser/prism_ruby.rb, line 523 def change_method_to_module_function(names) current_container.set_visibility_for(names, :private, false) new_methods = [] current_container.methods_matching(names) do |m| s_m = m.dup record_location(s_m) s_m.singleton = true new_methods << s_m end new_methods.each do |method| case method when RDoc::AnyMethod then current_container.add_method(method) when RDoc::Attr then current_container.add_attribute(method) end method.visibility = :public end end
Handles ‘module_function :foo, :bar`
Source
# File lib/rdoc/parser/prism_ruby.rb, line 499 def change_method_visibility(names, visibility, singleton: singleton?) new_methods = [] current_container.methods_matching(names, singleton) do |m| if m.parent != current_container m = m.dup record_location(m) new_methods << m else m.visibility = visibility end end new_methods.each do |method| case method when RDoc::AnyMethod then current_container.add_method(method) when RDoc::Attr then current_container.add_attribute(method) end method.visibility = visibility end end
Handles ‘public :foo, :bar` `private :foo, :bar` and `protected :foo, :bar`
Source
# File lib/rdoc/parser/prism_ruby.rb, line 446 def consecutive_comment(line_no) return unless @unprocessed_comments.first&.first == line_no _line_no, start_line, text = @unprocessed_comments.shift parse_comment_text_to_directives(text, start_line) end
Returns consecutive comment linked to the given line number
Source
# File lib/rdoc/parser/prism_ruby.rb, line 60 def current_container current_nesting.container end
Current container code object (ClassModule or TopLevel) being processed
Source
# File lib/rdoc/parser/prism_ruby.rb, line 77 def current_visibility current_nesting.visibility end
Current method visibility (:public, :private, :protected)
Source
# File lib/rdoc/parser/prism_ruby.rb, line 81 def current_visibility=(v) current_nesting.visibility = v end
Source
# File lib/rdoc/parser/prism_ruby.rb, line 755 def find_or_create_constant_owner_name(constant_path) const_path, colon, name = constant_path.rpartition('::') if colon.empty? # class Foo # Within `class C` or `module C`, owner is C(== current container) # Within `class <<C`, owner is C.singleton_class # but RDoc don't track constants of a singleton class of module [(singleton? ? nil : current_container), name] elsif const_path.empty? # class ::Foo [@top_level, name] else # `class Foo::Bar` or `class ::Foo::Bar` [find_or_create_module_path(const_path, :module), name] end end
Returns a pair of owner module and constant name from a given constant path. Creates owner module if it does not exist.
Source
# File lib/rdoc/parser/prism_ruby.rb, line 699 def find_or_create_module_path(module_name, create_mode) root_name, *path, name = module_name.split('::') # Creates intermediate modules/classes if they do not exist. # The created module may not be documented if it does not have comment nor documentable children. add_module = ->(mod, name, mode) { created = case mode when :class mod.add_class(RDoc::NormalClass, name, 'Object').tap { |m| m.store = @store } when :module mod.add_module(RDoc::NormalModule, name).tap { |m| m.store = @store } end # Set to true later if this module receives comment or documentable children created.ignore created } if root_name.empty? mod = @top_level else @nestings.reverse_each do |nesting| next if nesting.singleton mod = nesting.container.get_module_named(root_name) break if mod # If a constant is found and it is not a module or class, RDoc can't document about it. # Return an anonymous module to avoid wrong document creation. return RDoc::NormalModule.new(nil) if nesting.container.find_constant_named(root_name) end last_nesting = @nestings.reverse_each.find { |nesting| !nesting.singleton } return mod || add_module.call(last_nesting.container, root_name, create_mode) unless name mod ||= add_module.call(last_nesting.container, root_name, :module) end path.each do |name| mod = mod.get_module_named(name) || add_module.call(mod, name, :module) end mod.get_module_named(name) || add_module.call(mod, name, create_mode) end
Find or create module or class from a given module name. If module or class does not exist, creates a module or a class according to create_mode argument.
Source
# File lib/rdoc/parser/prism_ruby.rb, line 335 def handle_meta_method_comment(comment, directives, node) apply_document_control_directive(directives) handle_code_object_directives(current_container, directives) is_call_node = node.is_a?(Prism::CallNode) singleton_method = false visibility = current_visibility attributes = rw = line_no = method_name = nil directives.each do |directive, (param, line)| case directive when 'attr', 'attr_reader', 'attr_writer', 'attr_accessor' attributes = [param] if param attributes ||= call_node_name_arguments(node) if is_call_node rw = directive == 'attr_writer' ? 'W' : directive == 'attr_accessor' ? 'RW' : 'R' when 'method' method_name = param if param line_no = line when 'singleton-method' method_name = param if param line_no = line singleton_method = true visibility = :public end end return unless container_accept_document?(current_container) if attributes attributes.each do |attr| a = RDoc::Attr.new(current_container, attr, rw, comment, singleton: singleton?) a.store = @store a.line = line_no record_location(a) current_container.add_attribute(a) mark_container_documentable(current_container) a.visibility = visibility end elsif line_no || node method_name ||= call_node_name_arguments(node).first if is_call_node if node tokens = visible_tokens_from_location(node.location) line_no = node.location.start_line else tokens = [file_line_comment_token(line_no)] end internal_add_method( method_name, current_container, comment: comment, directives: directives, dont_rename_initialize: false, line_no: line_no, visibility: visibility, singleton: singleton? || singleton_method, params: nil, calls_super: false, block_params: nil, tokens: tokens, ) end end
Handles meta method comments
Source
# File lib/rdoc/parser/prism_ruby.rb, line 72 def in_proc_block? current_nesting.block_level > 0 end
Returns true if currently inside a proc or block When true, self may not be the current container
Source
# File lib/rdoc/parser/prism_ruby.rb, line 46 def locally_mark_const_name_as_nodoc(const_name) @file_local_nodoc_names << const_name end
Mark the given const/class/module full name as nodoc in current file local scope.
Source
# File lib/rdoc/parser/prism_ruby.rb, line 51 def locally_marked_as_nodoc?(container) @file_local_nodoc_names.include?(container.full_name) end
Returns true if the given container is marked as nodoc in current file local scope.
Source
# File lib/rdoc/parser/prism_ruby.rb, line 90 def mark_container_documentable(container) return if container.received_nodoc || !container.ignored? record_location(container) container.start_doc mark_container_documentable(container.parent) if container.parent.is_a?(RDoc::ClassModule) end
Mark this container as documentable. When creating a container within nodoc scope, or creating intermediate modules when reached ‘class A::Intermediate::D`, the created container is marked as ignored. Documentable or not will be determined later. It may be undocumented if the container doesn’t have any comment or documentable children, and will be documentable when receiving comment or documentable children later.
Source
# File lib/rdoc/parser/prism_ruby.rb, line 281 def parse_comment_tomdoc(container, comment, line_no, start_line) return unless signature = RDoc::TomDoc.signature(comment) name, = signature.split %r%[ \(]%, 2 meth = RDoc::GhostMethod.new comment.text, name record_location(meth) meth.line = start_line meth.call_seq = signature return unless meth.name meth.start_collecting_tokens(:ruby) node = @line_nodes[line_no] tokens = node ? visible_tokens_from_location(node.location) : [file_line_comment_token(start_line)] tokens.each { |token| meth.token_stream << token } container.add_method meth meth.comment = comment @stats.add_method meth end
Creates an RDoc::Method on container from comment if there is a Signature section in the comment
Source
# File lib/rdoc/parser/prism_ruby.rb, line 231 def prepare_comments(comments) current = [] consecutive_comments = [current] @modifier_comments = {} comments.each do |comment| if comment.is_a? Prism::EmbDocComment consecutive_comments << [comment] << (current = []) elsif comment.location.start_line_slice.match?(/\S/) text = comment.slice text = RDoc::Encoding.change_encoding(text, @encoding) if @encoding @modifier_comments[comment.location.start_line] = text elsif current.empty? || current.last.location.end_line + 1 == comment.location.start_line current << comment else consecutive_comments << (current = [comment]) end end consecutive_comments.reject!(&:empty?) # Example: line_no = 5, start_line = 2, comment_text = "# comment_start_line\n# comment\n" # 1| class A # 2| # comment_start_line # 3| # comment # 4| # 5| def f; end # comment linked to this line # 6| end @unprocessed_comments = consecutive_comments.map! do |comments| start_line = comments.first.location.start_line line_no = comments.last.location.end_line + (comments.last.location.end_column == 0 ? 0 : 1) texts = comments.map do |c| c.is_a?(Prism::EmbDocComment) ? c.slice.lines[1...-1].join : c.slice end text = texts.join("\n") text = RDoc::Encoding.change_encoding(text, @encoding) if @encoding line_no += 1 while @lines[line_no - 1]&.match?(/\A\s*$/) [line_no, start_line, text] end # The first comment is special. It defines markup for the rest of the comments. _, first_comment_start_line, first_comment_text = @unprocessed_comments.first if first_comment_text && @lines[0...first_comment_start_line - 1].all? { |l| l.match?(/\A\s*$/) } _text, directives = @preprocess.parse_comment(first_comment_text, first_comment_start_line, :ruby) markup, = directives['markup'] @markup = markup.downcase if markup end end
Prepares comments for processing. Comments are grouped into consecutive. Consecutive comment is linked to the next non-blank line.
Example:
01| class A # modifier comment 1 02| def foo; end # modifier comment 2 03| 04| # consecutive comment 1 start_line: 4 05| # consecutive comment 1 linked to line: 7 06| 07| # consecutive comment 2 start_line: 7 08| # consecutive comment 2 linked to line: 10 09| 10| def bar; end # consecutive comment 2 linked to this line 11| end
Source
# File lib/rdoc/parser/prism_ruby.rb, line 421 def process_comments_until(line_no_until) while !@unprocessed_comments.empty? && @unprocessed_comments.first[0] <= line_no_until line_no, start_line, text = @unprocessed_comments.shift if @markup == 'tomdoc' comment = RDoc::Comment.new(text, @top_level, :ruby) comment.format = 'tomdoc' parse_comment_tomdoc(current_container, comment, line_no, start_line) @preprocess.run_post_processes(comment, current_container) elsif (comment_text, directives = parse_comment_text_to_directives(text, start_line)) handle_standalone_consecutive_comment_directive(comment_text, directives, text.start_with?(/#\#$/), line_no, start_line) end end end
Processes consecutive comments that were not linked to any documentable code until the given line number
Source
# File lib/rdoc/parser/prism_ruby.rb, line 739 def resolve_constant_path(constant_path) owner_name, path = constant_path.split('::', 2) return constant_path if owner_name.empty? # ::Foo, ::Foo::Bar mod = nil @nestings.reverse_each do |nesting| next if nesting.singleton mod = nesting.container.get_module_named(owner_name) break if mod end mod ||= @top_level.get_module_named(owner_name) [mod.full_name, path].compact.join('::') if mod end
Resolves constant path to a full path by searching module nesting
Source
# File lib/rdoc/parser/prism_ruby.rb, line 143 def scan @tokens = RDoc::Parser::RipperStateLex.parse(@content) @lines = @content.lines result = Prism.parse(@content) @program_node = result.value @line_nodes = {} prepare_line_nodes(@program_node) prepare_comments(result.comments) return if @top_level.done_documenting @first_non_meta_comment_start_line = nil if (_line_no, start_line = @unprocessed_comments.first) @first_non_meta_comment_start_line = start_line if start_line < @program_node.location.start_line end @program_node.accept(RDocVisitor.new(self, @top_level, @store)) process_comments_until(@lines.size + 1) end
Scans this Ruby file for Ruby constructs
Source
# File lib/rdoc/parser/prism_ruby.rb, line 66 def singleton? current_nesting.singleton end
Returns true if current container is a singleton class False when in a normal class/module class A; end, true when in a singleton class class << A; end
Source
# File lib/rdoc/parser/prism_ruby.rb, line 438 def skip_comments_until(line_no_until) while !@unprocessed_comments.empty? && @unprocessed_comments.first[0] <= line_no_until @unprocessed_comments.shift end end
Skips all undocumentable consecutive comments until the given line number. Undocumentable comments are comments written inside def or inside undocumentable class/module
Source
# File lib/rdoc/parser/prism_ruby.rb, line 486 def visible_tokens_from_location(location) position_comment = file_line_comment_token(location.start_line) newline_token = RDoc::Parser::RipperStateLex::Token.new(0, 0, :on_nl, "\n") indent_token = RDoc::Parser::RipperStateLex::Token.new(location.start_line, 0, :on_sp, ' ' * location.start_character_column) tokens = slice_tokens( [location.start_line, location.start_character_column], [location.end_line, location.end_character_column] ) [position_comment, newline_token, indent_token, *tokens] end
Returns tokens from the given location
Source
# File lib/rdoc/parser/prism_ruby.rb, line 113 def with_container(container, singleton: false) nesting = current_nesting nodoc = locally_marked_as_nodoc?(container) || container.received_nodoc @nestings << Nesting.new( container, singleton, 0, :public, nodoc, # Set to true if container is marked as nodoc file-locally or globally. Not inherited from parene nesting. nesting.doc_state # state(stardoc/stopdoc/enddoc) is inherited ) yield container ensure @nestings.pop end
Dive into another container
Source
# File lib/rdoc/parser/prism_ruby.rb, line 105 def with_in_proc_block current_nesting.block_level += 1 yield current_nesting.block_level -= 1 end
Suppress extend and include within block because they might be a metaprogramming block example: ‘Module.new { include M }` `M.module_eval { include N }`