Skip to content

Commit 6c00877

Browse files
committed
Support looking up global variables and predefined constants in ri
Add the ability to look up documentation for global variables (e.g., $<, $LOAD_PATH) and predefined constants (e.g., STDIN, ARGV, RUBY_VERSION) directly through the ri command. Documentation is extracted from the globals.rdoc page in the system store. Changes: - Add display_global method to look up and display global documentation - Add extract_global_section to parse hierarchical headings in globals.rdoc - Add predefined_global_constant? to identify STDIN, ARGV, RUBY_* etc. - Update display_name to handle $-prefixed names and predefined constants - Update expand_name to skip expansion for globals - Fix nil matches bug in error handling when unknown globals are queried - Update help text with examples for global variable lookups
1 parent c55c1f5 commit 6c00877

File tree

2 files changed

+280
-2
lines changed

2 files changed

+280
-2
lines changed

lib/rdoc/ri/driver.rb

Lines changed: 139 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,8 @@ def self.process_args(argv)
125125
126126
Class::method | Class#method | Class.method | method
127127
128+
$global_variable | PREDEFINED_CONSTANT
129+
128130
gem_name: | gem_name:README | gem_name:History
129131
130132
ruby: | ruby:NEWS | ruby:globals
@@ -152,9 +154,12 @@ def self.process_args(argv)
152154
#{opt.program_name} zip
153155
#{opt.program_name} rdoc:README
154156
#{opt.program_name} ruby:comments
157+
#{opt.program_name} ARGF
158+
#{opt.program_name} '$<'
159+
#{opt.program_name} '$LOAD_PATH'
155160
156161
Note that shell quoting or escaping may be required for method names
157-
containing punctuation:
162+
containing punctuation or for global variables:
158163
159164
#{opt.program_name} 'Array.[]'
160165
#{opt.program_name} compact\\!
@@ -843,13 +848,126 @@ def display_method(name)
843848
display out
844849
end
845850

851+
##
852+
# Pre-defined global constants that can be looked up from globals.rdoc
853+
854+
PREDEFINED_GLOBAL_CONSTANTS = %w[
855+
STDIN STDOUT STDERR ARGV ARGF DATA TOPLEVEL_BINDING
856+
].freeze
857+
858+
##
859+
# Prefixes for pre-defined global constants
860+
861+
PREDEFINED_GLOBAL_CONSTANT_PREFIXES = %w[RUBY_].freeze
862+
863+
##
864+
# Returns true if +name+ is a pre-defined global constant like STDIN, STDOUT,
865+
# RUBY_VERSION, etc.
866+
867+
def predefined_global_constant?(name)
868+
PREDEFINED_GLOBAL_CONSTANTS.include?(name) ||
869+
PREDEFINED_GLOBAL_CONSTANT_PREFIXES.any? { |prefix| name.start_with?(prefix) }
870+
end
871+
872+
##
873+
# Outputs formatted RI data for the global variable or pre-defined constant
874+
# +name+. Looks up the documentation in the globals.rdoc page from the system
875+
# store.
876+
877+
def display_global(name)
878+
store = @stores.find { |s| s.type == :system }
879+
880+
raise NotFoundError, name unless store
881+
882+
begin
883+
page = store.load_page('globals.rdoc')
884+
rescue RDoc::Store::MissingFileError
885+
raise NotFoundError, name
886+
end
887+
888+
document = page.comment.parse
889+
section = extract_global_section(document, name)
890+
891+
raise NotFoundError, name unless section
892+
893+
display section
894+
895+
true
896+
end
897+
898+
##
899+
# Extracts the section for global +name+ from +document+.
900+
# Returns an RDoc::Markup::Document containing just that section,
901+
# or nil if not found.
902+
#
903+
# The globals.rdoc document has a hierarchical structure with headings:
904+
# = Pre-Defined Global Variables (level 1)
905+
# == Streams (level 2)
906+
# === $< (ARGF or $stdin) (level 3)
907+
# paragraph content...
908+
# === $> (Default Output) (level 3)
909+
# paragraph content...
910+
#
911+
# This method finds the heading matching +name+ and collects all content
912+
# until the next heading at the same or higher level.
913+
914+
def extract_global_section(document, name)
915+
result = RDoc::Markup::Document.new
916+
in_section = false # true once we find the matching heading
917+
section_level = nil # heading level of the matched section (e.g., 3 for ===)
918+
919+
document.parts.each do |part|
920+
if RDoc::Markup::Heading === part
921+
if heading_matches_global?(part, name)
922+
# Found our target heading - start capturing content
923+
in_section = true
924+
section_level = part.level
925+
result << part
926+
elsif in_section && part.level <= section_level
927+
# Hit next section at same or higher level - stop capturing
928+
break
929+
elsif in_section
930+
# Sub-heading within our section - include it
931+
result << part
932+
end
933+
elsif in_section
934+
# Non-heading content (paragraphs, code blocks, etc.) - include it
935+
result << part
936+
end
937+
end
938+
939+
result.empty? ? nil : result
940+
end
941+
942+
##
943+
# Returns true if +heading+ matches the global +name+.
944+
# Handles formats like "$< (ARGF or $stdin)", "<tt>$<</tt> (ARGF...)", or just "STDOUT".
945+
946+
def heading_matches_global?(heading, name)
947+
text = heading.text
948+
949+
# Direct match: "STDOUT" or "$<"
950+
return true if text == name
951+
return true if text.start_with?("#{name} ") || text.start_with?("#{name}\t")
952+
953+
# Match with <tt> wrapper: "<tt>$<</tt> (description)"
954+
tt_wrapped = "<tt>#{name}</tt>"
955+
return true if text.start_with?(tt_wrapped)
956+
return true if text.start_with?("#{tt_wrapped} ")
957+
958+
false
959+
end
960+
846961
##
847962
# Outputs formatted RI data for the class or method +name+.
848963
#
849964
# Returns true if +name+ was found, false if it was not an alternative could
850965
# be guessed, raises an error if +name+ couldn't be guessed.
851966

852967
def display_name(name)
968+
# Handle global variables immediately (classes can't start with $)
969+
return display_global(name) if name.start_with?('$')
970+
853971
if name =~ /\w:(\w|$)/ then
854972
display_page name
855973
return true
@@ -859,10 +977,23 @@ def display_name(name)
859977

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

980+
# If no class was found and it's a predefined constant, try globals lookup
981+
# This handles ARGV, STDIN, etc. that look like class names but aren't
982+
return display_global(name) if predefined_global_constant?(name)
983+
862984
true
863985
rescue NotFoundError
986+
# Before giving up, check if it's a predefined global constant
987+
if predefined_global_constant?(name)
988+
begin
989+
return display_global(name)
990+
rescue NotFoundError
991+
# Fall through to original error handling
992+
end
993+
end
994+
864995
matches = list_methods_matching name if name =~ /::|#|\./
865-
matches = classes.keys.grep(/^#{Regexp.escape name}/) if matches.empty?
996+
matches = classes.keys.grep(/^#{Regexp.escape name}/) if matches.nil? || matches.empty?
866997

867998
raise if matches.empty?
868999

@@ -983,6 +1114,12 @@ def expand_class(klass)
9831114
# #expand_class.
9841115

9851116
def expand_name(name)
1117+
# Global variables don't need expansion
1118+
return name if name.start_with?('$')
1119+
1120+
# Predefined global constants don't need expansion
1121+
return name if predefined_global_constant?(name)
1122+
9861123
klass, selector, method = parse_name name
9871124

9881125
return [selector, method].join if klass.empty?

test/rdoc/ri/driver_test.rb

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -951,6 +951,147 @@ def test_display_page_list
951951
assert_match %r%OTHER\.rdoc%, out
952952
end
953953

954+
def test_display_global_variable
955+
util_store
956+
957+
# Create a globals page in the store
958+
globals = @store1.add_file 'globals.rdoc'
959+
globals.parser = RDoc::Parser::Simple
960+
globals.comment = RDoc::Comment.from_document(doc(
961+
head(1, 'Pre-Defined Global Variables'),
962+
head(2, 'Streams'),
963+
head(3, '$< (ARGF or $stdin)'),
964+
para('Points to stream ARGF if not empty, else to stream $stdin; read-only.'),
965+
head(3, '$> (Default Standard Output)'),
966+
para('An output stream, initially $stdout.')
967+
))
968+
@store1.save_page globals
969+
@store1.type = :system
970+
971+
out, = capture_output do
972+
@driver.display_global '$<'
973+
end
974+
975+
assert_match %r%\$< \(ARGF or \$stdin\)%, out
976+
assert_match %r%Points to stream ARGF%, out
977+
refute_match %r%\$>%, out
978+
end
979+
980+
def test_display_global_constant
981+
util_store
982+
983+
# Create a globals page in the store
984+
globals = @store1.add_file 'globals.rdoc'
985+
globals.parser = RDoc::Parser::Simple
986+
globals.comment = RDoc::Comment.from_document(doc(
987+
head(1, 'Pre-Defined Global Constants'),
988+
head(2, 'Streams'),
989+
head(3, 'STDIN'),
990+
para('The standard input stream.'),
991+
head(3, 'STDOUT'),
992+
para('The standard output stream.')
993+
))
994+
@store1.save_page globals
995+
@store1.type = :system
996+
997+
out, = capture_output do
998+
@driver.display_global 'STDOUT'
999+
end
1000+
1001+
assert_match %r%STDOUT%, out
1002+
assert_match %r%standard output stream%, out
1003+
refute_match %r%STDIN%, out
1004+
end
1005+
1006+
def test_display_global_not_found
1007+
util_store
1008+
1009+
# Create a globals page in the store (without the requested global)
1010+
globals = @store1.add_file 'globals.rdoc'
1011+
globals.parser = RDoc::Parser::Simple
1012+
globals.comment = RDoc::Comment.from_document(doc(
1013+
head(1, 'Pre-Defined Global Variables'),
1014+
head(3, '$<'),
1015+
para('Some doc')
1016+
))
1017+
@store1.save_page globals
1018+
@store1.type = :system
1019+
1020+
assert_raise RDoc::RI::Driver::NotFoundError do
1021+
@driver.display_global '$NONEXISTENT'
1022+
end
1023+
end
1024+
1025+
def test_display_global_no_system_store
1026+
util_store
1027+
# Store type is :home by default, not :system
1028+
1029+
assert_raise RDoc::RI::Driver::NotFoundError do
1030+
@driver.display_global '$<'
1031+
end
1032+
end
1033+
1034+
def test_display_name_global_variable
1035+
util_store
1036+
1037+
# Create a globals page in the store
1038+
globals = @store1.add_file 'globals.rdoc'
1039+
globals.parser = RDoc::Parser::Simple
1040+
globals.comment = RDoc::Comment.from_document(doc(
1041+
head(1, 'Pre-Defined Global Variables'),
1042+
head(3, '$< (ARGF or $stdin)'),
1043+
para('Points to stream ARGF.')
1044+
))
1045+
@store1.save_page globals
1046+
@store1.type = :system
1047+
1048+
out, = capture_output do
1049+
@driver.display_name '$<'
1050+
end
1051+
1052+
assert_match %r%\$<%, out
1053+
assert_match %r%ARGF%, out
1054+
end
1055+
1056+
def test_display_name_predefined_constant
1057+
util_store
1058+
1059+
# Create a globals page in the store
1060+
globals = @store1.add_file 'globals.rdoc'
1061+
globals.parser = RDoc::Parser::Simple
1062+
globals.comment = RDoc::Comment.from_document(doc(
1063+
head(1, 'Pre-Defined Global Constants'),
1064+
head(3, 'ARGV'),
1065+
para('An array of the given command-line arguments.')
1066+
))
1067+
@store1.save_page globals
1068+
@store1.type = :system
1069+
1070+
out, = capture_output do
1071+
@driver.display_name 'ARGV'
1072+
end
1073+
1074+
assert_match %r%ARGV%, out
1075+
assert_match %r%command-line arguments%, out
1076+
end
1077+
1078+
def test_predefined_global_constant?
1079+
assert @driver.predefined_global_constant?('STDIN')
1080+
assert @driver.predefined_global_constant?('STDOUT')
1081+
assert @driver.predefined_global_constant?('STDERR')
1082+
assert @driver.predefined_global_constant?('ARGV')
1083+
assert @driver.predefined_global_constant?('ARGF')
1084+
assert @driver.predefined_global_constant?('DATA')
1085+
assert @driver.predefined_global_constant?('TOPLEVEL_BINDING')
1086+
assert @driver.predefined_global_constant?('RUBY_VERSION')
1087+
assert @driver.predefined_global_constant?('RUBY_PLATFORM')
1088+
1089+
refute @driver.predefined_global_constant?('ENV') # ENV is a class, not a simple constant
1090+
refute @driver.predefined_global_constant?('MyClass')
1091+
refute @driver.predefined_global_constant?('Foo')
1092+
refute @driver.predefined_global_constant?('$<')
1093+
end
1094+
9541095
def test_expand_class
9551096
util_store
9561097

0 commit comments

Comments
 (0)