PageRenderTime 38ms CodeModel.GetById 14ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/rdoc/rdoc.rb

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