lib/yard/cli/yardoc.rb in yard-0.9.18 vs lib/yard/cli/yardoc.rb in yard-0.9.19

- old
+ new

@@ -1,788 +1,789 @@ -# frozen_string_literal: true -require 'digest/sha1' -require 'fileutils' - -module YARD - module CLI - # Default options used in +yard doc+ command. - class YardocOptions < Templates::TemplateOptions - # @return [Array<CodeObjects::ExtraFileObject>] - # the list of extra files rendered along with objects - default_attr :files, lambda { [] } - - # @return [String] the default title appended to each generated page - default_attr :title, "Documentation by YARD #{YARD::VERSION}" - - # @return [Verifier] the default verifier object to filter queries - default_attr :verifier, lambda { Verifier.new } - - # @return [Serializers::Base] the default serializer for generating output - # to disk. - default_attr :serializer, lambda { Serializers::FileSystemSerializer.new } - - # @return [Symbol] the default output format (:html). - default_attr :format, :html - - # @return [Boolean] whether the data should be rendered in a single page, - # if the template supports it. - default_attr :onefile, false - - # @return [CodeObjects::ExtraFileObject] the README file object rendered - # along with objects - attr_accessor :readme - - # @return [Array<CodeObjects::Base>] the list of code objects to render - # the templates with. - attr_accessor :objects - - # @return [Numeric] An index value for rendering sequentially related templates - attr_accessor :index - - # @return [CodeObjects::Base] an extra item to send to a template that is not - # the main rendered object - attr_accessor :item - - # @return [CodeObjects::ExtraFileObject] the file object being rendered. - # The +object+ key is not used so that a file may be rendered in the context - # of an object's namespace (for generating links). - attr_accessor :file - - # @return [String] the current locale - attr_accessor :locale - end - - # Yardoc is the default YARD CLI command (+yard doc+ and historic +yardoc+ - # executable) used to generate and output (mainly) HTML documentation given - # a set of source files. - # - # == Usage - # - # Main usage for this command is: - # - # $ yardoc [options] [source_files [- extra_files]] - # - # See +yardoc --help+ for details on valid options. - # - # == Options File (+.yardopts+) - # - # If a +.yardopts+ file is found in the source directory being processed, - # YARD will use the contents of the file as arguments to the command, - # treating newlines as spaces. You can use shell-style quotations to - # group space delimited arguments, just like on the command line. - # - # A valid +.yardopts+ file might look like: - # - # --no-private - # --title "My Title" - # --exclude foo --exclude bar - # lib/**/*.erb - # lib/**/*.rb - - # HACKING.rdoc LEGAL COPYRIGHT - # - # Note that Yardoc also supports the legacy RDoc style +.document+ file, - # though this file can only specify source globs to parse, not options. - # - # == Queries (+--query+) - # - # Yardoc supports queries to select specific code objects for which to - # generate documentation. For example, you might want to generate - # documentation only for your public API. If you've documented your public - # methods with +@api public+, you can use the following query to select - # all of these objects: - # - # --query '@api.text == "public"' - # - # Note that the syntax for queries is mostly Ruby with a few syntactic - # simplifications for meta-data tags. See the {Verifier} class for an - # overview of this syntax. - # - # == Adding Custom Ad-Hoc Meta-data Tags (+--tag+) - # - # YARD allows specification of {file:docs/Tags.md meta-data tags} - # programmatically via the {YARD::Tags::Library} class, but often this is not - # practical for users writing documentation. To make adding custom tags - # easier, Yardoc has a few command-line switches for creating basic tags - # and displaying them in generated HTML output. - # - # To specify a custom tag to be displayed in output, use any of the - # following: - # - # * +--tag+ TAG:TITLE - # * +--name-tag+ TAG:TITLE - # * +--type-tag+ TAG:TITLE - # * +--type-name-tag+ TAG:TITLE - # * +--title-tag+ TAG:TITLE - # - # "TAG:TITLE" is of the form: name:"Display Title", for example: - # - # --tag overload:"Overloaded Method" - # - # See +yard help doc+ for a description of the various options. - # - # Tags added in this way are automatically displayed in output. To add - # a meta-data tag that does not show up in output, use +--hide-tag TAG+. - # Note that you can also use this option on existing tags to hide - # builtin tags, for instance. - # - # == Processed Data Storage (+.yardoc+ directory) - # - # When Yardoc parses a source directory, it creates a +.yardoc+ directory - # (by default, override with +-b+) at the root of the project. This directory - # contains marshal dumps for all raw object data in the source, so that - # you can access it later for various commands (+stats+, +graph+, etc.). - # This directory is also used as a cache for any future calls to +yardoc+ - # so as to process only the files which have changed since the last call. - # - # When Yardoc uses the cache in subsequent calls to +yardoc+, methods - # or classes that have been deleted from source since the last parsing - # will not be erased from the cache (YARD never deletes objects). In such - # a case, you should wipe the cache and do a clean parsing of the source tree. - # You can do this by deleting the +.yardoc+ directory manually, or running - # Yardoc without +--use-cache+ (+-c+). - # - # @since 0.2.1 - # @see Verifier - class Yardoc < YardoptsCommand - # @return [Hash] the hash of options passed to the template. - # @see Templates::Engine#render - attr_reader :options - - # @return [Array<String>] list of Ruby source files to process - attr_accessor :files - - # @return [Array<String>] list of excluded paths (regexp matches) - # @since 0.5.3 - attr_accessor :excluded - - # @return [Boolean] whether to use the existing yardoc db if the - # .yardoc already exists. Also makes use of file checksums to - # parse only changed files. - attr_accessor :use_cache - - # @return [Boolean] whether objects should be serialized to .yardoc db - attr_accessor :save_yardoc - - # @return [Boolean] whether to generate output - attr_accessor :generate - - # @return [Boolean] whether to print a list of objects - # @since 0.5.5 - attr_accessor :list - - # Keep track of which visibilities are to be shown - # @return [Array<Symbol>] a list of visibilities - # @since 0.5.6 - attr_accessor :visibilities - - # Keep track of which APIs are to be shown - # @return [Array<String>] a list of APIs - # @since 0.8.1 - attr_accessor :apis - - # Keep track of which APIs are to be hidden - # @return [Array<String>] a list of APIs to be hidden - # @since 0.8.7 - attr_accessor :hidden_apis - - # @return [Array<Symbol>] a list of tags to hide from templates - # @since 0.6.0 - attr_accessor :hidden_tags - - # @return [Boolean] whether to print statistics after parsing - # @since 0.6.0 - attr_accessor :statistics - - # @return [Array<String>] a list of assets to copy after generation - # @since 0.6.0 - attr_accessor :assets - - # @return [Boolean] whether markup option was specified - # @since 0.7.0 - attr_accessor :has_markup - - # @return [Boolean] whether yard exits with error status code if a warning occurs - attr_accessor :fail_on_warning - - # Creates a new instance of the commandline utility - def initialize - super - @options = YardocOptions.new - @options.reset_defaults - @visibilities = [:public] - @apis = [] - @hidden_apis = [] - @assets = {} - @excluded = [] - @files = [] - @hidden_tags = [] - @use_cache = false - @generate = true - @statistics = true - @list = false - @save_yardoc = true - @has_markup = false - @fail_on_warning = false - - if defined?(::Encoding) && ::Encoding.respond_to?(:default_external=) - utf8 = ::Encoding.find('utf-8') - - ::Encoding.default_external = utf8 unless ::Encoding.default_external == utf8 - ::Encoding.default_internal = utf8 unless ::Encoding.default_internal == utf8 - end - end - - def description - "Generates documentation" - end - - # Runs the commandline utility, parsing arguments and generating - # output if set. - # - # @param [Array<String>] args the list of arguments. If the list only - # contains a single nil value, skip calling of {#parse_arguments} - # @return [void] - def run(*args) - log.show_progress = true - if args.empty? || !args.first.nil? - # fail early if arguments are not valid - return unless parse_arguments(*args) - end - - checksums = nil - if use_cache - Registry.load - checksums = Registry.checksums.dup - end - - if save_yardoc - Registry.lock_for_writing do - YARD.parse(files, excluded) - Registry.save(use_cache) - end - else - YARD.parse(files, excluded) - end - - if generate - run_generate(checksums) - copy_assets - elsif list - print_list - end - - if !list && statistics && log.level < Logger::ERROR - Registry.load_all - log.enter_level(Logger::ERROR) do - Stats.new(false).run(*args) - end - end - - abort if fail_on_warning && log.warned - - true - ensure - log.show_progress = false - end - - # Parses commandline arguments - # @param [Array<String>] args the list of arguments - # @return [Boolean] whether or not arguments are valid - # @since 0.5.6 - def parse_arguments(*args) - super(*args) - - # Last minute modifications - self.files = Parser::SourceParser::DEFAULT_PATH_GLOB if files.empty? - files.delete_if {|x| x =~ /\A\s*\Z/ } # remove empty ones - readme = Dir.glob('README{,*[^~]}').first - readme ||= Dir.glob(files.first).first if options.onefile - options.readme ||= CodeObjects::ExtraFileObject.new(readme) if readme - options.files.unshift(options.readme).uniq! if options.readme - - Tags::Library.visible_tags -= hidden_tags - add_visibility_verifier - add_api_verifier - - apply_locale - - # US-ASCII is invalid encoding for onefile - if defined?(::Encoding) && options.onefile - if ::Encoding.default_internal == ::Encoding::US_ASCII - log.warn "--one-file is not compatible with US-ASCII encoding, using ASCII-8BIT" - ::Encoding.default_external, ::Encoding.default_internal = ['ascii-8bit'] * 2 - end - end - - if generate && !verify_markup_options - false - else - true - end - end - - # The list of all objects to process. Override this method to change - # which objects YARD should generate documentation for. - # - # @deprecated To hide methods use the +@private+ tag instead. - # @return [Array<CodeObjects::Base>] a list of code objects to process - def all_objects - Registry.all(:root, :module, :class) - end - - private - - # Generates output for objects - # @param [Hash, nil] checksums if supplied, a list of checkums for files. - # @return [void] - # @since 0.5.1 - def run_generate(checksums) - if checksums - changed_files = [] - Registry.checksums.each do |file, hash| - changed_files << file if checksums[file] != hash - end - end - Registry.load_all if use_cache - objects = run_verifier(all_objects).reject do |object| - serialized = !options.serializer || options.serializer.exists?(object) - if checksums && serialized && !object.files.any? {|f, _line| changed_files.include?(f) } - true - else - log.debug "Re-generating object #{object.path}..." - false - end - end - Templates::Engine.generate(objects, options) - end - - # Verifies that the markup options are valid before parsing any code. - # Failing early is better than failing late. - # - # @return (see YARD::Templates::Helpers::MarkupHelper#load_markup_provider) - def verify_markup_options - result = false - lvl = has_markup ? log.level : Logger::FATAL - obj = Struct.new(:options).new(options) - obj.extend(Templates::Helpers::MarkupHelper) - options.files.each do |file| - markup = file.attributes[:markup] || obj.markup_for_file('', file.filename) - result = obj.load_markup_provider(markup) - return false if !result && markup != :rdoc - end - options.markup = :rdoc unless has_markup - log.enter_level(lvl) { result = obj.load_markup_provider } - if !result && !has_markup - log.warn "Could not load default RDoc formatter, " \ - "ignoring any markup (install RDoc to get default formatting)." - options.markup = :none - true - else - result - end - end - - # Copies any assets to the output directory - # @return [void] - # @since 0.6.0 - def copy_assets - return unless options.serializer - outpath = options.serializer.basepath - assets.each do |from, to| - to = File.join(outpath, to) - log.debug "Copying asset '#{from}' to '#{to}'" - from += '/.' if File.directory?(from) - FileUtils.cp_r(from, to) - end - end - - # Prints a list of all objects - # @return [void] - # @since 0.5.5 - def print_list - Registry.load_all - run_verifier(Registry.all). - sort_by {|item| [item.file || '', item.line || 0] }.each do |item| - log.puts "#{item.file}:#{item.line}: #{item.path}" - end - end - - # Adds a set of extra documentation files to be processed - # @param [Array<String>] files the set of documentation files - def add_extra_files(*files) - files.map! {|f| f.include?("*") ? Dir.glob(f) : f }.flatten! - files.each do |file| - if extra_file_valid?(file) - options.files << CodeObjects::ExtraFileObject.new(file) - end - end - end - - # @param file [String] the filename to validate - # @param check_exists [Boolean] whether the file should exist on disk - # @return [Boolean] whether the file is allowed to be used - def extra_file_valid?(file, check_exists = true) - if file =~ %r{^(?:\.\./|/)} - log.warn "Invalid file: #{file}" - false - elsif check_exists && !File.file?(file) - log.warn "Could not find file: #{file}" - false - else - true - end - end - - # Parses the file arguments into Ruby files and extra files, which are - # separated by a '-' element. - # - # @example Parses a set of Ruby source files - # parse_files %w(file1 file2 file3) - # @example Parses a set of Ruby files with a separator and extra files - # parse_files %w(file1 file2 - extrafile1 extrafile2) - # @param [Array<String>] files the list of files to parse - # @return [void] - def parse_files(*files) - seen_extra_files_marker = false - - files.each do |file| - if file == "-" - seen_extra_files_marker = true - next - end - - if seen_extra_files_marker - add_extra_files(file) - else - self.files << file - end - end - end - - # Adds verifier rule for visibilities - # @return [void] - # @since 0.5.6 - def add_visibility_verifier - vis_expr = "#{visibilities.uniq.inspect}.include?(object.visibility)" - options.verifier.add_expressions(vis_expr) - end - - # Adds verifier rule for APIs - # @return [void] - # @since 0.8.1 - def add_api_verifier - no_api = true if apis.delete('') - exprs = [] - - exprs << "#{apis.uniq.inspect}.include?(@api.text)" unless apis.empty? - - unless hidden_apis.empty? - exprs << "!#{hidden_apis.uniq.inspect}.include?(@api.text)" - end - - exprs = !exprs.empty? ? [exprs.join(' && ')] : [] - exprs << "!@api" if no_api - - expr = exprs.join(' || ') - options.verifier.add_expressions(expr) unless expr.empty? - end - - # Applies the specified locale to collected objects - # @return [void] - # @since 0.8.3 - def apply_locale - YARD::I18n::Locale.default = options.locale - options.files.each do |file| - file.locale = options.locale - end - end - - # (see Templates::Helpers::BaseHelper#run_verifier) - def run_verifier(list) - options.verifier ? options.verifier.run(list) : list - end - - # @since 0.6.0 - def add_tag(tag_data, factory_method = nil) - tag, title = *tag_data.split(':') - title ||= tag.capitalize - Tags::Library.define_tag(title, tag.to_sym, factory_method) - Tags::Library.visible_tags |= [tag.to_sym] - end - - # Parses commandline options. - # @param [Array<String>] args each tokenized argument - def optparse(*args) - opts = OptionParser.new - opts.banner = "Usage: yard doc [options] [source_files [- extra_files]]" - - opts.separator "(if a list of source files is omitted, " - opts.separator " {lib,app}/**/*.rb ext/**/*.{c,rb} is used.)" - opts.separator "" - opts.separator "Example: yardoc -o documentation/ - FAQ LICENSE" - opts.separator " The above example outputs documentation for files in" - opts.separator " lib/**/*.rb to documentation/ including the extra files" - opts.separator " FAQ and LICENSE." - opts.separator "" - opts.separator "A base set of options can be specified by adding a .yardopts" - opts.separator "file to your base path containing all extra options separated" - opts.separator "by whitespace." - - general_options(opts) - output_options(opts) - tag_options(opts) - common_options(opts) - parse_options(opts, args) - parse_files(*args) unless args.empty? - end - - # Adds general options - def general_options(opts) - opts.separator "" - opts.separator "General Options:" - - opts.on('-b', '--db FILE', 'Use a specified .yardoc db to load from or save to', - ' (defaults to .yardoc)') do |yfile| - YARD::Registry.yardoc_file = yfile - end - - opts.on('--[no-]single-db', 'Whether code objects should be stored to single', - ' database file (advanced)') do |use_single_db| - Registry.single_object_db = use_single_db - end - - opts.on('-n', '--no-output', 'Only generate .yardoc database, no documentation.') do - self.generate = false - end - - opts.on('-c', '--use-cache [FILE]', - "Use the cached .yardoc db to generate documentation.", - " (defaults to no cache)") do |file| - YARD::Registry.yardoc_file = file if file - self.use_cache = true - end - - opts.on('--no-cache', "Clear .yardoc db before parsing source.") do - self.use_cache = false - end - - yardopts_options(opts) - - opts.on('--no-save', 'Do not save the parsed data to the yardoc db') do - self.save_yardoc = false - end - - opts.on('--exclude REGEXP', 'Ignores a file if it matches path match (regexp)') do |path| - excluded << path - end - - opts.on('--fail-on-warning', 'Exit with error status code if a warning occurs') do - self.fail_on_warning = true - end - end - - # Adds output options - def output_options(opts) - opts.separator "" - opts.separator "Output options:" - - opts.on('--one-file', 'Generates output as a single file') do - options.onefile = true - end - - opts.on('--list', 'List objects to standard out (implies -n)') do |_format| - self.generate = false - self.list = true - end - - opts.on('--no-public', "Don't show public methods. (default shows public)") do - visibilities.delete(:public) - end - - opts.on('--protected', "Show protected methods. (default hides protected)") do - visibilities.push(:protected) - end - - opts.on('--private', "Show private methods. (default hides private)") do - visibilities.push(:private) - end - - opts.on('--no-private', "Hide objects with @private tag") do - options.verifier.add_expressions '!object.tag(:private) && - (object.namespace.is_a?(CodeObjects::Proxy) || !object.namespace.tag(:private))' - end - - opts.on('--[no-]api API', 'Generates documentation for a given API', - '(objects which define the correct @api tag).', - 'If --no-api is given, displays objects with', - 'no @api tag.') do |api| - api = '' if api == false - apis.push(api) - end - - opts.on('--hide-api API', 'Hides given @api tag from documentation') do |api| - hidden_apis.push(api) - end - - opts.on('--embed-mixins', "Embeds mixin methods into class documentation") do - options.embed_mixins << '*' - end - - opts.on('--embed-mixin [MODULE]', "Embeds mixin methods from a particular", - " module into class documentation") do |mod| - options.embed_mixins << mod - end - - opts.on('--no-highlight', "Don't highlight code blocks in output.") do - options.highlight = false - end - - opts.on('--default-return TYPE', "Shown if method has no return type. ", - " (defaults to 'Object')") do |type| - options.default_return = type - end - - opts.on('--hide-void-return', "Hides return types specified as 'void'. ", - " (default is shown)") do - options.hide_void_return = true - end - - opts.on('--query QUERY', "Only show objects that match a specific query") do |query| - next if YARD::Config.options[:safe_mode] - options.verifier.add_expressions(query.taint) - end - - opts.on('--title TITLE', 'Add a specific title to HTML documents') do |title| - options.title = title - end - - opts.on('-r', '--readme FILE', '--main FILE', 'The readme file used as the title page', - ' of documentation.') do |readme| - if extra_file_valid?(readme) - options.readme = CodeObjects::ExtraFileObject.new(readme) - end - end - - opts.on('--files FILE1,FILE2,...', 'Any extra comma separated static files to be ', - ' included (eg. FAQ)') do |files| - add_extra_files(*files.split(",")) - end - - opts.on('--asset FROM[:TO]', 'A file or directory to copy over to output ', - ' directory after generating') do |asset| - from, to = *asset.split(':').map {|f| File.cleanpath(f, true) } - to ||= from - if extra_file_valid?(from, false) && extra_file_valid?(to, false) - assets[from] = to - end - end - - opts.on('-o', '--output-dir PATH', - 'The output directory. (defaults to ./doc)') do |dir| - options.serializer.basepath = dir - end - - opts.on('-m', '--markup MARKUP', - 'Markup style used in documentation, like textile, ', - ' markdown or rdoc. (defaults to rdoc)') do |markup| - self.has_markup = true - options.markup = markup.to_sym - end - - opts.on('-M', '--markup-provider MARKUP_PROVIDER', - 'Overrides the library used to process markup ', - ' formatting (specify the gem name)') do |markup_provider| - options.markup_provider = markup_provider.to_sym - end - - opts.on('--charset ENC', 'Character set to use when parsing files ', - ' (default is system locale)') do |encoding| - begin - if defined?(Encoding) && Encoding.respond_to?(:default_external=) - Encoding.default_external = encoding - Encoding.default_internal = encoding - end - rescue ArgumentError => e - raise OptionParser::InvalidOption, e - end - end - - opts.on('-t', '--template TEMPLATE', - 'The template to use. (defaults to "default")') do |template| - options.template = template.to_sym - end - - opts.on('-p', '--template-path PATH', - 'The template path to look for templates in.', - ' (used with -t).') do |path| - next if YARD::Config.options[:safe_mode] - YARD::Templates::Engine.register_template_path(File.expand_path(path)) - end - - opts.on('-f', '--format FORMAT', - 'The output format for the template.', - ' (defaults to html)') do |format| - options.format = format.to_sym - end - - opts.on('--no-stats', 'Don\'t print statistics') do - self.statistics = false - end - - opts.on('--no-progress', 'Don\'t show progress bar') do - log.show_progress = false - end - - opts.on('--locale LOCALE', - 'The locale for generated documentation.', - ' (defaults to en)') do |locale| - options.locale = locale - end - - opts.on('--po-dir DIR', - 'The directory that has .po files.', - " (defaults to #{YARD::Registry.po_dir})") do |dir| - YARD::Registry.po_dir = dir - end - end - - # Adds tag options - # @since 0.6.0 - def tag_options(opts) - opts.separator "" - opts.separator "Tag options: (TAG:TITLE looks like: 'overload:Overloaded Method')" - - opts.on('--tag TAG:TITLE', 'Registers a new free-form metadata @tag') do |tag| - add_tag(tag) - end - - opts.on('--type-tag TAG:TITLE', 'Tag with an optional types field') do |tag| - add_tag(tag, :with_types) - end - - opts.on('--type-name-tag TAG:TITLE', 'Tag with optional types and a name field') do |tag| - add_tag(tag, :with_types_and_name) - end - - opts.on('--name-tag TAG:TITLE', 'Tag with a name field') do |tag| - add_tag(tag, :with_name) - end - - opts.on('--title-tag TAG:TITLE', 'Tag with first line as title field') do |tag| - add_tag(tag, :with_title_and_text) - end - - opts.on('--hide-tag TAG', 'Hides a previously defined tag from templates') do |tag| - self.hidden_tags |= [tag.to_sym] - end - - opts.on('--transitive-tag TAG', 'Marks a tag as transitive') do |tag| - Tags::Library.transitive_tags |= [tag.to_sym] - end - - opts.on('--non-transitive-tag TAG', 'Marks a tag as not transitive') do |tag| - Tags::Library.transitive_tags -= [tag.to_sym] - end - end - end - end -end +# frozen_string_literal: true +require 'digest/sha1' +require 'fileutils' + +module YARD + module CLI + # Default options used in +yard doc+ command. + class YardocOptions < Templates::TemplateOptions + # @return [Array<CodeObjects::ExtraFileObject>] + # the list of extra files rendered along with objects + default_attr :files, lambda { [] } + + # @return [String] the default title appended to each generated page + default_attr :title, "Documentation by YARD #{YARD::VERSION}" + + # @return [Verifier] the default verifier object to filter queries + default_attr :verifier, lambda { Verifier.new } + + # @return [Serializers::Base] the default serializer for generating output + # to disk. + default_attr :serializer, lambda { Serializers::FileSystemSerializer.new } + + # @return [Symbol] the default output format (:html). + default_attr :format, :html + + # @return [Boolean] whether the data should be rendered in a single page, + # if the template supports it. + default_attr :onefile, false + + # @return [CodeObjects::ExtraFileObject] the README file object rendered + # along with objects + attr_accessor :readme + + # @return [Array<CodeObjects::Base>] the list of code objects to render + # the templates with. + attr_accessor :objects + + # @return [Numeric] An index value for rendering sequentially related templates + attr_accessor :index + + # @return [CodeObjects::Base] an extra item to send to a template that is not + # the main rendered object + attr_accessor :item + + # @return [CodeObjects::ExtraFileObject] the file object being rendered. + # The +object+ key is not used so that a file may be rendered in the context + # of an object's namespace (for generating links). + attr_accessor :file + + # @return [String] the current locale + attr_accessor :locale + end + + # Yardoc is the default YARD CLI command (+yard doc+ and historic +yardoc+ + # executable) used to generate and output (mainly) HTML documentation given + # a set of source files. + # + # == Usage + # + # Main usage for this command is: + # + # $ yardoc [options] [source_files [- extra_files]] + # + # See +yardoc --help+ for details on valid options. + # + # == Options File (+.yardopts+) + # + # If a +.yardopts+ file is found in the source directory being processed, + # YARD will use the contents of the file as arguments to the command, + # treating newlines as spaces. You can use shell-style quotations to + # group space delimited arguments, just like on the command line. + # + # A valid +.yardopts+ file might look like: + # + # --no-private + # --title "My Title" + # --exclude foo --exclude bar + # lib/**/*.erb + # lib/**/*.rb - + # HACKING.rdoc LEGAL COPYRIGHT + # + # Note that Yardoc also supports the legacy RDoc style +.document+ file, + # though this file can only specify source globs to parse, not options. + # + # == Queries (+--query+) + # + # Yardoc supports queries to select specific code objects for which to + # generate documentation. For example, you might want to generate + # documentation only for your public API. If you've documented your public + # methods with +@api public+, you can use the following query to select + # all of these objects: + # + # --query '@api.text == "public"' + # + # Note that the syntax for queries is mostly Ruby with a few syntactic + # simplifications for meta-data tags. See the {Verifier} class for an + # overview of this syntax. + # + # == Adding Custom Ad-Hoc Meta-data Tags (+--tag+) + # + # YARD allows specification of {file:docs/Tags.md meta-data tags} + # programmatically via the {YARD::Tags::Library} class, but often this is not + # practical for users writing documentation. To make adding custom tags + # easier, Yardoc has a few command-line switches for creating basic tags + # and displaying them in generated HTML output. + # + # To specify a custom tag to be displayed in output, use any of the + # following: + # + # * +--tag+ TAG:TITLE + # * +--name-tag+ TAG:TITLE + # * +--type-tag+ TAG:TITLE + # * +--type-name-tag+ TAG:TITLE + # * +--title-tag+ TAG:TITLE + # + # "TAG:TITLE" is of the form: name:"Display Title", for example: + # + # --tag overload:"Overloaded Method" + # + # See +yard help doc+ for a description of the various options. + # + # Tags added in this way are automatically displayed in output. To add + # a meta-data tag that does not show up in output, use +--hide-tag TAG+. + # Note that you can also use this option on existing tags to hide + # builtin tags, for instance. + # + # == Processed Data Storage (+.yardoc+ directory) + # + # When Yardoc parses a source directory, it creates a +.yardoc+ directory + # (by default, override with +-b+) at the root of the project. This directory + # contains marshal dumps for all raw object data in the source, so that + # you can access it later for various commands (+stats+, +graph+, etc.). + # This directory is also used as a cache for any future calls to +yardoc+ + # so as to process only the files which have changed since the last call. + # + # When Yardoc uses the cache in subsequent calls to +yardoc+, methods + # or classes that have been deleted from source since the last parsing + # will not be erased from the cache (YARD never deletes objects). In such + # a case, you should wipe the cache and do a clean parsing of the source tree. + # You can do this by deleting the +.yardoc+ directory manually, or running + # Yardoc without +--use-cache+ (+-c+). + # + # @since 0.2.1 + # @see Verifier + class Yardoc < YardoptsCommand + # @return [Hash] the hash of options passed to the template. + # @see Templates::Engine#render + attr_reader :options + + # @return [Array<String>] list of Ruby source files to process + attr_accessor :files + + # @return [Array<String>] list of excluded paths (regexp matches) + # @since 0.5.3 + attr_accessor :excluded + + # @return [Boolean] whether to use the existing yardoc db if the + # .yardoc already exists. Also makes use of file checksums to + # parse only changed files. + attr_accessor :use_cache + + # @return [Boolean] whether objects should be serialized to .yardoc db + attr_accessor :save_yardoc + + # @return [Boolean] whether to generate output + attr_accessor :generate + + # @return [Boolean] whether to print a list of objects + # @since 0.5.5 + attr_accessor :list + + # Keep track of which visibilities are to be shown + # @return [Array<Symbol>] a list of visibilities + # @since 0.5.6 + attr_accessor :visibilities + + # Keep track of which APIs are to be shown + # @return [Array<String>] a list of APIs + # @since 0.8.1 + attr_accessor :apis + + # Keep track of which APIs are to be hidden + # @return [Array<String>] a list of APIs to be hidden + # @since 0.8.7 + attr_accessor :hidden_apis + + # @return [Array<Symbol>] a list of tags to hide from templates + # @since 0.6.0 + attr_accessor :hidden_tags + + # @return [Boolean] whether to print statistics after parsing + # @since 0.6.0 + attr_accessor :statistics + + # @return [Array<String>] a list of assets to copy after generation + # @since 0.6.0 + attr_accessor :assets + + # @return [Boolean] whether markup option was specified + # @since 0.7.0 + attr_accessor :has_markup + + # @return [Boolean] whether yard exits with error status code if a warning occurs + attr_accessor :fail_on_warning + + # Creates a new instance of the commandline utility + def initialize + super + @options = YardocOptions.new + @options.reset_defaults + @visibilities = [:public] + @apis = [] + @hidden_apis = [] + @assets = {} + @excluded = [] + @files = [] + @hidden_tags = [] + @use_cache = false + @generate = true + @statistics = true + @list = false + @save_yardoc = true + @has_markup = false + @fail_on_warning = false + + if defined?(::Encoding) && ::Encoding.respond_to?(:default_external=) + utf8 = ::Encoding.find('utf-8') + + ::Encoding.default_external = utf8 unless ::Encoding.default_external == utf8 + ::Encoding.default_internal = utf8 unless ::Encoding.default_internal == utf8 + end + end + + def description + "Generates documentation" + end + + # Runs the commandline utility, parsing arguments and generating + # output if set. + # + # @param [Array<String>] args the list of arguments. If the list only + # contains a single nil value, skip calling of {#parse_arguments} + # @return [void] + def run(*args) + log.show_progress = true + if args.empty? || !args.first.nil? + # fail early if arguments are not valid + return unless parse_arguments(*args) + end + + checksums = nil + if use_cache + Registry.load + checksums = Registry.checksums.dup + end + + if save_yardoc + Registry.lock_for_writing do + YARD.parse(files, excluded) + Registry.save(use_cache) + end + else + YARD.parse(files, excluded) + end + + if generate + run_generate(checksums) + copy_assets + elsif list + print_list + end + + if !list && statistics && log.level < Logger::ERROR + Registry.load_all + log.enter_level(Logger::ERROR) do + Stats.new(false).run(*args) + end + end + + abort if fail_on_warning && log.warned + + true + ensure + log.show_progress = false + end + + # Parses commandline arguments + # @param [Array<String>] args the list of arguments + # @return [Boolean] whether or not arguments are valid + # @since 0.5.6 + def parse_arguments(*args) + super(*args) + + # Last minute modifications + self.files = Parser::SourceParser::DEFAULT_PATH_GLOB if files.empty? + files.delete_if {|x| x =~ /\A\s*\Z/ } # remove empty ones + readme = Dir.glob('README{,*[^~]}'). + sort_by {|r| [r.count('.'), r.index('.'), r] }.first + readme ||= Dir.glob(files.first).first if options.onefile && !files.empty? + options.readme ||= CodeObjects::ExtraFileObject.new(readme) if readme + options.files.unshift(options.readme).uniq! if options.readme + + Tags::Library.visible_tags -= hidden_tags + add_visibility_verifier + add_api_verifier + + apply_locale + + # US-ASCII is invalid encoding for onefile + if defined?(::Encoding) && options.onefile + if ::Encoding.default_internal == ::Encoding::US_ASCII + log.warn "--one-file is not compatible with US-ASCII encoding, using ASCII-8BIT" + ::Encoding.default_external, ::Encoding.default_internal = ['ascii-8bit'] * 2 + end + end + + if generate && !verify_markup_options + false + else + true + end + end + + # The list of all objects to process. Override this method to change + # which objects YARD should generate documentation for. + # + # @deprecated To hide methods use the +@private+ tag instead. + # @return [Array<CodeObjects::Base>] a list of code objects to process + def all_objects + Registry.all(:root, :module, :class) + end + + private + + # Generates output for objects + # @param [Hash, nil] checksums if supplied, a list of checkums for files. + # @return [void] + # @since 0.5.1 + def run_generate(checksums) + if checksums + changed_files = [] + Registry.checksums.each do |file, hash| + changed_files << file if checksums[file] != hash + end + end + Registry.load_all if use_cache + objects = run_verifier(all_objects).reject do |object| + serialized = !options.serializer || options.serializer.exists?(object) + if checksums && serialized && !object.files.any? {|f, _line| changed_files.include?(f) } + true + else + log.debug "Re-generating object #{object.path}..." + false + end + end + Templates::Engine.generate(objects, options) + end + + # Verifies that the markup options are valid before parsing any code. + # Failing early is better than failing late. + # + # @return (see YARD::Templates::Helpers::MarkupHelper#load_markup_provider) + def verify_markup_options + result = false + lvl = has_markup ? log.level : Logger::FATAL + obj = Struct.new(:options).new(options) + obj.extend(Templates::Helpers::MarkupHelper) + options.files.each do |file| + markup = file.attributes[:markup] || obj.markup_for_file('', file.filename) + result = obj.load_markup_provider(markup) + return false if !result && markup != :rdoc + end + options.markup = :rdoc unless has_markup + log.enter_level(lvl) { result = obj.load_markup_provider } + if !result && !has_markup + log.warn "Could not load default RDoc formatter, " \ + "ignoring any markup (install RDoc to get default formatting)." + options.markup = :none + true + else + result + end + end + + # Copies any assets to the output directory + # @return [void] + # @since 0.6.0 + def copy_assets + return unless options.serializer + outpath = options.serializer.basepath + assets.each do |from, to| + to = File.join(outpath, to) + log.debug "Copying asset '#{from}' to '#{to}'" + from += '/.' if File.directory?(from) + FileUtils.cp_r(from, to) + end + end + + # Prints a list of all objects + # @return [void] + # @since 0.5.5 + def print_list + Registry.load_all + run_verifier(Registry.all). + sort_by {|item| [item.file || '', item.line || 0] }.each do |item| + log.puts "#{item.file}:#{item.line}: #{item.path}" + end + end + + # Adds a set of extra documentation files to be processed + # @param [Array<String>] files the set of documentation files + def add_extra_files(*files) + files.map! {|f| f.include?("*") ? Dir.glob(f) : f }.flatten! + files.each do |file| + if extra_file_valid?(file) + options.files << CodeObjects::ExtraFileObject.new(file) + end + end + end + + # @param file [String] the filename to validate + # @param check_exists [Boolean] whether the file should exist on disk + # @return [Boolean] whether the file is allowed to be used + def extra_file_valid?(file, check_exists = true) + if file =~ %r{^(?:\.\./|/)} + log.warn "Invalid file: #{file}" + false + elsif check_exists && !File.file?(file) + log.warn "Could not find file: #{file}" + false + else + true + end + end + + # Parses the file arguments into Ruby files and extra files, which are + # separated by a '-' element. + # + # @example Parses a set of Ruby source files + # parse_files %w(file1 file2 file3) + # @example Parses a set of Ruby files with a separator and extra files + # parse_files %w(file1 file2 - extrafile1 extrafile2) + # @param [Array<String>] files the list of files to parse + # @return [void] + def parse_files(*files) + seen_extra_files_marker = false + + files.each do |file| + if file == "-" + seen_extra_files_marker = true + next + end + + if seen_extra_files_marker + add_extra_files(file) + else + self.files << file + end + end + end + + # Adds verifier rule for visibilities + # @return [void] + # @since 0.5.6 + def add_visibility_verifier + vis_expr = "#{visibilities.uniq.inspect}.include?(object.visibility)" + options.verifier.add_expressions(vis_expr) + end + + # Adds verifier rule for APIs + # @return [void] + # @since 0.8.1 + def add_api_verifier + no_api = true if apis.delete('') + exprs = [] + + exprs << "#{apis.uniq.inspect}.include?(@api.text)" unless apis.empty? + + unless hidden_apis.empty? + exprs << "!#{hidden_apis.uniq.inspect}.include?(@api.text)" + end + + exprs = !exprs.empty? ? [exprs.join(' && ')] : [] + exprs << "!@api" if no_api + + expr = exprs.join(' || ') + options.verifier.add_expressions(expr) unless expr.empty? + end + + # Applies the specified locale to collected objects + # @return [void] + # @since 0.8.3 + def apply_locale + YARD::I18n::Locale.default = options.locale + options.files.each do |file| + file.locale = options.locale + end + end + + # (see Templates::Helpers::BaseHelper#run_verifier) + def run_verifier(list) + options.verifier ? options.verifier.run(list) : list + end + + # @since 0.6.0 + def add_tag(tag_data, factory_method = nil) + tag, title = *tag_data.split(':') + title ||= tag.capitalize + Tags::Library.define_tag(title, tag.to_sym, factory_method) + Tags::Library.visible_tags |= [tag.to_sym] + end + + # Parses commandline options. + # @param [Array<String>] args each tokenized argument + def optparse(*args) + opts = OptionParser.new + opts.banner = "Usage: yard doc [options] [source_files [- extra_files]]" + + opts.separator "(if a list of source files is omitted, " + opts.separator " {lib,app}/**/*.rb ext/**/*.{c,rb} is used.)" + opts.separator "" + opts.separator "Example: yardoc -o documentation/ - FAQ LICENSE" + opts.separator " The above example outputs documentation for files in" + opts.separator " lib/**/*.rb to documentation/ including the extra files" + opts.separator " FAQ and LICENSE." + opts.separator "" + opts.separator "A base set of options can be specified by adding a .yardopts" + opts.separator "file to your base path containing all extra options separated" + opts.separator "by whitespace." + + general_options(opts) + output_options(opts) + tag_options(opts) + common_options(opts) + parse_options(opts, args) + parse_files(*args) unless args.empty? + end + + # Adds general options + def general_options(opts) + opts.separator "" + opts.separator "General Options:" + + opts.on('-b', '--db FILE', 'Use a specified .yardoc db to load from or save to', + ' (defaults to .yardoc)') do |yfile| + YARD::Registry.yardoc_file = yfile + end + + opts.on('--[no-]single-db', 'Whether code objects should be stored to single', + ' database file (advanced)') do |use_single_db| + Registry.single_object_db = use_single_db + end + + opts.on('-n', '--no-output', 'Only generate .yardoc database, no documentation.') do + self.generate = false + end + + opts.on('-c', '--use-cache [FILE]', + "Use the cached .yardoc db to generate documentation.", + " (defaults to no cache)") do |file| + YARD::Registry.yardoc_file = file if file + self.use_cache = true + end + + opts.on('--no-cache', "Clear .yardoc db before parsing source.") do + self.use_cache = false + end + + yardopts_options(opts) + + opts.on('--no-save', 'Do not save the parsed data to the yardoc db') do + self.save_yardoc = false + end + + opts.on('--exclude REGEXP', 'Ignores a file if it matches path match (regexp)') do |path| + excluded << path + end + + opts.on('--fail-on-warning', 'Exit with error status code if a warning occurs') do + self.fail_on_warning = true + end + end + + # Adds output options + def output_options(opts) + opts.separator "" + opts.separator "Output options:" + + opts.on('--one-file', 'Generates output as a single file') do + options.onefile = true + end + + opts.on('--list', 'List objects to standard out (implies -n)') do |_format| + self.generate = false + self.list = true + end + + opts.on('--no-public', "Don't show public methods. (default shows public)") do + visibilities.delete(:public) + end + + opts.on('--protected', "Show protected methods. (default hides protected)") do + visibilities.push(:protected) + end + + opts.on('--private', "Show private methods. (default hides private)") do + visibilities.push(:private) + end + + opts.on('--no-private', "Hide objects with @private tag") do + options.verifier.add_expressions '!object.tag(:private) && + (object.namespace.is_a?(CodeObjects::Proxy) || !object.namespace.tag(:private))' + end + + opts.on('--[no-]api API', 'Generates documentation for a given API', + '(objects which define the correct @api tag).', + 'If --no-api is given, displays objects with', + 'no @api tag.') do |api| + api = '' if api == false + apis.push(api) + end + + opts.on('--hide-api API', 'Hides given @api tag from documentation') do |api| + hidden_apis.push(api) + end + + opts.on('--embed-mixins', "Embeds mixin methods into class documentation") do + options.embed_mixins << '*' + end + + opts.on('--embed-mixin [MODULE]', "Embeds mixin methods from a particular", + " module into class documentation") do |mod| + options.embed_mixins << mod + end + + opts.on('--no-highlight', "Don't highlight code blocks in output.") do + options.highlight = false + end + + opts.on('--default-return TYPE', "Shown if method has no return type. ", + " (defaults to 'Object')") do |type| + options.default_return = type + end + + opts.on('--hide-void-return', "Hides return types specified as 'void'. ", + " (default is shown)") do + options.hide_void_return = true + end + + opts.on('--query QUERY', "Only show objects that match a specific query") do |query| + next if YARD::Config.options[:safe_mode] + options.verifier.add_expressions(query.taint) + end + + opts.on('--title TITLE', 'Add a specific title to HTML documents') do |title| + options.title = title + end + + opts.on('-r', '--readme FILE', '--main FILE', 'The readme file used as the title page', + ' of documentation.') do |readme| + if extra_file_valid?(readme) + options.readme = CodeObjects::ExtraFileObject.new(readme) + end + end + + opts.on('--files FILE1,FILE2,...', 'Any extra comma separated static files to be ', + ' included (eg. FAQ)') do |files| + add_extra_files(*files.split(",")) + end + + opts.on('--asset FROM[:TO]', 'A file or directory to copy over to output ', + ' directory after generating') do |asset| + from, to = *asset.split(':').map {|f| File.cleanpath(f, true) } + to ||= from + if extra_file_valid?(from, false) && extra_file_valid?(to, false) + assets[from] = to + end + end + + opts.on('-o', '--output-dir PATH', + 'The output directory. (defaults to ./doc)') do |dir| + options.serializer.basepath = dir + end + + opts.on('-m', '--markup MARKUP', + 'Markup style used in documentation, like textile, ', + ' markdown or rdoc. (defaults to rdoc)') do |markup| + self.has_markup = true + options.markup = markup.to_sym + end + + opts.on('-M', '--markup-provider MARKUP_PROVIDER', + 'Overrides the library used to process markup ', + ' formatting (specify the gem name)') do |markup_provider| + options.markup_provider = markup_provider.to_sym + end + + opts.on('--charset ENC', 'Character set to use when parsing files ', + ' (default is system locale)') do |encoding| + begin + if defined?(Encoding) && Encoding.respond_to?(:default_external=) + Encoding.default_external = encoding + Encoding.default_internal = encoding + end + rescue ArgumentError => e + raise OptionParser::InvalidOption, e + end + end + + opts.on('-t', '--template TEMPLATE', + 'The template to use. (defaults to "default")') do |template| + options.template = template.to_sym + end + + opts.on('-p', '--template-path PATH', + 'The template path to look for templates in.', + ' (used with -t).') do |path| + next if YARD::Config.options[:safe_mode] + YARD::Templates::Engine.register_template_path(File.expand_path(path)) + end + + opts.on('-f', '--format FORMAT', + 'The output format for the template.', + ' (defaults to html)') do |format| + options.format = format.to_sym + end + + opts.on('--no-stats', 'Don\'t print statistics') do + self.statistics = false + end + + opts.on('--no-progress', 'Don\'t show progress bar') do + log.show_progress = false + end + + opts.on('--locale LOCALE', + 'The locale for generated documentation.', + ' (defaults to en)') do |locale| + options.locale = locale + end + + opts.on('--po-dir DIR', + 'The directory that has .po files.', + " (defaults to #{YARD::Registry.po_dir})") do |dir| + YARD::Registry.po_dir = dir + end + end + + # Adds tag options + # @since 0.6.0 + def tag_options(opts) + opts.separator "" + opts.separator "Tag options: (TAG:TITLE looks like: 'overload:Overloaded Method')" + + opts.on('--tag TAG:TITLE', 'Registers a new free-form metadata @tag') do |tag| + add_tag(tag) + end + + opts.on('--type-tag TAG:TITLE', 'Tag with an optional types field') do |tag| + add_tag(tag, :with_types) + end + + opts.on('--type-name-tag TAG:TITLE', 'Tag with optional types and a name field') do |tag| + add_tag(tag, :with_types_and_name) + end + + opts.on('--name-tag TAG:TITLE', 'Tag with a name field') do |tag| + add_tag(tag, :with_name) + end + + opts.on('--title-tag TAG:TITLE', 'Tag with first line as title field') do |tag| + add_tag(tag, :with_title_and_text) + end + + opts.on('--hide-tag TAG', 'Hides a previously defined tag from templates') do |tag| + self.hidden_tags |= [tag.to_sym] + end + + opts.on('--transitive-tag TAG', 'Marks a tag as transitive') do |tag| + Tags::Library.transitive_tags |= [tag.to_sym] + end + + opts.on('--non-transitive-tag TAG', 'Marks a tag as not transitive') do |tag| + Tags::Library.transitive_tags -= [tag.to_sym] + end + end + end + end +end