diff --git a/lib/puppet-strings/yard.rb b/lib/puppet-strings/yard.rb index 9fa71c0..8c92acd 100644 --- a/lib/puppet-strings/yard.rb +++ b/lib/puppet-strings/yard.rb @@ -18,6 +18,15 @@ module PuppetStrings::Yard # Register our handlers YARD::Handlers::Processor.register_handler_namespace(:puppet, PuppetStrings::Yard::Handlers::Puppet) + YARD::Handlers::Processor.register_handler_namespace(:puppet_ruby, PuppetStrings::Yard::Handlers::Ruby) + + # Register the tag directives + PuppetStrings::Yard::Tags::ParameterDirective.register! + PuppetStrings::Yard::Tags::PropertyDirective.register! + + # Ignore documentation on Puppet DSL calls + # This prevents the YARD DSL parser from emitting warnings for Puppet's Ruby DSL + YARD::Handlers::Ruby::DSLHandlerMethods::IGNORE_METHODS['newtype'] = true end end @@ -31,6 +40,7 @@ class YARD::CLI::Yardoc :class, :puppet_class, :puppet_defined_type, + :puppet_type, ) end end @@ -47,6 +57,10 @@ class YARD::CLI::Stats output 'Puppet Defined Types', *type_statistics_all(:puppet_defined_type) end + def stats_for_puppet_types + output 'Puppet Types', *type_statistics_all(:puppet_type) + end + def output(name, data, undoc = nil) # Monkey patch output to accommodate our larger header widths @total += data if data.is_a?(Integer) && undoc diff --git a/lib/puppet-strings/yard/code_objects.rb b/lib/puppet-strings/yard/code_objects.rb index da6af39..dc6b9e3 100644 --- a/lib/puppet-strings/yard/code_objects.rb +++ b/lib/puppet-strings/yard/code_objects.rb @@ -2,4 +2,5 @@ module PuppetStrings::Yard::CodeObjects require 'puppet-strings/yard/code_objects/class' require 'puppet-strings/yard/code_objects/defined_type' + require 'puppet-strings/yard/code_objects/type' end diff --git a/lib/puppet-strings/yard/code_objects/type.rb b/lib/puppet-strings/yard/code_objects/type.rb new file mode 100644 index 0000000..bf80e14 --- /dev/null +++ b/lib/puppet-strings/yard/code_objects/type.rb @@ -0,0 +1,110 @@ +require 'puppet-strings/yard/code_objects/group' + +# Implements the group for Puppet resource types. +class PuppetStrings::Yard::CodeObjects::Types < PuppetStrings::Yard::CodeObjects::Group + # Gets the singleton instance of the group. + # @return Returns the singleton instance of the group. + def self.instance + super(:puppet_types) + end + + # Gets the display name of the group. + # @param [Boolean] prefix whether to show a prefix. Ignored for Puppet group namespaces. + # @return [String] Returns the display name of the group. + def name(prefix = false) + 'Resource Types' + end +end + +# Implements the Puppet resource type code object. +class PuppetStrings::Yard::CodeObjects::Type < PuppetStrings::Yard::CodeObjects::Base + # Represents a resource type parameter. + class Parameter + attr_reader :name, :values, :aliases + attr_accessor :docstring, :isnamevar, :default + + # Initializes a resource type parameter or property. + # @param [String] name The name of the parameter or property. + # @param [String] docstring The docstring for the parameter or property.s + def initialize(name, docstring = nil) + @name = name + @docstring = docstring || '' + @values = [] + @aliases = {} + @isnamevar = false + @default = nil + end + + # Adds a value to the parameter or property. + # @param [String] value The value to add. + # @return [void] + def add(value) + @values << value + end + + # Aliases a value to another value. + # @param [String] new The new (alias) value. + # @param [String] old The old (existing) value. + # @return [void] + def alias(new, old) + @values << new unless @values.include? new + @aliases[new] = old + end + end + + # Represents a resource type property (same attributes as a parameter). + class Property < Parameter + end + + # Represents a resource type feature. + class Feature + attr_reader :name, :docstring + + # Initializes a new feature. + # @param [String] name The name of the feature. + # @param [String] docstring The docstring of the feature. + def initialize(name, docstring) + @name = name + @docstring = docstring + end + end + + attr_reader :properties, :parameters, :features + + # Initializes a new resource type. + # @param [String] name The resource type name. + # @return [void] + def initialize(name) + super(PuppetStrings::Yard::CodeObjects::Types.instance, name) + end + + # Gets the type of the code object. + # @return Returns the type of the code object. + def type + :puppet_type + end + + # Adds a parameter to the resource type + # @param [PuppetStrings::Yard::CodeObjects::Type::Parameter] parameter The parameter to add. + # @return [void] + def add_parameter(parameter) + @parameters ||= [] + @parameters << parameter + end + + # Adds a property to the resource type + # @param [PuppetStrings::Yard::CodeObjects::Type::Property] property The property to add. + # @return [void] + def add_property(property) + @properties ||= [] + @properties << property + end + + # Adds a feature to the resource type. + # @param [PuppetStrings::Yard::CodeObjects::Type::Feature] feature The feature to add. + # @return [void] + def add_feature(feature) + @features ||= [] + @features << feature + end +end diff --git a/lib/puppet-strings/yard/handlers.rb b/lib/puppet-strings/yard/handlers.rb index 9eadc55..f3c683c 100644 --- a/lib/puppet-strings/yard/handlers.rb +++ b/lib/puppet-strings/yard/handlers.rb @@ -1,5 +1,10 @@ # The module for custom YARD handlers. module PuppetStrings::Yard::Handlers + # The module for custom Ruby YARD handlers. + module Ruby + require 'puppet-strings/yard/handlers/ruby/type_handler' + end + # The module for custom Puppet YARD handlers. module Puppet require 'puppet-strings/yard/handlers/puppet/class_handler' diff --git a/lib/puppet-strings/yard/handlers/ruby/base.rb b/lib/puppet-strings/yard/handlers/ruby/base.rb new file mode 100644 index 0000000..d2fb041 --- /dev/null +++ b/lib/puppet-strings/yard/handlers/ruby/base.rb @@ -0,0 +1,38 @@ +require 'ripper' + +# Implements the base handler for Ruby language handlers. +class PuppetStrings::Yard::Handlers::Ruby::Base < YARD::Handlers::Ruby::Base + # A regular expression for detecting the start of a Ruby heredoc. + # Note: the first character of the heredoc start may have been cut off by YARD. + HEREDOC_START = /^<[\-~]?['"]?(\w+)['"]?[^\n]*[\n]?/ + + protected + # Converts the given Ruby AST node to a string representation. + # @param node The Ruby AST node to convert. + # @return [String] Returns a string representation of the node or nil if a string representation was not possible. + def node_as_string(node) + return nil unless node + case node.type + when :symbol, :symbol_literal + node.source[1..-1] + when :label + node.source[0..-2] + when :dyna_symbol + node.source + when :string_literal + content = node.jump(:tstring_content) + return content.source if content != node + + # This attempts to work around a bug in YARD (https://github.com/lsegal/yard/issues/779) + # Check to see if the string source appears to have a heredoc open tag (or "most" of one) + # If so, remove the first line and the last line (if the latter contains the heredoc tag) + source = node.source + if source =~ HEREDOC_START + lines = source.split("\n") + source = lines[1..(lines.last.include?($1) ? -2 : -1)].join("\n") if lines.size > 1 + end + + source + end + end +end diff --git a/lib/puppet-strings/yard/handlers/ruby/type_handler.rb b/lib/puppet-strings/yard/handlers/ruby/type_handler.rb new file mode 100644 index 0000000..86ee94e --- /dev/null +++ b/lib/puppet-strings/yard/handlers/ruby/type_handler.rb @@ -0,0 +1,194 @@ +require 'puppet-strings/yard/handlers/ruby/base' +require 'puppet-strings/yard/code_objects' +require 'puppet/util' + +# Implements the handler for Puppet resource types written in Ruby. +class PuppetStrings::Yard::Handlers::Ruby::TypeHandler < PuppetStrings::Yard::Handlers::Ruby::Base + # The default docstring when ensurable is used without given a docstring. + DEFAULT_ENSURABLE_DOCSTRING = 'The basic property that the resource should be in.'.freeze + + namespace_only + handles method_call(:newtype) + + process do + # Only accept calls to Puppet::Type + return unless statement.count > 1 + module_name = statement[0].source + return unless module_name == 'Puppet::Type' || module_name == 'Type' + + object = PuppetStrings::Yard::CodeObjects::Type.new(get_name) + register object + + docstring = find_docstring(statement, "Puppet resource type '#{object.name}'") + register_docstring(object, docstring, nil) if docstring + + # Populate the parameters/properties/features to the type + populate_type_data(object) + + # Set the default namevar + set_default_namevar(object) + + # Mark the type as public if it doesn't already have an api tag + object.add_tag YARD::Tags::Tag.new(:api, 'public') unless object.has_tag? :api + end + + private + def get_name + parameters = statement.parameters(false) + raise YARD::Parser::UndocumentableError, "Expected at least one parameter to Puppet::Type.newtype at #{statement.file}:#{statement.line}." if parameters.empty? + name = node_as_string(parameters.first) + raise YARD::Parser::UndocumentableError, "Expected a symbol or string literal for first parameter but found '#{parameters.first.type}' at #{statement.file}:#{statement.line}." unless name + name + end + + def find_docstring(node, kind) + # Walk the tree searching for assignments or calls to desc/doc= + node.traverse do |child| + if child.type == :assign + ivar = child.jump(:ivar) + next unless ivar != child && ivar.source == '@doc' + docstring = node_as_string(child[1]) + log.error "Failed to parse docstring for #{kind} near #{child.file}:#{child.line}." and return nil unless docstring + return Puppet::Util::Docs.scrub(docstring) + elsif child.is_a?(YARD::Parser::Ruby::MethodCallNode) + # Look for a call to a dispatch method with a block + next unless child.method_name && + (child.method_name.source == 'desc' || child.method_name.source == 'doc=') && + child.parameters(false).count == 1 + + docstring = node_as_string(child.parameters[0]) + log.error "Failed to parse docstring for #{kind} near #{child.file}:#{child.line}." and return nil unless docstring + return Puppet::Util::Docs.scrub(docstring) + end + end + log.warn "Missing a description for #{kind} at #{node.file}:#{node.line}." + nil + end + + def populate_type_data(object) + # Traverse the block looking for properties/parameters/features + block = statement.block + return unless block && block.count >= 2 + block[1].children.each do |node| + next unless node.is_a?(YARD::Parser::Ruby::MethodCallNode) && + node.method_name + + method_name = node.method_name.source + parameters = node.parameters(false) + + if method_name == 'newproperty' + # Add a property to the object + next unless parameters.count >= 1 + name = node_as_string(parameters[0]) + next unless name + object.add_property(create_property(name, node)) + elsif method_name == 'newparam' + # Add a parameter to the object + next unless parameters.count >= 1 + name = node_as_string(parameters[0]) + next unless name + object.add_parameter(create_parameter(name, node)) + elsif method_name == 'feature' + # Add a feature to the object + next unless parameters.count >= 2 + name = node_as_string(parameters[0]) + next unless name + + docstring = node_as_string(parameters[1]) + next unless docstring + + object.add_feature(PuppetStrings::Yard::CodeObjects::Type::Feature.new(name, docstring)) + elsif method_name == 'ensurable' + if node.block + property = create_property('ensure', node) + property.docstring = DEFAULT_ENSURABLE_DOCSTRING if property.docstring.empty? + else + property = PuppetStrings::Yard::CodeObjects::Type::Property.new('ensure', DEFAULT_ENSURABLE_DOCSTRING) + property.add('present') + property.add('absent') + property.default = 'present' + end + object.add_property property + end + end + end + + def create_parameter(name, node) + parameter = PuppetStrings::Yard::CodeObjects::Type::Parameter.new(name, find_docstring(node, "Puppet resource parameter '#{name}'")) + set_values(node, parameter) + parameter + end + + def create_property(name, node) + property = PuppetStrings::Yard::CodeObjects::Type::Property.new(name, find_docstring(node, "Puppet resource property '#{name}'")) + set_values(node, property) + property + end + + def set_values(node, object) + return unless node.block && node.block.count >= 2 + + node.block[1].children.each do |child| + next unless child.is_a?(YARD::Parser::Ruby::MethodCallNode) && child.method_name + + method_name = child.method_name.source + parameters = child.parameters(false) + + if method_name == 'newvalue' + next unless parameters.count >= 1 + object.add(node_as_string(parameters[0]) || parameters[0].source) + elsif method_name == 'newvalues' + parameters.each do |p| + object.add(node_as_string(p) || p.source) + end + elsif method_name == 'aliasvalue' + next unless parameters.count >= 2 + object.alias(node_as_string(parameters[0]) || parameters[0].source, node_as_string(parameters[1]) || parameters[1].source) + elsif method_name == 'defaultto' + next unless parameters.count >= 1 + object.default = node_as_string(parameters[0]) || parameters[0].source + elsif method_name == 'isnamevar' + object.isnamevar = true + elsif method_name == 'defaultvalues' && object.name == 'ensure' + object.add('present') + object.add('absent') + object.default = 'present' + end + end + if object.is_a? PuppetStrings::Yard::CodeObjects::Type::Parameter + # Process the options for parameter base types + parameters = node.parameters(false) + if parameters.count >= 2 + parameters[1].each do |kvp| + next unless kvp.count == 2 + next unless node_as_string(kvp[0]) == 'parent' + if kvp[1].source == 'Puppet::Parameter::Boolean' + object.add('true') unless object.values.include? 'true' + object.add('false') unless object.values.include? 'false' + object.add('yes') unless object.values.include? 'yes' + object.add('no') unless object.values.include? 'no' + end + break + end + end + end + end + + def set_default_namevar(object) + return unless object.properties || object.parameters + default = nil + if object.properties + object.properties.each do |property| + return nil if property.isnamevar + default = property if property.name == 'name' + end + end + if object.parameters + object.parameters.each do |parameter| + return nil if parameter.isnamevar + default ||= parameter if parameter.name == 'name' + end + end + default.isnamevar = true if default + end +end diff --git a/lib/puppet-strings/yard/tags.rb b/lib/puppet-strings/yard/tags.rb index 8bd1d4b..ed0ffa9 100644 --- a/lib/puppet-strings/yard/tags.rb +++ b/lib/puppet-strings/yard/tags.rb @@ -1,3 +1,5 @@ # The module for custom YARD tags. module PuppetStrings::Yard::Tags + require 'puppet-strings/yard/tags/parameter_directive' + require 'puppet-strings/yard/tags/property_directive' end diff --git a/lib/puppet-strings/yard/tags/parameter_directive.rb b/lib/puppet-strings/yard/tags/parameter_directive.rb new file mode 100644 index 0000000..9cf68f9 --- /dev/null +++ b/lib/puppet-strings/yard/tags/parameter_directive.rb @@ -0,0 +1,24 @@ +require 'puppet-strings/yard/code_objects' + +# Implements a parameter directive (e.g. #@!puppet.type.param) for documenting Puppet resource types. +class PuppetStrings::Yard::Tags::ParameterDirective < YARD::Tags::Directive + # Called to invoke the directive. + # @return [void] + def call + return unless object && object.respond_to?(:add_parameter) + # Add a parameter to the resource + parameter = PuppetStrings::Yard::CodeObjects::Type::Parameter.new(tag.name, tag.text) + if tag.types + tag.types.each do |value| + parameter.add(value) + end + end + object.add_parameter parameter + end + + # Registers the directive with YARD. + # @return [void] + def self.register! + YARD::Tags::Library.define_directive('puppet.type.param', :with_types_and_name, self) + end +end diff --git a/lib/puppet-strings/yard/tags/property_directive.rb b/lib/puppet-strings/yard/tags/property_directive.rb new file mode 100644 index 0000000..a1c1e00 --- /dev/null +++ b/lib/puppet-strings/yard/tags/property_directive.rb @@ -0,0 +1,24 @@ +require 'puppet-strings/yard/code_objects' + +# Implements a parameter directive (e.g. #@!puppet.type.property) for documenting Puppet resource types. +class PuppetStrings::Yard::Tags::PropertyDirective < YARD::Tags::Directive + # Called to invoke the directive. + # @return [void] + def call + return unless object && object.respond_to?(:add_property) + # Add a property to the resource + property = PuppetStrings::Yard::CodeObjects::Type::Property.new(tag.name, tag.text) + if tag.types + tag.types.each do |value| + property.add(value) + end + end + object.add_property property + end + + # Registers the directive with YARD. + # @return [void] + def self.register! + YARD::Tags::Library.define_directive('puppet.type.property', :with_types_and_name, self) + end +end diff --git a/lib/puppet-strings/yard/templates/default/fulldoc/html/full_list_puppet_type.erb b/lib/puppet-strings/yard/templates/default/fulldoc/html/full_list_puppet_type.erb new file mode 100644 index 0000000..095188f --- /dev/null +++ b/lib/puppet-strings/yard/templates/default/fulldoc/html/full_list_puppet_type.erb @@ -0,0 +1,9 @@ +<% even = false %> +<% @items.each do |item| %> +