rdoc.rb 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532
  1. require 'rdoc'
  2. require 'find'
  3. require 'fileutils'
  4. require 'time'
  5. ##
  6. # This is the driver for generating RDoc output. It file parsing and
  7. # generation of output.
  8. #
  9. # To use this class to generate RDoc output via the API, the recommended way
  10. # is:
  11. #
  12. # rdoc = RDoc::RDoc.new
  13. # options = rdoc.load_options # returns an RDoc::Options instance
  14. # # set extra options
  15. # rdoc.document options
  16. #
  17. # You can also generate output like the +rdoc+ executable:
  18. #
  19. # rdoc = RDoc::RDoc.new
  20. # rdoc.document argv
  21. #
  22. # Where +argv+ is an array of strings, each corresponding to an argument you'd
  23. # give rdoc on the command line. See <tt>rdoc --help<tt> for details.
  24. class RDoc::RDoc
  25. @current = nil
  26. ##
  27. # This is the list of supported output generators
  28. GENERATORS = {}
  29. ##
  30. # File pattern to exclude
  31. attr_accessor :exclude
  32. ##
  33. # Generator instance used for creating output
  34. attr_accessor :generator
  35. ##
  36. # Hash of files and their last modified times.
  37. attr_reader :last_modified
  38. ##
  39. # RDoc options
  40. attr_accessor :options
  41. ##
  42. # Accessor for statistics. Available after each call to parse_files
  43. attr_reader :stats
  44. ##
  45. # Add +klass+ that can generate output after parsing
  46. def self.add_generator(klass)
  47. name = klass.name.sub(/^RDoc::Generator::/, '').downcase
  48. GENERATORS[name] = klass
  49. end
  50. ##
  51. # Active RDoc::RDoc instance
  52. def self.current
  53. @current
  54. end
  55. ##
  56. # Sets the active RDoc::RDoc instance
  57. def self.current= rdoc
  58. @current = rdoc
  59. end
  60. ##
  61. # Resets all internal state
  62. def self.reset
  63. RDoc::TopLevel.reset
  64. RDoc::Parser::C.reset
  65. RDoc::RDoc.current = nil
  66. end
  67. ##
  68. # Creates a new RDoc::RDoc instance. Call #document to parse files and
  69. # generate documentation.
  70. def initialize
  71. @current = nil
  72. @exclude = nil
  73. @generator = nil
  74. @last_modified = {}
  75. @old_siginfo = nil
  76. @options = nil
  77. @stats = nil
  78. end
  79. ##
  80. # Report an error message and exit
  81. def error(msg)
  82. raise RDoc::Error, msg
  83. end
  84. ##
  85. # Gathers a set of parseable files from the files and directories listed in
  86. # +files+.
  87. def gather_files files
  88. files = ["."] if files.empty?
  89. file_list = normalized_file_list files, true, @exclude
  90. file_list = file_list.uniq
  91. file_list = remove_unparseable file_list
  92. end
  93. ##
  94. # Turns RDoc from stdin into HTML
  95. def handle_pipe
  96. @html = RDoc::Markup::ToHtml.new
  97. parser = RDoc::Text::MARKUP_FORMAT[@options.markup]
  98. document = parser.parse $stdin.read
  99. out = @html.convert document
  100. $stdout.write out
  101. end
  102. ##
  103. # Installs a siginfo handler that prints the current filename.
  104. def install_siginfo_handler
  105. return unless Signal.list.include? 'INFO'
  106. @old_siginfo = trap 'INFO' do
  107. puts @current if @current
  108. end
  109. end
  110. ##
  111. # Loads options from .rdoc_options if the file exists, otherwise creates a
  112. # new RDoc::Options instance.
  113. def load_options
  114. options_file = File.expand_path '.rdoc_options'
  115. return RDoc::Options.new unless File.exist? options_file
  116. RDoc.load_yaml
  117. parse_error = if Object.const_defined? :Psych then
  118. Psych::SyntaxError
  119. else
  120. ArgumentError
  121. end
  122. begin
  123. options = YAML.load_file '.rdoc_options'
  124. rescue *parse_error
  125. end
  126. raise RDoc::Error, "#{options_file} is not a valid rdoc options file" unless
  127. RDoc::Options === options
  128. options
  129. end
  130. ##
  131. # Create an output dir if it doesn't exist. If it does exist, but doesn't
  132. # contain the flag file <tt>created.rid</tt> then we refuse to use it, as
  133. # we may clobber some manually generated documentation
  134. def setup_output_dir(dir, force)
  135. flag_file = output_flag_file dir
  136. last = {}
  137. if @options.dry_run then
  138. # do nothing
  139. elsif File.exist? dir then
  140. error "#{dir} exists and is not a directory" unless File.directory? dir
  141. begin
  142. open flag_file do |io|
  143. unless force then
  144. Time.parse io.gets
  145. io.each do |line|
  146. file, time = line.split "\t", 2
  147. time = Time.parse(time) rescue next
  148. last[file] = time
  149. end
  150. end
  151. end
  152. rescue SystemCallError, TypeError
  153. error <<-ERROR
  154. Directory #{dir} already exists, but it looks like it isn't an RDoc directory.
  155. Because RDoc doesn't want to risk destroying any of your existing files,
  156. you'll need to specify a different output directory name (using the --op <dir>
  157. option)
  158. ERROR
  159. end unless @options.force_output
  160. else
  161. FileUtils.mkdir_p dir
  162. FileUtils.touch output_flag_file dir
  163. end
  164. last
  165. end
  166. ##
  167. # Update the flag file in an output directory.
  168. def update_output_dir(op_dir, time, last = {})
  169. return if @options.dry_run or not @options.update_output_dir
  170. open output_flag_file(op_dir), "w" do |f|
  171. f.puts time.rfc2822
  172. last.each do |n, t|
  173. f.puts "#{n}\t#{t.rfc2822}"
  174. end
  175. end
  176. end
  177. ##
  178. # Return the path name of the flag file in an output directory.
  179. def output_flag_file(op_dir)
  180. File.join op_dir, "created.rid"
  181. end
  182. ##
  183. # The .document file contains a list of file and directory name patterns,
  184. # representing candidates for documentation. It may also contain comments
  185. # (starting with '#')
  186. def parse_dot_doc_file in_dir, filename
  187. # read and strip comments
  188. patterns = File.read(filename).gsub(/#.*/, '')
  189. result = []
  190. patterns.split.each do |patt|
  191. candidates = Dir.glob(File.join(in_dir, patt))
  192. result.concat normalized_file_list(candidates)
  193. end
  194. result
  195. end
  196. ##
  197. # Given a list of files and directories, create a list of all the Ruby
  198. # files they contain.
  199. #
  200. # If +force_doc+ is true we always add the given files, if false, only
  201. # add files that we guarantee we can parse. It is true when looking at
  202. # files given on the command line, false when recursing through
  203. # subdirectories.
  204. #
  205. # The effect of this is that if you want a file with a non-standard
  206. # extension parsed, you must name it explicitly.
  207. def normalized_file_list(relative_files, force_doc = false,
  208. exclude_pattern = nil)
  209. file_list = []
  210. relative_files.each do |rel_file_name|
  211. next if exclude_pattern && exclude_pattern =~ rel_file_name
  212. stat = File.stat rel_file_name rescue next
  213. case type = stat.ftype
  214. when "file" then
  215. next if last_modified = @last_modified[rel_file_name] and
  216. stat.mtime.to_i <= last_modified.to_i
  217. if force_doc or RDoc::Parser.can_parse(rel_file_name) then
  218. file_list << rel_file_name.sub(/^\.\//, '')
  219. @last_modified[rel_file_name] = stat.mtime
  220. end
  221. when "directory" then
  222. next if rel_file_name == "CVS" || rel_file_name == ".svn"
  223. dot_doc = File.join rel_file_name, RDoc::DOT_DOC_FILENAME
  224. if File.file? dot_doc then
  225. file_list << parse_dot_doc_file(rel_file_name, dot_doc)
  226. else
  227. file_list << list_files_in_directory(rel_file_name)
  228. end
  229. else
  230. raise RDoc::Error, "I can't deal with a #{type} #{rel_file_name}"
  231. end
  232. end
  233. file_list.flatten
  234. end
  235. ##
  236. # Return a list of the files to be processed in a directory. We know that
  237. # this directory doesn't have a .document file, so we're looking for real
  238. # files. However we may well contain subdirectories which must be tested
  239. # for .document files.
  240. def list_files_in_directory dir
  241. files = Dir.glob File.join(dir, "*")
  242. normalized_file_list files, false, @options.exclude
  243. end
  244. ##
  245. # Parses +filename+ and returns an RDoc::TopLevel
  246. def parse_file filename
  247. if defined?(Encoding) then
  248. encoding = @options.encoding
  249. filename = filename.encode encoding
  250. end
  251. @stats.add_file filename
  252. content = RDoc::Encoding.read_file filename, encoding
  253. return unless content
  254. top_level = RDoc::TopLevel.new filename
  255. parser = RDoc::Parser.for top_level, filename, content, @options, @stats
  256. return unless parser
  257. parser.scan
  258. # restart documentation for the classes & modules found
  259. top_level.classes_or_modules.each do |cm|
  260. cm.done_documenting = false
  261. end
  262. top_level
  263. rescue => e
  264. $stderr.puts <<-EOF
  265. Before reporting this, could you check that the file you're documenting
  266. has proper syntax:
  267. #{Gem.ruby} -c #{filename}
  268. RDoc is not a full Ruby parser and will fail when fed invalid ruby programs.
  269. The internal error was:
  270. \t(#{e.class}) #{e.message}
  271. EOF
  272. $stderr.puts e.backtrace.join("\n\t") if $DEBUG_RDOC
  273. raise e
  274. nil
  275. end
  276. ##
  277. # Parse each file on the command line, recursively entering directories.
  278. def parse_files files
  279. file_list = gather_files files
  280. @stats = RDoc::Stats.new file_list.size, @options.verbosity
  281. return [] if file_list.empty?
  282. file_info = []
  283. @stats.begin_adding
  284. file_info = file_list.map do |filename|
  285. @current = filename
  286. parse_file filename
  287. end.compact
  288. @stats.done_adding
  289. file_info
  290. end
  291. ##
  292. # Removes file extensions known to be unparseable from +files+ and TAGS
  293. # files for emacs and vim.
  294. def remove_unparseable files
  295. files.reject do |file|
  296. file =~ /\.(?:class|eps|erb|scpt\.txt|ttf|yml)$/i or
  297. (file =~ /tags$/i and
  298. open(file, 'rb') { |io|
  299. io.read(100) =~ /\A(\f\n[^,]+,\d+$|!_TAG_)/
  300. })
  301. end
  302. end
  303. ##
  304. # Generates documentation or a coverage report depending upon the settings
  305. # in +options+.
  306. #
  307. # +options+ can be either an RDoc::Options instance or an array of strings
  308. # equivalent to the strings that would be passed on the command line like
  309. # <tt>%w[-q -o doc -t My\ Doc\ Title]</tt>. #document will automatically
  310. # call RDoc::Options#finish if an options instance was given.
  311. #
  312. # For a list of options, see either RDoc::Options or <tt>rdoc --help</tt>.
  313. #
  314. # By default, output will be stored in a directory called "doc" below the
  315. # current directory, so make sure you're somewhere writable before invoking.
  316. def document options
  317. RDoc::RDoc.reset
  318. RDoc::RDoc.current = self
  319. if RDoc::Options === options then
  320. @options = options
  321. @options.finish
  322. else
  323. @options = load_options
  324. @options.parse options
  325. end
  326. if @options.pipe then
  327. handle_pipe
  328. exit
  329. end
  330. @exclude = @options.exclude
  331. unless @options.coverage_report then
  332. @last_modified = setup_output_dir @options.op_dir, @options.force_update
  333. end
  334. @start_time = Time.now
  335. file_info = parse_files @options.files
  336. @options.default_title = "RDoc Documentation"
  337. RDoc::TopLevel.complete @options.visibility
  338. @stats.coverage_level = @options.coverage_report
  339. if @options.coverage_report then
  340. puts
  341. puts @stats.report
  342. elsif file_info.empty? then
  343. $stderr.puts "\nNo newer files." unless @options.quiet
  344. else
  345. gen_klass = @options.generator
  346. @generator = gen_klass.new @options
  347. generate file_info
  348. end
  349. if @stats and (@options.coverage_report or not @options.quiet) then
  350. puts
  351. puts @stats.summary
  352. end
  353. exit @stats.fully_documented? if @options.coverage_report
  354. end
  355. ##
  356. # Generates documentation for +file_info+ (from #parse_files) into the
  357. # output dir using the generator selected
  358. # by the RDoc options
  359. def generate file_info
  360. Dir.chdir @options.op_dir do
  361. unless @options.quiet then
  362. $stderr.puts "\nGenerating #{@generator.class.name.sub(/^.*::/, '')} format into #{Dir.pwd}..."
  363. end
  364. @generator.generate file_info
  365. update_output_dir '.', @start_time, @last_modified
  366. end
  367. end
  368. ##
  369. # Removes a siginfo handler and replaces the previous
  370. def remove_siginfo_handler
  371. return unless Signal.list.key? 'INFO'
  372. handler = @old_siginfo || 'DEFAULT'
  373. trap 'INFO', handler
  374. end
  375. end
  376. begin
  377. require 'rubygems'
  378. if Gem.respond_to? :find_files then
  379. rdoc_extensions = Gem.find_files 'rdoc/discover'
  380. rdoc_extensions.each do |extension|
  381. begin
  382. load extension
  383. rescue => e
  384. warn "error loading #{extension.inspect}: #{e.message} (#{e.class})"
  385. warn "\t#{e.backtrace.join "\n\t"}" if $DEBUG
  386. end
  387. end
  388. end
  389. rescue LoadError
  390. end
  391. # require built-in generators after discovery in case they've been replaced
  392. require 'rdoc/generator/darkfish'
  393. require 'rdoc/generator/ri'