# 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{,*[^~]}').
select {|f| extra_file_valid?(f)}.
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 && extra_file_valid?(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 checksums 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]
query.taint if query.respond_to?(:taint)
options.verifier.add_expressions(query)
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