Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
141 changes: 139 additions & 2 deletions lib/rdoc/ri/driver.rb
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,8 @@ def self.process_args(argv)

Class::method | Class#method | Class.method | method

$global_variable | PREDEFINED_CONSTANT

gem_name: | gem_name:README | gem_name:History

ruby: | ruby:NEWS | ruby:globals
Expand Down Expand Up @@ -152,9 +154,12 @@ def self.process_args(argv)
#{opt.program_name} zip
#{opt.program_name} rdoc:README
#{opt.program_name} ruby:comments
#{opt.program_name} ARGF
#{opt.program_name} '$<'
#{opt.program_name} '$LOAD_PATH'

Note that shell quoting or escaping may be required for method names
containing punctuation:
containing punctuation or for global variables:

#{opt.program_name} 'Array.[]'
#{opt.program_name} compact\\!
Expand Down Expand Up @@ -843,13 +848,126 @@ def display_method(name)
display out
end

##
# Pre-defined global constants that can be looked up from globals.rdoc

PREDEFINED_GLOBAL_CONSTANTS = %w[
STDIN STDOUT STDERR ARGV ARGF DATA TOPLEVEL_BINDING
].freeze

##
# Prefixes for pre-defined global constants

PREDEFINED_GLOBAL_CONSTANT_PREFIXES = %w[RUBY_].freeze

##
# Returns true if +name+ is a pre-defined global constant like STDIN, STDOUT,
# RUBY_VERSION, etc.

def predefined_global_constant?(name)
PREDEFINED_GLOBAL_CONSTANTS.include?(name) ||
PREDEFINED_GLOBAL_CONSTANT_PREFIXES.any? { |prefix| name.start_with?(prefix) }
end

##
# Outputs formatted RI data for the global variable or pre-defined constant
# +name+. Looks up the documentation in the globals.rdoc page from the system
# store.

def display_global(name)
store = @stores.find { |s| s.type == :system }

raise NotFoundError, name unless store

begin
page = store.load_page('globals.rdoc')
rescue RDoc::Store::MissingFileError
raise NotFoundError, name
end

document = page.comment.parse
section = extract_global_section(document, name)

raise NotFoundError, name unless section

display section

true
end

##
# Extracts the section for global +name+ from +document+.
# Returns an RDoc::Markup::Document containing just that section,
# or nil if not found.
#
# The globals.rdoc document has a hierarchical structure with headings:
# = Pre-Defined Global Variables (level 1)
# == Streams (level 2)
# === $< (ARGF or $stdin) (level 3)
# paragraph content...
# === $> (Default Output) (level 3)
# paragraph content...
#
# This method finds the heading matching +name+ and collects all content
# until the next heading at the same or higher level.

def extract_global_section(document, name)
result = RDoc::Markup::Document.new
in_section = false # true once we find the matching heading
section_level = nil # heading level of the matched section (e.g., 3 for ===)

document.parts.each do |part|
if RDoc::Markup::Heading === part
if heading_matches_global?(part, name)
# Found our target heading - start capturing content
in_section = true
section_level = part.level
result << part
elsif in_section && part.level <= section_level
# Hit next section at same or higher level - stop capturing
break
elsif in_section
# Sub-heading within our section - include it
result << part
end
elsif in_section
# Non-heading content (paragraphs, code blocks, etc.) - include it
result << part
end
end

result.empty? ? nil : result
end

##
# Returns true if +heading+ matches the global +name+.
# Handles formats like "$< (ARGF or $stdin)", "<tt>$<</tt> (ARGF...)", or just "STDOUT".

def heading_matches_global?(heading, name)
text = heading.text

# Direct match: "STDOUT" or "$<"
return true if text == name
return true if text.start_with?("#{name} ") || text.start_with?("#{name}\t")

# Match with <tt> wrapper: "<tt>$<</tt> (description)"
tt_wrapped = "<tt>#{name}</tt>"
return true if text.start_with?(tt_wrapped)
return true if text.start_with?("#{tt_wrapped} ")

false
end

##
# Outputs formatted RI data for the class or method +name+.
#
# Returns true if +name+ was found, false if it was not an alternative could
# be guessed, raises an error if +name+ couldn't be guessed.

def display_name(name)
# Handle global variables immediately (classes can't start with $)
return display_global(name) if name.start_with?('$')

if name =~ /\w:(\w|$)/ then
display_page name
return true
Expand All @@ -859,10 +977,23 @@ def display_name(name)

display_method name if name =~ /::|#|\./

# If no class was found and it's a predefined constant, try globals lookup
# This handles ARGV, STDIN, etc. that look like class names but aren't
return display_global(name) if predefined_global_constant?(name)

true
rescue NotFoundError
# Before giving up, check if it's a predefined global constant
if predefined_global_constant?(name)
begin
return display_global(name)
rescue NotFoundError
# Fall through to original error handling
end
end

matches = list_methods_matching name if name =~ /::|#|\./
matches = classes.keys.grep(/^#{Regexp.escape name}/) if matches.empty?
matches = classes.keys.grep(/^#{Regexp.escape name}/) if matches.nil? || matches.empty?

raise if matches.empty?

Expand Down Expand Up @@ -983,6 +1114,12 @@ def expand_class(klass)
# #expand_class.

def expand_name(name)
# Global variables don't need expansion
return name if name.start_with?('$')

# Predefined global constants don't need expansion
return name if predefined_global_constant?(name)

klass, selector, method = parse_name name

return [selector, method].join if klass.empty?
Expand Down
141 changes: 141 additions & 0 deletions test/rdoc/ri/driver_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -951,6 +951,147 @@ def test_display_page_list
assert_match %r%OTHER\.rdoc%, out
end

def test_display_global_variable
util_store

# Create a globals page in the store
globals = @store1.add_file 'globals.rdoc'
globals.parser = RDoc::Parser::Simple
globals.comment = RDoc::Comment.from_document(doc(
head(1, 'Pre-Defined Global Variables'),
head(2, 'Streams'),
head(3, '$< (ARGF or $stdin)'),
para('Points to stream ARGF if not empty, else to stream $stdin; read-only.'),
head(3, '$> (Default Standard Output)'),
para('An output stream, initially $stdout.')
))
@store1.save_page globals
@store1.type = :system

out, = capture_output do
@driver.display_global '$<'
end

assert_match %r%\$< \(ARGF or \$stdin\)%, out
assert_match %r%Points to stream ARGF%, out
refute_match %r%\$>%, out
end

def test_display_global_constant
util_store

# Create a globals page in the store
globals = @store1.add_file 'globals.rdoc'
globals.parser = RDoc::Parser::Simple
globals.comment = RDoc::Comment.from_document(doc(
head(1, 'Pre-Defined Global Constants'),
head(2, 'Streams'),
head(3, 'STDIN'),
para('The standard input stream.'),
head(3, 'STDOUT'),
para('The standard output stream.')
))
@store1.save_page globals
@store1.type = :system

out, = capture_output do
@driver.display_global 'STDOUT'
end

assert_match %r%STDOUT%, out
assert_match %r%standard output stream%, out
refute_match %r%STDIN%, out
end

def test_display_global_not_found
util_store

# Create a globals page in the store (without the requested global)
globals = @store1.add_file 'globals.rdoc'
globals.parser = RDoc::Parser::Simple
globals.comment = RDoc::Comment.from_document(doc(
head(1, 'Pre-Defined Global Variables'),
head(3, '$<'),
para('Some doc')
))
@store1.save_page globals
@store1.type = :system

assert_raise RDoc::RI::Driver::NotFoundError do
@driver.display_global '$NONEXISTENT'
end
end

def test_display_global_no_system_store
util_store
# Store type is :home by default, not :system

assert_raise RDoc::RI::Driver::NotFoundError do
@driver.display_global '$<'
end
end

def test_display_name_global_variable
util_store

# Create a globals page in the store
globals = @store1.add_file 'globals.rdoc'
globals.parser = RDoc::Parser::Simple
globals.comment = RDoc::Comment.from_document(doc(
head(1, 'Pre-Defined Global Variables'),
head(3, '$< (ARGF or $stdin)'),
para('Points to stream ARGF.')
))
@store1.save_page globals
@store1.type = :system

out, = capture_output do
@driver.display_name '$<'
end

assert_match %r%\$<%, out
assert_match %r%ARGF%, out
end

def test_display_name_predefined_constant
util_store

# Create a globals page in the store
globals = @store1.add_file 'globals.rdoc'
globals.parser = RDoc::Parser::Simple
globals.comment = RDoc::Comment.from_document(doc(
head(1, 'Pre-Defined Global Constants'),
head(3, 'ARGV'),
para('An array of the given command-line arguments.')
))
@store1.save_page globals
@store1.type = :system

out, = capture_output do
@driver.display_name 'ARGV'
end

assert_match %r%ARGV%, out
assert_match %r%command-line arguments%, out
end

def test_predefined_global_constant?
assert @driver.predefined_global_constant?('STDIN')
assert @driver.predefined_global_constant?('STDOUT')
assert @driver.predefined_global_constant?('STDERR')
assert @driver.predefined_global_constant?('ARGV')
assert @driver.predefined_global_constant?('ARGF')
assert @driver.predefined_global_constant?('DATA')
assert @driver.predefined_global_constant?('TOPLEVEL_BINDING')
assert @driver.predefined_global_constant?('RUBY_VERSION')
assert @driver.predefined_global_constant?('RUBY_PLATFORM')

refute @driver.predefined_global_constant?('ENV') # ENV is a class, not a simple constant
refute @driver.predefined_global_constant?('MyClass')
refute @driver.predefined_global_constant?('Foo')
refute @driver.predefined_global_constant?('$<')
end

def test_expand_class
util_store

Expand Down