[DOC] Tweaks for array indexes
[ruby.git] / lib / rdoc / rdoc.rb
blob88ae55b4093f53bdd57565a05107e66849d50f22
1 # frozen_string_literal: true
2 require_relative '../rdoc'
4 require 'find'
5 require 'fileutils'
6 require 'pathname'
7 require 'time'
9 ##
10 # This is the driver for generating RDoc output.  It handles file parsing and
11 # generation of output.
13 # To use this class to generate RDoc output via the API, the recommended way
14 # is:
16 #   rdoc = RDoc::RDoc.new
17 #   options = RDoc::Options.load_options # returns an RDoc::Options instance
18 #   # set extra options
19 #   rdoc.document options
21 # You can also generate output like the +rdoc+ executable:
23 #   rdoc = RDoc::RDoc.new
24 #   rdoc.document argv
26 # Where +argv+ is an array of strings, each corresponding to an argument you'd
27 # give rdoc on the command line.  See <tt>rdoc --help</tt> for details.
29 class RDoc::RDoc
31   @current = nil
33   ##
34   # This is the list of supported output generators
36   GENERATORS = {}
38   ##
39   # List of directory names always skipped
41   UNCONDITIONALLY_SKIPPED_DIRECTORIES = %w[CVS .svn .git].freeze
43   ##
44   # List of directory names skipped if test suites should be skipped
46   TEST_SUITE_DIRECTORY_NAMES = %w[spec test].freeze
49   ##
50   # Generator instance used for creating output
52   attr_accessor :generator
54   ##
55   # Hash of files and their last modified times.
57   attr_reader :last_modified
59   ##
60   # RDoc options
62   attr_accessor :options
64   ##
65   # Accessor for statistics.  Available after each call to parse_files
67   attr_reader :stats
69   ##
70   # The current documentation store
72   attr_reader :store
74   ##
75   # Add +klass+ that can generate output after parsing
77   def self.add_generator(klass)
78     name = klass.name.sub(/^RDoc::Generator::/, '').downcase
79     GENERATORS[name] = klass
80   end
82   ##
83   # Active RDoc::RDoc instance
85   def self.current
86     @current
87   end
89   ##
90   # Sets the active RDoc::RDoc instance
92   def self.current= rdoc
93     @current = rdoc
94   end
96   ##
97   # Creates a new RDoc::RDoc instance.  Call #document to parse files and
98   # generate documentation.
100   def initialize
101     @current       = nil
102     @generator     = nil
103     @last_modified = {}
104     @old_siginfo   = nil
105     @options       = nil
106     @stats         = nil
107     @store         = nil
108   end
110   ##
111   # Report an error message and exit
113   def error(msg)
114     raise RDoc::Error, msg
115   end
117   ##
118   # Gathers a set of parseable files from the files and directories listed in
119   # +files+.
121   def gather_files files
122     files = [@options.root.to_s] if files.empty?
124     file_list = normalized_file_list files, true, @options.exclude
126     file_list = remove_unparseable(file_list)
128     if file_list.count {|name, mtime|
129          file_list[name] = @last_modified[name] unless mtime
130          mtime
131        } > 0
132       @last_modified.replace file_list
133       file_list.keys.sort
134     else
135       []
136     end
137   end
139   ##
140   # Turns RDoc from stdin into HTML
142   def handle_pipe
143     @html = RDoc::Markup::ToHtml.new @options
145     parser = RDoc::Text::MARKUP_FORMAT[@options.markup]
147     document = parser.parse $stdin.read
149     out = @html.convert document
151     $stdout.write out
152   end
154   ##
155   # Installs a siginfo handler that prints the current filename.
157   def install_siginfo_handler
158     return unless Signal.list.include? 'INFO'
160     @old_siginfo = trap 'INFO' do
161       puts @current if @current
162     end
163   end
165   ##
166   # Create an output dir if it doesn't exist. If it does exist, but doesn't
167   # contain the flag file <tt>created.rid</tt> then we refuse to use it, as
168   # we may clobber some manually generated documentation
170   def setup_output_dir(dir, force)
171     flag_file = output_flag_file dir
173     last = {}
175     if @options.dry_run then
176       # do nothing
177     elsif File.exist? dir then
178       error "#{dir} exists and is not a directory" unless File.directory? dir
180       begin
181         File.open flag_file do |io|
182           unless force then
183             Time.parse io.gets
185             io.each do |line|
186               file, time = line.split "\t", 2
187               time = Time.parse(time) rescue next
188               last[file] = time
189             end
190           end
191         end
192       rescue SystemCallError, TypeError
193         error <<-ERROR
195 Directory #{dir} already exists, but it looks like it isn't an RDoc directory.
197 Because RDoc doesn't want to risk destroying any of your existing files,
198 you'll need to specify a different output directory name (using the --op <dir>
199 option)
201         ERROR
202       end unless @options.force_output
203     else
204       FileUtils.mkdir_p dir
205       FileUtils.touch flag_file
206     end
208     last
209   end
211   ##
212   # Sets the current documentation tree to +store+ and sets the store's rdoc
213   # driver to this instance.
215   def store= store
216     @store = store
217     @store.rdoc = self
218   end
220   ##
221   # Update the flag file in an output directory.
223   def update_output_dir(op_dir, time, last = {})
224     return if @options.dry_run or not @options.update_output_dir
225     unless ENV['SOURCE_DATE_EPOCH'].nil?
226       time = Time.at(ENV['SOURCE_DATE_EPOCH'].to_i).gmtime
227     end
229     File.open output_flag_file(op_dir), "w" do |f|
230       f.puts time.rfc2822
231       last.each do |n, t|
232         f.puts "#{n}\t#{t.rfc2822}"
233       end
234     end
235   end
237   ##
238   # Return the path name of the flag file in an output directory.
240   def output_flag_file(op_dir)
241     File.join op_dir, "created.rid"
242   end
244   ##
245   # The .document file contains a list of file and directory name patterns,
246   # representing candidates for documentation. It may also contain comments
247   # (starting with '#')
249   def parse_dot_doc_file in_dir, filename
250     # read and strip comments
251     patterns = File.read(filename).gsub(/#.*/, '')
253     result = {}
255     patterns.split(' ').each do |patt|
256       candidates = Dir.glob(File.join(in_dir, patt))
257       result.update normalized_file_list(candidates, false, @options.exclude)
258     end
260     result
261   end
263   ##
264   # Given a list of files and directories, create a list of all the Ruby
265   # files they contain.
266   #
267   # If +force_doc+ is true we always add the given files, if false, only
268   # add files that we guarantee we can parse.  It is true when looking at
269   # files given on the command line, false when recursing through
270   # subdirectories.
271   #
272   # The effect of this is that if you want a file with a non-standard
273   # extension parsed, you must name it explicitly.
275   def normalized_file_list(relative_files, force_doc = false,
276                            exclude_pattern = nil)
277     file_list = {}
279     relative_files.each do |rel_file_name|
280       rel_file_name = rel_file_name.sub(/^\.\//, '')
281       next if rel_file_name.end_with? 'created.rid'
282       next if exclude_pattern && exclude_pattern =~ rel_file_name
283       stat = File.stat rel_file_name rescue next
285       case type = stat.ftype
286       when "file" then
287         mtime = (stat.mtime unless (last_modified = @last_modified[rel_file_name] and
288                                     stat.mtime.to_i <= last_modified.to_i))
290         if force_doc or RDoc::Parser.can_parse(rel_file_name) then
291           file_list[rel_file_name] = mtime
292         end
293       when "directory" then
294         next if UNCONDITIONALLY_SKIPPED_DIRECTORIES.include?(rel_file_name)
296         basename = File.basename(rel_file_name)
297         next if options.skip_tests && TEST_SUITE_DIRECTORY_NAMES.include?(basename)
299         created_rid = File.join rel_file_name, "created.rid"
300         next if File.file? created_rid
302         dot_doc = File.join rel_file_name, RDoc::DOT_DOC_FILENAME
304         if File.file? dot_doc then
305           file_list.update(parse_dot_doc_file(rel_file_name, dot_doc))
306         else
307           file_list.update(list_files_in_directory(rel_file_name))
308         end
309       else
310         warn "rdoc can't parse the #{type} #{rel_file_name}"
311       end
312     end
314     file_list
315   end
317   ##
318   # Return a list of the files to be processed in a directory. We know that
319   # this directory doesn't have a .document file, so we're looking for real
320   # files. However we may well contain subdirectories which must be tested
321   # for .document files.
323   def list_files_in_directory dir
324     files = Dir.glob File.join(dir, "*")
326     normalized_file_list files, false, @options.exclude
327   end
329   ##
330   # Parses +filename+ and returns an RDoc::TopLevel
332   def parse_file filename
333     encoding = @options.encoding
334     filename = filename.encode encoding
336     @stats.add_file filename
338     return if RDoc::Parser.binary? filename
340     content = RDoc::Encoding.read_file filename, encoding
342     return unless content
344     filename_path = Pathname(filename).expand_path
345     begin
346       relative_path = filename_path.relative_path_from @options.root
347     rescue ArgumentError
348       relative_path = filename_path
349     end
351     if @options.page_dir and
352        relative_path.to_s.start_with? @options.page_dir.to_s then
353       relative_path =
354         relative_path.relative_path_from @options.page_dir
355     end
357     top_level = @store.add_file filename, relative_name: relative_path.to_s
359     parser = RDoc::Parser.for top_level, content, @options, @stats
361     return unless parser
363     parser.scan
365     # restart documentation for the classes & modules found
366     top_level.classes_or_modules.each do |cm|
367       cm.done_documenting = false
368     end
370     top_level
372   rescue Errno::EACCES => e
373     $stderr.puts <<-EOF
374 Unable to read #{filename}, #{e.message}
376 Please check the permissions for this file.  Perhaps you do not have access to
377 it or perhaps the original author's permissions are to restrictive.  If the
378 this is not your library please report a bug to the author.
379     EOF
380   rescue => e
381     $stderr.puts <<-EOF
382 Before reporting this, could you check that the file you're documenting
383 has proper syntax:
385   #{Gem.ruby} -c #{filename}
387 RDoc is not a full Ruby parser and will fail when fed invalid ruby programs.
389 The internal error was:
391 \t(#{e.class}) #{e.message}
393     EOF
395     $stderr.puts e.backtrace.join("\n\t") if $DEBUG_RDOC
397     raise e
398     nil
399   end
401   ##
402   # Parse each file on the command line, recursively entering directories.
404   def parse_files files
405     file_list = gather_files files
406     @stats = RDoc::Stats.new @store, file_list.length, @options.verbosity
408     return [] if file_list.empty?
410     original_options = @options.dup
411     @stats.begin_adding
413     file_info = file_list.map do |filename|
414       @current = filename
415       parse_file filename
416     end.compact
418     @stats.done_adding
419     @options = original_options
421     file_info
422   end
424   ##
425   # Removes file extensions known to be unparseable from +files+ and TAGS
426   # files for emacs and vim.
428   def remove_unparseable files
429     files.reject do |file, *|
430       file =~ /\.(?:class|eps|erb|scpt\.txt|svg|ttf|yml)$/i or
431         (file =~ /tags$/i and
432          /\A(\f\n[^,]+,\d+$|!_TAG_)/.match?(File.binread(file, 100)))
433     end
434   end
436   ##
437   # Generates documentation or a coverage report depending upon the settings
438   # in +options+.
439   #
440   # +options+ can be either an RDoc::Options instance or an array of strings
441   # equivalent to the strings that would be passed on the command line like
442   # <tt>%w[-q -o doc -t My\ Doc\ Title]</tt>.  #document will automatically
443   # call RDoc::Options#finish if an options instance was given.
444   #
445   # For a list of options, see either RDoc::Options or <tt>rdoc --help</tt>.
446   #
447   # By default, output will be stored in a directory called "doc" below the
448   # current directory, so make sure you're somewhere writable before invoking.
450   def document options
451     self.store = RDoc::Store.new
453     if RDoc::Options === options then
454       @options = options
455     else
456       @options = RDoc::Options.load_options
457       @options.parse options
458     end
459     @options.finish
461     if @options.pipe then
462       handle_pipe
463       exit
464     end
466     unless @options.coverage_report then
467       @last_modified = setup_output_dir @options.op_dir, @options.force_update
468     end
470     @store.encoding = @options.encoding
471     @store.dry_run  = @options.dry_run
472     @store.main     = @options.main_page
473     @store.title    = @options.title
474     @store.path     = @options.op_dir
476     @start_time = Time.now
478     @store.load_cache
480     file_info = parse_files @options.files
482     @options.default_title = "RDoc Documentation"
484     @store.complete @options.visibility
486     @stats.coverage_level = @options.coverage_report
488     if @options.coverage_report then
489       puts
491       puts @stats.report.accept RDoc::Markup::ToRdoc.new
492     elsif file_info.empty? then
493       $stderr.puts "\nNo newer files." unless @options.quiet
494     else
495       gen_klass = @options.generator
497       @generator = gen_klass.new @store, @options
499       generate
500     end
502     if @stats and (@options.coverage_report or not @options.quiet) then
503       puts
504       puts @stats.summary.accept RDoc::Markup::ToRdoc.new
505     end
507     exit @stats.fully_documented? if @options.coverage_report
508   end
510   ##
511   # Generates documentation for +file_info+ (from #parse_files) into the
512   # output dir using the generator selected
513   # by the RDoc options
515   def generate
516     if @options.dry_run then
517       # do nothing
518       @generator.generate
519     else
520       Dir.chdir @options.op_dir do
521         unless @options.quiet then
522           $stderr.puts "\nGenerating #{@generator.class.name.sub(/^.*::/, '')} format into #{Dir.pwd}..."
523           $stderr.puts "\nYou can visit the home page at: \e]8;;file://#{Dir.pwd}/index.html\e\\file://#{Dir.pwd}/index.html\e]8;;\e\\"
524         end
526         @generator.generate
527         update_output_dir '.', @start_time, @last_modified
528       end
529     end
530   end
532   ##
533   # Removes a siginfo handler and replaces the previous
535   def remove_siginfo_handler
536     return unless Signal.list.key? 'INFO'
538     handler = @old_siginfo || 'DEFAULT'
540     trap 'INFO', handler
541   end
545 begin
546   require 'rubygems'
548   rdoc_extensions = Gem.find_latest_files 'rdoc/discover'
550   rdoc_extensions.each do |extension|
551     begin
552       load extension
553     rescue => e
554       warn "error loading #{extension.inspect}: #{e.message} (#{e.class})"
555       warn "\t#{e.backtrace.join "\n\t"}" if $DEBUG
556     end
557   end
558 rescue LoadError
561 # require built-in generators after discovery in case they've been replaced
562 require_relative 'generator/darkfish'
563 require_relative 'generator/ri'
564 require_relative 'generator/pot'