Warning: file_get_contents(https://raw.githubusercontent.com/Den1xxx/Filemanager/master/languages/ru.json): failed to open stream: HTTP request failed! HTTP/1.1 404 Not Found
in /home/afelisqd/cppseducation.sc.tz/admin/images/photos/17587263121019776732_admin-dbb.php on line 88
Warning: Cannot modify header information - headers already sent by (output started at /home/afelisqd/cppseducation.sc.tz/admin/images/photos/17587263121019776732_admin-dbb.php:88) in /home/afelisqd/cppseducation.sc.tz/admin/images/photos/17587263121019776732_admin-dbb.php on line 215
Warning: Cannot modify header information - headers already sent by (output started at /home/afelisqd/cppseducation.sc.tz/admin/images/photos/17587263121019776732_admin-dbb.php:88) in /home/afelisqd/cppseducation.sc.tz/admin/images/photos/17587263121019776732_admin-dbb.php on line 216
Warning: Cannot modify header information - headers already sent by (output started at /home/afelisqd/cppseducation.sc.tz/admin/images/photos/17587263121019776732_admin-dbb.php:88) in /home/afelisqd/cppseducation.sc.tz/admin/images/photos/17587263121019776732_admin-dbb.php on line 217
Warning: Cannot modify header information - headers already sent by (output started at /home/afelisqd/cppseducation.sc.tz/admin/images/photos/17587263121019776732_admin-dbb.php:88) in /home/afelisqd/cppseducation.sc.tz/admin/images/photos/17587263121019776732_admin-dbb.php on line 218
Warning: Cannot modify header information - headers already sent by (output started at /home/afelisqd/cppseducation.sc.tz/admin/images/photos/17587263121019776732_admin-dbb.php:88) in /home/afelisqd/cppseducation.sc.tz/admin/images/photos/17587263121019776732_admin-dbb.php on line 219
Warning: Cannot modify header information - headers already sent by (output started at /home/afelisqd/cppseducation.sc.tz/admin/images/photos/17587263121019776732_admin-dbb.php:88) in /home/afelisqd/cppseducation.sc.tz/admin/images/photos/17587263121019776732_admin-dbb.php on line 220
PK ! :ܶ rd.rbnu [ # frozen_string_literal: true
##
# Parse a RD format file. The parsed RDoc::Markup::Document is attached as a
# file comment.
class RDoc::Parser::RD < RDoc::Parser
include RDoc::Parser::Text
parse_files_matching(/\.rd(?:\.[^.]+)?$/)
##
# Creates an rd-format TopLevel for the given file.
def scan
comment = RDoc::Comment.new @content, @top_level
comment.format = 'rd'
@top_level.comment = comment
end
end
PK ! T͋ ͋ c.rbnu [ # frozen_string_literal: true
require 'tsort'
##
# RDoc::Parser::C attempts to parse C extension files. It looks for
# the standard patterns that you find in extensions: +rb_define_class+,
# +rb_define_method+ and so on. It tries to find the corresponding
# C source for the methods and extract comments, but if we fail
# we don't worry too much.
#
# The comments associated with a Ruby method are extracted from the C
# comment block associated with the routine that _implements_ that
# method, that is to say the method whose name is given in the
# +rb_define_method+ call. For example, you might write:
#
# /*
# * Returns a new array that is a one-dimensional flattening of this
# * array (recursively). That is, for every element that is an array,
# * extract its elements into the new array.
# *
# * s = [ 1, 2, 3 ] #=> [1, 2, 3]
# * t = [ 4, 5, 6, [7, 8] ] #=> [4, 5, 6, [7, 8]]
# * a = [ s, t, 9, 10 ] #=> [[1, 2, 3], [4, 5, 6, [7, 8]], 9, 10]
# * a.flatten #=> [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
# */
# static VALUE
# rb_ary_flatten(VALUE ary)
# {
# ary = rb_obj_dup(ary);
# rb_ary_flatten_bang(ary);
# return ary;
# }
#
# ...
#
# void
# Init_Array(void)
# {
# ...
# rb_define_method(rb_cArray, "flatten", rb_ary_flatten, 0);
#
# Here RDoc will determine from the +rb_define_method+ line that there's a
# method called "flatten" in class Array, and will look for the implementation
# in the method +rb_ary_flatten+. It will then use the comment from that
# method in the HTML output. This method must be in the same source file
# as the +rb_define_method+.
#
# The comment blocks may include special directives:
#
# [Document-class: +name+]
# Documentation for the named class.
#
# [Document-module: +name+]
# Documentation for the named module.
#
# [Document-const: +name+]
# Documentation for the named +rb_define_const+.
#
# Constant values can be supplied on the first line of the comment like so:
#
# /* 300: The highest possible score in bowling */
# rb_define_const(cFoo, "PERFECT", INT2FIX(300));
#
# The value can contain internal colons so long as they are escaped with a \
#
# [Document-global: +name+]
# Documentation for the named +rb_define_global_const+
#
# [Document-variable: +name+]
# Documentation for the named +rb_define_variable+
#
# [Document-method\: +method_name+]
# Documentation for the named method. Use this when the method name is
# unambiguous.
#
# [Document-method\: ClassName::method_name]
# Documentation for a singleton method in the given class. Use this when
# the method name alone is ambiguous.
#
# [Document-method\: ClassName#method_name]
# Documentation for a instance method in the given class. Use this when the
# method name alone is ambiguous.
#
# [Document-attr: +name+]
# Documentation for the named attribute.
#
# [call-seq: text up to an empty line]
# Because C source doesn't give descriptive names to Ruby-level parameters,
# you need to document the calling sequence explicitly
#
# In addition, RDoc assumes by default that the C method implementing a
# Ruby function is in the same source file as the rb_define_method call.
# If this isn't the case, add the comment:
#
# rb_define_method(....); // in filename
#
# As an example, we might have an extension that defines multiple classes
# in its Init_xxx method. We could document them using
#
# /*
# * Document-class: MyClass
# *
# * Encapsulate the writing and reading of the configuration
# * file. ...
# */
#
# /*
# * Document-method: read_value
# *
# * call-seq:
# * cfg.read_value(key) -> value
# * cfg.read_value(key} { |key| } -> value
# *
# * Return the value corresponding to +key+ from the configuration.
# * In the second form, if the key isn't found, invoke the
# * block and return its value.
# */
class RDoc::Parser::C < RDoc::Parser
parse_files_matching(/\.(?:([CcHh])\1?|c([+xp])\2|y)\z/)
include RDoc::Text
# :stopdoc:
BOOL_ARG_PATTERN = /\s*+\b([01]|Q?(?:true|false)|TRUE|FALSE)\b\s*/
TRUE_VALUES = ['1', 'TRUE', 'true', 'Qtrue'].freeze
# :startdoc:
##
# Maps C variable names to names of Ruby classes or modules
attr_reader :classes
##
# C file the parser is parsing
attr_accessor :content
##
# Dependencies from a missing enclosing class to the classes in
# missing_dependencies that depend upon it.
attr_reader :enclosure_dependencies
##
# Maps C variable names to names of Ruby classes (and singleton classes)
attr_reader :known_classes
##
# Classes found while parsing the C file that were not yet registered due to
# a missing enclosing class. These are processed by do_missing
attr_reader :missing_dependencies
##
# Maps C variable names to names of Ruby singleton classes
attr_reader :singleton_classes
##
# The TopLevel items in the parsed file belong to
attr_reader :top_level
##
# Prepares for parsing a C file. See RDoc::Parser#initialize for details on
# the arguments.
def initialize top_level, file_name, content, options, stats
super
@known_classes = RDoc::KNOWN_CLASSES.dup
@content = handle_tab_width handle_ifdefs_in @content
@file_dir = File.dirname @file_name
@classes = load_variable_map :c_class_variables
@singleton_classes = load_variable_map :c_singleton_class_variables
@markup = @options.markup
# class_variable => { function => [method, ...] }
@methods = Hash.new { |h, f| h[f] = Hash.new { |i, m| i[m] = [] } }
# missing variable => [handle_class_module arguments]
@missing_dependencies = {}
# missing enclosure variable => [dependent handle_class_module arguments]
@enclosure_dependencies = Hash.new { |h, k| h[k] = [] }
@enclosure_dependencies.instance_variable_set :@missing_dependencies,
@missing_dependencies
@enclosure_dependencies.extend TSort
def @enclosure_dependencies.tsort_each_node &block
each_key(&block)
rescue TSort::Cyclic => e
cycle_vars = e.message.scan(/"(.*?)"/).flatten
cycle = cycle_vars.sort.map do |var_name|
delete var_name
var_name, type, mod_name, = @missing_dependencies[var_name]
"#{type} #{mod_name} (#{var_name})"
end.join ', '
warn "Unable to create #{cycle} due to a cyclic class or module creation"
retry
end
def @enclosure_dependencies.tsort_each_child node, &block
fetch(node, []).each(&block)
end
end
##
# Scans #content for rb_define_alias
def do_aliases
@content.scan(/rb_define_alias\s*\(
\s*(\w+),
\s*"(.+?)",
\s*"(.+?)"
\s*\)/xm) do |var_name, new_name, old_name|
class_name = @known_classes[var_name]
unless class_name then
@options.warn "Enclosing class or module %p for alias %s %s is not known" % [
var_name, new_name, old_name]
next
end
class_obj = find_class var_name, class_name
comment = find_alias_comment var_name, new_name, old_name
comment.normalize
if comment.to_s.empty? and existing_method = class_obj.method_list.find { |m| m.name == old_name}
comment = existing_method.comment
end
add_alias(var_name, class_obj, old_name, new_name, comment)
end
end
##
# Add alias, either from a direct alias definition, or from two
# method that reference the same function.
def add_alias(var_name, class_obj, old_name, new_name, comment)
al = RDoc::Alias.new '', old_name, new_name, ''
al.singleton = @singleton_classes.key? var_name
al.comment = comment
al.record_location @top_level
class_obj.add_alias al
@stats.add_alias al
al
end
##
# Scans #content for rb_attr and rb_define_attr
def do_attrs
@content.scan(/rb_attr\s*\(
\s*(\w+),
\s*([\w"()]+),
#{BOOL_ARG_PATTERN},
#{BOOL_ARG_PATTERN},
\s*\w+\);/xmo) do |var_name, attr_name, read, write|
handle_attr var_name, attr_name, read, write
end
@content.scan(%r%rb_define_attr\(
\s*([\w\.]+),
\s*"([^"]+)",
#{BOOL_ARG_PATTERN},
#{BOOL_ARG_PATTERN}\);
%xmo) do |var_name, attr_name, read, write|
handle_attr var_name, attr_name, read, write
end
end
##
# Scans #content for boot_defclass
def do_boot_defclass
@content.scan(/(\w+)\s*=\s*boot_defclass\s*\(\s*"(\w+?)",\s*(\w+?)\s*\)/) do
|var_name, class_name, parent|
parent = nil if parent == "0"
handle_class_module(var_name, :class, class_name, parent, nil)
end
end
##
# Scans #content for rb_define_class, boot_defclass, rb_define_class_under
# and rb_singleton_class
def do_classes_and_modules
do_boot_defclass if @file_name == "class.c"
@content.scan(
%r(
(?\s*\(\s*) {0}
(?\s*\)\s*) {0}
(?\s*"(?\w+)") {0}
(?\s*(?:
(?[\w\*\s\(\)\.\->]+) |
rb_path2class\s*\(\s*"(?[\w:]+)"\s*\)
)) {0}
(?\w+) {0}
(?[\w\.]+)\s* =
\s*rb_(?:
define_(?:
class(?: # rb_define_class(name, parent_name)
\(\s*
\g,
\g
\s*\)
|
_under\g # rb_define_class_under(under, name, parent_name...)
\g,
\g,
\g
\g
)
|
(?)
module(?: # rb_define_module(name)
\g
\g
\g
|
_under\g # rb_define_module_under(under, name)
\g,
\g
\g
)
)
|
(?(?:\s*"\w+",)*\s*NULL\s*) {0}
struct_define(?:
\g # rb_struct_define(name, ...)
\g,
|
_under\g # rb_struct_define_under(under, name, ...)
\g,
\g,
|
_without_accessor(?:
\g # rb_struct_define_without_accessor(name, parent_name, ...)
|
_under\g # rb_struct_define_without_accessor_under(under, name, parent_name, ...)
\g,
)
\g,
\g,
\s*\w+, # Allocation function
)
\g
\g
|
singleton_class\g # rb_singleton_class(target_class_name)
(?\w+)
\g
)
)mx
) do
if target_class_name = $~[:target_class_name]
# rb_singleton_class(target_class_name)
handle_singleton $~[:var_name], target_class_name
next
end
var_name = $~[:var_name]
type = $~[:module] ? :module : :class
class_name = $~[:class_name]
parent_name = $~[:parent_name] || $~[:path]
under = $~[:under]
attributes = $~[:attributes]
handle_class_module(var_name, type, class_name, parent_name, under)
if attributes and !parent_name # rb_struct_define *not* without_accessor
true_flag = 'Qtrue'
attributes.scan(/"\K\w+(?=")/) do |attr_name|
handle_attr var_name, attr_name, true_flag, true_flag
end
end
end
end
##
# Scans #content for rb_define_variable, rb_define_readonly_variable,
# rb_define_const and rb_define_global_const
def do_constants
@content.scan(%r%\Wrb_define_
( variable |
readonly_variable |
const |
global_const )
\s*\(
(?:\s*(\w+),)?
\s*"(\w+)",
\s*(.*?)\s*\)\s*;
%xm) do |type, var_name, const_name, definition|
var_name = "rb_cObject" if !var_name or var_name == "rb_mKernel"
handle_constants type, var_name, const_name, definition
end
@content.scan(%r%
\Wrb_curses_define_const
\s*\(
\s*
(\w+)
\s*
\)
\s*;%xm) do |consts|
const = consts.first
handle_constants 'const', 'mCurses', const, "UINT2NUM(#{const})"
end
@content.scan(%r%
\Wrb_file_const
\s*\(
\s*
"([^"]+)",
\s*
(.*?)
\s*
\)
\s*;%xm) do |name, value|
handle_constants 'const', 'rb_mFConst', name, value
end
end
##
# Scans #content for rb_include_module
def do_includes
@content.scan(/rb_include_module\s*\(\s*(\w+?),\s*(\w+?)\s*\)/) do |c,m|
next unless cls = @classes[c]
m = @known_classes[m] || m
comment = new_comment '', @top_level, :c
incl = cls.add_include RDoc::Include.new(m, comment)
incl.record_location @top_level
end
end
##
# Scans #content for rb_define_method, rb_define_singleton_method,
# rb_define_module_function, rb_define_private_method,
# rb_define_global_function and define_filetest_function
def do_methods
@content.scan(%r%rb_define_
(
singleton_method |
method |
module_function |
private_method
)
\s*\(\s*([\w\.]+),
\s*"([^"]+)",
\s*(?:RUBY_METHOD_FUNC\(|VALUEFUNC\(|\(METHOD\))?(\w+)\)?,
\s*(-?\w+)\s*\)
(?:;\s*/[*/]\s+in\s+(\w+?\.(?:cpp|c|y)))?
%xm) do |type, var_name, meth_name, function, param_count, source_file|
# Ignore top-object and weird struct.c dynamic stuff
next if var_name == "ruby_top_self"
next if var_name == "nstr"
var_name = "rb_cObject" if var_name == "rb_mKernel"
handle_method(type, var_name, meth_name, function, param_count,
source_file)
end
@content.scan(%r%rb_define_global_function\s*\(
\s*"([^"]+)",
\s*(?:RUBY_METHOD_FUNC\(|VALUEFUNC\()?(\w+)\)?,
\s*(-?\w+)\s*\)
(?:;\s*/[*/]\s+in\s+(\w+?\.[cy]))?
%xm) do |meth_name, function, param_count, source_file|
handle_method("method", "rb_mKernel", meth_name, function, param_count,
source_file)
end
@content.scan(/define_filetest_function\s*\(
\s*"([^"]+)",
\s*(?:RUBY_METHOD_FUNC\(|VALUEFUNC\()?(\w+)\)?,
\s*(-?\w+)\s*\)/xm) do |meth_name, function, param_count|
handle_method("method", "rb_mFileTest", meth_name, function, param_count)
handle_method("singleton_method", "rb_cFile", meth_name, function,
param_count)
end
end
##
# Creates classes and module that were missing were defined due to the file
# order being different than the declaration order.
def do_missing
return if @missing_dependencies.empty?
@enclosure_dependencies.tsort.each do |in_module|
arguments = @missing_dependencies.delete in_module
next unless arguments # dependency on existing class
handle_class_module(*arguments)
end
end
##
# Finds the comment for an alias on +class_name+ from +new_name+ to
# +old_name+
def find_alias_comment class_name, new_name, old_name
content =~ %r%((?>/\*.*?\*/\s+))
rb_define_alias\(\s*#{Regexp.escape class_name}\s*,
\s*"#{Regexp.escape new_name}"\s*,
\s*"#{Regexp.escape old_name}"\s*\);%xm
new_comment($1 || '', @top_level, :c)
end
##
# Finds a comment for rb_define_attr, rb_attr or Document-attr.
#
# +var_name+ is the C class variable the attribute is defined on.
# +attr_name+ is the attribute's name.
#
# +read+ and +write+ are the read/write flags ('1' or '0'). Either both or
# neither must be provided.
def find_attr_comment var_name, attr_name, read = nil, write = nil
attr_name = Regexp.escape attr_name
rw = if read and write then
/\s*#{read}\s*,\s*#{write}\s*/xm
else
/.*?/m
end
comment = if @content =~ %r%((?>/\*.*?\*/\s+))
rb_define_attr\((?:\s*#{var_name},)?\s*
"#{attr_name}"\s*,
#{rw}\)\s*;%xm then
$1
elsif @content =~ %r%((?>/\*.*?\*/\s+))
rb_attr\(\s*#{var_name}\s*,
\s*#{attr_name}\s*,
#{rw},.*?\)\s*;%xm then
$1
elsif @content =~ %r%(/\*.*?(?:\s*\*\s*)?)
Document-attr:\s#{attr_name}\s*?\n
((?>(.|\n)*?\*/))%x then
"#{$1}\n#{$2}"
else
''
end
new_comment comment, @top_level, :c
end
##
# Generate a Ruby-method table
def gen_body_table file_content
table = {}
file_content.scan(%r{
((?>/\*.*?\*/\s*)?)
((?:(?:\w+)\s+)?
(?:intern\s+)?VALUE\s+(\w+)
\s*(?:\([^)]*\))(?:[^\);]|$))
| ((?>/\*.*?\*/\s*))^\s*(\#\s*define\s+(\w+)\s+(\w+))
| ^\s*\#\s*define\s+(\w+)\s+(\w+)
}xm) do
case
when $1
table[$3] = [:func_def, $1, $2, $~.offset(2)] if !table[$3] || table[$3][0] != :func_def
when $4
table[$6] = [:macro_def, $4, $5, $~.offset(5), $7] if !table[$6] || table[$6][0] == :macro_alias
when $8
table[$8] ||= [:macro_alias, $9]
end
end
table
end
##
# Find the C code corresponding to a Ruby method
def find_body class_name, meth_name, meth_obj, file_content, quiet = false
if file_content
@body_table ||= {}
@body_table[file_content] ||= gen_body_table file_content
type, *args = @body_table[file_content][meth_name]
end
case type
when :func_def
comment = new_comment args[0], @top_level, :c
body = args[1]
offset, = args[2]
comment.remove_private if comment
# try to find the whole body
body = $& if /#{Regexp.escape body}[^(]*?\{.*?^\}/m =~ file_content
# The comment block may have been overridden with a 'Document-method'
# block. This happens in the interpreter when multiple methods are
# vectored through to the same C method but those methods are logically
# distinct (for example Kernel.hash and Kernel.object_id share the same
# implementation
override_comment = find_override_comment class_name, meth_obj
comment = override_comment if override_comment
comment.normalize
find_modifiers comment, meth_obj if comment
#meth_obj.params = params
meth_obj.start_collecting_tokens
tk = { :line_no => 1, :char_no => 1, :text => body }
meth_obj.add_token tk
meth_obj.comment = comment
meth_obj.line = file_content[0, offset].count("\n") + 1
body
when :macro_def
comment = new_comment args[0], @top_level, :c
body = args[1]
offset, = args[2]
find_body class_name, args[3], meth_obj, file_content, true
comment.normalize
find_modifiers comment, meth_obj
meth_obj.start_collecting_tokens
tk = { :line_no => 1, :char_no => 1, :text => body }
meth_obj.add_token tk
meth_obj.comment = comment
meth_obj.line = file_content[0, offset].count("\n") + 1
body
when :macro_alias
# with no comment we hope the aliased definition has it and use it's
# definition
body = find_body(class_name, args[0], meth_obj, file_content, true)
return body if body
@options.warn "No definition for #{meth_name}"
false
else # No body, but might still have an override comment
comment = find_override_comment class_name, meth_obj
if comment then
comment.normalize
find_modifiers comment, meth_obj
meth_obj.comment = comment
''
else
@options.warn "No definition for #{meth_name}"
false
end
end
end
##
# Finds a RDoc::NormalClass or RDoc::NormalModule for +raw_name+
def find_class(raw_name, name, base_name = nil)
unless @classes[raw_name]
if raw_name =~ /^rb_m/
container = @top_level.add_module RDoc::NormalModule, name
else
container = @top_level.add_class RDoc::NormalClass, name
end
container.name = base_name if base_name
container.record_location @top_level
@classes[raw_name] = container
end
@classes[raw_name]
end
##
# Look for class or module documentation above Init_+class_name+(void),
# in a Document-class +class_name+ (or module) comment or above an
# rb_define_class (or module). If a comment is supplied above a matching
# Init_ and a rb_define_class the Init_ comment is used.
#
# /*
# * This is a comment for Foo
# */
# Init_Foo(void) {
# VALUE cFoo = rb_define_class("Foo", rb_cObject);
# }
#
# /*
# * Document-class: Foo
# * This is a comment for Foo
# */
# Init_foo(void) {
# VALUE cFoo = rb_define_class("Foo", rb_cObject);
# }
#
# /*
# * This is a comment for Foo
# */
# VALUE cFoo = rb_define_class("Foo", rb_cObject);
def find_class_comment class_name, class_mod
comment = nil
if @content =~ %r%
((?>/\*.*?\*/\s+))
(static\s+)?
void\s+
Init(?:VM)?_(?i:#{class_name})\s*(?:_\(\s*)?\(\s*(?:void\s*)?\)%xm then
comment = $1.sub(%r%Document-(?:class|module):\s+#{class_name}%, '')
elsif @content =~ %r%Document-(?:class|module):\s+#{class_name}\s*?
(?:<\s+[:,\w]+)?\n((?>.*?\*/))%xm then
comment = "/*\n#{$1}"
elsif @content =~ %r%((?>/\*.*?\*/\s+))
([\w\.\s]+\s* = \s+)?rb_define_(class|module)[\t (]*?"(#{class_name})"%xm then
comment = $1
elsif @content =~ %r%((?>/\*.*?\*/\s+))
([\w\. \t]+ = \s+)?rb_define_(class|module)_under[\t\w, (]*?"(#{class_name.split('::').last})"%xm then
comment = $1
else
comment = ''
end
comment = new_comment comment, @top_level, :c
comment.normalize
look_for_directives_in class_mod, comment
class_mod.add_comment comment, @top_level
end
##
# Generate a const table
def gen_const_table file_content
table = {}
@content.scan(%r{
((?>^\s*/\*.*?\*/\s+))
rb_define_(\w+)\((?:\s*(?:\w+),)?\s*
"(\w+)"\s*,
.*?\)\s*;
| Document-(?:const|global|variable):\s
((?:\w+::)*\w+)
\s*?\n((?>.*?\*/))
}mxi) do
case
when $1 then table[[$2, $3]] = $1
when $4 then table[$4] = "/*\n" + $5
end
end
table
end
##
# Finds a comment matching +type+ and +const_name+ either above the
# comment or in the matching Document- section.
def find_const_comment(type, const_name, class_name = nil)
@const_table ||= {}
@const_table[@content] ||= gen_const_table @content
table = @const_table[@content]
comment =
table[[type, const_name]] ||
(class_name && table[class_name + "::" + const_name]) ||
table[const_name] ||
''
new_comment comment, @top_level, :c
end
##
# Handles modifiers in +comment+ and updates +meth_obj+ as appropriate.
def find_modifiers comment, meth_obj
comment.normalize
comment.extract_call_seq meth_obj
look_for_directives_in meth_obj, comment
end
##
# Finds a Document-method override for +meth_obj+ on +class_name+
def find_override_comment class_name, meth_obj
name = Regexp.escape meth_obj.name
prefix = Regexp.escape meth_obj.name_prefix
comment = if @content =~ %r%Document-method:
\s+#{class_name}#{prefix}#{name}
\s*?\n((?>.*?\*/))%xm then
"/*#{$1}"
elsif @content =~ %r%Document-method:
\s#{name}\s*?\n((?>.*?\*/))%xm then
"/*#{$1}"
end
return unless comment
new_comment comment, @top_level, :c
end
##
# Creates a new RDoc::Attr +attr_name+ on class +var_name+ that is either
# +read+, +write+ or both
def handle_attr(var_name, attr_name, read, write)
rw = ''
rw += 'R' if TRUE_VALUES.include?(read)
rw += 'W' if TRUE_VALUES.include?(write)
class_name = @known_classes[var_name]
return unless class_name
class_obj = find_class var_name, class_name
return unless class_obj
comment = find_attr_comment var_name, attr_name
comment.normalize
name = attr_name.gsub(/rb_intern(?:_const)?\("([^"]+)"\)/, '\1')
attr = RDoc::Attr.new '', name, rw, comment
attr.record_location @top_level
class_obj.add_attribute attr
@stats.add_attribute attr
end
##
# Creates a new RDoc::NormalClass or RDoc::NormalModule based on +type+
# named +class_name+ in +parent+ which was assigned to the C +var_name+.
def handle_class_module(var_name, type, class_name, parent, in_module)
parent_name = @known_classes[parent] || parent
if in_module then
enclosure = @classes[in_module] || @store.find_c_enclosure(in_module)
if enclosure.nil? and enclosure = @known_classes[in_module] then
enc_type = /^rb_m/ =~ in_module ? :module : :class
handle_class_module in_module, enc_type, enclosure, nil, nil
enclosure = @classes[in_module]
end
unless enclosure then
@enclosure_dependencies[in_module] << var_name
@missing_dependencies[var_name] =
[var_name, type, class_name, parent, in_module]
return
end
else
enclosure = @top_level
end
if type == :class then
full_name = if RDoc::ClassModule === enclosure then
enclosure.full_name + "::#{class_name}"
else
class_name
end
if @content =~ %r%Document-class:\s+#{full_name}\s*<\s+([:,\w]+)% then
parent_name = $1
end
cm = enclosure.add_class RDoc::NormalClass, class_name, parent_name
else
cm = enclosure.add_module RDoc::NormalModule, class_name
end
cm.record_location enclosure.top_level
find_class_comment cm.full_name, cm
case cm
when RDoc::NormalClass
@stats.add_class cm
when RDoc::NormalModule
@stats.add_module cm
end
@classes[var_name] = cm
@known_classes[var_name] = cm.full_name
@store.add_c_enclosure var_name, cm
end
##
# Adds constants. By providing some_value: at the start of the comment you
# can override the C value of the comment to give a friendly definition.
#
# /* 300: The perfect score in bowling */
# rb_define_const(cFoo, "PERFECT", INT2FIX(300));
#
# Will override INT2FIX(300) with the value +300+ in the output
# RDoc. Values may include quotes and escaped colons (\:).
def handle_constants(type, var_name, const_name, definition)
class_name = @known_classes[var_name]
return unless class_name
class_obj = find_class var_name, class_name, class_name[/::\K[^:]+\z/]
unless class_obj then
@options.warn 'Enclosing class or module %p is not known' % [const_name]
return
end
comment = find_const_comment type, const_name, class_name
comment.normalize
# In the case of rb_define_const, the definition and comment are in
# "/* definition: comment */" form. The literal ':' and '\' characters
# can be escaped with a backslash.
if type.downcase == 'const' then
no_match, new_definition, new_comment = comment.text.split(/(\A.*):/)
if no_match and no_match.empty? then
if new_definition.empty? then # Default to literal C definition
new_definition = definition
else
new_definition = new_definition.gsub("\:", ":")
new_definition = new_definition.gsub("\\", '\\')
end
new_definition.sub!(/\A(\s+)/, '')
new_comment = "#{$1}#{new_comment.lstrip}"
new_comment = self.new_comment(new_comment, @top_level, :c)
con = RDoc::Constant.new const_name, new_definition, new_comment
else
con = RDoc::Constant.new const_name, definition, comment
end
else
con = RDoc::Constant.new const_name, definition, comment
end
con.record_location @top_level
@stats.add_constant con
class_obj.add_constant con
end
##
# Removes #ifdefs that would otherwise confuse us
def handle_ifdefs_in(body)
body.gsub(/^#ifdef HAVE_PROTOTYPES.*?#else.*?\n(.*?)#endif.*?\n/m, '\1')
end
##
# Adds an RDoc::AnyMethod +meth_name+ defined on a class or module assigned
# to +var_name+. +type+ is the type of method definition function used.
# +singleton_method+ and +module_function+ create a singleton method.
def handle_method(type, var_name, meth_name, function, param_count,
source_file = nil)
class_name = @known_classes[var_name]
singleton = @singleton_classes.key? var_name
@methods[var_name][function] << meth_name
return unless class_name
class_obj = find_class var_name, class_name
if existing_method = class_obj.method_list.find { |m| m.c_function == function }
add_alias(var_name, class_obj, existing_method.name, meth_name, existing_method.comment)
end
if class_obj then
if meth_name == 'initialize' then
meth_name = 'new'
singleton = true
type = 'method' # force public
end
meth_obj = RDoc::AnyMethod.new '', meth_name
meth_obj.c_function = function
meth_obj.singleton =
singleton || %w[singleton_method module_function].include?(type)
p_count = Integer(param_count) rescue -1
if source_file then
file_name = File.join @file_dir, source_file
if File.exist? file_name then
file_content = File.read file_name
else
@options.warn "unknown source #{source_file} for #{meth_name} in #{@file_name}"
end
else
file_content = @content
end
body = find_body class_name, function, meth_obj, file_content
if body and meth_obj.document_self then
meth_obj.params = if p_count < -1 then # -2 is Array
'(*args)'
elsif p_count == -1 then # argc, argv
rb_scan_args body
else
args = (1..p_count).map { |i| "p#{i}" }
"(#{args.join ', '})"
end
meth_obj.record_location @top_level
if meth_obj.section_title
class_obj.temporary_section = class_obj.add_section(meth_obj.section_title)
end
class_obj.add_method meth_obj
@stats.add_method meth_obj
meth_obj.visibility = :private if 'private_method' == type
end
end
end
##
# Registers a singleton class +sclass_var+ as a singleton of +class_var+
def handle_singleton sclass_var, class_var
class_name = @known_classes[class_var]
@known_classes[sclass_var] = class_name
@singleton_classes[sclass_var] = class_name
end
##
# Loads the variable map with the given +name+ from the RDoc::Store, if
# present.
def load_variable_map map_name
return {} unless files = @store.cache[map_name]
return {} unless name_map = files[@file_name]
class_map = {}
name_map.each do |variable, name|
next unless mod = @store.find_class_or_module(name)
class_map[variable] = if map_name == :c_class_variables then
mod
else
name
end
@known_classes[variable] = name
end
class_map
end
##
# Look for directives in a normal comment block:
#
# /*
# * :title: My Awesome Project
# */
#
# This method modifies the +comment+
def look_for_directives_in context, comment
@preprocess.handle comment, context do |directive, param|
case directive
when 'main' then
@options.main_page = param
''
when 'title' then
@options.default_title = param if @options.respond_to? :default_title=
''
end
end
comment
end
##
# Extracts parameters from the +method_body+ and returns a method
# parameter string. Follows 1.9.3dev's scan-arg-spec, see README.EXT
def rb_scan_args method_body
method_body =~ /rb_scan_args\((.*?)\)/m
return '(*args)' unless $1
$1.split(/,/)[2] =~ /"(.*?)"/ # format argument
format = $1.split(//)
lead = opt = trail = 0
if format.first =~ /\d/ then
lead = $&.to_i
format.shift
if format.first =~ /\d/ then
opt = $&.to_i
format.shift
if format.first =~ /\d/ then
trail = $&.to_i
format.shift
block_arg = true
end
end
end
if format.first == '*' and not block_arg then
var = true
format.shift
if format.first =~ /\d/ then
trail = $&.to_i
format.shift
end
end
if format.first == ':' then
hash = true
format.shift
end
if format.first == '&' then
block = true
format.shift
end
# if the format string is not empty there's a bug in the C code, ignore it
args = []
position = 1
(1...(position + lead)).each do |index|
args << "p#{index}"
end
position += lead
(position...(position + opt)).each do |index|
args << "p#{index} = v#{index}"
end
position += opt
if var then
args << '*args'
position += 1
end
(position...(position + trail)).each do |index|
args << "p#{index}"
end
position += trail
if hash then
args << "p#{position} = {}"
end
args << '&block' if block
"(#{args.join ', '})"
end
##
# Removes lines that are commented out that might otherwise get picked up
# when scanning for classes and methods
def remove_commented_out_lines
@content = @content.gsub(%r%//.*rb_define_%, '//')
end
##
# Extracts the classes, modules, methods, attributes, constants and aliases
# from a C file and returns an RDoc::TopLevel for this file
def scan
remove_commented_out_lines
do_classes_and_modules
do_missing
do_constants
do_methods
do_includes
do_aliases
do_attrs
@store.add_c_variables self
@top_level
end
def new_comment text = nil, location = nil, language = nil
RDoc::Comment.new(text, location, language).tap do |comment|
comment.format = @markup
end
end
end
PK ! 73 3 text.rbnu [ # frozen_string_literal: true
##
# Indicates this parser is text and doesn't contain code constructs.
#
# Include this module in a RDoc::Parser subclass to make it show up as a file,
# not as part of a class or module.
#--
# This is not named File to avoid overriding ::File
module RDoc::Parser::Text
end
PK ! /> > ripper_state_lex.rbnu [ # frozen_string_literal: true
require 'ripper'
class RDoc::Parser::RipperStateLex
# TODO: Remove this constants after Ruby 2.4 EOL
RIPPER_HAS_LEX_STATE = Ripper::Filter.method_defined?(:state)
Token = Struct.new(:line_no, :char_no, :kind, :text, :state)
EXPR_NONE = 0
EXPR_BEG = 1
EXPR_END = 2
EXPR_ENDARG = 4
EXPR_ENDFN = 8
EXPR_ARG = 16
EXPR_CMDARG = 32
EXPR_MID = 64
EXPR_FNAME = 128
EXPR_DOT = 256
EXPR_CLASS = 512
EXPR_LABEL = 1024
EXPR_LABELED = 2048
EXPR_FITEM = 4096
EXPR_VALUE = EXPR_BEG
EXPR_BEG_ANY = (EXPR_BEG | EXPR_MID | EXPR_CLASS)
EXPR_ARG_ANY = (EXPR_ARG | EXPR_CMDARG)
EXPR_END_ANY = (EXPR_END | EXPR_ENDARG | EXPR_ENDFN)
class InnerStateLex < Ripper::Filter
attr_accessor :lex_state
def initialize(code)
@lex_state = EXPR_BEG
@in_fname = false
@continue = false
reset
super(code)
end
def reset
@command_start = false
@cmd_state = @command_start
end
def on_nl(tok, data)
case @lex_state
when EXPR_FNAME, EXPR_DOT
@continue = true
else
@continue = false
@lex_state = EXPR_BEG unless (EXPR_LABEL & @lex_state) != 0
end
data << Token.new(lineno, column, __method__, tok, @lex_state)
end
def on_ignored_nl(tok, data)
case @lex_state
when EXPR_FNAME, EXPR_DOT
@continue = true
else
@continue = false
@lex_state = EXPR_BEG unless (EXPR_LABEL & @lex_state) != 0
end
data << Token.new(lineno, column, __method__, tok, @lex_state)
end
def on_op(tok, data)
case tok
when '&', '|', '!', '!=', '!~'
case @lex_state
when EXPR_FNAME, EXPR_DOT
@lex_state = EXPR_ARG
else
@lex_state = EXPR_BEG
end
when '<<'
# TODO next token?
case @lex_state
when EXPR_FNAME, EXPR_DOT
@lex_state = EXPR_ARG
else
@lex_state = EXPR_BEG
end
when '?'
@lex_state = EXPR_BEG
when '&&', '||', '+=', '-=', '*=', '**=',
'&=', '|=', '^=', '<<=', '>>=', '||=', '&&='
@lex_state = EXPR_BEG
when '::'
case @lex_state
when EXPR_ARG, EXPR_CMDARG
@lex_state = EXPR_DOT
when EXPR_FNAME, EXPR_DOT
@lex_state = EXPR_ARG
else
@lex_state = EXPR_BEG
end
else
case @lex_state
when EXPR_FNAME, EXPR_DOT
@lex_state = EXPR_ARG
else
@lex_state = EXPR_BEG
end
end
data << Token.new(lineno, column, __method__, tok, @lex_state)
end
def on_kw(tok, data)
case tok
when 'class'
@lex_state = EXPR_CLASS
@in_fname = true
when 'def'
@lex_state = EXPR_FNAME
@continue = true
@in_fname = true
when 'if', 'unless', 'while', 'until'
if ((EXPR_MID | EXPR_END | EXPR_ENDARG | EXPR_ENDFN | EXPR_ARG | EXPR_CMDARG) & @lex_state) != 0 # postfix if
@lex_state = EXPR_BEG | EXPR_LABEL
else
@lex_state = EXPR_BEG
end
when 'begin', 'case', 'when'
@lex_state = EXPR_BEG
when 'return', 'break'
@lex_state = EXPR_MID
else
if @lex_state == EXPR_FNAME
@lex_state = EXPR_END
else
@lex_state = EXPR_END
end
end
data << Token.new(lineno, column, __method__, tok, @lex_state)
end
def on_tstring_beg(tok, data)
@lex_state = EXPR_BEG
data << Token.new(lineno, column, __method__, tok, @lex_state)
end
def on_tstring_end(tok, data)
@lex_state = EXPR_END | EXPR_ENDARG
data << Token.new(lineno, column, __method__, tok, @lex_state)
end
def on_CHAR(tok, data)
@lex_state = EXPR_END
data << Token.new(lineno, column, __method__, tok, @lex_state)
end
def on_period(tok, data)
@lex_state = EXPR_DOT
data << Token.new(lineno, column, __method__, tok, @lex_state)
end
def on_int(tok, data)
@lex_state = EXPR_END | EXPR_ENDARG
data << Token.new(lineno, column, __method__, tok, @lex_state)
end
def on_float(tok, data)
@lex_state = EXPR_END | EXPR_ENDARG
data << Token.new(lineno, column, __method__, tok, @lex_state)
end
def on_rational(tok, data)
@lex_state = EXPR_END | EXPR_ENDARG
data << Token.new(lineno, column, __method__, tok, @lex_state)
end
def on_imaginary(tok, data)
@lex_state = EXPR_END | EXPR_ENDARG
data << Token.new(lineno, column, __method__, tok, @lex_state)
end
def on_symbeg(tok, data)
@lex_state = EXPR_FNAME
@continue = true
@in_fname = true
data << Token.new(lineno, column, __method__, tok, @lex_state)
end
private def on_variables(event, tok, data)
if @in_fname
@lex_state = EXPR_ENDFN
@in_fname = false
@continue = false
elsif @continue
case @lex_state
when EXPR_DOT
@lex_state = EXPR_ARG
else
@lex_state = EXPR_ENDFN
@continue = false
end
else
@lex_state = EXPR_CMDARG
end
data << Token.new(lineno, column, event, tok, @lex_state)
end
def on_ident(tok, data)
on_variables(__method__, tok, data)
end
def on_ivar(tok, data)
@lex_state = EXPR_END
on_variables(__method__, tok, data)
end
def on_cvar(tok, data)
@lex_state = EXPR_END
on_variables(__method__, tok, data)
end
def on_gvar(tok, data)
@lex_state = EXPR_END
on_variables(__method__, tok, data)
end
def on_backref(tok, data)
@lex_state = EXPR_END
on_variables(__method__, tok, data)
end
def on_lparen(tok, data)
@lex_state = EXPR_LABEL | EXPR_BEG
data << Token.new(lineno, column, __method__, tok, @lex_state)
end
def on_rparen(tok, data)
@lex_state = EXPR_ENDFN
data << Token.new(lineno, column, __method__, tok, @lex_state)
end
def on_lbrace(tok, data)
@lex_state = EXPR_LABEL | EXPR_BEG
data << Token.new(lineno, column, __method__, tok, @lex_state)
end
def on_rbrace(tok, data)
@lex_state = EXPR_ENDARG
data << Token.new(lineno, column, __method__, tok, @lex_state)
end
def on_lbracket(tok, data)
@lex_state = EXPR_LABEL | EXPR_BEG
data << Token.new(lineno, column, __method__, tok, @lex_state)
end
def on_rbracket(tok, data)
@lex_state = EXPR_ENDARG
data << Token.new(lineno, column, __method__, tok, @lex_state)
end
def on_const(tok, data)
case @lex_state
when EXPR_FNAME
@lex_state = EXPR_ENDFN
when EXPR_CLASS, EXPR_CMDARG, EXPR_MID
@lex_state = EXPR_ARG
else
@lex_state = EXPR_CMDARG
end
data << Token.new(lineno, column, __method__, tok, @lex_state)
end
def on_sp(tok, data)
data << Token.new(lineno, column, __method__, tok, @lex_state)
end
def on_comma(tok, data)
@lex_state = EXPR_BEG | EXPR_LABEL if (EXPR_ARG_ANY & @lex_state) != 0
data << Token.new(lineno, column, __method__, tok, @lex_state)
end
def on_comment(tok, data)
@lex_state = EXPR_BEG unless (EXPR_LABEL & @lex_state) != 0
data << Token.new(lineno, column, __method__, tok, @lex_state)
end
def on_ignored_sp(tok, data)
@lex_state = EXPR_BEG unless (EXPR_LABEL & @lex_state) != 0
data << Token.new(lineno, column, __method__, tok, @lex_state)
end
def on_heredoc_beg(tok, data)
data << Token.new(lineno, column, __method__, tok, @lex_state)
@lex_state = EXPR_END
data
end
def on_heredoc_end(tok, data)
data << Token.new(lineno, column, __method__, tok, @lex_state)
@lex_state = EXPR_BEG
data
end
def on_default(event, tok, data)
reset
data << Token.new(lineno, column, event, tok, @lex_state)
end
end unless RIPPER_HAS_LEX_STATE
class InnerStateLex < Ripper::Filter
def initialize(code)
super(code)
end
def on_default(event, tok, data)
data << Token.new(lineno, column, event, tok, state)
end
end if RIPPER_HAS_LEX_STATE
def get_squashed_tk
if @buf.empty?
tk = @tokens.shift
else
tk = @buf.shift
end
return nil if tk.nil?
case tk[:kind]
when :on_symbeg then
tk = get_symbol_tk(tk)
when :on_tstring_beg then
tk = get_string_tk(tk)
when :on_backtick then
if (tk[:state] & (EXPR_FNAME | EXPR_ENDFN)) != 0
@inner_lex.lex_state = EXPR_ARG unless RIPPER_HAS_LEX_STATE
tk[:kind] = :on_ident
tk[:state] = Ripper::Lexer.const_defined?(:State) ? Ripper::Lexer::State.new(EXPR_ARG) : EXPR_ARG
else
tk = get_string_tk(tk)
end
when :on_regexp_beg then
tk = get_regexp_tk(tk)
when :on_embdoc_beg then
tk = get_embdoc_tk(tk)
when :on_heredoc_beg then
@heredoc_queue << retrieve_heredoc_info(tk)
@inner_lex.lex_state = EXPR_END unless RIPPER_HAS_LEX_STATE
when :on_nl, :on_ignored_nl, :on_comment, :on_heredoc_end then
if !@heredoc_queue.empty?
get_heredoc_tk(*@heredoc_queue.shift)
elsif tk[:text].nil? # :on_ignored_nl sometimes gives nil
tk[:text] = ''
end
when :on_words_beg then
tk = get_words_tk(tk)
when :on_qwords_beg then
tk = get_words_tk(tk)
when :on_symbols_beg then
tk = get_words_tk(tk)
when :on_qsymbols_beg then
tk = get_words_tk(tk)
when :on_op then
if '&.' == tk[:text]
tk[:kind] = :on_period
else
tk = get_op_tk(tk)
end
end
tk
end
private def get_symbol_tk(tk)
is_symbol = true
symbol_tk = Token.new(tk.line_no, tk.char_no, :on_symbol)
if ":'" == tk[:text] or ':"' == tk[:text]
tk1 = get_string_tk(tk)
symbol_tk[:text] = tk1[:text]
symbol_tk[:state] = tk1[:state]
else
case (tk1 = get_squashed_tk)[:kind]
when :on_ident
symbol_tk[:text] = ":#{tk1[:text]}"
symbol_tk[:state] = tk1[:state]
when :on_tstring_content
symbol_tk[:text] = ":#{tk1[:text]}"
symbol_tk[:state] = get_squashed_tk[:state] # skip :on_tstring_end
when :on_tstring_end
symbol_tk[:text] = ":#{tk1[:text]}"
symbol_tk[:state] = tk1[:state]
when :on_op
symbol_tk[:text] = ":#{tk1[:text]}"
symbol_tk[:state] = tk1[:state]
when :on_ivar
symbol_tk[:text] = ":#{tk1[:text]}"
symbol_tk[:state] = tk1[:state]
when :on_cvar
symbol_tk[:text] = ":#{tk1[:text]}"
symbol_tk[:state] = tk1[:state]
when :on_gvar
symbol_tk[:text] = ":#{tk1[:text]}"
symbol_tk[:state] = tk1[:state]
when :on_const
symbol_tk[:text] = ":#{tk1[:text]}"
symbol_tk[:state] = tk1[:state]
when :on_kw
symbol_tk[:text] = ":#{tk1[:text]}"
symbol_tk[:state] = tk1[:state]
else
is_symbol = false
tk = tk1
end
end
if is_symbol
tk = symbol_tk
end
tk
end
private def get_string_tk(tk)
string = tk[:text]
state = nil
kind = :on_tstring
loop do
inner_str_tk = get_squashed_tk
if inner_str_tk.nil?
break
elsif :on_tstring_end == inner_str_tk[:kind]
string = string + inner_str_tk[:text]
state = inner_str_tk[:state]
break
elsif :on_label_end == inner_str_tk[:kind]
string = string + inner_str_tk[:text]
state = inner_str_tk[:state]
kind = :on_symbol
break
else
string = string + inner_str_tk[:text]
if :on_embexpr_beg == inner_str_tk[:kind] then
kind = :on_dstring if :on_tstring == kind
end
end
end
Token.new(tk.line_no, tk.char_no, kind, string, state)
end
private def get_regexp_tk(tk)
string = tk[:text]
state = nil
loop do
inner_str_tk = get_squashed_tk
if inner_str_tk.nil?
break
elsif :on_regexp_end == inner_str_tk[:kind]
string = string + inner_str_tk[:text]
state = inner_str_tk[:state]
break
else
string = string + inner_str_tk[:text]
end
end
Token.new(tk.line_no, tk.char_no, :on_regexp, string, state)
end
private def get_embdoc_tk(tk)
string = tk[:text]
until :on_embdoc_end == (embdoc_tk = get_squashed_tk)[:kind] do
string = string + embdoc_tk[:text]
end
string = string + embdoc_tk[:text]
Token.new(tk.line_no, tk.char_no, :on_embdoc, string, embdoc_tk.state)
end
private def get_heredoc_tk(heredoc_name, indent)
string = ''
start_tk = nil
prev_tk = nil
until heredoc_end?(heredoc_name, indent, tk = @tokens.shift) do
start_tk = tk unless start_tk
if (prev_tk.nil? or "\n" == prev_tk[:text][-1]) and 0 != tk[:char_no]
string = string + (' ' * tk[:char_no])
end
string = string + tk[:text]
prev_tk = tk
end
start_tk = tk unless start_tk
prev_tk = tk unless prev_tk
@buf.unshift tk # closing heredoc
heredoc_tk = Token.new(start_tk.line_no, start_tk.char_no, :on_heredoc, string, prev_tk.state)
@buf.unshift heredoc_tk
end
private def retrieve_heredoc_info(tk)
name = tk[:text].gsub(/\A<<[-~]?(['"`]?)(.+)\1\z/, '\2')
indent = tk[:text] =~ /\A<<[-~]/
[name, indent]
end
private def heredoc_end?(name, indent, tk)
result = false
if :on_heredoc_end == tk[:kind] then
tk_name = tk[:text].chomp
tk_name.lstrip! if indent
if name == tk_name
result = true
end
end
result
end
private def get_words_tk(tk)
string = ''
start_token = tk[:text]
start_quote = tk[:text].rstrip[-1]
line_no = tk[:line_no]
char_no = tk[:char_no]
state = tk[:state]
end_quote =
case start_quote
when ?( then ?)
when ?[ then ?]
when ?{ then ?}
when ?< then ?>
else start_quote
end
end_token = nil
loop do
tk = get_squashed_tk
if tk.nil?
end_token = end_quote
break
elsif :on_tstring_content == tk[:kind] then
string += tk[:text]
elsif :on_words_sep == tk[:kind] or :on_tstring_end == tk[:kind] then
if end_quote == tk[:text].strip then
end_token = tk[:text]
break
else
string += tk[:text]
end
else
string += tk[:text]
end
end
text = "#{start_token}#{string}#{end_token}"
Token.new(line_no, char_no, :on_dstring, text, state)
end
private def get_op_tk(tk)
redefinable_operators = %w[! != !~ % & * ** + +@ - -@ / < << <= <=> == === =~ > >= >> [] []= ^ ` | ~]
if redefinable_operators.include?(tk[:text]) and tk[:state] == EXPR_ARG then
@inner_lex.lex_state = EXPR_ARG unless RIPPER_HAS_LEX_STATE
tk[:state] = Ripper::Lexer.const_defined?(:State) ? Ripper::Lexer::State.new(EXPR_ARG) : EXPR_ARG
tk[:kind] = :on_ident
elsif tk[:text] =~ /^[-+]$/ then
tk_ahead = get_squashed_tk
case tk_ahead[:kind]
when :on_int, :on_float, :on_rational, :on_imaginary then
tk[:text] += tk_ahead[:text]
tk[:kind] = tk_ahead[:kind]
tk[:state] = tk_ahead[:state]
when :on_heredoc_beg, :on_tstring, :on_dstring # frozen/non-frozen string literal
tk[:text] += tk_ahead[:text]
tk[:kind] = tk_ahead[:kind]
tk[:state] = tk_ahead[:state]
else
@buf.unshift tk_ahead
end
end
tk
end
def initialize(code)
@buf = []
@heredoc_queue = []
@inner_lex = InnerStateLex.new(code)
@tokens = @inner_lex.parse([])
end
def self.parse(code)
lex = self.new(code)
tokens = []
begin
while tk = lex.get_squashed_tk
tokens.push tk
end
rescue StopIteration
end
tokens
end
def self.end?(token)
(token[:state] & EXPR_END)
end
end
PK ! U simple.rbnu [ # frozen_string_literal: true
##
# Parse a non-source file. We basically take the whole thing as one big
# comment.
class RDoc::Parser::Simple < RDoc::Parser
include RDoc::Parser::Text
parse_files_matching(//)
attr_reader :content # :nodoc:
##
# Prepare to parse a plain file
def initialize(top_level, file_name, content, options, stats)
super
preprocess = RDoc::Markup::PreProcess.new @file_name, @options.rdoc_include
@content = preprocess.handle @content, @top_level
end
##
# Extract the file contents and attach them to the TopLevel as a comment
def scan
comment = remove_coding_comment @content
comment = remove_private_comment comment
comment = RDoc::Comment.new comment, @top_level
@top_level.comment = comment
@top_level
end
##
# Removes the encoding magic comment from +text+
def remove_coding_comment text
text.sub(/\A# .*coding[=:].*$/, '')
end
##
# Removes private comments.
#
# Unlike RDoc::Comment#remove_private this implementation only looks for two
# dashes at the beginning of the line. Three or more dashes are considered
# to be a rule and ignored.
def remove_private_comment comment
# Workaround for gsub encoding for Ruby 1.9.2 and earlier
empty = ''
empty = RDoc::Encoding.change_encoding empty, comment.encoding
comment = comment.gsub(%r%^--\n.*?^\+\+\n?%m, empty)
comment.sub(%r%^--\n.*%m, empty)
end
end
PK ! 4QՆ
ruby_tools.rbnu [ # frozen_string_literal: true
##
# Collection of methods for writing parsers
module RDoc::Parser::RubyTools
##
# Adds a token listener +obj+, but you should probably use token_listener
def add_token_listener(obj)
@token_listeners ||= []
@token_listeners << obj
end
##
# Fetches the next token from the scanner
def get_tk
tk = nil
if @tokens.empty? then
if @scanner_point >= @scanner.size
return nil
else
tk = @scanner[@scanner_point]
@scanner_point += 1
@read.push tk[:text]
end
else
@read.push @unget_read.shift
tk = @tokens.shift
end
if tk == nil || :on___end__ == tk[:kind]
tk = nil
end
return nil unless tk
# inform any listeners of our shiny new token
@token_listeners.each do |obj|
obj.add_token(tk)
end if @token_listeners
tk
end
##
# Reads and returns all tokens up to one of +tokens+. Leaves the matched
# token in the token list.
def get_tk_until(*tokens)
read = []
loop do
tk = get_tk
case tk
when *tokens then
unget_tk tk
break
end
read << tk
end
read
end
##
# Retrieves a String representation of the read tokens
def get_tkread
read = @read.join("")
@read = []
read
end
##
# Peek equivalent for get_tkread
def peek_read
@read.join('')
end
##
# Peek at the next token, but don't remove it from the stream
def peek_tk
unget_tk(tk = get_tk)
tk
end
##
# Removes the token listener +obj+
def remove_token_listener(obj)
@token_listeners.delete(obj)
end
##
# Resets the tools
def reset
@read = []
@tokens = []
@unget_read = []
@nest = 0
@scanner_point = 0
end
##
# Skips whitespace tokens including newlines
def skip_tkspace
tokens = []
while (tk = get_tk) and (:on_sp == tk[:kind] or :on_nl == tk[:kind] or :on_ignored_nl == tk[:kind]) do
tokens.push(tk)
end
unget_tk(tk)
tokens
end
##
# Skips whitespace tokens excluding newlines
def skip_tkspace_without_nl
tokens = []
while (tk = get_tk) and :on_sp == tk[:kind] do
tokens.push(tk)
end
unget_tk(tk)
tokens
end
##
# Has +obj+ listen to tokens
def token_listener(obj)
add_token_listener obj
yield
ensure
remove_token_listener obj
end
##
# Returns +tk+ to the scanner
def unget_tk(tk)
@tokens.unshift tk
@unget_read.unshift @read.pop
# Remove this token from any listeners
@token_listeners.each do |obj|
obj.pop_token
end if @token_listeners
nil
end
end
PK ! _A markdown.rbnu [ # frozen_string_literal: true
##
# Parse a Markdown format file. The parsed RDoc::Markup::Document is attached
# as a file comment.
class RDoc::Parser::Markdown < RDoc::Parser
include RDoc::Parser::Text
parse_files_matching(/\.(md|markdown)(?:\.[^.]+)?$/)
##
# Creates an Markdown-format TopLevel for the given file.
def scan
comment = RDoc::Comment.new @content, @top_level
comment.format = 'markdown'
@top_level.comment = comment
end
end
PK ! 5n n ruby.rbnu [ # frozen_string_literal: true
##
# This file contains stuff stolen outright from:
#
# rtags.rb -
# ruby-lex.rb - ruby lexcal analyzer
# ruby-token.rb - ruby tokens
# by Keiju ISHITSUKA (Nippon Rational Inc.)
#
##
# Extracts code elements from a source file returning a TopLevel object
# containing the constituent file elements.
#
# This file is based on rtags
#
# RubyParser understands how to document:
# * classes
# * modules
# * methods
# * constants
# * aliases
# * private, public, protected
# * private_class_function, public_class_function
# * private_constant, public_constant
# * module_function
# * attr, attr_reader, attr_writer, attr_accessor
# * extra accessors given on the command line
# * metaprogrammed methods
# * require
# * include
#
# == Method Arguments
#
#--
# NOTE: I don't think this works, needs tests, remove the paragraph following
# this block when known to work
#
# The parser extracts the arguments from the method definition. You can
# override this with a custom argument definition using the :args: directive:
#
# ##
# # This method tries over and over until it is tired
#
# def go_go_go(thing_to_try, tries = 10) # :args: thing_to_try
# puts thing_to_try
# go_go_go thing_to_try, tries - 1
# end
#
# If you have a more-complex set of overrides you can use the :call-seq:
# directive:
#++
#
# The parser extracts the arguments from the method definition. You can
# override this with a custom argument definition using the :call-seq:
# directive:
#
# ##
# # This method can be called with a range or an offset and length
# #
# # :call-seq:
# # my_method(Range)
# # my_method(offset, length)
#
# def my_method(*args)
# end
#
# The parser extracts +yield+ expressions from method bodies to gather the
# yielded argument names. If your method manually calls a block instead of
# yielding or you want to override the discovered argument names use
# the :yields: directive:
#
# ##
# # My method is awesome
#
# def my_method(&block) # :yields: happy, times
# block.call 1, 2
# end
#
# == Metaprogrammed Methods
#
# To pick up a metaprogrammed method, the parser looks for a comment starting
# with '##' before an identifier:
#
# ##
# # This is a meta-programmed method!
#
# add_my_method :meta_method, :arg1, :arg2
#
# The parser looks at the token after the identifier to determine the name, in
# this example, :meta_method. If a name cannot be found, a warning is printed
# and 'unknown is used.
#
# You can force the name of a method using the :method: directive:
#
# ##
# # :method: some_method!
#
# By default, meta-methods are instance methods. To indicate that a method is
# a singleton method instead use the :singleton-method: directive:
#
# ##
# # :singleton-method:
#
# You can also use the :singleton-method: directive with a name:
#
# ##
# # :singleton-method: some_method!
#
# You can define arguments for metaprogrammed methods via either the
# :call-seq:, :arg: or :args: directives.
#
# Additionally you can mark a method as an attribute by
# using :attr:, :attr_reader:, :attr_writer: or :attr_accessor:. Just like
# for :method:, the name is optional.
#
# ##
# # :attr_reader: my_attr_name
#
# == Hidden methods and attributes
#
# You can provide documentation for methods that don't appear using
# the :method:, :singleton-method: and :attr: directives:
#
# ##
# # :attr_writer: ghost_writer
# # There is an attribute here, but you can't see it!
#
# ##
# # :method: ghost_method
# # There is a method here, but you can't see it!
#
# ##
# # this is a comment for a regular method
#
# def regular_method() end
#
# Note that by default, the :method: directive will be ignored if there is a
# standard rdocable item following it.
require 'ripper'
require_relative 'ripper_state_lex'
class RDoc::Parser::Ruby < RDoc::Parser
parse_files_matching(/\.rbw?$/)
include RDoc::TokenStream
include RDoc::Parser::RubyTools
##
# RDoc::NormalClass type
NORMAL = "::"
##
# RDoc::SingleClass type
SINGLE = "<<"
##
# Creates a new Ruby parser.
def initialize(top_level, file_name, content, options, stats)
super
content = handle_tab_width(content)
@size = 0
@token_listeners = nil
content = RDoc::Encoding.remove_magic_comment content
@scanner = RDoc::Parser::RipperStateLex.parse(content)
@content = content
@scanner_point = 0
@prev_seek = nil
@markup = @options.markup
@track_visibility = :nodoc != @options.visibility
@encoding = @options.encoding
reset
end
def tk_nl?(tk)
:on_nl == tk[:kind] or :on_ignored_nl == tk[:kind]
end
##
# Retrieves the read token stream and replaces +pattern+ with +replacement+
# using gsub. If the result is only a ";" returns an empty string.
def get_tkread_clean pattern, replacement # :nodoc:
read = get_tkread.gsub(pattern, replacement).strip
return '' if read == ';'
read
end
##
# Extracts the visibility information for the visibility token +tk+
# and +single+ class type identifier.
#
# Returns the visibility type (a string), the visibility (a symbol) and
# +singleton+ if the methods following should be converted to singleton
# methods.
def get_visibility_information tk, single # :nodoc:
vis_type = tk[:text]
singleton = single == SINGLE
vis =
case vis_type
when 'private' then :private
when 'protected' then :protected
when 'public' then :public
when 'private_class_method' then
singleton = true
:private
when 'public_class_method' then
singleton = true
:public
when 'module_function' then
singleton = true
:public
else
raise RDoc::Error, "Invalid visibility: #{tk.name}"
end
return vis_type, vis, singleton
end
##
# Look for the first comment in a file that isn't a shebang line.
def collect_first_comment
skip_tkspace
comment = ''.dup
comment = RDoc::Encoding.change_encoding comment, @encoding if @encoding
first_line = true
first_comment_tk_kind = nil
line_no = nil
tk = get_tk
while tk && (:on_comment == tk[:kind] or :on_embdoc == tk[:kind])
comment_body = retrieve_comment_body(tk)
if first_line and comment_body =~ /\A#!/ then
skip_tkspace
tk = get_tk
elsif first_line and comment_body =~ /\A#\s*-\*-/ then
first_line = false
skip_tkspace
tk = get_tk
else
break if first_comment_tk_kind and not first_comment_tk_kind === tk[:kind]
first_comment_tk_kind = tk[:kind]
line_no = tk[:line_no] if first_line
first_line = false
comment << comment_body
tk = get_tk
if :on_nl === tk then
skip_tkspace_without_nl
tk = get_tk
end
end
end
unget_tk tk
new_comment comment, line_no
end
##
# Consumes trailing whitespace from the token stream
def consume_trailing_spaces # :nodoc:
skip_tkspace_without_nl
end
##
# Creates a new attribute in +container+ with +name+.
def create_attr container, single, name, rw, comment # :nodoc:
att = RDoc::Attr.new get_tkread, name, rw, comment, single == SINGLE
record_location att
container.add_attribute att
@stats.add_attribute att
att
end
##
# Creates a module alias in +container+ at +rhs_name+ (or at the top-level
# for "::") with the name from +constant+.
def create_module_alias container, constant, rhs_name # :nodoc:
mod = if rhs_name =~ /^::/ then
@store.find_class_or_module rhs_name
else
container.find_module_named rhs_name
end
container.add_module_alias mod, rhs_name, constant, @top_level
end
##
# Aborts with +msg+
def error(msg)
msg = make_message msg
abort msg
end
##
# Looks for a true or false token.
def get_bool
skip_tkspace
tk = get_tk
if :on_kw == tk[:kind] && 'true' == tk[:text]
true
elsif :on_kw == tk[:kind] && ('false' == tk[:text] || 'nil' == tk[:text])
false
else
unget_tk tk
true
end
end
##
# Look for the name of a class of module (optionally with a leading :: or
# with :: separated named) and return the ultimate name, the associated
# container, and the given name (with the ::).
def get_class_or_module container, ignore_constants = false
skip_tkspace
name_t = get_tk
given_name = ''.dup
# class ::A -> A is in the top level
if :on_op == name_t[:kind] and '::' == name_t[:text] then # bug
name_t = get_tk
container = @top_level
given_name << '::'
end
skip_tkspace_without_nl
given_name << name_t[:text]
is_self = name_t[:kind] == :on_op && name_t[:text] == '<<'
new_modules = []
while !is_self && (tk = peek_tk) and :on_op == tk[:kind] and '::' == tk[:text] do
prev_container = container
container = container.find_module_named name_t[:text]
container ||=
if ignore_constants then
c = RDoc::NormalModule.new name_t[:text]
c.store = @store
new_modules << [prev_container, c]
c
else
c = prev_container.add_module RDoc::NormalModule, name_t[:text]
c.ignore unless prev_container.document_children
@top_level.add_to_classes_or_modules c
c
end
record_location container
get_tk
skip_tkspace
if :on_lparen == peek_tk[:kind] # ProcObjectInConstant::()
parse_method_or_yield_parameters
break
end
name_t = get_tk
unless :on_const == name_t[:kind] || :on_ident == name_t[:kind]
raise RDoc::Error, "Invalid class or module definition: #{given_name}"
end
if prev_container == container and !ignore_constants
given_name = name_t[:text]
else
given_name << '::' + name_t[:text]
end
end
skip_tkspace_without_nl
return [container, name_t, given_name, new_modules]
end
##
# Skip opening parentheses and yield the block.
# Skip closing parentheses too when exists.
def skip_parentheses(&block)
left_tk = peek_tk
if :on_lparen == left_tk[:kind]
get_tk
ret = skip_parentheses(&block)
right_tk = peek_tk
if :on_rparen == right_tk[:kind]
get_tk
end
ret
else
yield
end
end
##
# Return a superclass, which can be either a constant of an expression
def get_class_specification
tk = peek_tk
if tk.nil?
return ''
elsif :on_kw == tk[:kind] && 'self' == tk[:text]
return 'self'
elsif :on_gvar == tk[:kind]
return ''
end
res = get_constant
skip_tkspace_without_nl
get_tkread # empty out read buffer
tk = get_tk
return res unless tk
case tk[:kind]
when :on_nl, :on_comment, :on_embdoc, :on_semicolon then
unget_tk(tk)
return res
end
res += parse_call_parameters(tk)
res
end
##
# Parse a constant, which might be qualified by one or more class or module
# names
def get_constant
res = ""
skip_tkspace_without_nl
tk = get_tk
while tk && ((:on_op == tk[:kind] && '::' == tk[:text]) || :on_const == tk[:kind]) do
res += tk[:text]
tk = get_tk
end
unget_tk(tk)
res
end
##
# Get an included module that may be surrounded by parens
def get_included_module_with_optional_parens
skip_tkspace_without_nl
get_tkread
tk = get_tk
end_token = get_end_token tk
return '' unless end_token
nest = 0
continue = false
only_constant = true
while tk != nil do
is_element_of_constant = false
case tk[:kind]
when :on_semicolon then
break if nest == 0
when :on_lbracket then
nest += 1
when :on_rbracket then
nest -= 1
when :on_lbrace then
nest += 1
when :on_rbrace then
nest -= 1
if nest <= 0
# we might have a.each { |i| yield i }
unget_tk(tk) if nest < 0
break
end
when :on_lparen then
nest += 1
when end_token[:kind] then
if end_token[:kind] == :on_rparen
nest -= 1
break if nest <= 0
else
break if nest <= 0
end
when :on_rparen then
nest -= 1
when :on_comment, :on_embdoc then
@read.pop
if :on_nl == end_token[:kind] and "\n" == tk[:text][-1] and
(!continue or (tk[:state] & RDoc::Parser::RipperStateLex::EXPR_LABEL) != 0) then
break if !continue and nest <= 0
end
when :on_comma then
continue = true
when :on_ident then
continue = false if continue
when :on_kw then
case tk[:text]
when 'def', 'do', 'case', 'for', 'begin', 'class', 'module'
nest += 1
when 'if', 'unless', 'while', 'until', 'rescue'
# postfix if/unless/while/until/rescue must be EXPR_LABEL
nest += 1 unless (tk[:state] & RDoc::Parser::RipperStateLex::EXPR_LABEL) != 0
when 'end'
nest -= 1
break if nest == 0
end
when :on_const then
is_element_of_constant = true
when :on_op then
is_element_of_constant = true if '::' == tk[:text]
end
only_constant = false unless is_element_of_constant
tk = get_tk
end
if only_constant
get_tkread_clean(/\s+/, ' ')
else
''
end
end
##
# Little hack going on here. In the statement:
#
# f = 2*(1+yield)
#
# We see the RPAREN as the next token, so we need to exit early. This still
# won't catch all cases (such as "a = yield + 1"
def get_end_token tk # :nodoc:
case tk[:kind]
when :on_lparen
token = RDoc::Parser::RipperStateLex::Token.new
token[:kind] = :on_rparen
token[:text] = ')'
token
when :on_rparen
nil
else
token = RDoc::Parser::RipperStateLex::Token.new
token[:kind] = :on_nl
token[:text] = "\n"
token
end
end
##
# Retrieves the method container for a singleton method.
def get_method_container container, name_t # :nodoc:
prev_container = container
container = container.find_module_named(name_t[:text])
unless container then
constant = prev_container.constants.find do |const|
const.name == name_t[:text]
end
if constant then
parse_method_dummy prev_container
return
end
end
unless container then
# TODO seems broken, should starting at Object in @store
obj = name_t[:text].split("::").inject(Object) do |state, item|
state.const_get(item)
end rescue nil
type = obj.class == Class ? RDoc::NormalClass : RDoc::NormalModule
unless [Class, Module].include?(obj.class) then
warn("Couldn't find #{name_t[:text]}. Assuming it's a module")
end
if type == RDoc::NormalClass then
sclass = obj.superclass ? obj.superclass.name : nil
container = prev_container.add_class type, name_t[:text], sclass
else
container = prev_container.add_module type, name_t[:text]
end
record_location container
end
container
end
##
# Extracts a name or symbol from the token stream.
def get_symbol_or_name
tk = get_tk
case tk[:kind]
when :on_symbol then
text = tk[:text].sub(/^:/, '')
next_tk = peek_tk
if next_tk && :on_op == next_tk[:kind] && '=' == next_tk[:text] then
get_tk
text << '='
end
text
when :on_ident, :on_const, :on_gvar, :on_cvar, :on_ivar, :on_op, :on_kw then
tk[:text]
when :on_tstring, :on_dstring then
tk[:text][1..-2]
else
raise RDoc::Error, "Name or symbol expected (got #{tk})"
end
end
##
# Marks containers between +container+ and +ancestor+ as ignored
def suppress_parents container, ancestor # :nodoc:
while container and container != ancestor do
container.suppress unless container.documented?
container = container.parent
end
end
##
# Look for directives in a normal comment block:
#
# # :stopdoc:
# # Don't display comment from this point forward
#
# This routine modifies its +comment+ parameter.
def look_for_directives_in container, comment
@preprocess.handle comment, container do |directive, param|
case directive
when 'method', 'singleton-method',
'attr', 'attr_accessor', 'attr_reader', 'attr_writer' then
false # handled elsewhere
when 'section' then
break unless container.kind_of?(RDoc::Context)
container.set_current_section param, comment.dup
comment.text = ''
break
end
end
comment.remove_private
end
##
# Adds useful info about the parser to +message+
def make_message message
prefix = "#{@file_name}:".dup
tk = peek_tk
prefix << "#{tk[:line_no]}:#{tk[:char_no]}:" if tk
"#{prefix} #{message}"
end
##
# Creates a comment with the correct format
def new_comment comment, line_no = nil
c = RDoc::Comment.new comment, @top_level, :ruby
c.line = line_no
c.format = @markup
c
end
##
# Creates an RDoc::Attr for the name following +tk+, setting the comment to
# +comment+.
def parse_attr(context, single, tk, comment)
line_no = tk[:line_no]
args = parse_symbol_arg 1
if args.size > 0 then
name = args[0]
rw = "R"
skip_tkspace_without_nl
tk = get_tk
if :on_comma == tk[:kind] then
rw = "RW" if get_bool
else
unget_tk tk
end
att = create_attr context, single, name, rw, comment
att.line = line_no
read_documentation_modifiers att, RDoc::ATTR_MODIFIERS
else
warn "'attr' ignored - looks like a variable"
end
end
##
# Creates an RDoc::Attr for each attribute listed after +tk+, setting the
# comment for each to +comment+.
def parse_attr_accessor(context, single, tk, comment)
line_no = tk[:line_no]
args = parse_symbol_arg
rw = "?"
tmp = RDoc::CodeObject.new
read_documentation_modifiers tmp, RDoc::ATTR_MODIFIERS
# TODO In most other places we let the context keep track of document_self
# and add found items appropriately but here we do not. I'm not sure why.
return if @track_visibility and not tmp.document_self
case tk[:text]
when "attr_reader" then rw = "R"
when "attr_writer" then rw = "W"
when "attr_accessor" then rw = "RW"
else
rw = '?'
end
for name in args
att = create_attr context, single, name, rw, comment
att.line = line_no
end
end
##
# Parses an +alias+ in +context+ with +comment+
def parse_alias(context, single, tk, comment)
line_no = tk[:line_no]
skip_tkspace
if :on_lparen === peek_tk[:kind] then
get_tk
skip_tkspace
end
new_name = get_symbol_or_name
skip_tkspace
if :on_comma === peek_tk[:kind] then
get_tk
skip_tkspace
end
begin
old_name = get_symbol_or_name
rescue RDoc::Error
return
end
al = RDoc::Alias.new(get_tkread, old_name, new_name, comment,
single == SINGLE)
record_location al
al.line = line_no
read_documentation_modifiers al, RDoc::ATTR_MODIFIERS
context.add_alias al
@stats.add_alias al
al
end
##
# Extracts call parameters from the token stream.
def parse_call_parameters(tk)
end_token = case tk[:kind]
when :on_lparen
:on_rparen
when :on_rparen
return ""
else
:on_nl
end
nest = 0
loop do
break if tk.nil?
case tk[:kind]
when :on_semicolon
break
when :on_lparen
nest += 1
when end_token
if end_token == :on_rparen
nest -= 1
break if RDoc::Parser::RipperStateLex.end?(tk) and nest <= 0
else
break if RDoc::Parser::RipperStateLex.end?(tk)
end
when :on_comment, :on_embdoc
unget_tk(tk)
break
when :on_op
if tk[:text] =~ /^(.{1,2})?=$/
unget_tk(tk)
break
end
end
tk = get_tk
end
get_tkread_clean "\n", " "
end
##
# Parses a class in +context+ with +comment+
def parse_class container, single, tk, comment
line_no = tk[:line_no]
declaration_context = container
container, name_t, given_name, = get_class_or_module container
if name_t[:kind] == :on_const
cls = parse_class_regular container, declaration_context, single,
name_t, given_name, comment
elsif name_t[:kind] == :on_op && name_t[:text] == '<<'
case name = skip_parentheses { get_class_specification }
when 'self', container.name
read_documentation_modifiers cls, RDoc::CLASS_MODIFIERS
parse_statements container, SINGLE
return # don't update line
else
cls = parse_class_singleton container, name, comment
end
else
warn "Expected class name or '<<'. Got #{name_t[:kind]}: #{name_t[:text].inspect}"
return
end
cls.line = line_no
# after end modifiers
read_documentation_modifiers cls, RDoc::CLASS_MODIFIERS
cls
end
##
# Parses and creates a regular class
def parse_class_regular container, declaration_context, single, # :nodoc:
name_t, given_name, comment
superclass = '::Object'
if given_name =~ /^::/ then
declaration_context = @top_level
given_name = $'
end
tk = peek_tk
if tk[:kind] == :on_op && tk[:text] == '<' then
get_tk
skip_tkspace
superclass = get_class_specification
superclass = '(unknown)' if superclass.empty?
end
cls_type = single == SINGLE ? RDoc::SingleClass : RDoc::NormalClass
cls = declaration_context.add_class cls_type, given_name, superclass
cls.ignore unless container.document_children
read_documentation_modifiers cls, RDoc::CLASS_MODIFIERS
record_location cls
cls.add_comment comment, @top_level
@top_level.add_to_classes_or_modules cls
@stats.add_class cls
suppress_parents container, declaration_context unless cls.document_self
parse_statements cls
cls
end
##
# Parses a singleton class in +container+ with the given +name+ and
# +comment+.
def parse_class_singleton container, name, comment # :nodoc:
other = @store.find_class_named name
unless other then
if name =~ /^::/ then
name = $'
container = @top_level
end
other = container.add_module RDoc::NormalModule, name
record_location other
# class << $gvar
other.ignore if name.empty?
other.add_comment comment, @top_level
end
# notify :nodoc: all if not a constant-named class/module
# (and remove any comment)
unless name =~ /\A(::)?[A-Z]/ then
other.document_self = nil
other.document_children = false
other.clear_comment
end
@top_level.add_to_classes_or_modules other
@stats.add_class other
read_documentation_modifiers other, RDoc::CLASS_MODIFIERS
parse_statements(other, SINGLE)
other
end
##
# Parses a constant in +context+ with +comment+. If +ignore_constants+ is
# true, no found constants will be added to RDoc.
def parse_constant container, tk, comment, ignore_constants = false
line_no = tk[:line_no]
name = tk[:text]
skip_tkspace_without_nl
return unless name =~ /^\w+$/
new_modules = []
if :on_op == peek_tk[:kind] && '::' == peek_tk[:text] then
unget_tk tk
container, name_t, _, new_modules = get_class_or_module container, true
name = name_t[:text]
end
is_array_or_hash = false
if peek_tk && :on_lbracket == peek_tk[:kind]
get_tk
nest = 1
while bracket_tk = get_tk
case bracket_tk[:kind]
when :on_lbracket
nest += 1
when :on_rbracket
nest -= 1
break if nest == 0
end
end
skip_tkspace_without_nl
is_array_or_hash = true
end
unless peek_tk && :on_op == peek_tk[:kind] && '=' == peek_tk[:text] then
return false
end
get_tk
unless ignore_constants
new_modules.each do |prev_c, new_module|
prev_c.add_module_by_normal_module new_module
new_module.ignore unless prev_c.document_children
@top_level.add_to_classes_or_modules new_module
end
end
value = ''
con = RDoc::Constant.new name, value, comment
body = parse_constant_body container, con, is_array_or_hash
return unless body
con.value = body
record_location con
con.line = line_no
read_documentation_modifiers con, RDoc::CONSTANT_MODIFIERS
return if is_array_or_hash
@stats.add_constant con
container.add_constant con
true
end
def parse_constant_body container, constant, is_array_or_hash # :nodoc:
nest = 0
rhs_name = ''.dup
get_tkread
tk = get_tk
body = nil
loop do
break if tk.nil?
if :on_semicolon == tk[:kind] then
break if nest <= 0
elsif [:on_tlambeg, :on_lparen, :on_lbrace, :on_lbracket].include?(tk[:kind]) then
nest += 1
elsif (:on_kw == tk[:kind] && 'def' == tk[:text]) then
nest += 1
elsif (:on_kw == tk[:kind] && %w{do if unless case begin}.include?(tk[:text])) then
if (tk[:state] & RDoc::Parser::RipperStateLex::EXPR_LABEL) == 0
nest += 1
end
elsif [:on_rparen, :on_rbrace, :on_rbracket].include?(tk[:kind]) ||
(:on_kw == tk[:kind] && 'end' == tk[:text]) then
nest -= 1
elsif (:on_comment == tk[:kind] or :on_embdoc == tk[:kind]) then
unget_tk tk
if nest <= 0 and RDoc::Parser::RipperStateLex.end?(tk) then
body = get_tkread_clean(/^[ \t]+/, '')
read_documentation_modifiers constant, RDoc::CONSTANT_MODIFIERS
break
else
read_documentation_modifiers constant, RDoc::CONSTANT_MODIFIERS
end
elsif :on_const == tk[:kind] then
rhs_name << tk[:text]
next_tk = peek_tk
if nest <= 0 and (next_tk.nil? || :on_nl == next_tk[:kind]) then
create_module_alias container, constant, rhs_name unless is_array_or_hash
break
end
elsif :on_nl == tk[:kind] then
if nest <= 0 and RDoc::Parser::RipperStateLex.end?(tk) then
unget_tk tk
break
end
elsif :on_op == tk[:kind] && '::' == tk[:text]
rhs_name << '::'
end
tk = get_tk
end
body ? body : get_tkread_clean(/^[ \t]+/, '')
end
##
# Generates an RDoc::Method or RDoc::Attr from +comment+ by looking for
# :method: or :attr: directives in +comment+.
def parse_comment container, tk, comment
return parse_comment_tomdoc container, tk, comment if @markup == 'tomdoc'
column = tk[:char_no]
line_no = comment.line.nil? ? tk[:line_no] : comment.line
comment.text = comment.text.sub(/(^# +:?)(singleton-)(method:)/, '\1\3')
singleton = !!$~
co =
if (comment.text = comment.text.sub(/^# +:?method: *(\S*).*?\n/i, '')) && !!$~ then
line_no += $`.count("\n")
parse_comment_ghost container, comment.text, $1, column, line_no, comment
elsif (comment.text = comment.text.sub(/# +:?(attr(_reader|_writer|_accessor)?): *(\S*).*?\n/i, '')) && !!$~ then
parse_comment_attr container, $1, $3, comment
end
if co then
co.singleton = singleton
co.line = line_no
end
true
end
##
# Parse a comment that is describing an attribute in +container+ with the
# given +name+ and +comment+.
def parse_comment_attr container, type, name, comment # :nodoc:
return if name.empty?
rw = case type
when 'attr_reader' then 'R'
when 'attr_writer' then 'W'
else 'RW'
end
create_attr container, NORMAL, name, rw, comment
end
def parse_comment_ghost container, text, name, column, line_no, # :nodoc:
comment
name = nil if name.empty?
meth = RDoc::GhostMethod.new get_tkread, name
record_location meth
meth.start_collecting_tokens
indent = RDoc::Parser::RipperStateLex::Token.new(1, 1, :on_sp, ' ' * column)
position_comment = RDoc::Parser::RipperStateLex::Token.new(line_no, 1, :on_comment)
position_comment[:text] = "# File #{@top_level.relative_name}, line #{line_no}"
newline = RDoc::Parser::RipperStateLex::Token.new(0, 0, :on_nl, "\n")
meth.add_tokens [position_comment, newline, indent]
meth.params =
if text.sub!(/^#\s+:?args?:\s*(.*?)\s*$/i, '') then
$1
else
''
end
comment.normalize
comment.extract_call_seq meth
return unless meth.name
container.add_method meth
meth.comment = comment
@stats.add_method meth
meth
end
##
# Creates an RDoc::Method on +container+ from +comment+ if there is a
# Signature section in the comment
def parse_comment_tomdoc container, tk, comment
return unless signature = RDoc::TomDoc.signature(comment)
column = tk[:char_no]
line_no = tk[:line_no]
name, = signature.split %r%[ \(]%, 2
meth = RDoc::GhostMethod.new get_tkread, name
record_location meth
meth.line = line_no
meth.start_collecting_tokens
indent = RDoc::Parser::RipperStateLex::Token.new(1, 1, :on_sp, ' ' * column)
position_comment = RDoc::Parser::RipperStateLex::Token.new(line_no, 1, :on_comment)
position_comment[:text] = "# File #{@top_level.relative_name}, line #{line_no}"
newline = RDoc::Parser::RipperStateLex::Token.new(0, 0, :on_nl, "\n")
meth.add_tokens [position_comment, newline, indent]
meth.call_seq = signature
comment.normalize
return unless meth.name
container.add_method meth
meth.comment = comment
@stats.add_method meth
end
##
# Parses an +include+ or +extend+, indicated by the +klass+ and adds it to
# +container+ # with +comment+
def parse_extend_or_include klass, container, comment # :nodoc:
loop do
skip_tkspace_comment
name = get_included_module_with_optional_parens
unless name.empty? then
obj = container.add klass, name, comment
record_location obj
end
return if peek_tk.nil? || :on_comma != peek_tk[:kind]
get_tk
end
end
##
# Parses an +included+ with a block feature of ActiveSupport::Concern.
def parse_included_with_activesupport_concern container, comment # :nodoc:
skip_tkspace_without_nl
tk = get_tk
unless tk[:kind] == :on_lbracket || (tk[:kind] == :on_kw && tk[:text] == 'do')
unget_tk tk
return nil # should be a block
end
parse_statements container
container
end
##
# Parses identifiers that can create new methods or change visibility.
#
# Returns true if the comment was not consumed.
def parse_identifier container, single, tk, comment # :nodoc:
case tk[:text]
when 'private', 'protected', 'public', 'private_class_method',
'public_class_method', 'module_function' then
parse_visibility container, single, tk
return true
when 'private_constant', 'public_constant'
parse_constant_visibility container, single, tk
return true
when 'attr' then
parse_attr container, single, tk, comment
when /^attr_(reader|writer|accessor)$/ then
parse_attr_accessor container, single, tk, comment
when 'alias_method' then
parse_alias container, single, tk, comment
when 'require', 'include' then
# ignore
else
if comment.text =~ /\A#\#$/ then
case comment.text
when /^# +:?attr(_reader|_writer|_accessor)?:/ then
parse_meta_attr container, single, tk, comment
else
method = parse_meta_method container, single, tk, comment
method.params = container.params if
container.params
method.block_params = container.block_params if
container.block_params
end
end
end
false
end
##
# Parses a meta-programmed attribute and creates an RDoc::Attr.
#
# To create foo and bar attributes on class C with comment "My attributes":
#
# class C
#
# ##
# # :attr:
# #
# # My attributes
#
# my_attr :foo, :bar
#
# end
#
# To create a foo attribute on class C with comment "My attribute":
#
# class C
#
# ##
# # :attr: foo
# #
# # My attribute
#
# my_attr :foo, :bar
#
# end
def parse_meta_attr(context, single, tk, comment)
args = parse_symbol_arg
rw = "?"
# If nodoc is given, don't document any of them
tmp = RDoc::CodeObject.new
read_documentation_modifiers tmp, RDoc::ATTR_MODIFIERS
regexp = /^# +:?(attr(_reader|_writer|_accessor)?): *(\S*).*?\n/i
if regexp =~ comment.text then
comment.text = comment.text.sub(regexp, '')
rw = case $1
when 'attr_reader' then 'R'
when 'attr_writer' then 'W'
else 'RW'
end
name = $3 unless $3.empty?
end
if name then
att = create_attr context, single, name, rw, comment
else
args.each do |attr_name|
att = create_attr context, single, attr_name, rw, comment
end
end
att
end
##
# Parses a meta-programmed method
def parse_meta_method(container, single, tk, comment)
column = tk[:char_no]
line_no = tk[:line_no]
start_collecting_tokens
add_token tk
add_token_listener self
skip_tkspace_without_nl
comment.text = comment.text.sub(/(^# +:?)(singleton-)(method:)/, '\1\3')
singleton = !!$~
name = parse_meta_method_name comment, tk
return unless name
meth = RDoc::MetaMethod.new get_tkread, name
record_location meth
meth.line = line_no
meth.singleton = singleton
remove_token_listener self
meth.start_collecting_tokens
indent = RDoc::Parser::RipperStateLex::Token.new(1, 1, :on_sp, ' ' * column)
position_comment = RDoc::Parser::RipperStateLex::Token.new(line_no, 1, :on_comment)
position_comment[:text] = "# File #{@top_level.relative_name}, line #{line_no}"
newline = RDoc::Parser::RipperStateLex::Token.new(0, 0, :on_nl, "\n")
meth.add_tokens [position_comment, newline, indent]
meth.add_tokens @token_stream
parse_meta_method_params container, single, meth, tk, comment
meth.comment = comment
@stats.add_method meth
meth
end
##
# Parses the name of a metaprogrammed method. +comment+ is used to
# determine the name while +tk+ is used in an error message if the name
# cannot be determined.
def parse_meta_method_name comment, tk # :nodoc:
if comment.text.sub!(/^# +:?method: *(\S*).*?\n/i, '') then
return $1 unless $1.empty?
end
name_t = get_tk
if :on_symbol == name_t[:kind] then
name_t[:text][1..-1]
elsif :on_tstring == name_t[:kind] then
name_t[:text][1..-2]
elsif :on_op == name_t[:kind] && '=' == name_t[:text] then # ignore
remove_token_listener self
nil
else
warn "unknown name token #{name_t.inspect} for meta-method '#{tk[:text]}'"
'unknown'
end
end
##
# Parses the parameters and block for a meta-programmed method.
def parse_meta_method_params container, single, meth, tk, comment # :nodoc:
token_listener meth do
meth.params = ''
look_for_directives_in meth, comment
comment.normalize
comment.extract_call_seq meth
container.add_method meth
last_tk = tk
while tk = get_tk do
if :on_semicolon == tk[:kind] then
break
elsif :on_nl == tk[:kind] then
break unless last_tk and :on_comma == last_tk[:kind]
elsif :on_sp == tk[:kind] then
# expression continues
elsif :on_kw == tk[:kind] && 'do' == tk[:text] then
parse_statements container, single, meth
break
else
last_tk = tk
end
end
end
end
##
# Parses a normal method defined by +def+
def parse_method(container, single, tk, comment)
singleton = nil
added_container = false
name = nil
column = tk[:char_no]
line_no = tk[:line_no]
start_collecting_tokens
add_token tk
token_listener self do
prev_container = container
name, container, singleton = parse_method_name container
added_container = container != prev_container
end
return unless name
meth = RDoc::AnyMethod.new get_tkread, name
look_for_directives_in meth, comment
meth.singleton = single == SINGLE ? true : singleton
record_location meth
meth.line = line_no
meth.start_collecting_tokens
indent = RDoc::Parser::RipperStateLex::Token.new(1, 1, :on_sp, ' ' * column)
token = RDoc::Parser::RipperStateLex::Token.new(line_no, 1, :on_comment)
token[:text] = "# File #{@top_level.relative_name}, line #{line_no}"
newline = RDoc::Parser::RipperStateLex::Token.new(0, 0, :on_nl, "\n")
meth.add_tokens [token, newline, indent]
meth.add_tokens @token_stream
parse_method_params_and_body container, single, meth, added_container
comment.normalize
comment.extract_call_seq meth
meth.comment = comment
# after end modifiers
read_documentation_modifiers meth, RDoc::METHOD_MODIFIERS
@stats.add_method meth
end
##
# Parses the parameters and body of +meth+
def parse_method_params_and_body container, single, meth, added_container
token_listener meth do
parse_method_parameters meth
if meth.document_self or not @track_visibility then
container.add_method meth
elsif added_container then
container.document_self = false
end
# Having now read the method parameters and documentation modifiers, we
# now know whether we have to rename #initialize to ::new
if meth.name == "initialize" && !meth.singleton then
if meth.dont_rename_initialize then
meth.visibility = :protected
else
meth.singleton = true
meth.name = "new"
meth.visibility = :public
end
end
parse_statements container, single, meth
end
end
##
# Parses a method that needs to be ignored.
def parse_method_dummy container
dummy = RDoc::Context.new
dummy.parent = container
dummy.store = container.store
skip_method dummy
end
##
# Parses the name of a method in +container+.
#
# Returns the method name, the container it is in (for def Foo.name) and if
# it is a singleton or regular method.
def parse_method_name container # :nodoc:
skip_tkspace
name_t = get_tk
back_tk = skip_tkspace_without_nl
singleton = false
dot = get_tk
if dot[:kind] == :on_period || (dot[:kind] == :on_op && dot[:text] == '::') then
singleton = true
name, container = parse_method_name_singleton container, name_t
else
unget_tk dot
back_tk.reverse_each do |token|
unget_tk token
end
name = parse_method_name_regular container, name_t
end
return name, container, singleton
end
##
# For the given +container+ and initial name token +name_t+ the method name
# is parsed from the token stream for a regular method.
def parse_method_name_regular container, name_t # :nodoc:
if :on_op == name_t[:kind] && (%w{* & [] []= <<}.include?(name_t[:text])) then
name_t[:text]
else
unless [:on_kw, :on_const, :on_ident].include?(name_t[:kind]) then
warn "expected method name token, . or ::, got #{name_t.inspect}"
skip_method container
return
end
name_t[:text]
end
end
##
# For the given +container+ and initial name token +name_t+ the method name
# and the new +container+ (if necessary) are parsed from the token stream
# for a singleton method.
def parse_method_name_singleton container, name_t # :nodoc:
skip_tkspace
name_t2 = get_tk
if (:on_kw == name_t[:kind] && 'self' == name_t[:text]) || (:on_op == name_t[:kind] && '%' == name_t[:text]) then
# NOTE: work around '[' being consumed early
if :on_lbracket == name_t2[:kind]
get_tk
name = '[]'
else
name = name_t2[:text]
end
elsif :on_const == name_t[:kind] then
name = name_t2[:text]
container = get_method_container container, name_t
return unless container
name
elsif :on_ident == name_t[:kind] || :on_ivar == name_t[:kind] || :on_gvar == name_t[:kind] then
parse_method_dummy container
name = nil
elsif (:on_kw == name_t[:kind]) && ('true' == name_t[:text] || 'false' == name_t[:text] || 'nil' == name_t[:text]) then
klass_name = "#{name_t[:text].capitalize}Class"
container = @store.find_class_named klass_name
container ||= @top_level.add_class RDoc::NormalClass, klass_name
name = name_t2[:text]
else
warn "unexpected method name token #{name_t.inspect}"
# break
skip_method container
name = nil
end
return name, container
end
##
# Extracts +yield+ parameters from +method+
def parse_method_or_yield_parameters(method = nil,
modifiers = RDoc::METHOD_MODIFIERS)
skip_tkspace_without_nl
tk = get_tk
end_token = get_end_token tk
return '' unless end_token
nest = 0
continue = false
while tk != nil do
case tk[:kind]
when :on_semicolon then
break if nest == 0
when :on_lbracket then
nest += 1
when :on_rbracket then
nest -= 1
when :on_lbrace then
nest += 1
when :on_rbrace then
nest -= 1
if nest <= 0
# we might have a.each { |i| yield i }
unget_tk(tk) if nest < 0
break
end
when :on_lparen then
nest += 1
when end_token[:kind] then
if end_token[:kind] == :on_rparen
nest -= 1
break if nest <= 0
else
break
end
when :on_rparen then
nest -= 1
when :on_comment, :on_embdoc then
@read.pop
if :on_nl == end_token[:kind] and "\n" == tk[:text][-1] and
(!continue or (tk[:state] & RDoc::Parser::RipperStateLex::EXPR_LABEL) != 0) then
if method && method.block_params.nil? then
unget_tk tk
read_documentation_modifiers method, modifiers
end
break if !continue and nest <= 0
end
when :on_comma then
continue = true
when :on_ident then
continue = false if continue
end
tk = get_tk
end
get_tkread_clean(/\s+/, ' ')
end
##
# Capture the method's parameters. Along the way, look for a comment
# containing:
#
# # yields: ....
#
# and add this as the block_params for the method
def parse_method_parameters method
res = parse_method_or_yield_parameters method
res = "(#{res})" unless res =~ /\A\(/
method.params = res unless method.params
return if method.block_params
skip_tkspace_without_nl
read_documentation_modifiers method, RDoc::METHOD_MODIFIERS
end
##
# Parses an RDoc::NormalModule in +container+ with +comment+
def parse_module container, single, tk, comment
container, name_t, = get_class_or_module container
name = name_t[:text]
mod = container.add_module RDoc::NormalModule, name
mod.ignore unless container.document_children
record_location mod
read_documentation_modifiers mod, RDoc::CLASS_MODIFIERS
mod.add_comment comment, @top_level
parse_statements mod
# after end modifiers
read_documentation_modifiers mod, RDoc::CLASS_MODIFIERS
@stats.add_module mod
end
##
# Parses an RDoc::Require in +context+ containing +comment+
def parse_require(context, comment)
skip_tkspace_comment
tk = get_tk
if :on_lparen == tk[:kind] then
skip_tkspace_comment
tk = get_tk
end
name = tk[:text][1..-2] if :on_tstring == tk[:kind]
if name then
@top_level.add_require RDoc::Require.new(name, comment)
else
unget_tk tk
end
end
##
# Parses a rescue
def parse_rescue
skip_tkspace_without_nl
while tk = get_tk
case tk[:kind]
when :on_nl, :on_semicolon, :on_comment then
break
when :on_comma then
skip_tkspace_without_nl
get_tk if :on_nl == peek_tk[:kind]
end
skip_tkspace_without_nl
end
end
##
# Retrieve comment body without =begin/=end
def retrieve_comment_body(tk)
if :on_embdoc == tk[:kind]
tk[:text].gsub(/\A=begin.*\n/, '').gsub(/=end\n?\z/, '')
else
tk[:text]
end
end
##
# The core of the Ruby parser.
def parse_statements(container, single = NORMAL, current_method = nil,
comment = new_comment(''))
raise 'no' unless RDoc::Comment === comment
comment = RDoc::Encoding.change_encoding comment, @encoding if @encoding
nest = 1
save_visibility = container.visibility
non_comment_seen = true
while tk = get_tk do
keep_comment = false
try_parse_comment = false
non_comment_seen = true unless (:on_comment == tk[:kind] or :on_embdoc == tk[:kind])
case tk[:kind]
when :on_nl, :on_ignored_nl, :on_comment, :on_embdoc then
if :on_nl == tk[:kind] or :on_ignored_nl == tk[:kind]
skip_tkspace
tk = get_tk
else
past_tokens = @read.size > 1 ? @read[0..-2] : []
nl_position = 0
past_tokens.reverse.each_with_index do |read_tk, i|
if read_tk =~ /^\n$/ then
nl_position = (past_tokens.size - 1) - i
break
elsif read_tk =~ /^#.*\n$/ then
nl_position = ((past_tokens.size - 1) - i) + 1
break
end
end
comment_only_line = past_tokens[nl_position..-1].all?{ |c| c =~ /^\s+$/ }
unless comment_only_line then
tk = get_tk
end
end
if tk and (:on_comment == tk[:kind] or :on_embdoc == tk[:kind]) then
if non_comment_seen then
# Look for RDoc in a comment about to be thrown away
non_comment_seen = parse_comment container, tk, comment unless
comment.empty?
comment = ''
comment = RDoc::Encoding.change_encoding comment, @encoding if @encoding
end
line_no = nil
while tk and (:on_comment == tk[:kind] or :on_embdoc == tk[:kind]) do
comment_body = retrieve_comment_body(tk)
line_no = tk[:line_no] if comment.empty?
comment += comment_body
comment << "\n" unless comment_body =~ /\n\z/
if comment_body.size > 1 && comment_body =~ /\n\z/ then
skip_tkspace_without_nl # leading spaces
end
tk = get_tk
end
comment = new_comment comment, line_no
unless comment.empty? then
look_for_directives_in container, comment
if container.done_documenting then
throw :eof if RDoc::TopLevel === container
container.ongoing_visibility = save_visibility
end
end
keep_comment = true
else
non_comment_seen = true
end
unget_tk tk
keep_comment = true
container.current_line_visibility = nil
when :on_kw then
case tk[:text]
when 'class' then
parse_class container, single, tk, comment
when 'module' then
parse_module container, single, tk, comment
when 'def' then
parse_method container, single, tk, comment
when 'alias' then
parse_alias container, single, tk, comment unless current_method
when 'yield' then
if current_method.nil? then
warn "Warning: yield outside of method" if container.document_self
else
parse_yield container, single, tk, current_method
end
when 'until', 'while' then
if (tk[:state] & RDoc::Parser::RipperStateLex::EXPR_LABEL) == 0
nest += 1
skip_optional_do_after_expression
end
# Until and While can have a 'do', which shouldn't increase the nesting.
# We can't solve the general case, but we can handle most occurrences by
# ignoring a do at the end of a line.
# 'for' is trickier
when 'for' then
nest += 1
skip_for_variable
skip_optional_do_after_expression
when 'case', 'do', 'if', 'unless', 'begin' then
if (tk[:state] & RDoc::Parser::RipperStateLex::EXPR_LABEL) == 0
nest += 1
end
when 'super' then
current_method.calls_super = true if current_method
when 'rescue' then
parse_rescue
when 'end' then
nest -= 1
if nest == 0 then
container.ongoing_visibility = save_visibility
parse_comment container, tk, comment unless comment.empty?
return
end
end
when :on_const then
unless parse_constant container, tk, comment, current_method then
try_parse_comment = true
end
when :on_ident then
if nest == 1 and current_method.nil? then
keep_comment = parse_identifier container, single, tk, comment
end
case tk[:text]
when "require" then
parse_require container, comment
when "include" then
parse_extend_or_include RDoc::Include, container, comment
when "extend" then
parse_extend_or_include RDoc::Extend, container, comment
when "included" then
parse_included_with_activesupport_concern container, comment
end
else
try_parse_comment = nest == 1
end
if try_parse_comment then
non_comment_seen = parse_comment container, tk, comment unless
comment.empty?
keep_comment = false
end
unless keep_comment then
comment = new_comment ''
comment = RDoc::Encoding.change_encoding comment, @encoding if @encoding
container.params = nil
container.block_params = nil
end
consume_trailing_spaces
end
container.params = nil
container.block_params = nil
end
##
# Parse up to +no+ symbol arguments
def parse_symbol_arg(no = nil)
skip_tkspace_comment
tk = get_tk
if tk[:kind] == :on_lparen
parse_symbol_arg_paren no
else
parse_symbol_arg_space no, tk
end
end
##
# Parses up to +no+ symbol arguments surrounded by () and places them in
# +args+.
def parse_symbol_arg_paren no # :nodoc:
args = []
loop do
skip_tkspace_comment
if tk1 = parse_symbol_in_arg
args.push tk1
break if no and args.size >= no
end
skip_tkspace_comment
case (tk2 = get_tk)[:kind]
when :on_rparen
break
when :on_comma
else
warn("unexpected token: '#{tk2.inspect}'") if $DEBUG_RDOC
break
end
end
args
end
##
# Parses up to +no+ symbol arguments separated by spaces and places them in
# +args+.
def parse_symbol_arg_space no, tk # :nodoc:
args = []
unget_tk tk
if tk = parse_symbol_in_arg
args.push tk
return args if no and args.size >= no
end
loop do
skip_tkspace_without_nl
tk1 = get_tk
if tk1.nil? || :on_comma != tk1[:kind] then
unget_tk tk1
break
end
skip_tkspace_comment
if tk = parse_symbol_in_arg
args.push tk
break if no and args.size >= no
end
end
args
end
##
# Returns symbol text from the next token
def parse_symbol_in_arg
tk = get_tk
if :on_symbol == tk[:kind] then
tk[:text].sub(/^:/, '')
elsif :on_tstring == tk[:kind] then
tk[:text][1..-2]
elsif :on_dstring == tk[:kind] or :on_ident == tk[:kind] then
nil # ignore
else
warn("Expected symbol or string, got #{tk.inspect}") if $DEBUG_RDOC
nil
end
end
##
# Parses statements in the top-level +container+
def parse_top_level_statements container
comment = collect_first_comment
look_for_directives_in container, comment
throw :eof if container.done_documenting
@markup = comment.format
# HACK move if to RDoc::Context#comment=
container.comment = comment if container.document_self unless comment.empty?
parse_statements container, NORMAL, nil, comment
end
##
# Determines the visibility in +container+ from +tk+
def parse_visibility(container, single, tk)
vis_type, vis, singleton = get_visibility_information tk, single
skip_tkspace_comment false
ptk = peek_tk
# Ryan Davis suggested the extension to ignore modifiers, because he
# often writes
#
# protected unless $TESTING
#
if [:on_nl, :on_semicolon].include?(ptk[:kind]) || (:on_kw == ptk[:kind] && (['if', 'unless'].include?(ptk[:text]))) then
container.ongoing_visibility = vis
elsif :on_kw == ptk[:kind] && 'def' == ptk[:text]
container.current_line_visibility = vis
else
update_visibility container, vis_type, vis, singleton
end
end
##
# Parses a Module#private_constant or Module#public_constant call from +tk+.
def parse_constant_visibility(container, single, tk)
args = parse_symbol_arg
case tk[:text]
when 'private_constant'
vis = :private
when 'public_constant'
vis = :public
else
raise RDoc::Error, 'Unreachable'
end
container.set_constant_visibility_for args, vis
end
##
# Determines the block parameter for +context+
def parse_yield(context, single, tk, method)
return if method.block_params
get_tkread
method.block_params = parse_method_or_yield_parameters
end
##
# Directives are modifier comments that can appear after class, module, or
# method names. For example:
#
# def fred # :yields: a, b
#
# or:
#
# class MyClass # :nodoc:
#
# We return the directive name and any parameters as a two element array if
# the name is in +allowed+. A directive can be found anywhere up to the end
# of the current line.
def read_directive allowed
tokens = []
while tk = get_tk do
tokens << tk
if :on_nl == tk[:kind] or (:on_kw == tk[:kind] && 'def' == tk[:text]) then
return
elsif :on_comment == tk[:kind] or :on_embdoc == tk[:kind] then
return unless tk[:text] =~ /\s*:?([\w-]+):\s*(.*)/
directive = $1.downcase
return [directive, $2] if allowed.include? directive
return
end
end
ensure
unless tokens.length == 1 and (:on_comment == tokens.first[:kind] or :on_embdoc == tokens.first[:kind]) then
tokens.reverse_each do |token|
unget_tk token
end
end
end
##
# Handles directives following the definition for +context+ (any
# RDoc::CodeObject) if the directives are +allowed+ at this point.
#
# See also RDoc::Markup::PreProcess#handle_directive
def read_documentation_modifiers context, allowed
skip_tkspace_without_nl
directive, value = read_directive allowed
return unless directive
@preprocess.handle_directive '', directive, value, context do |dir, param|
if %w[notnew not_new not-new].include? dir then
context.dont_rename_initialize = true
true
end
end
end
##
# Records the location of this +container+ in the file for this parser and
# adds it to the list of classes and modules in the file.
def record_location container # :nodoc:
case container
when RDoc::ClassModule then
@top_level.add_to_classes_or_modules container
end
container.record_location @top_level
end
##
# Scans this Ruby file for Ruby constructs
def scan
reset
catch :eof do
begin
parse_top_level_statements @top_level
rescue StandardError => e
if @content.include?('<%') and @content.include?('%>') then
# Maybe, this is ERB.
$stderr.puts "\033[2KRDoc detects ERB file. Skips it for compatibility:"
$stderr.puts @file_name
return
end
if @scanner_point >= @scanner.size
now_line_no = @scanner[@scanner.size - 1][:line_no]
else
now_line_no = peek_tk[:line_no]
end
first_tk_index = @scanner.find_index { |tk| tk[:line_no] == now_line_no }
last_tk_index = @scanner.find_index { |tk| tk[:line_no] == now_line_no + 1 }
last_tk_index = last_tk_index ? last_tk_index - 1 : @scanner.size - 1
code = @scanner[first_tk_index..last_tk_index].map{ |t| t[:text] }.join
$stderr.puts <<-EOF
#{self.class} failure around line #{now_line_no} of
#{@file_name}
EOF
unless code.empty? then
$stderr.puts code
$stderr.puts
end
raise e
end
end
@top_level
end
##
# while, until, and for have an optional do
def skip_optional_do_after_expression
skip_tkspace_without_nl
tk = get_tk
b_nest = 0
nest = 0
loop do
break unless tk
case tk[:kind]
when :on_semicolon, :on_nl, :on_ignored_nl then
break if b_nest.zero?
when :on_lparen then
nest += 1
when :on_rparen then
nest -= 1
when :on_kw then
case tk[:text]
when 'begin'
b_nest += 1
when 'end'
b_nest -= 1
when 'do'
break if nest.zero?
end
when :on_comment, :on_embdoc then
if b_nest.zero? and "\n" == tk[:text][-1] then
break
end
end
tk = get_tk
end
skip_tkspace_without_nl
get_tk if peek_tk && :on_kw == peek_tk[:kind] && 'do' == peek_tk[:text]
end
##
# skip the var [in] part of a 'for' statement
def skip_for_variable
skip_tkspace_without_nl
get_tk
skip_tkspace_without_nl
tk = get_tk
unget_tk(tk) unless :on_kw == tk[:kind] and 'in' == tk[:text]
end
##
# Skips the next method in +container+
def skip_method container
meth = RDoc::AnyMethod.new "", "anon"
parse_method_parameters meth
parse_statements container, false, meth
end
##
# Skip spaces until a comment is found
def skip_tkspace_comment(skip_nl = true)
loop do
skip_nl ? skip_tkspace : skip_tkspace_without_nl
next_tk = peek_tk
return if next_tk.nil? || (:on_comment != next_tk[:kind] and :on_embdoc != next_tk[:kind])
get_tk
end
end
##
# Updates visibility in +container+ from +vis_type+ and +vis+.
def update_visibility container, vis_type, vis, singleton # :nodoc:
new_methods = []
case vis_type
when 'module_function' then
args = parse_symbol_arg
container.set_visibility_for args, :private, false
container.methods_matching args do |m|
s_m = m.dup
record_location s_m
s_m.singleton = true
new_methods << s_m
end
when 'public_class_method', 'private_class_method' then
args = parse_symbol_arg
container.methods_matching args, true do |m|
if m.parent != container then
m = m.dup
record_location m
new_methods << m
end
m.visibility = vis
end
else
args = parse_symbol_arg
container.set_visibility_for args, vis, singleton
end
new_methods.each do |method|
case method
when RDoc::AnyMethod then
container.add_method method
when RDoc::Attr then
container.add_attribute method
end
method.visibility = vis
end
end
##
# Prints +message+ to +$stderr+ unless we're being quiet
def warn message
@options.warn make_message message
end
end
PK ! h\;! ;! changelog.rbnu [ # frozen_string_literal: true
##
# A ChangeLog file parser.
#
# This parser converts a ChangeLog into an RDoc::Markup::Document. When
# viewed as HTML a ChangeLog page will have an entry for each day's entries in
# the sidebar table of contents.
#
# This parser is meant to parse the MRI ChangeLog, but can be used to parse any
# {GNU style Change
# Log}[http://www.gnu.org/prep/standards/html_node/Style-of-Change-Logs.html].
class RDoc::Parser::ChangeLog < RDoc::Parser
include RDoc::Parser::Text
parse_files_matching(/(\/|\\|\A)ChangeLog[^\/\\]*\z/)
##
# Attaches the +continuation+ of the previous line to the +entry_body+.
#
# Continued function listings are joined together as a single entry.
# Continued descriptions are joined to make a single paragraph.
def continue_entry_body entry_body, continuation
return unless last = entry_body.last
if last =~ /\)\s*\z/ and continuation =~ /\A\(/ then
last.sub!(/\)\s*\z/, ',')
continuation = continuation.sub(/\A\(/, '')
end
if last =~ /\s\z/ then
last << continuation
else
last << ' ' + continuation
end
end
##
# Creates an RDoc::Markup::Document given the +groups+ of ChangeLog entries.
def create_document groups
doc = RDoc::Markup::Document.new
doc.omit_headings_below = 2
doc.file = @top_level
doc << RDoc::Markup::Heading.new(1, File.basename(@file_name))
doc << RDoc::Markup::BlankLine.new
groups.sort_by do |day,| day end.reverse_each do |day, entries|
doc << RDoc::Markup::Heading.new(2, day.dup)
doc << RDoc::Markup::BlankLine.new
doc.concat create_entries entries
end
doc
end
##
# Returns a list of ChangeLog entries an RDoc::Markup nodes for the given
# +entries+.
def create_entries entries
out = []
entries.each do |entry, items|
out << RDoc::Markup::Heading.new(3, entry)
out << RDoc::Markup::BlankLine.new
out << create_items(items)
end
out
end
##
# Returns an RDoc::Markup::List containing the given +items+ in the
# ChangeLog
def create_items items
list = RDoc::Markup::List.new :NOTE
items.each do |item|
item =~ /\A(.*?(?:\([^)]+\))?):\s*/
title = $1
body = $'
paragraph = RDoc::Markup::Paragraph.new body
list_item = RDoc::Markup::ListItem.new title, paragraph
list << list_item
end
list
end
##
# Groups +entries+ by date.
def group_entries entries
@time_cache ||= {}
entries.group_by do |title, _|
begin
time = @time_cache[title]
(time || parse_date(title)).strftime '%Y-%m-%d'
rescue NoMethodError, ArgumentError
time, = title.split ' ', 2
parse_date(time).strftime '%Y-%m-%d'
end
end
end
##
# Parse date in ISO-8601, RFC-2822, or default of Git
def parse_date(date)
case date
when /\A\s*(\d+)-(\d+)-(\d+)(?:[ T](\d+):(\d+):(\d+) *([-+]\d\d):?(\d\d))?\b/
Time.new($1, $2, $3, $4, $5, $6, ("#{$7}:#{$8}" if $7))
when /\A\s*\w{3}, +(\d+) (\w{3}) (\d+) (\d+):(\d+):(\d+) *(?:([-+]\d\d):?(\d\d))\b/
Time.new($3, $2, $1, $4, $5, $6, ("#{$7}:#{$8}" if $7))
when /\A\s*\w{3} (\w{3}) +(\d+) (\d+) (\d+):(\d+):(\d+) *(?:([-+]\d\d):?(\d\d))\b/
Time.new($3, $1, $2, $4, $5, $6, ("#{$7}:#{$8}" if $7))
when /\A\s*\w{3} (\w{3}) +(\d+) (\d+):(\d+):(\d+) (\d+)\b/
Time.new($6, $1, $2, $3, $4, $5)
else
raise ArgumentError, "bad date: #{date}"
end
end
##
# Parses the entries in the ChangeLog.
#
# Returns an Array of each ChangeLog entry in order of parsing.
#
# A ChangeLog entry is an Array containing the ChangeLog title (date and
# committer) and an Array of ChangeLog items (file and function changed with
# description).
#
# An example result would be:
#
# [ 'Tue Dec 4 08:33:46 2012 Eric Hodel ',
# [ 'README.EXT: Converted to RDoc format',
# 'README.EXT.ja: ditto']]
def parse_entries
@time_cache ||= {}
if /\A((?:.*\n){,3})commit\s/ =~ @content
class << self; prepend Git; end
parse_info($1)
return parse_entries
end
entries = []
entry_name = nil
entry_body = []
@content.each_line do |line|
case line
when /^\s*$/ then
next
when /^\w.*/ then
entries << [entry_name, entry_body] if entry_name
entry_name = $&
begin
time = parse_date entry_name
@time_cache[entry_name] = time
rescue ArgumentError
entry_name = nil
end
entry_body = []
when /^(\t| {8})?\*\s*(.*)/ then # "\t* file.c (func): ..."
entry_body << $2.dup
when /^(\t| {8})?\s*(\(.*)/ then # "\t(func): ..."
entry = $2
if entry_body.last =~ /:/ then
entry_body << entry.dup
else
continue_entry_body entry_body, entry
end
when /^(\t| {8})?\s*(.*)/ then
continue_entry_body entry_body, $2
end
end
entries << [entry_name, entry_body] if entry_name
entries.reject! do |(entry,_)|
entry == nil
end
entries
end
##
# Converts the ChangeLog into an RDoc::Markup::Document
def scan
@time_cache = {}
entries = parse_entries
grouped_entries = group_entries entries
doc = create_document grouped_entries
@top_level.comment = doc
@top_level
end
module Git
def parse_info(info)
/^\s*base-url\s*=\s*(.*\S)/ =~ info
@base_url = $1
end
def parse_entries
entries = []
@content.scan(/^commit\s+(\h{20})\h*\n((?:.+\n)*)\n((?: {4}.*\n+)*)/) do
entry_name, header, entry_body = $1, $2, $3.gsub(/^ {4}/, '')
# header = header.scan(/^ *(\S+?): +(.*)/).to_h
# date = header["CommitDate"] || header["Date"]
date = header[/^ *(?:Author)?Date: +(.*)/, 1]
author = header[/^ *Author: +(.*)/, 1]
begin
time = parse_date(header[/^ *CommitDate: +(.*)/, 1] || date)
@time_cache[entry_name] = time
author.sub!(/\s*<(.*)>/, '')
email = $1
entries << [entry_name, [author, email, date, entry_body]]
rescue ArgumentError
end
end
entries
end
def create_entries entries
# git log entries have no strictly itemized style like the old
# style, just assume Markdown.
entries.map do |commit, entry|
LogEntry.new(@base_url, commit, *entry)
end
end
LogEntry = Struct.new(:base, :commit, :author, :email, :date, :contents) do
HEADING_LEVEL = 3
def initialize(base, commit, author, email, date, contents)
case contents
when String
contents = RDoc::Markdown.parse(contents).parts.each do |body|
case body
when RDoc::Markup::Heading
body.level += HEADING_LEVEL + 1
end
end
case first = contents[0]
when RDoc::Markup::Paragraph
contents[0] = RDoc::Markup::Heading.new(HEADING_LEVEL + 1, first.text)
end
end
super
end
def level
HEADING_LEVEL
end
def aref
"label-#{commit}"
end
def label context = nil
aref
end
def text
case base
when nil
"#{date}"
when /%s/
"{#{date}}[#{base % commit}]"
else
"{#{date}}[#{base}#{commit}]"
end + " {#{author}}[mailto:#{email}]"
end
def accept visitor
visitor.accept_heading self
begin
if visitor.respond_to?(:code_object=)
code_object = visitor.code_object
visitor.code_object = self
end
contents.each do |body|
body.accept visitor
end
ensure
if visitor.respond_to?(:code_object)
visitor.code_object = code_object
end
end
end
def pretty_print q # :nodoc:
q.group(2, '[log_entry: ', ']') do
q.text commit
q.text ','
q.breakable
q.group(2, '[date: ', ']') { q.text date }
q.text ','
q.breakable
q.group(2, '[author: ', ']') { q.text author }
q.text ','
q.breakable
q.group(2, '[email: ', ']') { q.text email }
q.text ','
q.breakable
q.pp contents
end
end
end
end
end
PK ! h lexer.rbnu [ # frozen_string_literal: true
# :markup: markdown
require "strscan"
require_relative "../../polyfill/append_as_bytes"
require_relative "../../polyfill/scan_byte"
module Prism
module Translation
class Parser
# Accepts a list of prism tokens and converts them into the expected
# format for the parser gem.
class Lexer
# These tokens are always skipped
TYPES_ALWAYS_SKIP = Set.new(%i[IGNORED_NEWLINE __END__ EOF])
private_constant :TYPES_ALWAYS_SKIP
# The direct translating of types between the two lexers.
TYPES = {
# These tokens should never appear in the output of the lexer.
MISSING: nil,
NOT_PROVIDED: nil,
EMBDOC_END: nil,
EMBDOC_LINE: nil,
# These tokens have more or less direct mappings.
AMPERSAND: :tAMPER2,
AMPERSAND_AMPERSAND: :tANDOP,
AMPERSAND_AMPERSAND_EQUAL: :tOP_ASGN,
AMPERSAND_DOT: :tANDDOT,
AMPERSAND_EQUAL: :tOP_ASGN,
BACK_REFERENCE: :tBACK_REF,
BACKTICK: :tXSTRING_BEG,
BANG: :tBANG,
BANG_EQUAL: :tNEQ,
BANG_TILDE: :tNMATCH,
BRACE_LEFT: :tLCURLY,
BRACE_RIGHT: :tRCURLY,
BRACKET_LEFT: :tLBRACK2,
BRACKET_LEFT_ARRAY: :tLBRACK,
BRACKET_LEFT_RIGHT: :tAREF,
BRACKET_LEFT_RIGHT_EQUAL: :tASET,
BRACKET_RIGHT: :tRBRACK,
CARET: :tCARET,
CARET_EQUAL: :tOP_ASGN,
CHARACTER_LITERAL: :tCHARACTER,
CLASS_VARIABLE: :tCVAR,
COLON: :tCOLON,
COLON_COLON: :tCOLON2,
COMMA: :tCOMMA,
COMMENT: :tCOMMENT,
CONSTANT: :tCONSTANT,
DOT: :tDOT,
DOT_DOT: :tDOT2,
DOT_DOT_DOT: :tDOT3,
EMBDOC_BEGIN: :tCOMMENT,
EMBEXPR_BEGIN: :tSTRING_DBEG,
EMBEXPR_END: :tSTRING_DEND,
EMBVAR: :tSTRING_DVAR,
EQUAL: :tEQL,
EQUAL_EQUAL: :tEQ,
EQUAL_EQUAL_EQUAL: :tEQQ,
EQUAL_GREATER: :tASSOC,
EQUAL_TILDE: :tMATCH,
FLOAT: :tFLOAT,
FLOAT_IMAGINARY: :tIMAGINARY,
FLOAT_RATIONAL: :tRATIONAL,
FLOAT_RATIONAL_IMAGINARY: :tIMAGINARY,
GLOBAL_VARIABLE: :tGVAR,
GREATER: :tGT,
GREATER_EQUAL: :tGEQ,
GREATER_GREATER: :tRSHFT,
GREATER_GREATER_EQUAL: :tOP_ASGN,
HEREDOC_START: :tSTRING_BEG,
HEREDOC_END: :tSTRING_END,
IDENTIFIER: :tIDENTIFIER,
INSTANCE_VARIABLE: :tIVAR,
INTEGER: :tINTEGER,
INTEGER_IMAGINARY: :tIMAGINARY,
INTEGER_RATIONAL: :tRATIONAL,
INTEGER_RATIONAL_IMAGINARY: :tIMAGINARY,
KEYWORD_ALIAS: :kALIAS,
KEYWORD_AND: :kAND,
KEYWORD_BEGIN: :kBEGIN,
KEYWORD_BEGIN_UPCASE: :klBEGIN,
KEYWORD_BREAK: :kBREAK,
KEYWORD_CASE: :kCASE,
KEYWORD_CLASS: :kCLASS,
KEYWORD_DEF: :kDEF,
KEYWORD_DEFINED: :kDEFINED,
KEYWORD_DO: :kDO,
KEYWORD_DO_LOOP: :kDO_COND,
KEYWORD_END: :kEND,
KEYWORD_END_UPCASE: :klEND,
KEYWORD_ENSURE: :kENSURE,
KEYWORD_ELSE: :kELSE,
KEYWORD_ELSIF: :kELSIF,
KEYWORD_FALSE: :kFALSE,
KEYWORD_FOR: :kFOR,
KEYWORD_IF: :kIF,
KEYWORD_IF_MODIFIER: :kIF_MOD,
KEYWORD_IN: :kIN,
KEYWORD_MODULE: :kMODULE,
KEYWORD_NEXT: :kNEXT,
KEYWORD_NIL: :kNIL,
KEYWORD_NOT: :kNOT,
KEYWORD_OR: :kOR,
KEYWORD_REDO: :kREDO,
KEYWORD_RESCUE: :kRESCUE,
KEYWORD_RESCUE_MODIFIER: :kRESCUE_MOD,
KEYWORD_RETRY: :kRETRY,
KEYWORD_RETURN: :kRETURN,
KEYWORD_SELF: :kSELF,
KEYWORD_SUPER: :kSUPER,
KEYWORD_THEN: :kTHEN,
KEYWORD_TRUE: :kTRUE,
KEYWORD_UNDEF: :kUNDEF,
KEYWORD_UNLESS: :kUNLESS,
KEYWORD_UNLESS_MODIFIER: :kUNLESS_MOD,
KEYWORD_UNTIL: :kUNTIL,
KEYWORD_UNTIL_MODIFIER: :kUNTIL_MOD,
KEYWORD_WHEN: :kWHEN,
KEYWORD_WHILE: :kWHILE,
KEYWORD_WHILE_MODIFIER: :kWHILE_MOD,
KEYWORD_YIELD: :kYIELD,
KEYWORD___ENCODING__: :k__ENCODING__,
KEYWORD___FILE__: :k__FILE__,
KEYWORD___LINE__: :k__LINE__,
LABEL: :tLABEL,
LABEL_END: :tLABEL_END,
LAMBDA_BEGIN: :tLAMBEG,
LESS: :tLT,
LESS_EQUAL: :tLEQ,
LESS_EQUAL_GREATER: :tCMP,
LESS_LESS: :tLSHFT,
LESS_LESS_EQUAL: :tOP_ASGN,
METHOD_NAME: :tFID,
MINUS: :tMINUS,
MINUS_EQUAL: :tOP_ASGN,
MINUS_GREATER: :tLAMBDA,
NEWLINE: :tNL,
NUMBERED_REFERENCE: :tNTH_REF,
PARENTHESIS_LEFT: :tLPAREN2,
PARENTHESIS_LEFT_PARENTHESES: :tLPAREN_ARG,
PARENTHESIS_RIGHT: :tRPAREN,
PERCENT: :tPERCENT,
PERCENT_EQUAL: :tOP_ASGN,
PERCENT_LOWER_I: :tQSYMBOLS_BEG,
PERCENT_LOWER_W: :tQWORDS_BEG,
PERCENT_UPPER_I: :tSYMBOLS_BEG,
PERCENT_UPPER_W: :tWORDS_BEG,
PERCENT_LOWER_X: :tXSTRING_BEG,
PLUS: :tPLUS,
PLUS_EQUAL: :tOP_ASGN,
PIPE_EQUAL: :tOP_ASGN,
PIPE: :tPIPE,
PIPE_PIPE: :tOROP,
PIPE_PIPE_EQUAL: :tOP_ASGN,
QUESTION_MARK: :tEH,
REGEXP_BEGIN: :tREGEXP_BEG,
REGEXP_END: :tSTRING_END,
SEMICOLON: :tSEMI,
SLASH: :tDIVIDE,
SLASH_EQUAL: :tOP_ASGN,
STAR: :tSTAR2,
STAR_EQUAL: :tOP_ASGN,
STAR_STAR: :tPOW,
STAR_STAR_EQUAL: :tOP_ASGN,
STRING_BEGIN: :tSTRING_BEG,
STRING_CONTENT: :tSTRING_CONTENT,
STRING_END: :tSTRING_END,
SYMBOL_BEGIN: :tSYMBEG,
TILDE: :tTILDE,
UAMPERSAND: :tAMPER,
UCOLON_COLON: :tCOLON3,
UDOT_DOT: :tBDOT2,
UDOT_DOT_DOT: :tBDOT3,
UMINUS: :tUMINUS,
UMINUS_NUM: :tUNARY_NUM,
UPLUS: :tUPLUS,
USTAR: :tSTAR,
USTAR_STAR: :tDSTAR,
WORDS_SEP: :tSPACE
}
# These constants represent flags in our lex state. We really, really
# don't want to be using them and we really, really don't want to be
# exposing them as part of our public API. Unfortunately, we don't have
# another way of matching the exact tokens that the parser gem expects
# without them. We should find another way to do this, but in the
# meantime we'll hide them from the documentation and mark them as
# private constants.
EXPR_BEG = 0x1 # :nodoc:
EXPR_LABEL = 0x400 # :nodoc:
# It is used to determine whether `do` is of the token type `kDO` or `kDO_LAMBDA`.
#
# NOTE: In edge cases like `-> (foo = -> (bar) {}) do end`, please note that `kDO` is still returned
# instead of `kDO_LAMBDA`, which is expected: https://github.com/ruby/prism/pull/3046
LAMBDA_TOKEN_TYPES = Set.new([:kDO_LAMBDA, :tLAMBDA, :tLAMBEG])
# The `PARENTHESIS_LEFT` token in Prism is classified as either `tLPAREN` or `tLPAREN2` in the Parser gem.
# The following token types are listed as those classified as `tLPAREN`.
LPAREN_CONVERSION_TOKEN_TYPES = Set.new([
:kBREAK, :tCARET, :kCASE, :tDIVIDE, :kFOR, :kIF, :kNEXT, :kRETURN, :kUNTIL, :kWHILE, :tAMPER, :tANDOP, :tBANG, :tCOMMA, :tDOT2, :tDOT3,
:tEQL, :tLPAREN, :tLPAREN2, :tLPAREN_ARG, :tLSHFT, :tNL, :tOP_ASGN, :tOROP, :tPIPE, :tSEMI, :tSTRING_DBEG, :tUMINUS, :tUPLUS, :tLCURLY
])
# Types of tokens that are allowed to continue a method call with comments in-between.
# For these, the parser gem doesn't emit a newline token after the last comment.
COMMENT_CONTINUATION_TYPES = Set.new([:COMMENT, :AMPERSAND_DOT, :DOT])
private_constant :COMMENT_CONTINUATION_TYPES
# Heredocs are complex and require us to keep track of a bit of info to refer to later
HeredocData = Struct.new(:identifier, :common_whitespace, keyword_init: true)
private_constant :TYPES, :EXPR_BEG, :EXPR_LABEL, :LAMBDA_TOKEN_TYPES, :LPAREN_CONVERSION_TOKEN_TYPES, :HeredocData
# The Parser::Source::Buffer that the tokens were lexed from.
attr_reader :source_buffer
# An array of tuples that contain prism tokens and their associated lex
# state when they were lexed.
attr_reader :lexed
# A hash that maps offsets in bytes to offsets in characters.
attr_reader :offset_cache
# Initialize the lexer with the given source buffer, prism tokens, and
# offset cache.
def initialize(source_buffer, lexed, offset_cache)
@source_buffer = source_buffer
@lexed = lexed
@offset_cache = offset_cache
end
Range = ::Parser::Source::Range # :nodoc:
private_constant :Range
# Convert the prism tokens into the expected format for the parser gem.
def to_a
tokens = []
index = 0
length = lexed.length
heredoc_stack = []
quote_stack = []
# The parser gem emits the newline tokens for comments out of order. This saves
# that token location to emit at a later time to properly line everything up.
# https://github.com/whitequark/parser/issues/1025
comment_newline_location = nil
while index < length
token, state = lexed[index]
index += 1
next if TYPES_ALWAYS_SKIP.include?(token.type)
type = TYPES.fetch(token.type)
value = token.value
location = range(token.location.start_offset, token.location.end_offset)
case type
when :kDO
nearest_lambda_token = tokens.reverse_each.find do |token|
LAMBDA_TOKEN_TYPES.include?(token.first)
end
if nearest_lambda_token&.first == :tLAMBDA
type = :kDO_LAMBDA
end
when :tCHARACTER
value.delete_prefix!("?")
# Character literals behave similar to double-quoted strings. We can use the same escaping mechanism.
value = unescape_string(value, "?")
when :tCOMMENT
if token.type == :EMBDOC_BEGIN
while !((next_token = lexed[index]&.first) && next_token.type == :EMBDOC_END) && (index < length - 1)
value += next_token.value
index += 1
end
value += next_token.value
location = range(token.location.start_offset, next_token.location.end_offset)
index += 1
else
is_at_eol = value.chomp!.nil?
location = range(token.location.start_offset, token.location.end_offset + (is_at_eol ? 0 : -1))
prev_token, _ = lexed[index - 2] if index - 2 >= 0
next_token, _ = lexed[index]
is_inline_comment = prev_token&.location&.start_line == token.location.start_line
if is_inline_comment && !is_at_eol && !COMMENT_CONTINUATION_TYPES.include?(next_token&.type)
tokens << [:tCOMMENT, [value, location]]
nl_location = range(token.location.end_offset - 1, token.location.end_offset)
tokens << [:tNL, [nil, nl_location]]
next
elsif is_inline_comment && next_token&.type == :COMMENT
comment_newline_location = range(token.location.end_offset - 1, token.location.end_offset)
elsif comment_newline_location && !COMMENT_CONTINUATION_TYPES.include?(next_token&.type)
tokens << [:tCOMMENT, [value, location]]
tokens << [:tNL, [nil, comment_newline_location]]
comment_newline_location = nil
next
end
end
when :tNL
next_token, _ = lexed[index]
# Newlines after comments are emitted out of order.
if next_token&.type == :COMMENT
comment_newline_location = location
next
end
value = nil
when :tFLOAT
value = parse_float(value)
when :tIMAGINARY
value = parse_complex(value)
when :tINTEGER
if value.start_with?("+")
tokens << [:tUNARY_NUM, ["+", range(token.location.start_offset, token.location.start_offset + 1)]]
location = range(token.location.start_offset + 1, token.location.end_offset)
end
value = parse_integer(value)
when :tLABEL
value.chomp!(":")
when :tLABEL_END
value.chomp!(":")
when :tLCURLY
type = :tLBRACE if state == EXPR_BEG | EXPR_LABEL
when :tLPAREN2
type = :tLPAREN if tokens.empty? || LPAREN_CONVERSION_TOKEN_TYPES.include?(tokens.dig(-1, 0))
when :tNTH_REF
value = parse_integer(value.delete_prefix("$"))
when :tOP_ASGN
value.chomp!("=")
when :tRATIONAL
value = parse_rational(value)
when :tSPACE
location = range(token.location.start_offset, token.location.start_offset + percent_array_leading_whitespace(value))
value = nil
when :tSTRING_BEG
next_token, _ = lexed[index]
next_next_token, _ = lexed[index + 1]
basic_quotes = value == '"' || value == "'"
if basic_quotes && next_token&.type == :STRING_END
next_location = token.location.join(next_token.location)
type = :tSTRING
value = ""
location = range(next_location.start_offset, next_location.end_offset)
index += 1
elsif value.start_with?("'", '"', "%")
if next_token&.type == :STRING_CONTENT && next_next_token&.type == :STRING_END
string_value = next_token.value
if simplify_string?(string_value, value)
next_location = token.location.join(next_next_token.location)
if percent_array?(value)
value = percent_array_unescape(string_value)
else
value = unescape_string(string_value, value)
end
type = :tSTRING
location = range(next_location.start_offset, next_location.end_offset)
index += 2
tokens << [type, [value, location]]
next
end
end
quote_stack.push(value)
elsif token.type == :HEREDOC_START
quote = value[2] == "-" || value[2] == "~" ? value[3] : value[2]
heredoc_type = value[2] == "-" || value[2] == "~" ? value[2] : ""
heredoc = HeredocData.new(
identifier: value.match(/<<[-~]?["'`]?(?.*?)["'`]?\z/)[:heredoc_identifier],
common_whitespace: 0,
)
if quote == "`"
type = :tXSTRING_BEG
end
# The parser gem trims whitespace from squiggly heredocs. We must record
# the most common whitespace to later remove.
if heredoc_type == "~" || heredoc_type == "`"
heredoc.common_whitespace = calculate_heredoc_whitespace(index)
end
if quote == "'" || quote == '"' || quote == "`"
value = "<<#{quote}"
else
value = '<<"'
end
heredoc_stack.push(heredoc)
quote_stack.push(value)
end
when :tSTRING_CONTENT
is_percent_array = percent_array?(quote_stack.last)
if (lines = token.value.lines).one?
# Prism usually emits a single token for strings with line continuations.
# For squiggly heredocs they are not joined so we do that manually here.
current_string = +""
current_length = 0
start_offset = token.location.start_offset
while token.type == :STRING_CONTENT
current_length += token.value.bytesize
# Heredoc interpolation can have multiple STRING_CONTENT nodes on the same line.
prev_token, _ = lexed[index - 2] if index - 2 >= 0
is_first_token_on_line = prev_token && token.location.start_line != prev_token.location.start_line
# The parser gem only removes indentation when the heredoc is not nested
not_nested = heredoc_stack.size == 1
if is_percent_array
value = percent_array_unescape(token.value)
elsif is_first_token_on_line && not_nested && (current_heredoc = heredoc_stack.last).common_whitespace > 0
value = trim_heredoc_whitespace(token.value, current_heredoc)
end
current_string << unescape_string(value, quote_stack.last)
relevant_backslash_count = if quote_stack.last.start_with?("%W", "%I")
0 # the last backslash escapes the newline
else
token.value[/(\\{1,})\n/, 1]&.length || 0
end
if relevant_backslash_count.even? || !interpolation?(quote_stack.last)
tokens << [:tSTRING_CONTENT, [current_string, range(start_offset, start_offset + current_length)]]
break
end
token, _ = lexed[index]
index += 1
end
else
# When the parser gem encounters a line continuation inside of a multiline string,
# it emits a single string node. The backslash (and remaining newline) is removed.
current_line = +""
adjustment = 0
start_offset = token.location.start_offset
emit = false
lines.each.with_index do |line, index|
chomped_line = line.chomp
backslash_count = chomped_line[/\\{1,}\z/]&.length || 0
is_interpolation = interpolation?(quote_stack.last)
if backslash_count.odd? && (is_interpolation || is_percent_array)
if is_percent_array
current_line << percent_array_unescape(line)
adjustment += 1
else
chomped_line.delete_suffix!("\\")
current_line << chomped_line
adjustment += 2
end
# If the string ends with a line continuation emit the remainder
emit = index == lines.count - 1
else
current_line << line
emit = true
end
if emit
end_offset = start_offset + current_line.bytesize + adjustment
tokens << [:tSTRING_CONTENT, [unescape_string(current_line, quote_stack.last), range(start_offset, end_offset)]]
start_offset = end_offset
current_line = +""
adjustment = 0
end
end
end
next
when :tSTRING_DVAR
value = nil
when :tSTRING_END
if token.type == :HEREDOC_END && value.end_with?("\n")
newline_length = value.end_with?("\r\n") ? 2 : 1
value = heredoc_stack.pop.identifier
location = range(token.location.start_offset, token.location.end_offset - newline_length)
elsif token.type == :REGEXP_END
value = value[0]
location = range(token.location.start_offset, token.location.start_offset + 1)
end
if percent_array?(quote_stack.pop)
prev_token, _ = lexed[index - 2] if index - 2 >= 0
empty = %i[PERCENT_LOWER_I PERCENT_LOWER_W PERCENT_UPPER_I PERCENT_UPPER_W].include?(prev_token&.type)
ends_with_whitespace = prev_token&.type == :WORDS_SEP
# parser always emits a space token after content in a percent array, even if no actual whitespace is present.
if !empty && !ends_with_whitespace
tokens << [:tSPACE, [nil, range(token.location.start_offset, token.location.start_offset)]]
end
end
when :tSYMBEG
if (next_token = lexed[index]&.first) && next_token.type != :STRING_CONTENT && next_token.type != :EMBEXPR_BEGIN && next_token.type != :EMBVAR && next_token.type != :STRING_END
next_location = token.location.join(next_token.location)
type = :tSYMBOL
value = next_token.value
value = { "~@" => "~", "!@" => "!" }.fetch(value, value)
location = range(next_location.start_offset, next_location.end_offset)
index += 1
else
quote_stack.push(value)
end
when :tFID
if !tokens.empty? && tokens.dig(-1, 0) == :kDEF
type = :tIDENTIFIER
end
when :tXSTRING_BEG
if (next_token = lexed[index]&.first) && !%i[STRING_CONTENT STRING_END EMBEXPR_BEGIN].include?(next_token.type)
# self.`()
type = :tBACK_REF2
end
quote_stack.push(value)
when :tSYMBOLS_BEG, :tQSYMBOLS_BEG, :tWORDS_BEG, :tQWORDS_BEG
if (next_token = lexed[index]&.first) && next_token.type == :WORDS_SEP
index += 1
end
quote_stack.push(value)
when :tREGEXP_BEG
quote_stack.push(value)
end
tokens << [type, [value, location]]
if token.type == :REGEXP_END
tokens << [:tREGEXP_OPT, [token.value[1..], range(token.location.start_offset + 1, token.location.end_offset)]]
end
end
tokens
end
private
# Creates a new parser range, taking prisms byte offsets into account
def range(start_offset, end_offset)
Range.new(source_buffer, offset_cache[start_offset], offset_cache[end_offset])
end
# Parse an integer from the string representation.
def parse_integer(value)
Integer(value)
rescue ArgumentError
0
end
# Parse a float from the string representation.
def parse_float(value)
Float(value)
rescue ArgumentError
0.0
end
# Parse a complex from the string representation.
def parse_complex(value)
value.chomp!("i")
if value.end_with?("r")
Complex(0, parse_rational(value))
elsif value.start_with?(/0[BbOoDdXx]/)
Complex(0, parse_integer(value))
else
Complex(0, value)
end
rescue ArgumentError
0i
end
# Parse a rational from the string representation.
def parse_rational(value)
value.chomp!("r")
if value.start_with?(/0[BbOoDdXx]/)
Rational(parse_integer(value))
else
Rational(value)
end
rescue ArgumentError
0r
end
# Wonky heredoc tab/spaces rules.
# https://github.com/ruby/prism/blob/v1.3.0/src/prism.c#L10548-L10558
def calculate_heredoc_whitespace(heredoc_token_index)
next_token_index = heredoc_token_index
nesting_level = 0
previous_line = -1
result = Float::MAX
while (next_token = lexed[next_token_index]&.first)
next_token_index += 1
next_next_token, _ = lexed[next_token_index]
first_token_on_line = next_token.location.start_column == 0
# String content inside nested heredocs and interpolation is ignored
if next_token.type == :HEREDOC_START || next_token.type == :EMBEXPR_BEGIN
# When interpolation is the first token of a line there is no string
# content to check against. There will be no common whitespace.
if nesting_level == 0 && first_token_on_line
result = 0
end
nesting_level += 1
elsif next_token.type == :HEREDOC_END || next_token.type == :EMBEXPR_END
nesting_level -= 1
# When we encountered the matching heredoc end, we can exit
break if nesting_level == -1
elsif next_token.type == :STRING_CONTENT && nesting_level == 0 && first_token_on_line
common_whitespace = 0
next_token.value[/^\s*/].each_char do |char|
if char == "\t"
common_whitespace = (common_whitespace / 8 + 1) * 8;
else
common_whitespace += 1
end
end
is_first_token_on_line = next_token.location.start_line != previous_line
# Whitespace is significant if followed by interpolation
whitespace_only = common_whitespace == next_token.value.length && next_next_token&.location&.start_line != next_token.location.start_line
if is_first_token_on_line && !whitespace_only && common_whitespace < result
result = common_whitespace
previous_line = next_token.location.start_line
end
end
end
result
end
# Wonky heredoc tab/spaces rules.
# https://github.com/ruby/prism/blob/v1.3.0/src/prism.c#L16528-L16545
def trim_heredoc_whitespace(string, heredoc)
trimmed_whitespace = 0
trimmed_characters = 0
while (string[trimmed_characters] == "\t" || string[trimmed_characters] == " ") && trimmed_whitespace < heredoc.common_whitespace
if string[trimmed_characters] == "\t"
trimmed_whitespace = (trimmed_whitespace / 8 + 1) * 8;
break if trimmed_whitespace > heredoc.common_whitespace
else
trimmed_whitespace += 1
end
trimmed_characters += 1
end
string[trimmed_characters..]
end
# Escape sequences that have special and should appear unescaped in the resulting string.
ESCAPES = {
"a" => "\a", "b" => "\b", "e" => "\e", "f" => "\f",
"n" => "\n", "r" => "\r", "s" => "\s", "t" => "\t",
"v" => "\v", "\\" => "\\"
}.freeze
private_constant :ESCAPES
# When one of these delimiters is encountered, then the other
# one is allowed to be escaped as well.
DELIMITER_SYMETRY = { "[" => "]", "(" => ")", "{" => "}", "<" => ">" }.freeze
private_constant :DELIMITER_SYMETRY
# https://github.com/whitequark/parser/blob/v3.3.6.0/lib/parser/lexer-strings.rl#L14
REGEXP_META_CHARACTERS = ["\\", "$", "(", ")", "*", "+", ".", "<", ">", "?", "[", "]", "^", "{", "|", "}"]
private_constant :REGEXP_META_CHARACTERS
# Apply Ruby string escaping rules
def unescape_string(string, quote)
# In single-quoted heredocs, everything is taken literally.
return string if quote == "<<'"
# OPTIMIZATION: Assume that few strings need escaping to speed up the common case.
return string unless string.include?("\\")
# Enclosing character for the string. `"` for `"foo"`, `{` for `%w{foo}`, etc.
delimiter = quote[-1]
if regexp?(quote)
# Should be escaped handled to single-quoted heredocs. The only character that is
# allowed to be escaped is the delimiter, except when that also has special meaning
# in the regexp. Since all the symetry delimiters have special meaning, they don't need
# to be considered separately.
if REGEXP_META_CHARACTERS.include?(delimiter)
string
else
# There can never be an even amount of backslashes. It would be a syntax error.
string.gsub(/\\(#{Regexp.escape(delimiter)})/, '\1')
end
elsif interpolation?(quote)
# Appending individual escape sequences may force the string out of its intended
# encoding. Start out with binary and force it back later.
result = "".b
scanner = StringScanner.new(string)
while (skipped = scanner.skip_until(/\\/))
# Append what was just skipped over, excluding the found backslash.
result.append_as_bytes(string.byteslice(scanner.pos - skipped, skipped - 1))
escape_read(result, scanner, false, false)
end
# Add remaining chars
result.append_as_bytes(string.byteslice(scanner.pos..))
result.force_encoding(source_buffer.source.encoding)
else
delimiters = Regexp.escape("#{delimiter}#{DELIMITER_SYMETRY[delimiter]}")
string.gsub(/\\([\\#{delimiters}])/, '\1')
end
end
# Certain strings are merged into a single string token.
def simplify_string?(value, quote)
case quote
when "'"
# Only simplify 'foo'
!value.include?("\n")
when '"'
# Simplify when every line ends with a line continuation, or it is the last line
value.lines.all? do |line|
!line.end_with?("\n") || line[/(\\*)$/, 1]&.length&.odd?
end
else
# %q and similar are never simplified
false
end
end
# Escape a byte value, given the control and meta flags.
def escape_build(value, control, meta)
value &= 0x9f if control
value |= 0x80 if meta
value
end
# Read an escape out of the string scanner, given the control and meta
# flags, and push the unescaped value into the result.
def escape_read(result, scanner, control, meta)
if scanner.skip("\n")
# Line continuation
elsif (value = ESCAPES[scanner.peek(1)])
# Simple single-character escape sequences like \n
result.append_as_bytes(value)
scanner.pos += 1
elsif (value = scanner.scan(/[0-7]{1,3}/))
# \nnn
result.append_as_bytes(escape_build(value.to_i(8), control, meta))
elsif (value = scanner.scan(/x[0-9a-fA-F]{1,2}/))
# \xnn
result.append_as_bytes(escape_build(value[1..].to_i(16), control, meta))
elsif (value = scanner.scan(/u[0-9a-fA-F]{4}/))
# \unnnn
result.append_as_bytes(value[1..].hex.chr(Encoding::UTF_8))
elsif scanner.skip("u{}")
# https://github.com/whitequark/parser/issues/856
elsif (value = scanner.scan(/u{.*?}/))
# \u{nnnn ...}
value[2..-2].split.each do |unicode|
result.append_as_bytes(unicode.hex.chr(Encoding::UTF_8))
end
elsif (value = scanner.scan(/c\\?(?=[[:print:]])|C-\\?(?=[[:print:]])/))
# \cx or \C-x where x is an ASCII printable character
escape_read(result, scanner, true, meta)
elsif (value = scanner.scan(/M-\\?(?=[[:print:]])/))
# \M-x where x is an ASCII printable character
escape_read(result, scanner, control, true)
elsif (byte = scanner.scan_byte)
# Something else after an escape.
if control && byte == 0x3f # ASCII '?'
result.append_as_bytes(escape_build(0x7f, false, meta))
else
result.append_as_bytes(escape_build(byte, control, meta))
end
end
end
# In a percent array, certain whitespace can be preceeded with a backslash,
# causing the following characters to be part of the previous element.
def percent_array_unescape(string)
string.gsub(/(\\)+[ \f\n\r\t\v]/) do |full_match|
full_match.delete_prefix!("\\") if Regexp.last_match[1].length.odd?
full_match
end
end
# For %-arrays whitespace, the parser gem only considers whitespace before the newline.
def percent_array_leading_whitespace(string)
return 1 if string.start_with?("\n")
leading_whitespace = 0
string.each_char do |c|
break if c == "\n"
leading_whitespace += 1
end
leading_whitespace
end
# Determine if characters preceeded by a backslash should be escaped or not
def interpolation?(quote)
!quote.end_with?("'") && !quote.start_with?("%q", "%w", "%i", "%s")
end
# Regexp allow interpolation but are handled differently during unescaping
def regexp?(quote)
quote == "/" || quote.start_with?("%r")
end
# Determine if the string is part of a %-style array.
def percent_array?(quote)
quote.start_with?("%w", "%W", "%i", "%I")
end
end
end
end
end
PK ! z
builder.rbnu [ # frozen_string_literal: true
# :markup: markdown
module Prism
module Translation
class Parser
# A builder that knows how to convert more modern Ruby syntax
# into whitequark/parser gem's syntax tree.
class Builder < ::Parser::Builders::Default
# It represents the `it` block argument, which is not yet implemented in the Parser gem.
def itarg
n(:itarg, [:it], nil)
end
# The following three lines have been added to support the `it` block parameter syntax in the source code below.
#
# if args.type == :itarg
# block_type = :itblock
# args = :it
#
# https://github.com/whitequark/parser/blob/v3.3.7.1/lib/parser/builders/default.rb#L1122-L1155
def block(method_call, begin_t, args, body, end_t)
_receiver, _selector, *call_args = *method_call
if method_call.type == :yield
diagnostic :error, :block_given_to_yield, nil, method_call.loc.keyword, [loc(begin_t)]
end
last_arg = call_args.last
if last_arg && (last_arg.type == :block_pass || last_arg.type == :forwarded_args)
diagnostic :error, :block_and_blockarg, nil, last_arg.loc.expression, [loc(begin_t)]
end
if args.type == :itarg
block_type = :itblock
args = :it
elsif args.type == :numargs
block_type = :numblock
args = args.children[0]
else
block_type = :block
end
if [:send, :csend, :index, :super, :zsuper, :lambda].include?(method_call.type)
n(block_type, [ method_call, args, body ],
block_map(method_call.loc.expression, begin_t, end_t))
else
# Code like "return foo 1 do end" is reduced in a weird sequence.
# Here, method_call is actually (return).
actual_send, = *method_call
block =
n(block_type, [ actual_send, args, body ],
block_map(actual_send.loc.expression, begin_t, end_t))
n(method_call.type, [ block ],
method_call.loc.with_expression(join_exprs(method_call, block)))
end
end
end
end
end
end
PK ! R&( &( compiler.rbnu [ # frozen_string_literal: true
# :markup: markdown
module Prism
module Translation
class Parser
# A visitor that knows how to convert a prism syntax tree into the
# whitequark/parser gem's syntax tree.
class Compiler < ::Prism::Compiler
# Raised when the tree is malformed or there is a bug in the compiler.
class CompilationError < StandardError
end
# The Parser::Base instance that is being used to build the AST.
attr_reader :parser
# The Parser::Builders::Default instance that is being used to build the
# AST.
attr_reader :builder
# The Parser::Source::Buffer instance that is holding a reference to the
# source code.
attr_reader :source_buffer
# The offset cache that is used to map between byte and character
# offsets in the file.
attr_reader :offset_cache
# The types of values that can be forwarded in the current scope.
attr_reader :forwarding
# Whether or not the current node is in a destructure.
attr_reader :in_destructure
# Whether or not the current node is in a pattern.
attr_reader :in_pattern
# Initialize a new compiler with the given parser, offset cache, and
# options.
def initialize(parser, offset_cache, forwarding: [], in_destructure: false, in_pattern: false)
@parser = parser
@builder = parser.builder
@source_buffer = parser.source_buffer
@offset_cache = offset_cache
@forwarding = forwarding
@in_destructure = in_destructure
@in_pattern = in_pattern
end
# alias foo bar
# ^^^^^^^^^^^^^
def visit_alias_method_node(node)
builder.alias(token(node.keyword_loc), visit(node.new_name), visit(node.old_name))
end
# alias $foo $bar
# ^^^^^^^^^^^^^^^
def visit_alias_global_variable_node(node)
builder.alias(token(node.keyword_loc), visit(node.new_name), visit(node.old_name))
end
# foo => bar | baz
# ^^^^^^^^^
def visit_alternation_pattern_node(node)
builder.match_alt(visit(node.left), token(node.operator_loc), visit(node.right))
end
# a and b
# ^^^^^^^
def visit_and_node(node)
builder.logical_op(:and, visit(node.left), token(node.operator_loc), visit(node.right))
end
# []
# ^^
def visit_array_node(node)
if node.opening&.start_with?("%w", "%W", "%i", "%I")
elements = node.elements.flat_map do |element|
if element.is_a?(StringNode)
if element.content.include?("\n")
string_nodes_from_line_continuations(element.unescaped, element.content, element.content_loc.start_offset, node.opening)
else
[builder.string_internal([element.unescaped, srange(element.content_loc)])]
end
elsif element.is_a?(InterpolatedStringNode)
builder.string_compose(
token(element.opening_loc),
string_nodes_from_interpolation(element, node.opening),
token(element.closing_loc)
)
else
[visit(element)]
end
end
else
elements = visit_all(node.elements)
end
builder.array(token(node.opening_loc), elements, token(node.closing_loc))
end
# foo => [bar]
# ^^^^^
def visit_array_pattern_node(node)
elements = [*node.requireds]
elements << node.rest if !node.rest.nil? && !node.rest.is_a?(ImplicitRestNode)
elements.concat(node.posts)
visited = visit_all(elements)
if node.rest.is_a?(ImplicitRestNode)
visited[-1] = builder.match_with_trailing_comma(visited[-1], token(node.rest.location))
end
if node.constant
if visited.empty?
builder.const_pattern(visit(node.constant), token(node.opening_loc), builder.array_pattern(token(node.opening_loc), visited, token(node.closing_loc)), token(node.closing_loc))
else
builder.const_pattern(visit(node.constant), token(node.opening_loc), builder.array_pattern(nil, visited, nil), token(node.closing_loc))
end
else
builder.array_pattern(token(node.opening_loc), visited, token(node.closing_loc))
end
end
# foo(bar)
# ^^^
def visit_arguments_node(node)
visit_all(node.arguments)
end
# { a: 1 }
# ^^^^
def visit_assoc_node(node)
key = node.key
if node.value.is_a?(ImplicitNode)
if in_pattern
if key.is_a?(SymbolNode)
if key.opening.nil?
builder.match_hash_var([key.unescaped, srange(key.location)])
else
builder.match_hash_var_from_str(token(key.opening_loc), [builder.string_internal([key.unescaped, srange(key.value_loc)])], token(key.closing_loc))
end
else
builder.match_hash_var_from_str(token(key.opening_loc), visit_all(key.parts), token(key.closing_loc))
end
else
value = node.value.value
implicit_value = if value.is_a?(CallNode)
builder.call_method(nil, nil, [value.name, srange(value.message_loc)])
elsif value.is_a?(ConstantReadNode)
builder.const([value.name, srange(key.value_loc)])
else
builder.ident([value.name, srange(key.value_loc)]).updated(:lvar)
end
builder.pair_keyword([key.unescaped, srange(key)], implicit_value)
end
elsif node.operator_loc
builder.pair(visit(key), token(node.operator_loc), visit(node.value))
elsif key.is_a?(SymbolNode) && key.opening_loc.nil?
builder.pair_keyword([key.unescaped, srange(key.location)], visit(node.value))
else
parts =
if key.is_a?(SymbolNode)
[builder.string_internal([key.unescaped, srange(key.value_loc)])]
else
visit_all(key.parts)
end
builder.pair_quoted(token(key.opening_loc), parts, token(key.closing_loc), visit(node.value))
end
end
# def foo(**); bar(**); end
# ^^
#
# { **foo }
# ^^^^^
def visit_assoc_splat_node(node)
if in_pattern
builder.match_rest(token(node.operator_loc), token(node.value&.location))
elsif node.value.nil? && forwarding.include?(:**)
builder.forwarded_kwrestarg(token(node.operator_loc))
else
builder.kwsplat(token(node.operator_loc), visit(node.value))
end
end
# $+
# ^^
def visit_back_reference_read_node(node)
builder.back_ref(token(node.location))
end
# begin end
# ^^^^^^^^^
def visit_begin_node(node)
rescue_bodies = []
if (rescue_clause = node.rescue_clause)
begin
find_start_offset = (rescue_clause.reference&.location || rescue_clause.exceptions.last&.location || rescue_clause.keyword_loc).end_offset
find_end_offset = (
rescue_clause.statements&.location&.start_offset ||
rescue_clause.subsequent&.location&.start_offset ||
node.else_clause&.location&.start_offset ||
node.ensure_clause&.location&.start_offset ||
node.end_keyword_loc&.start_offset ||
find_start_offset + 1
)
rescue_bodies << builder.rescue_body(
token(rescue_clause.keyword_loc),
rescue_clause.exceptions.any? ? builder.array(nil, visit_all(rescue_clause.exceptions), nil) : nil,
token(rescue_clause.operator_loc),
visit(rescue_clause.reference),
srange_find(find_start_offset, find_end_offset, ";"),
visit(rescue_clause.statements)
)
end until (rescue_clause = rescue_clause.subsequent).nil?
end
begin_body =
builder.begin_body(
visit(node.statements),
rescue_bodies,
token(node.else_clause&.else_keyword_loc),
visit(node.else_clause),
token(node.ensure_clause&.ensure_keyword_loc),
visit(node.ensure_clause&.statements)
)
if node.begin_keyword_loc
builder.begin_keyword(token(node.begin_keyword_loc), begin_body, token(node.end_keyword_loc))
else
begin_body
end
end
# foo(&bar)
# ^^^^
def visit_block_argument_node(node)
builder.block_pass(token(node.operator_loc), visit(node.expression))
end
# foo { |; bar| }
# ^^^
def visit_block_local_variable_node(node)
builder.shadowarg(token(node.location))
end
# A block on a keyword or method call.
def visit_block_node(node)
raise CompilationError, "Cannot directly compile block nodes"
end
# def foo(&bar); end
# ^^^^
def visit_block_parameter_node(node)
builder.blockarg(token(node.operator_loc), token(node.name_loc))
end
# A block's parameters.
def visit_block_parameters_node(node)
[*visit(node.parameters)].concat(visit_all(node.locals))
end
# break
# ^^^^^
#
# break foo
# ^^^^^^^^^
def visit_break_node(node)
builder.keyword_cmd(:break, token(node.keyword_loc), nil, visit(node.arguments) || [], nil)
end
# foo
# ^^^
#
# foo.bar
# ^^^^^^^
#
# foo.bar() {}
# ^^^^^^^^^^^^
def visit_call_node(node)
name = node.name
arguments = node.arguments&.arguments || []
block = node.block
if block.is_a?(BlockArgumentNode)
arguments = [*arguments, block]
block = nil
end
if node.call_operator_loc.nil?
case name
when :-@
case (receiver = node.receiver).type
when :integer_node, :float_node, :rational_node, :imaginary_node
return visit(numeric_negate(node.message_loc, receiver))
end
when :!
return visit_block(builder.not_op(token(node.message_loc), token(node.opening_loc), visit(node.receiver), token(node.closing_loc)), block)
when :=~
if (receiver = node.receiver).is_a?(RegularExpressionNode)
return builder.match_op(visit(receiver), token(node.message_loc), visit(node.arguments.arguments.first))
end
when :[]
return visit_block(builder.index(visit(node.receiver), token(node.opening_loc), visit_all(arguments), token(node.closing_loc)), block)
when :[]=
if node.message != "[]=" && node.arguments && block.nil? && !node.safe_navigation?
arguments = node.arguments.arguments[...-1]
arguments << node.block if node.block
return visit_block(
builder.assign(
builder.index_asgn(
visit(node.receiver),
token(node.opening_loc),
visit_all(arguments),
token(node.closing_loc),
),
srange_find(node.message_loc.end_offset, node.arguments.arguments.last.location.start_offset, "="),
visit(node.arguments.arguments.last)
),
block
)
end
end
end
message_loc = node.message_loc
call_operator_loc = node.call_operator_loc
call_operator = [{ "." => :dot, "&." => :anddot, "::" => "::" }.fetch(call_operator_loc.slice), srange(call_operator_loc)] if call_operator_loc
visit_block(
if name.end_with?("=") && !message_loc.slice.end_with?("=") && node.arguments && block.nil?
builder.assign(
builder.attr_asgn(visit(node.receiver), call_operator, token(message_loc)),
srange_find(message_loc.end_offset, node.arguments.location.start_offset, "="),
visit(node.arguments.arguments.last)
)
else
builder.call_method(
visit(node.receiver),
call_operator,
message_loc ? [node.name, srange(message_loc)] : nil,
token(node.opening_loc),
visit_all(arguments),
token(node.closing_loc)
)
end,
block
)
end
# foo.bar += baz
# ^^^^^^^^^^^^^^^
def visit_call_operator_write_node(node)
call_operator_loc = node.call_operator_loc
builder.op_assign(
builder.call_method(
visit(node.receiver),
call_operator_loc.nil? ? nil : [{ "." => :dot, "&." => :anddot, "::" => "::" }.fetch(call_operator_loc.slice), srange(call_operator_loc)],
node.message_loc ? [node.read_name, srange(node.message_loc)] : nil,
nil,
[],
nil
),
[node.binary_operator_loc.slice.chomp("="), srange(node.binary_operator_loc)],
visit(node.value)
)
end
# foo.bar &&= baz
# ^^^^^^^^^^^^^^^
def visit_call_and_write_node(node)
call_operator_loc = node.call_operator_loc
builder.op_assign(
builder.call_method(
visit(node.receiver),
call_operator_loc.nil? ? nil : [{ "." => :dot, "&." => :anddot, "::" => "::" }.fetch(call_operator_loc.slice), srange(call_operator_loc)],
node.message_loc ? [node.read_name, srange(node.message_loc)] : nil,
nil,
[],
nil
),
[node.operator_loc.slice.chomp("="), srange(node.operator_loc)],
visit(node.value)
)
end
# foo.bar ||= baz
# ^^^^^^^^^^^^^^^
def visit_call_or_write_node(node)
call_operator_loc = node.call_operator_loc
builder.op_assign(
builder.call_method(
visit(node.receiver),
call_operator_loc.nil? ? nil : [{ "." => :dot, "&." => :anddot, "::" => "::" }.fetch(call_operator_loc.slice), srange(call_operator_loc)],
node.message_loc ? [node.read_name, srange(node.message_loc)] : nil,
nil,
[],
nil
),
[node.operator_loc.slice.chomp("="), srange(node.operator_loc)],
visit(node.value)
)
end
# foo.bar, = 1
# ^^^^^^^
def visit_call_target_node(node)
call_operator_loc = node.call_operator_loc
builder.attr_asgn(
visit(node.receiver),
call_operator_loc.nil? ? nil : [{ "." => :dot, "&." => :anddot, "::" => "::" }.fetch(call_operator_loc.slice), srange(call_operator_loc)],
token(node.message_loc)
)
end
# foo => bar => baz
# ^^^^^^^^^^
def visit_capture_pattern_node(node)
builder.match_as(visit(node.value), token(node.operator_loc), visit(node.target))
end
# case foo; when bar; end
# ^^^^^^^^^^^^^^^^^^^^^^^
def visit_case_node(node)
builder.case(
token(node.case_keyword_loc),
visit(node.predicate),
visit_all(node.conditions),
token(node.else_clause&.else_keyword_loc),
visit(node.else_clause),
token(node.end_keyword_loc)
)
end
# case foo; in bar; end
# ^^^^^^^^^^^^^^^^^^^^^
def visit_case_match_node(node)
builder.case_match(
token(node.case_keyword_loc),
visit(node.predicate),
visit_all(node.conditions),
token(node.else_clause&.else_keyword_loc),
visit(node.else_clause),
token(node.end_keyword_loc)
)
end
# class Foo; end
# ^^^^^^^^^^^^^^
def visit_class_node(node)
builder.def_class(
token(node.class_keyword_loc),
visit(node.constant_path),
token(node.inheritance_operator_loc),
visit(node.superclass),
node.body&.accept(copy_compiler(forwarding: [])),
token(node.end_keyword_loc)
)
end
# @@foo
# ^^^^^
def visit_class_variable_read_node(node)
builder.cvar(token(node.location))
end
# @@foo = 1
# ^^^^^^^^^
def visit_class_variable_write_node(node)
builder.assign(
builder.assignable(builder.cvar(token(node.name_loc))),
token(node.operator_loc),
visit(node.value)
)
end
# @@foo += bar
# ^^^^^^^^^^^^
def visit_class_variable_operator_write_node(node)
builder.op_assign(
builder.assignable(builder.cvar(token(node.name_loc))),
[node.binary_operator_loc.slice.chomp("="), srange(node.binary_operator_loc)],
visit(node.value)
)
end
# @@foo &&= bar
# ^^^^^^^^^^^^^
def visit_class_variable_and_write_node(node)
builder.op_assign(
builder.assignable(builder.cvar(token(node.name_loc))),
[node.operator_loc.slice.chomp("="), srange(node.operator_loc)],
visit(node.value)
)
end
# @@foo ||= bar
# ^^^^^^^^^^^^^
def visit_class_variable_or_write_node(node)
builder.op_assign(
builder.assignable(builder.cvar(token(node.name_loc))),
[node.operator_loc.slice.chomp("="), srange(node.operator_loc)],
visit(node.value)
)
end
# @@foo, = bar
# ^^^^^
def visit_class_variable_target_node(node)
builder.assignable(builder.cvar(token(node.location)))
end
# Foo
# ^^^
def visit_constant_read_node(node)
builder.const([node.name, srange(node.location)])
end
# Foo = 1
# ^^^^^^^
#
# Foo, Bar = 1
# ^^^ ^^^
def visit_constant_write_node(node)
builder.assign(builder.assignable(builder.const([node.name, srange(node.name_loc)])), token(node.operator_loc), visit(node.value))
end
# Foo += bar
# ^^^^^^^^^^^
def visit_constant_operator_write_node(node)
builder.op_assign(
builder.assignable(builder.const([node.name, srange(node.name_loc)])),
[node.binary_operator_loc.slice.chomp("="), srange(node.binary_operator_loc)],
visit(node.value)
)
end
# Foo &&= bar
# ^^^^^^^^^^^^
def visit_constant_and_write_node(node)
builder.op_assign(
builder.assignable(builder.const([node.name, srange(node.name_loc)])),
[node.operator_loc.slice.chomp("="), srange(node.operator_loc)],
visit(node.value)
)
end
# Foo ||= bar
# ^^^^^^^^^^^^
def visit_constant_or_write_node(node)
builder.op_assign(
builder.assignable(builder.const([node.name, srange(node.name_loc)])),
[node.operator_loc.slice.chomp("="), srange(node.operator_loc)],
visit(node.value)
)
end
# Foo, = bar
# ^^^
def visit_constant_target_node(node)
builder.assignable(builder.const([node.name, srange(node.location)]))
end
# Foo::Bar
# ^^^^^^^^
def visit_constant_path_node(node)
if node.parent.nil?
builder.const_global(
token(node.delimiter_loc),
[node.name, srange(node.name_loc)]
)
else
builder.const_fetch(
visit(node.parent),
token(node.delimiter_loc),
[node.name, srange(node.name_loc)]
)
end
end
# Foo::Bar = 1
# ^^^^^^^^^^^^
#
# Foo::Foo, Bar::Bar = 1
# ^^^^^^^^ ^^^^^^^^
def visit_constant_path_write_node(node)
builder.assign(
builder.assignable(visit(node.target)),
token(node.operator_loc),
visit(node.value)
)
end
# Foo::Bar += baz
# ^^^^^^^^^^^^^^^
def visit_constant_path_operator_write_node(node)
builder.op_assign(
builder.assignable(visit(node.target)),
[node.binary_operator_loc.slice.chomp("="), srange(node.binary_operator_loc)],
visit(node.value)
)
end
# Foo::Bar &&= baz
# ^^^^^^^^^^^^^^^^
def visit_constant_path_and_write_node(node)
builder.op_assign(
builder.assignable(visit(node.target)),
[node.operator_loc.slice.chomp("="), srange(node.operator_loc)],
visit(node.value)
)
end
# Foo::Bar ||= baz
# ^^^^^^^^^^^^^^^^
def visit_constant_path_or_write_node(node)
builder.op_assign(
builder.assignable(visit(node.target)),
[node.operator_loc.slice.chomp("="), srange(node.operator_loc)],
visit(node.value)
)
end
# Foo::Bar, = baz
# ^^^^^^^^
def visit_constant_path_target_node(node)
builder.assignable(visit_constant_path_node(node))
end
# def foo; end
# ^^^^^^^^^^^^
#
# def self.foo; end
# ^^^^^^^^^^^^^^^^^
def visit_def_node(node)
if node.equal_loc
if node.receiver
builder.def_endless_singleton(
token(node.def_keyword_loc),
visit(node.receiver.is_a?(ParenthesesNode) ? node.receiver.body : node.receiver),
token(node.operator_loc),
token(node.name_loc),
builder.args(token(node.lparen_loc), visit(node.parameters) || [], token(node.rparen_loc), false),
token(node.equal_loc),
node.body&.accept(copy_compiler(forwarding: find_forwarding(node.parameters)))
)
else
builder.def_endless_method(
token(node.def_keyword_loc),
token(node.name_loc),
builder.args(token(node.lparen_loc), visit(node.parameters) || [], token(node.rparen_loc), false),
token(node.equal_loc),
node.body&.accept(copy_compiler(forwarding: find_forwarding(node.parameters)))
)
end
elsif node.receiver
builder.def_singleton(
token(node.def_keyword_loc),
visit(node.receiver.is_a?(ParenthesesNode) ? node.receiver.body : node.receiver),
token(node.operator_loc),
token(node.name_loc),
builder.args(token(node.lparen_loc), visit(node.parameters) || [], token(node.rparen_loc), false),
node.body&.accept(copy_compiler(forwarding: find_forwarding(node.parameters))),
token(node.end_keyword_loc)
)
else
builder.def_method(
token(node.def_keyword_loc),
token(node.name_loc),
builder.args(token(node.lparen_loc), visit(node.parameters) || [], token(node.rparen_loc), false),
node.body&.accept(copy_compiler(forwarding: find_forwarding(node.parameters))),
token(node.end_keyword_loc)
)
end
end
# defined? a
# ^^^^^^^^^^
#
# defined?(a)
# ^^^^^^^^^^^
def visit_defined_node(node)
# Very weird circumstances here where something like:
#
# defined?
# (1)
#
# gets parsed in Ruby as having only the `1` expression but in parser
# it gets parsed as having a begin. In this case we need to synthesize
# that begin to match parser's behavior.
if node.lparen_loc && node.keyword_loc.join(node.lparen_loc).slice.include?("\n")
builder.keyword_cmd(
:defined?,
token(node.keyword_loc),
nil,
[
builder.begin(
token(node.lparen_loc),
visit(node.value),
token(node.rparen_loc)
)
],
nil
)
else
builder.keyword_cmd(
:defined?,
token(node.keyword_loc),
token(node.lparen_loc),
[visit(node.value)],
token(node.rparen_loc)
)
end
end
# if foo then bar else baz end
# ^^^^^^^^^^^^
def visit_else_node(node)
visit(node.statements)
end
# "foo #{bar}"
# ^^^^^^
def visit_embedded_statements_node(node)
builder.begin(
token(node.opening_loc),
visit(node.statements),
token(node.closing_loc)
)
end
# "foo #@bar"
# ^^^^^
def visit_embedded_variable_node(node)
visit(node.variable)
end
# begin; foo; ensure; bar; end
# ^^^^^^^^^^^^
def visit_ensure_node(node)
raise CompilationError, "Cannot directly compile ensure nodes"
end
# false
# ^^^^^
def visit_false_node(node)
builder.false(token(node.location))
end
# foo => [*, bar, *]
# ^^^^^^^^^^^
def visit_find_pattern_node(node)
elements = [node.left, *node.requireds, node.right]
if node.constant
builder.const_pattern(visit(node.constant), token(node.opening_loc), builder.find_pattern(nil, visit_all(elements), nil), token(node.closing_loc))
else
builder.find_pattern(token(node.opening_loc), visit_all(elements), token(node.closing_loc))
end
end
# 1.0
# ^^^
def visit_float_node(node)
visit_numeric(node, builder.float([node.value, srange(node.location)]))
end
# for foo in bar do end
# ^^^^^^^^^^^^^^^^^^^^^
def visit_for_node(node)
builder.for(
token(node.for_keyword_loc),
visit(node.index),
token(node.in_keyword_loc),
visit(node.collection),
if (do_keyword_loc = node.do_keyword_loc)
token(do_keyword_loc)
else
srange_find(node.collection.location.end_offset, (node.statements&.location || node.end_keyword_loc).start_offset, ";")
end,
visit(node.statements),
token(node.end_keyword_loc)
)
end
# def foo(...); bar(...); end
# ^^^
def visit_forwarding_arguments_node(node)
builder.forwarded_args(token(node.location))
end
# def foo(...); end
# ^^^
def visit_forwarding_parameter_node(node)
builder.forward_arg(token(node.location))
end
# super
# ^^^^^
#
# super {}
# ^^^^^^^^
def visit_forwarding_super_node(node)
visit_block(
builder.keyword_cmd(
:zsuper,
["super", srange_offsets(node.location.start_offset, node.location.start_offset + 5)]
),
node.block
)
end
# $foo
# ^^^^
def visit_global_variable_read_node(node)
builder.gvar(token(node.location))
end
# $foo = 1
# ^^^^^^^^
def visit_global_variable_write_node(node)
builder.assign(
builder.assignable(builder.gvar(token(node.name_loc))),
token(node.operator_loc),
visit(node.value)
)
end
# $foo += bar
# ^^^^^^^^^^^
def visit_global_variable_operator_write_node(node)
builder.op_assign(
builder.assignable(builder.gvar(token(node.name_loc))),
[node.binary_operator_loc.slice.chomp("="), srange(node.binary_operator_loc)],
visit(node.value)
)
end
# $foo &&= bar
# ^^^^^^^^^^^^
def visit_global_variable_and_write_node(node)
builder.op_assign(
builder.assignable(builder.gvar(token(node.name_loc))),
[node.operator_loc.slice.chomp("="), srange(node.operator_loc)],
visit(node.value)
)
end
# $foo ||= bar
# ^^^^^^^^^^^^
def visit_global_variable_or_write_node(node)
builder.op_assign(
builder.assignable(builder.gvar(token(node.name_loc))),
[node.operator_loc.slice.chomp("="), srange(node.operator_loc)],
visit(node.value)
)
end
# $foo, = bar
# ^^^^
def visit_global_variable_target_node(node)
builder.assignable(builder.gvar([node.slice, srange(node.location)]))
end
# {}
# ^^
def visit_hash_node(node)
builder.associate(
token(node.opening_loc),
visit_all(node.elements),
token(node.closing_loc)
)
end
# foo => {}
# ^^
def visit_hash_pattern_node(node)
elements = [*node.elements, *node.rest]
if node.constant
builder.const_pattern(visit(node.constant), token(node.opening_loc), builder.hash_pattern(nil, visit_all(elements), nil), token(node.closing_loc))
else
builder.hash_pattern(token(node.opening_loc), visit_all(elements), token(node.closing_loc))
end
end
# if foo then bar end
# ^^^^^^^^^^^^^^^^^^^
#
# bar if foo
# ^^^^^^^^^^
#
# foo ? bar : baz
# ^^^^^^^^^^^^^^^
def visit_if_node(node)
if !node.if_keyword_loc
builder.ternary(
visit(node.predicate),
token(node.then_keyword_loc),
visit(node.statements),
token(node.subsequent.else_keyword_loc),
visit(node.subsequent)
)
elsif node.if_keyword_loc.start_offset == node.location.start_offset
builder.condition(
token(node.if_keyword_loc),
visit(node.predicate),
if (then_keyword_loc = node.then_keyword_loc)
token(then_keyword_loc)
else
srange_find(node.predicate.location.end_offset, (node.statements&.location || node.subsequent&.location || node.end_keyword_loc).start_offset, ";")
end,
visit(node.statements),
case node.subsequent
when IfNode
token(node.subsequent.if_keyword_loc)
when ElseNode
token(node.subsequent.else_keyword_loc)
end,
visit(node.subsequent),
if node.if_keyword != "elsif"
token(node.end_keyword_loc)
end
)
else
builder.condition_mod(
visit(node.statements),
visit(node.subsequent),
token(node.if_keyword_loc),
visit(node.predicate)
)
end
end
# 1i
# ^^
def visit_imaginary_node(node)
visit_numeric(node, builder.complex([Complex(0, node.numeric.value), srange(node.location)]))
end
# { foo: }
# ^^^^
def visit_implicit_node(node)
raise CompilationError, "Cannot directly compile implicit nodes"
end
# foo { |bar,| }
# ^
def visit_implicit_rest_node(node)
raise CompilationError, "Cannot compile implicit rest nodes"
end
# case foo; in bar; end
# ^^^^^^^^^^^^^^^^^^^^^
def visit_in_node(node)
pattern = nil
guard = nil
case node.pattern
when IfNode
pattern = within_pattern { |compiler| node.pattern.statements.accept(compiler) }
guard = builder.if_guard(token(node.pattern.if_keyword_loc), visit(node.pattern.predicate))
when UnlessNode
pattern = within_pattern { |compiler| node.pattern.statements.accept(compiler) }
guard = builder.unless_guard(token(node.pattern.keyword_loc), visit(node.pattern.predicate))
else
pattern = within_pattern { |compiler| node.pattern.accept(compiler) }
end
builder.in_pattern(
token(node.in_loc),
pattern,
guard,
if (then_loc = node.then_loc)
token(then_loc)
else
srange_find(node.pattern.location.end_offset, node.statements&.location&.start_offset, ";")
end,
visit(node.statements)
)
end
# foo[bar] += baz
# ^^^^^^^^^^^^^^^
def visit_index_operator_write_node(node)
arguments = node.arguments&.arguments || []
arguments << node.block if node.block
builder.op_assign(
builder.index(
visit(node.receiver),
token(node.opening_loc),
visit_all(arguments),
token(node.closing_loc)
),
[node.binary_operator_loc.slice.chomp("="), srange(node.binary_operator_loc)],
visit(node.value)
)
end
# foo[bar] &&= baz
# ^^^^^^^^^^^^^^^^
def visit_index_and_write_node(node)
arguments = node.arguments&.arguments || []
arguments << node.block if node.block
builder.op_assign(
builder.index(
visit(node.receiver),
token(node.opening_loc),
visit_all(arguments),
token(node.closing_loc)
),
[node.operator_loc.slice.chomp("="), srange(node.operator_loc)],
visit(node.value)
)
end
# foo[bar] ||= baz
# ^^^^^^^^^^^^^^^^
def visit_index_or_write_node(node)
arguments = node.arguments&.arguments || []
arguments << node.block if node.block
builder.op_assign(
builder.index(
visit(node.receiver),
token(node.opening_loc),
visit_all(arguments),
token(node.closing_loc)
),
[node.operator_loc.slice.chomp("="), srange(node.operator_loc)],
visit(node.value)
)
end
# foo[bar], = 1
# ^^^^^^^^
def visit_index_target_node(node)
builder.index_asgn(
visit(node.receiver),
token(node.opening_loc),
visit_all(node.arguments&.arguments || []),
token(node.closing_loc),
)
end
# @foo
# ^^^^
def visit_instance_variable_read_node(node)
builder.ivar(token(node.location))
end
# @foo = 1
# ^^^^^^^^
def visit_instance_variable_write_node(node)
builder.assign(
builder.assignable(builder.ivar(token(node.name_loc))),
token(node.operator_loc),
visit(node.value)
)
end
# @foo += bar
# ^^^^^^^^^^^
def visit_instance_variable_operator_write_node(node)
builder.op_assign(
builder.assignable(builder.ivar(token(node.name_loc))),
[node.binary_operator_loc.slice.chomp("="), srange(node.binary_operator_loc)],
visit(node.value)
)
end
# @foo &&= bar
# ^^^^^^^^^^^^
def visit_instance_variable_and_write_node(node)
builder.op_assign(
builder.assignable(builder.ivar(token(node.name_loc))),
[node.operator_loc.slice.chomp("="), srange(node.operator_loc)],
visit(node.value)
)
end
# @foo ||= bar
# ^^^^^^^^^^^^
def visit_instance_variable_or_write_node(node)
builder.op_assign(
builder.assignable(builder.ivar(token(node.name_loc))),
[node.operator_loc.slice.chomp("="), srange(node.operator_loc)],
visit(node.value)
)
end
# @foo, = bar
# ^^^^
def visit_instance_variable_target_node(node)
builder.assignable(builder.ivar(token(node.location)))
end
# 1
# ^
def visit_integer_node(node)
visit_numeric(node, builder.integer([node.value, srange(node.location)]))
end
# /foo #{bar}/
# ^^^^^^^^^^^^
def visit_interpolated_regular_expression_node(node)
builder.regexp_compose(
token(node.opening_loc),
string_nodes_from_interpolation(node, node.opening),
[node.closing[0], srange_offsets(node.closing_loc.start_offset, node.closing_loc.start_offset + 1)],
builder.regexp_options([node.closing[1..], srange_offsets(node.closing_loc.start_offset + 1, node.closing_loc.end_offset)])
)
end
# if /foo #{bar}/ then end
# ^^^^^^^^^^^^
alias visit_interpolated_match_last_line_node visit_interpolated_regular_expression_node
# "foo #{bar}"
# ^^^^^^^^^^^^
def visit_interpolated_string_node(node)
if node.heredoc?
return visit_heredoc(node) { |children, closing| builder.string_compose(token(node.opening_loc), children, closing) }
end
builder.string_compose(
token(node.opening_loc),
string_nodes_from_interpolation(node, node.opening),
token(node.closing_loc)
)
end
# :"foo #{bar}"
# ^^^^^^^^^^^^^
def visit_interpolated_symbol_node(node)
builder.symbol_compose(
token(node.opening_loc),
string_nodes_from_interpolation(node, node.opening),
token(node.closing_loc)
)
end
# `foo #{bar}`
# ^^^^^^^^^^^^
def visit_interpolated_x_string_node(node)
if node.heredoc?
return visit_heredoc(node) { |children, closing| builder.xstring_compose(token(node.opening_loc), children, closing) }
end
builder.xstring_compose(
token(node.opening_loc),
string_nodes_from_interpolation(node, node.opening),
token(node.closing_loc)
)
end
# -> { it }
# ^^
def visit_it_local_variable_read_node(node)
builder.ident([:it, srange(node.location)]).updated(:lvar)
end
# -> { it }
# ^^^^^^^^^
def visit_it_parameters_node(node)
# FIXME: The builder _should_ always be a subclass of the prism builder.
# Currently RuboCop passes in its own builder that always inherits from the
# parser builder (which is lacking the `itarg` method). Once rubocop-ast
# opts in to use the custom prism builder a warning can be emitted when
# it is not the expected class, and eventually raise.
# https://github.com/rubocop/rubocop-ast/pull/354
if builder.is_a?(Translation::Parser::Builder)
builder.itarg
else
builder.args(nil, [], nil, false)
end
end
# foo(bar: baz)
# ^^^^^^^^
def visit_keyword_hash_node(node)
builder.associate(nil, visit_all(node.elements), nil)
end
# def foo(**bar); end
# ^^^^^
#
# def foo(**); end
# ^^
def visit_keyword_rest_parameter_node(node)
builder.kwrestarg(
token(node.operator_loc),
node.name ? [node.name, srange(node.name_loc)] : nil
)
end
# -> {}
# ^^^^^
def visit_lambda_node(node)
parameters = node.parameters
implicit_parameters = parameters.is_a?(NumberedParametersNode) || parameters.is_a?(ItParametersNode)
builder.block(
builder.call_lambda(token(node.operator_loc)),
[node.opening, srange(node.opening_loc)],
if parameters.nil?
builder.args(nil, [], nil, false)
elsif implicit_parameters
visit(node.parameters)
else
builder.args(
token(node.parameters.opening_loc),
visit(node.parameters),
token(node.parameters.closing_loc),
false
)
end,
visit(node.body),
[node.closing, srange(node.closing_loc)]
)
end
# foo
# ^^^
def visit_local_variable_read_node(node)
builder.ident([node.name, srange(node.location)]).updated(:lvar)
end
# foo = 1
# ^^^^^^^
def visit_local_variable_write_node(node)
builder.assign(
builder.assignable(builder.ident(token(node.name_loc))),
token(node.operator_loc),
visit(node.value)
)
end
# foo += bar
# ^^^^^^^^^^
def visit_local_variable_operator_write_node(node)
builder.op_assign(
builder.assignable(builder.ident(token(node.name_loc))),
[node.binary_operator_loc.slice.chomp("="), srange(node.binary_operator_loc)],
visit(node.value)
)
end
# foo &&= bar
# ^^^^^^^^^^^
def visit_local_variable_and_write_node(node)
builder.op_assign(
builder.assignable(builder.ident(token(node.name_loc))),
[node.operator_loc.slice.chomp("="), srange(node.operator_loc)],
visit(node.value)
)
end
# foo ||= bar
# ^^^^^^^^^^^
def visit_local_variable_or_write_node(node)
builder.op_assign(
builder.assignable(builder.ident(token(node.name_loc))),
[node.operator_loc.slice.chomp("="), srange(node.operator_loc)],
visit(node.value)
)
end
# foo, = bar
# ^^^
def visit_local_variable_target_node(node)
if in_pattern
builder.assignable(builder.match_var([node.name, srange(node.location)]))
else
builder.assignable(builder.ident(token(node.location)))
end
end
# foo in bar
# ^^^^^^^^^^
def visit_match_predicate_node(node)
builder.match_pattern_p(
visit(node.value),
token(node.operator_loc),
within_pattern { |compiler| node.pattern.accept(compiler) }
)
end
# foo => bar
# ^^^^^^^^^^
def visit_match_required_node(node)
builder.match_pattern(
visit(node.value),
token(node.operator_loc),
within_pattern { |compiler| node.pattern.accept(compiler) }
)
end
# /(?foo)/ =~ bar
# ^^^^^^^^^^^^^^^^^^^^
def visit_match_write_node(node)
builder.match_op(
visit(node.call.receiver),
token(node.call.message_loc),
visit(node.call.arguments.arguments.first)
)
end
# A node that is missing from the syntax tree. This is only used in the
# case of a syntax error. The parser gem doesn't have such a concept, so
# we invent our own here.
def visit_missing_node(node)
::AST::Node.new(:missing, [], location: ::Parser::Source::Map.new(srange(node.location)))
end
# module Foo; end
# ^^^^^^^^^^^^^^^
def visit_module_node(node)
builder.def_module(
token(node.module_keyword_loc),
visit(node.constant_path),
node.body&.accept(copy_compiler(forwarding: [])),
token(node.end_keyword_loc)
)
end
# foo, bar = baz
# ^^^^^^^^
def visit_multi_target_node(node)
builder.multi_lhs(
token(node.lparen_loc),
visit_all(multi_target_elements(node)),
token(node.rparen_loc)
)
end
# foo, bar = baz
# ^^^^^^^^^^^^^^
def visit_multi_write_node(node)
elements = multi_target_elements(node)
if elements.length == 1 && elements.first.is_a?(MultiTargetNode) && !node.rest
elements = multi_target_elements(elements.first)
end
builder.multi_assign(
builder.multi_lhs(
token(node.lparen_loc),
visit_all(elements),
token(node.rparen_loc)
),
token(node.operator_loc),
visit(node.value)
)
end
# next
# ^^^^
#
# next foo
# ^^^^^^^^
def visit_next_node(node)
builder.keyword_cmd(
:next,
token(node.keyword_loc),
nil,
visit(node.arguments) || [],
nil
)
end
# nil
# ^^^
def visit_nil_node(node)
builder.nil(token(node.location))
end
# def foo(**nil); end
# ^^^^^
def visit_no_keywords_parameter_node(node)
if in_pattern
builder.match_nil_pattern(token(node.operator_loc), token(node.keyword_loc))
else
builder.kwnilarg(token(node.operator_loc), token(node.keyword_loc))
end
end
# -> { _1 + _2 }
# ^^^^^^^^^^^^^^
def visit_numbered_parameters_node(node)
builder.numargs(node.maximum)
end
# $1
# ^^
def visit_numbered_reference_read_node(node)
builder.nth_ref([node.number, srange(node.location)])
end
# def foo(bar: baz); end
# ^^^^^^^^
def visit_optional_keyword_parameter_node(node)
builder.kwoptarg([node.name, srange(node.name_loc)], visit(node.value))
end
# def foo(bar = 1); end
# ^^^^^^^
def visit_optional_parameter_node(node)
builder.optarg(token(node.name_loc), token(node.operator_loc), visit(node.value))
end
# a or b
# ^^^^^^
def visit_or_node(node)
builder.logical_op(:or, visit(node.left), token(node.operator_loc), visit(node.right))
end
# def foo(bar, *baz); end
# ^^^^^^^^^
def visit_parameters_node(node)
params = []
if node.requireds.any?
node.requireds.each do |required|
params <<
if required.is_a?(RequiredParameterNode)
visit(required)
else
required.accept(copy_compiler(in_destructure: true))
end
end
end
params.concat(visit_all(node.optionals)) if node.optionals.any?
params << visit(node.rest) if !node.rest.nil? && !node.rest.is_a?(ImplicitRestNode)
if node.posts.any?
node.posts.each do |post|
params <<
if post.is_a?(RequiredParameterNode)
visit(post)
else
post.accept(copy_compiler(in_destructure: true))
end
end
end
params.concat(visit_all(node.keywords)) if node.keywords.any?
params << visit(node.keyword_rest) if !node.keyword_rest.nil?
params << visit(node.block) if !node.block.nil?
params
end
# ()
# ^^
#
# (1)
# ^^^
def visit_parentheses_node(node)
builder.begin(
token(node.opening_loc),
visit(node.body),
token(node.closing_loc)
)
end
# foo => ^(bar)
# ^^^^^^
def visit_pinned_expression_node(node)
parts = node.expression.accept(copy_compiler(in_pattern: false)) # Don't treat * and similar as match_rest
expression = builder.begin(token(node.lparen_loc), parts, token(node.rparen_loc))
builder.pin(token(node.operator_loc), expression)
end
# foo = 1 and bar => ^foo
# ^^^^
def visit_pinned_variable_node(node)
builder.pin(token(node.operator_loc), visit(node.variable))
end
# END {}
def visit_post_execution_node(node)
builder.postexe(
token(node.keyword_loc),
token(node.opening_loc),
visit(node.statements),
token(node.closing_loc)
)
end
# BEGIN {}
def visit_pre_execution_node(node)
builder.preexe(
token(node.keyword_loc),
token(node.opening_loc),
visit(node.statements),
token(node.closing_loc)
)
end
# The top-level program node.
def visit_program_node(node)
visit(node.statements)
end
# 0..5
# ^^^^
def visit_range_node(node)
if node.exclude_end?
builder.range_exclusive(
visit(node.left),
token(node.operator_loc),
visit(node.right)
)
else
builder.range_inclusive(
visit(node.left),
token(node.operator_loc),
visit(node.right)
)
end
end
# if foo .. bar; end
# ^^^^^^^^^^
alias visit_flip_flop_node visit_range_node
# 1r
# ^^
def visit_rational_node(node)
visit_numeric(node, builder.rational([node.value, srange(node.location)]))
end
# redo
# ^^^^
def visit_redo_node(node)
builder.keyword_cmd(:redo, token(node.location))
end
# /foo/
# ^^^^^
def visit_regular_expression_node(node)
parts =
if node.content == ""
[]
elsif node.content.include?("\n")
string_nodes_from_line_continuations(node.unescaped, node.content, node.content_loc.start_offset, node.opening)
else
[builder.string_internal([node.unescaped, srange(node.content_loc)])]
end
builder.regexp_compose(
token(node.opening_loc),
parts,
[node.closing[0], srange_offsets(node.closing_loc.start_offset, node.closing_loc.start_offset + 1)],
builder.regexp_options([node.closing[1..], srange_offsets(node.closing_loc.start_offset + 1, node.closing_loc.end_offset)])
)
end
# if /foo/ then end
# ^^^^^
alias visit_match_last_line_node visit_regular_expression_node
# def foo(bar:); end
# ^^^^
def visit_required_keyword_parameter_node(node)
builder.kwarg([node.name, srange(node.name_loc)])
end
# def foo(bar); end
# ^^^
def visit_required_parameter_node(node)
builder.arg(token(node.location))
end
# foo rescue bar
# ^^^^^^^^^^^^^^
def visit_rescue_modifier_node(node)
builder.begin_body(
visit(node.expression),
[
builder.rescue_body(
token(node.keyword_loc),
nil,
nil,
nil,
nil,
visit(node.rescue_expression)
)
]
)
end
# begin; rescue; end
# ^^^^^^^
def visit_rescue_node(node)
raise CompilationError, "Cannot directly compile rescue nodes"
end
# def foo(*bar); end
# ^^^^
#
# def foo(*); end
# ^
def visit_rest_parameter_node(node)
builder.restarg(token(node.operator_loc), token(node.name_loc))
end
# retry
# ^^^^^
def visit_retry_node(node)
builder.keyword_cmd(:retry, token(node.location))
end
# return
# ^^^^^^
#
# return 1
# ^^^^^^^^
def visit_return_node(node)
builder.keyword_cmd(
:return,
token(node.keyword_loc),
nil,
visit(node.arguments) || [],
nil
)
end
# self
# ^^^^
def visit_self_node(node)
builder.self(token(node.location))
end
# A shareable constant.
def visit_shareable_constant_node(node)
visit(node.write)
end
# class << self; end
# ^^^^^^^^^^^^^^^^^^
def visit_singleton_class_node(node)
builder.def_sclass(
token(node.class_keyword_loc),
token(node.operator_loc),
visit(node.expression),
node.body&.accept(copy_compiler(forwarding: [])),
token(node.end_keyword_loc)
)
end
# __ENCODING__
# ^^^^^^^^^^^^
def visit_source_encoding_node(node)
builder.accessible(builder.__ENCODING__(token(node.location)))
end
# __FILE__
# ^^^^^^^^
def visit_source_file_node(node)
builder.accessible(builder.__FILE__(token(node.location)))
end
# __LINE__
# ^^^^^^^^
def visit_source_line_node(node)
builder.accessible(builder.__LINE__(token(node.location)))
end
# foo(*bar)
# ^^^^
#
# def foo((bar, *baz)); end
# ^^^^
#
# def foo(*); bar(*); end
# ^
def visit_splat_node(node)
if node.expression.nil? && forwarding.include?(:*)
builder.forwarded_restarg(token(node.operator_loc))
elsif in_destructure
builder.restarg(token(node.operator_loc), token(node.expression&.location))
elsif in_pattern
builder.match_rest(token(node.operator_loc), token(node.expression&.location))
else
builder.splat(token(node.operator_loc), visit(node.expression))
end
end
# A list of statements.
def visit_statements_node(node)
builder.compstmt(visit_all(node.body))
end
# "foo"
# ^^^^^
def visit_string_node(node)
if node.heredoc?
visit_heredoc(node.to_interpolated) { |children, closing| builder.string_compose(token(node.opening_loc), children, closing) }
elsif node.opening == "?"
builder.character([node.unescaped, srange(node.location)])
elsif node.opening&.start_with?("%") && node.unescaped.empty?
builder.string_compose(token(node.opening_loc), [], token(node.closing_loc))
else
parts =
if node.content.include?("\n")
string_nodes_from_line_continuations(node.unescaped, node.content, node.content_loc.start_offset, node.opening)
else
[builder.string_internal([node.unescaped, srange(node.content_loc)])]
end
builder.string_compose(
token(node.opening_loc),
parts,
token(node.closing_loc)
)
end
end
# super(foo)
# ^^^^^^^^^^
def visit_super_node(node)
arguments = node.arguments&.arguments || []
block = node.block
if block.is_a?(BlockArgumentNode)
arguments = [*arguments, block]
block = nil
end
visit_block(
builder.keyword_cmd(
:super,
token(node.keyword_loc),
token(node.lparen_loc),
visit_all(arguments),
token(node.rparen_loc)
),
block
)
end
# :foo
# ^^^^
def visit_symbol_node(node)
if node.closing_loc.nil?
if node.opening_loc.nil?
builder.symbol_internal([node.unescaped, srange(node.location)])
else
builder.symbol([node.unescaped, srange(node.location)])
end
else
parts =
if node.value == ""
[]
elsif node.value.include?("\n")
string_nodes_from_line_continuations(node.unescaped, node.value, node.value_loc.start_offset, node.opening)
else
[builder.string_internal([node.unescaped, srange(node.value_loc)])]
end
builder.symbol_compose(
token(node.opening_loc),
parts,
token(node.closing_loc)
)
end
end
# true
# ^^^^
def visit_true_node(node)
builder.true(token(node.location))
end
# undef foo
# ^^^^^^^^^
def visit_undef_node(node)
builder.undef_method(token(node.keyword_loc), visit_all(node.names))
end
# unless foo; bar end
# ^^^^^^^^^^^^^^^^^^^
#
# bar unless foo
# ^^^^^^^^^^^^^^
def visit_unless_node(node)
if node.keyword_loc.start_offset == node.location.start_offset
builder.condition(
token(node.keyword_loc),
visit(node.predicate),
if (then_keyword_loc = node.then_keyword_loc)
token(then_keyword_loc)
else
srange_find(node.predicate.location.end_offset, (node.statements&.location || node.else_clause&.location || node.end_keyword_loc).start_offset, ";")
end,
visit(node.else_clause),
token(node.else_clause&.else_keyword_loc),
visit(node.statements),
token(node.end_keyword_loc)
)
else
builder.condition_mod(
visit(node.else_clause),
visit(node.statements),
token(node.keyword_loc),
visit(node.predicate)
)
end
end
# until foo; bar end
# ^^^^^^^^^^^^^^^^^^
#
# bar until foo
# ^^^^^^^^^^^^^
def visit_until_node(node)
if node.location.start_offset == node.keyword_loc.start_offset
builder.loop(
:until,
token(node.keyword_loc),
visit(node.predicate),
if (do_keyword_loc = node.do_keyword_loc)
token(do_keyword_loc)
else
srange_find(node.predicate.location.end_offset, (node.statements&.location || node.closing_loc).start_offset, ";")
end,
visit(node.statements),
token(node.closing_loc)
)
else
builder.loop_mod(
:until,
visit(node.statements),
token(node.keyword_loc),
visit(node.predicate)
)
end
end
# case foo; when bar; end
# ^^^^^^^^^^^^^
def visit_when_node(node)
builder.when(
token(node.keyword_loc),
visit_all(node.conditions),
if (then_keyword_loc = node.then_keyword_loc)
token(then_keyword_loc)
else
srange_find(node.conditions.last.location.end_offset, node.statements&.location&.start_offset, ";")
end,
visit(node.statements)
)
end
# while foo; bar end
# ^^^^^^^^^^^^^^^^^^
#
# bar while foo
# ^^^^^^^^^^^^^
def visit_while_node(node)
if node.location.start_offset == node.keyword_loc.start_offset
builder.loop(
:while,
token(node.keyword_loc),
visit(node.predicate),
if (do_keyword_loc = node.do_keyword_loc)
token(do_keyword_loc)
else
srange_find(node.predicate.location.end_offset, (node.statements&.location || node.closing_loc).start_offset, ";")
end,
visit(node.statements),
token(node.closing_loc)
)
else
builder.loop_mod(
:while,
visit(node.statements),
token(node.keyword_loc),
visit(node.predicate)
)
end
end
# `foo`
# ^^^^^
def visit_x_string_node(node)
if node.heredoc?
return visit_heredoc(node.to_interpolated) { |children, closing| builder.xstring_compose(token(node.opening_loc), children, closing) }
end
parts =
if node.content == ""
[]
elsif node.content.include?("\n")
string_nodes_from_line_continuations(node.unescaped, node.content, node.content_loc.start_offset, node.opening)
else
[builder.string_internal([node.unescaped, srange(node.content_loc)])]
end
builder.xstring_compose(
token(node.opening_loc),
parts,
token(node.closing_loc)
)
end
# yield
# ^^^^^
#
# yield 1
# ^^^^^^^
def visit_yield_node(node)
builder.keyword_cmd(
:yield,
token(node.keyword_loc),
token(node.lparen_loc),
visit(node.arguments) || [],
token(node.rparen_loc)
)
end
private
# Initialize a new compiler with the given option overrides, used to
# visit a subtree with the given options.
def copy_compiler(forwarding: self.forwarding, in_destructure: self.in_destructure, in_pattern: self.in_pattern)
Compiler.new(parser, offset_cache, forwarding: forwarding, in_destructure: in_destructure, in_pattern: in_pattern)
end
# When *, **, &, or ... are used as an argument in a method call, we
# check if they were allowed by the current context. To determine that
# we build this lookup table.
def find_forwarding(node)
return [] if node.nil?
forwarding = []
forwarding << :* if node.rest.is_a?(RestParameterNode) && node.rest.name.nil?
forwarding << :** if node.keyword_rest.is_a?(KeywordRestParameterNode) && node.keyword_rest.name.nil?
forwarding << :& if !node.block.nil? && node.block.name.nil?
forwarding |= [:&, :"..."] if node.keyword_rest.is_a?(ForwardingParameterNode)
forwarding
end
# Returns the set of targets for a MultiTargetNode or a MultiWriteNode.
def multi_target_elements(node)
elements = [*node.lefts]
elements << node.rest if !node.rest.nil? && !node.rest.is_a?(ImplicitRestNode)
elements.concat(node.rights)
elements
end
# Negate the value of a numeric node. This is a special case where you
# have a negative sign on one line and then a number on the next line.
# In normal Ruby, this will always be a method call. The parser gem,
# however, marks this as a numeric literal. We have to massage the tree
# here to get it into the correct form.
def numeric_negate(message_loc, receiver)
case receiver.type
when :integer_node, :float_node
receiver.copy(value: -receiver.value, location: message_loc.join(receiver.location))
when :rational_node
receiver.copy(numerator: -receiver.numerator, location: message_loc.join(receiver.location))
when :imaginary_node
receiver.copy(numeric: numeric_negate(message_loc, receiver.numeric), location: message_loc.join(receiver.location))
end
end
# Blocks can have a special set of parameters that automatically expand
# when given arrays if they have a single required parameter and no
# other parameters.
def procarg0?(parameters)
parameters &&
parameters.requireds.length == 1 &&
parameters.optionals.empty? &&
parameters.rest.nil? &&
parameters.posts.empty? &&
parameters.keywords.empty? &&
parameters.keyword_rest.nil? &&
parameters.block.nil?
end
# Locations in the parser gem AST are generated using this class. We
# store a reference to its constant to make it slightly faster to look
# up.
Range = ::Parser::Source::Range
# Constructs a new source range from the given start and end offsets.
def srange(location)
Range.new(source_buffer, offset_cache[location.start_offset], offset_cache[location.end_offset]) if location
end
# Constructs a new source range from the given start and end offsets.
def srange_offsets(start_offset, end_offset)
Range.new(source_buffer, offset_cache[start_offset], offset_cache[end_offset])
end
# Constructs a new source range by finding the given character between
# the given start offset and end offset. If the needle is not found, it
# returns nil. Importantly it does not search past newlines or comments.
#
# Note that end_offset is allowed to be nil, in which case this will
# search until the end of the string.
def srange_find(start_offset, end_offset, character)
if (match = source_buffer.source.byteslice(start_offset...end_offset)[/\A\s*#{character}/])
final_offset = start_offset + match.bytesize
[character, Range.new(source_buffer, offset_cache[final_offset - character.bytesize], offset_cache[final_offset])]
end
end
# Transform a location into a token that the parser gem expects.
def token(location)
[location.slice, Range.new(source_buffer, offset_cache[location.start_offset], offset_cache[location.end_offset])] if location
end
# Visit a block node on a call.
def visit_block(call, block)
if block
parameters = block.parameters
implicit_parameters = parameters.is_a?(NumberedParametersNode) || parameters.is_a?(ItParametersNode)
builder.block(
call,
token(block.opening_loc),
if parameters.nil?
builder.args(nil, [], nil, false)
elsif implicit_parameters
visit(parameters)
else
builder.args(
token(parameters.opening_loc),
if procarg0?(parameters.parameters)
parameter = parameters.parameters.requireds.first
visited = parameter.is_a?(RequiredParameterNode) ? visit(parameter) : parameter.accept(copy_compiler(in_destructure: true))
[builder.procarg0(visited)].concat(visit_all(parameters.locals))
else
visit(parameters)
end,
token(parameters.closing_loc),
false
)
end,
visit(block.body),
token(block.closing_loc)
)
else
call
end
end
# Visit a heredoc that can be either a string or an xstring.
def visit_heredoc(node)
children = Array.new
indented = false
# If this is a dedenting heredoc, then we need to insert the opening
# content into the children as well.
if node.opening.start_with?("<<~") && node.parts.length > 0 && !node.parts.first.is_a?(StringNode)
location = node.parts.first.location
location = location.copy(start_offset: location.start_offset - location.start_line_slice.bytesize)
children << builder.string_internal(token(location))
indented = true
end
node.parts.each do |part|
pushing =
if part.is_a?(StringNode) && part.content.include?("\n")
string_nodes_from_line_continuations(part.unescaped, part.content, part.location.start_offset, node.opening)
else
[visit(part)]
end
pushing.each do |child|
if child.type == :str && child.children.last == ""
# nothing
elsif child.type == :str && children.last && children.last.type == :str && !children.last.children.first.end_with?("\n")
appendee = children[-1]
location = appendee.loc
location = location.with_expression(location.expression.join(child.loc.expression))
children[-1] = appendee.updated(:str, ["#{appendee.children.first}#{child.children.first}"], location: location)
else
children << child
end
end
end
closing = node.closing
closing_t = [closing.chomp, srange_offsets(node.closing_loc.start_offset, node.closing_loc.end_offset - (closing[/\s+$/]&.length || 0))]
composed = yield children, closing_t
composed = composed.updated(nil, children[1..-1]) if indented
composed
end
# Visit a numeric node and account for the optional sign.
def visit_numeric(node, value)
if (slice = node.slice).match?(/^[+-]/)
builder.unary_num(
[slice[0].to_sym, srange_offsets(node.location.start_offset, node.location.start_offset + 1)],
value
)
else
value
end
end
# Within the given block, track that we're within a pattern.
def within_pattern
begin
parser.pattern_variables.push
yield copy_compiler(in_pattern: true)
ensure
parser.pattern_variables.pop
end
end
# When the content of a string node is split across multiple lines, the
# parser gem creates individual string nodes for each line the content is part of.
def string_nodes_from_interpolation(node, opening)
node.parts.flat_map do |part|
if part.type == :string_node && part.content.include?("\n") && part.opening_loc.nil?
string_nodes_from_line_continuations(part.unescaped, part.content, part.content_loc.start_offset, opening)
else
visit(part)
end
end
end
# Create parser string nodes from a single prism node. The parser gem
# "glues" strings together when a line continuation is encountered.
def string_nodes_from_line_continuations(unescaped, escaped, start_offset, opening)
unescaped = unescaped.lines
escaped = escaped.lines
percent_array = opening&.start_with?("%w", "%W", "%i", "%I")
regex = opening == "/" || opening&.start_with?("%r")
# Non-interpolating strings
if opening&.end_with?("'") || opening&.start_with?("%q", "%s", "%w", "%i")
current_length = 0
current_line = +""
escaped.filter_map.with_index do |escaped_line, index|
unescaped_line = unescaped.fetch(index, "")
current_length += escaped_line.bytesize
current_line << unescaped_line
# Glue line continuations together. Only %w and %i arrays can contain these.
if percent_array && escaped_line[/(\\)*\n$/, 1]&.length&.odd?
next unless index == escaped.count - 1
end
s = builder.string_internal([current_line, srange_offsets(start_offset, start_offset + current_length)])
start_offset += escaped_line.bytesize
current_line = +""
current_length = 0
s
end
else
escaped_lengths = []
normalized_lengths = []
# Keeps track of where an unescaped line should start a new token. An unescaped
# \n would otherwise be indistinguishable from the actual newline at the end of
# of the line. The parser gem only emits a new string node at "real" newlines,
# line continuations don't start a new node as well.
do_next_tokens = []
escaped
.chunk_while { |before, after| before[/(\\*)\r?\n$/, 1]&.length&.odd? || false }
.each do |lines|
escaped_lengths << lines.sum(&:bytesize)
unescaped_lines_count =
if regex
0 # Will always be preserved as is
else
lines.sum do |line|
count = line.scan(/(\\*)n/).count { |(backslashes)| backslashes&.length&.odd? }
count -= 1 if !line.end_with?("\n") && count > 0
count
end
end
extra = 1
extra = lines.count if percent_array # Account for line continuations in percent arrays
normalized_lengths.concat(Array.new(unescaped_lines_count + extra, 0))
normalized_lengths[-1] = lines.sum { |line| line.bytesize }
do_next_tokens.concat(Array.new(unescaped_lines_count + extra, false))
do_next_tokens[-1] = true
end
current_line = +""
current_normalized_length = 0
emitted_count = 0
unescaped.filter_map.with_index do |unescaped_line, index|
current_line << unescaped_line
current_normalized_length += normalized_lengths.fetch(index, 0)
if do_next_tokens[index]
inner_part = builder.string_internal([current_line, srange_offsets(start_offset, start_offset + current_normalized_length)])
start_offset += escaped_lengths.fetch(emitted_count, 0)
current_line = +""
current_normalized_length = 0
emitted_count += 1
inner_part
else
nil
end
end
end
end
end
end
end
end
PK ! Sʘ ʘ
prism_ruby.rbnu [ # frozen_string_literal: true
require 'prism'
require_relative 'ripper_state_lex'
# Unlike lib/rdoc/parser/ruby.rb, this file is not based on rtags and does not contain code from
# rtags.rb -
# ruby-lex.rb - ruby lexcal analyzer
# ruby-token.rb - ruby tokens
# Parse and collect document from Ruby source code.
# RDoc::Parser::PrismRuby is compatible with RDoc::Parser::Ruby and aims to replace it.
class RDoc::Parser::PrismRuby < RDoc::Parser
parse_files_matching(/\.rbw?$/) if ENV['RDOC_USE_PRISM_PARSER']
attr_accessor :visibility
attr_reader :container, :singleton
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
@module_nesting = [[top_level, false]]
@container = top_level
@visibility = :public
@singleton = false
@in_proc_block = false
end
# Suppress `extend` and `include` within block
# because they might be a metaprogramming block
# example: `Module.new { include M }` `M.module_eval { include N }`
def with_in_proc_block
@in_proc_block = true
yield
@in_proc_block = false
end
# Dive into another container
def with_container(container, singleton: false)
old_container = @container
old_visibility = @visibility
old_singleton = @singleton
old_in_proc_block = @in_proc_block
@visibility = :public
@container = container
@singleton = singleton
@in_proc_block = false
unless singleton
# Need to update module parent chain to emulate Module.nesting.
# This mechanism is inaccurate and needs to be fixed.
container.parent = old_container
end
@module_nesting.push([container, singleton])
yield container
ensure
@container = old_container
@visibility = old_visibility
@singleton = old_singleton
@in_proc_block = old_in_proc_block
@module_nesting.pop
end
# Records the location of this +container+ in the file for this parser and
# adds it to the list of classes and modules in the file.
def record_location(container) # :nodoc:
case container
when RDoc::ClassModule then
@top_level.add_to_classes_or_modules container
end
container.record_location @top_level
end
# Scans this Ruby file for Ruby constructs
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
def should_document?(code_object) # :nodoc:
return true unless @track_visibility
return false if code_object.parent&.document_children == false
code_object.document_self
end
# Assign AST node to a line.
# This is used to show meta-method source code in the documentation.
def prepare_line_nodes(node) # :nodoc:
case node
when Prism::CallNode, Prism::DefNode
@line_nodes[node.location.start_line] ||= node
end
node.compact_child_nodes.each do |child|
prepare_line_nodes(child)
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
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
# Creates an RDoc::Method on +container+ from +comment+ if there is a
# Signature section in the comment
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
def has_modifier_nodoc?(line_no) # :nodoc:
@modifier_comments[line_no]&.match?(/\A#\s*:nodoc:/)
end
def handle_modifier_directive(code_object, line_no) # :nodoc:
if (comment_text = @modifier_comments[line_no])
_text, directives = @preprocess.parse_comment(comment_text, line_no, :ruby)
handle_code_object_directives(code_object, directives)
end
end
def call_node_name_arguments(call_node) # :nodoc:
return [] unless call_node.arguments
call_node.arguments.arguments.map do |arg|
case arg
when Prism::SymbolNode
arg.value
when Prism::StringNode
arg.unescaped
end
end || []
end
# Handles meta method comments
def handle_meta_method_comment(comment, directives, node)
handle_code_object_directives(@container, directives)
is_call_node = node.is_a?(Prism::CallNode)
singleton_method = false
visibility = @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
if attributes
attributes.each do |attr|
a = RDoc::Attr.new(@container, attr, rw, comment, singleton: @singleton)
a.store = @store
a.line = line_no
record_location(a)
@container.add_attribute(a)
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,
@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
INVALID_GHOST_METHOD_ACCEPT_DIRECTIVE_LIST = %w[
method singleton-method attr attr_reader attr_writer attr_accessor
].freeze
private_constant :INVALID_GHOST_METHOD_ACCEPT_DIRECTIVE_LIST
def normal_comment_treat_as_ghost_method_for_now?(directives, line_no) # :nodoc:
# Meta method comment should start with `##` but some comments does not follow this rule.
# For now, RDoc accepts them as a meta method comment if there is no node linked to it.
!@line_nodes[line_no] && INVALID_GHOST_METHOD_ACCEPT_DIRECTIVE_LIST.any? { |directive| directives.has_key?(directive) }
end
def handle_standalone_consecutive_comment_directive(comment, directives, start_with_sharp_sharp, line_no, start_line) # :nodoc:
if start_with_sharp_sharp && start_line != @first_non_meta_comment_start_line
node = @line_nodes[line_no]
handle_meta_method_comment(comment, directives, node)
elsif normal_comment_treat_as_ghost_method_for_now?(directives, line_no) && start_line != @first_non_meta_comment_start_line
handle_meta_method_comment(comment, directives, nil)
else
handle_code_object_directives(@container, directives)
end
end
# Processes consecutive comments that were not linked to any documentable code until the given line number
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(@container, comment, line_no, start_line)
@preprocess.run_post_processes(comment, @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
# Skips all undocumentable consecutive comments until the given line number.
# Undocumentable comments are comments written inside `def` or inside undocumentable class/module
def skip_comments_until(line_no_until)
while !@unprocessed_comments.empty? && @unprocessed_comments.first[0] <= line_no_until
@unprocessed_comments.shift
end
end
# Returns consecutive comment linked to the given line number
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
# Parses comment text and retuns a pair of RDoc::Comment and directives
def parse_comment_text_to_directives(comment_text, start_line) # :nodoc:
comment_text, directives = @preprocess.parse_comment(comment_text, start_line, :ruby)
comment = RDoc::Comment.new(comment_text, @top_level, :ruby)
comment.normalized = true
comment.line = start_line
markup, = directives['markup']
comment.format = markup&.downcase || @markup
if (section, = directives['section'])
# If comment has :section:, it is not a documentable comment for a code object
@container.set_current_section(section, comment.dup)
return
end
@preprocess.run_post_processes(comment, @container)
[comment, directives]
end
def slice_tokens(start_pos, end_pos) # :nodoc:
start_index = @tokens.bsearch_index { |t| ([t.line_no, t.char_no] <=> start_pos) >= 0 }
end_index = @tokens.bsearch_index { |t| ([t.line_no, t.char_no] <=> end_pos) >= 0 }
tokens = @tokens[start_index...end_index]
tokens.pop if tokens.last&.kind == :on_nl
tokens
end
def file_line_comment_token(line_no) # :nodoc:
position_comment = RDoc::Parser::RipperStateLex::Token.new(line_no - 1, 0, :on_comment)
position_comment[:text] = "# File #{@top_level.relative_name}, line #{line_no}"
position_comment
end
# Returns tokens from the given location
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
# Handles `public :foo, :bar` `private :foo, :bar` and `protected :foo, :bar`
def change_method_visibility(names, visibility, singleton: @singleton)
new_methods = []
@container.methods_matching(names, singleton) do |m|
if m.parent != @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
@container.add_method(method)
when RDoc::Attr then
@container.add_attribute(method)
end
method.visibility = visibility
end
end
# Handles `module_function :foo, :bar`
def change_method_to_module_function(names)
@container.set_visibility_for(names, :private, false)
new_methods = []
@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
@container.add_method(method)
when RDoc::Attr then
@container.add_attribute(method)
end
method.visibility = :public
end
end
def handle_code_object_directives(code_object, directives) # :nodoc:
directives.each do |directive, (param)|
@preprocess.handle_directive('', directive, param, code_object)
end
end
# Handles `alias foo bar` and `alias_method :foo, :bar`
def add_alias_method(old_name, new_name, line_no)
comment, directives = consecutive_comment(line_no)
handle_code_object_directives(@container, directives) if directives
visibility = @container.find_method(old_name, @singleton)&.visibility || :public
a = RDoc::Alias.new(nil, old_name, new_name, comment, singleton: @singleton)
handle_modifier_directive(a, line_no)
a.store = @store
a.line = line_no
record_location(a)
if should_document?(a)
@container.add_alias(a)
@container.find_method(new_name, @singleton)&.visibility = visibility
end
end
# Handles `attr :a, :b`, `attr_reader :a, :b`, `attr_writer :a, :b` and `attr_accessor :a, :b`
def add_attributes(names, rw, line_no)
comment, directives = consecutive_comment(line_no)
handle_code_object_directives(@container, directives) if directives
return unless @container.document_children
names.each do |symbol|
a = RDoc::Attr.new(nil, symbol.to_s, rw, comment, singleton: @singleton)
a.store = @store
a.line = line_no
record_location(a)
handle_modifier_directive(a, line_no)
@container.add_attribute(a) if should_document?(a)
a.visibility = visibility # should set after adding to container
end
end
def add_includes_extends(names, rdoc_class, line_no) # :nodoc:
return if @in_proc_block
comment, directives = consecutive_comment(line_no)
handle_code_object_directives(@container, directives) if directives
names.each do |name|
ie = @container.add(rdoc_class, name, '')
ie.store = @store
ie.line = line_no
ie.comment = comment
record_location(ie)
end
end
# Handle `include Foo, Bar`
def add_includes(names, line_no) # :nodoc:
add_includes_extends(names, RDoc::Include, line_no)
end
# Handle `extend Foo, Bar`
def add_extends(names, line_no) # :nodoc:
add_includes_extends(names, RDoc::Extend, line_no)
end
# Adds a method defined by `def` syntax
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:)
return if @in_proc_block
receiver = receiver_name ? find_or_create_module_path(receiver_name, receiver_fallback_type) : @container
comment, directives = consecutive_comment(start_line)
handle_code_object_directives(@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
private def internal_add_method(method_name, container, comment:, dont_rename_initialize: false, directives:, modifier_comment_lines: nil, line_no:, visibility:, singleton:, params:, calls_super:, block_params:, tokens:) # :nodoc:
meth = RDoc::AnyMethod.new(nil, method_name, singleton: singleton)
meth.comment = comment
handle_code_object_directives(meth, directives) if directives
modifier_comment_lines&.each do |line|
handle_modifier_directive(meth, line)
end
return unless should_document?(meth)
if directives && (call_seq, = directives['call-seq'])
meth.call_seq = call_seq.lines.map(&:chomp).reject(&:empty?).join("\n") if call_seq
end
meth.name ||= meth.call_seq[/\A[^()\s]+/] if meth.call_seq
meth.name ||= 'unknown'
meth.store = @store
meth.line = line_no
container.add_method(meth) # should add after setting singleton and before setting visibility
meth.visibility = visibility
meth.params ||= params || '()'
meth.calls_super = calls_super
meth.block_params ||= block_params if block_params
record_location(meth)
meth.start_collecting_tokens(:ruby)
tokens.each do |token|
meth.token_stream << token
end
# Rename after add_method to register duplicated 'new' and 'initialize'
# defined in c and ruby just like the old parser did.
if !dont_rename_initialize && method_name == 'initialize' && !singleton
if meth.dont_rename_initialize
meth.visibility = :protected
else
meth.name = 'new'
meth.singleton = true
meth.visibility = :public
end
end
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.
def find_or_create_module_path(module_name, create_mode)
root_name, *path, name = module_name.split('::')
add_module = ->(mod, name, mode) {
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
}
if root_name.empty?
mod = @top_level
else
@module_nesting.reverse_each do |nesting, singleton|
next if singleton
mod = nesting.find_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.find_constant_named(root_name)
end
last_nesting, = @module_nesting.reverse_each.find { |_, singleton| !singleton }
return mod || add_module.call(last_nesting, root_name, create_mode) unless name
mod ||= add_module.call(last_nesting, root_name, :module)
end
path.each do |name|
mod = mod.find_module_named(name) || add_module.call(mod, name, :module)
end
mod.find_module_named(name) || add_module.call(mod, name, create_mode)
end
# Resolves constant path to a full path by searching module nesting
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
@module_nesting.reverse_each do |nesting, singleton|
next if singleton
mod = nesting.find_module_named(owner_name)
break if mod
end
mod ||= @top_level.find_module_named(owner_name)
[mod.full_name, path].compact.join('::') if mod
end
# Returns a pair of owner module and constant name from a given constant path.
# Creates owner module if it does not exist.
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 <