class PuppetStrings::Yard::Handlers::Ruby::DataTypeHandler

Implements the handler for Puppet Data Types written in Ruby.

Private Instance Methods

extract_data_type_interface() click to toggle source

@return [Hash{Object => Object}] The Puppet DataType interface definition as a ruby Hash

# File lib/puppet-strings/yard/handlers/ruby/data_type_handler.rb, line 60
def extract_data_type_interface
  block = statement.block
  return {} unless block

  # Declare the parsed interface outside of the closure
  parsed_interface = nil

  # Recursively traverse the block looking for the first valid 'interface' call
  find_ruby_ast_node(block, true) do |node|
    next false unless node.is_a?(YARD::Parser::Ruby::MethodCallNode) &&
                      node.method_name &&
                      node.method_name.source == 'interface'

    parameters = node.parameters(false)
    next false unless parameters.count >= 1

    interface_string = node_as_string(parameters[0])
    next false unless interface_string

    begin
      # Ref - https://github.com/puppetlabs/puppet/blob/ba4d1a1aba0095d3c70b98fea5c67434a4876a61/lib/puppet/datatypes.rb#L159
      parsed_interface = Puppet::Pops::Parser::EvaluatingParser.new.parse_string("{ #{interface_string} }").body
    rescue Puppet::Error => e
      log.warn "Invalid datatype definition at #{statement.file}:#{statement.line}: #{e.basic_message}"
      next false
    end
    !parsed_interface.nil?
  end

  # Now that we parsed the Puppet code (as a string) into a LiteralHash PCore type (Puppet AST),
  # We need to convert the LiteralHash into a conventional ruby hash of strings. The
  # LazyLiteralEvaluator does this by traversing the AST tree can converting objects to strings
  # where possible and ignoring object types which cannot (thus the 'Lazy' name)
  literal_eval = LazyLiteralEvaluator.new
  literal_eval.literal(parsed_interface)
end
extract_functions(object, hash) click to toggle source

Extracts the datatype functions from a Puppet Data Type interface hash. Returns a Hash with a :param_types key (Array of types for each parameter) and :return_type key (The return type of the function) @return Hash[Symbol => Hash] The Datatype Attributes as a hash

# File lib/puppet-strings/yard/handlers/ruby/data_type_handler.rb, line 249
def extract_functions(object, hash)
  funcs_hash = {}
  # Exit early if there are no entries in the hash
  return funcs_hash if hash.nil? || hash['functions'].nil? || hash['functions'].empty?

  hash['functions'].each do |key, func_type|
    func_hash = { param_types: [], return_type: nil }
    begin
      callable_type = Puppet::Pops::Types::TypeParser.singleton.parse(func_type)
      if callable_type.is_a?(Puppet::Pops::Types::PCallableType)
        func_hash[:param_types] = callable_type.param_types.map(&:to_s)
        func_hash[:return_type] = callable_type.return_type.to_s
      else
        log.warn "The function definition for '#{key}' near #{object.file}:#{object.line} is not a Callable type"
      end
    rescue Puppet::ParseError => e
      log.warn "Unable to parse the function definition for '#{key}' near #{object.file}:#{object.line}. #{e}"
    end
    funcs_hash[key] = func_hash
  end
  funcs_hash
end
extract_params(hash) click to toggle source

Extracts the datatype attributes from a Puppet Data Type interface hash. Returns a Hash with a :types key (Array of data types for the parameter) and :default key (The default value of the parameter) @return Hash[Symbol => Hash] The Datatype Attributes as a hash

# File lib/puppet-strings/yard/handlers/ruby/data_type_handler.rb, line 225
def extract_params(hash)
  params_hash = {}
  # Exit early if there are no entries in the hash
  return params_hash if hash.nil? || hash['attributes'].nil? || hash['attributes'].empty?

  hash['attributes'].each do |key, value|
    data_type = nil
    default = nil
    if value.is_a?(String)
      data_type = value
    elsif value.is_a?(Hash)
      data_type = value['type'] unless value['type'].nil?
      default   = value['value'] unless value['value'].nil?
    end
    data_type = [data_type] unless data_type.nil? || data_type.is_a?(Array)
    params_hash[key] = { types: data_type, default: default }
  end

  params_hash
end
find_ruby_ast_node(ast_node, recurse = false) { |ast_node| ... } click to toggle source

Find the first Ruby AST node within an AST Tree, optionally recursively. Returns nil of none could be found

@param [YARD::Parser::Ruby::AstNode] ast_node The root AST node object to inspect @param [Boolean] recurse Whether to search the tree recursively. Defaults to false @yieldparam [YARD::Parser::Ruby::AstNode] ast_node The AST Node that should be checked @yieldreturn [Boolean] Whether the node was what was searched for @return [YARD::Parser::Ruby::AstNode, nil]

# File lib/puppet-strings/yard/handlers/ruby/data_type_handler.rb, line 104
def find_ruby_ast_node(ast_node, recurse = false, &block)
  raise ArgumentError, 'find_ruby_ast_node requires a block' unless block

  is_found = yield ast_node
  return ast_node if is_found

  if ast_node.respond_to?(:children)
    ast_node.children.each do |child_node|
      child_found = find_ruby_ast_node(child_node, recurse, &block)
      return child_found unless child_found.nil?
    end
  end
  nil
end
get_datatype_yard_object(name) click to toggle source
# File lib/puppet-strings/yard/handlers/ruby/data_type_handler.rb, line 45
def get_datatype_yard_object(name)
  # Have to guess the path - if we create the object to get the true path from the code,
  # it also shows up in the .at call - self registering?
  guess_path = "puppet_data_types::#{name}"
  object = YARD::Registry.at(guess_path)

  return object unless object.nil?

  # Didn't find, create instead
  object = PuppetStrings::Yard::CodeObjects::DataType.new(name)
  register object
  object
end
validate_function_method!(object, meth, actual_function) click to toggle source

Validates and automatically fixes a single yard @method! Used by the validate_methods! method.

# File lib/puppet-strings/yard/handlers/ruby/data_type_handler.rb, line 373
def validate_function_method!(object, meth, actual_function)
  # Remove extra params
  if meth.docstring.tags(:param).count > actual_function[:param_types].count
    index = 0
    meth.docstring.delete_tag_if do |tag|
      if tag.tag_name == 'param'
        index += 1
        if index > actual_function[:param_types].count
          log.warn "The @param tag for '#{tag.name}' should not exist for function " \
                   "'#{meth.name}' that is defined near #{object.file}:#{object.line}. " \
                   "Expected only #{actual_function[:param_types].count} parameter/s"
          true
        else
          false
        end
      else
        false
      end
    end
  end

  # Add missing params
  if meth.docstring.tags(:param).count < actual_function[:param_types].count
    start = meth.docstring.tags(:param).count + 1
    (start..actual_function[:param_types].count).each do |param_type_index| # Using 1-based index here instead of usual zero
      meth.add_tag(YARD::Tags::Tag.new(:param, '', actual_function[:param_types][param_type_index - 1], "param#{param_type_index}"))
    end
  end

  # Ensure the parameter types are correct
  meth.docstring.tags(:param).each_with_index do |tag, actual_type_index|
    actual_types = [actual_function[:param_types][actual_type_index]]
    if tag.types != actual_types
      log.warn "The @param tag for '#{tag.name}' for function '#{meth.name}' has a different type definition than the actual function near #{object.file}:#{object.line}. Expected #{actual_types}"
      tag.types = actual_types
    end
  end
end
validate_methods!(object, actual_functions_hash) click to toggle source

Validates and automatically fixes yard @method! tags for the data type

# File lib/puppet-strings/yard/handlers/ruby/data_type_handler.rb, line 311
def validate_methods!(object, actual_functions_hash)
  actual_func_names = actual_functions_hash.keys
  tagged_func_names = object.meths.map { |meth| meth.name.to_s }

  # Log any errors
  # Find functions which are not documented
  (actual_func_names - tagged_func_names).each do |item|
    log.warn "Missing @!method tag for function '#{item}' near #{object.file}:#{object.line}."
  end
  # Find functions which are not defined
  (tagged_func_names - actual_func_names).each do |item|
    log.warn "The @!method tag for '#{item}' has no matching function definition near #{object.file}:#{object.line}."
  end
  # Functions with the wrong return type
  object.meths.each do |meth|
    next unless actual_func_names.include?(meth.name.to_s)

    return_tag = meth.docstring.tag(:return)
    next if return_tag.nil?

    actual_return_types = [actual_functions_hash[meth.name.to_s][:return_type]]
    next if return_tag.types == actual_return_types

    log.warn "The @return tag for '#{meth.name}' has a different type definition than the actual function near #{object.file}:#{object.line}. Expected #{actual_return_types}"
    return_tag.types = actual_return_types
  end

  # Automatically fix missing methods
  (actual_func_names - tagged_func_names).each do |name|
    object.add_function(name, actual_functions_hash[name][:return_type], actual_functions_hash[name][:param_types])
  end
  # Remove extra methods. Can't use `meths` as that's a derived property
  object.children.reject! { |child| child.is_a?(YARD::CodeObjects::MethodObject) && !actual_func_names.include?(child.name.to_s) }

  # Add the return type for the methods if missing
  object.meths.each do |meth|
    next unless meth.docstring.tag(:return).nil?

    meth.docstring.add_tag(YARD::Tags::Tag.new(:return, '', actual_functions_hash[meth.name.to_s][:return_type]))
  end

  # Sync the method properties and add the return type for the methods if missing
  object.meths.each do |meth|
    validate_function_method!(object, meth, actual_functions_hash[meth.name.to_s])
    next unless meth.docstring.tag(:return).nil?

    meth.docstring.add_tag(YARD::Tags::Tag.new(:return, '', actual_functions_hash[meth.name.to_s][:return_type]))
  end

  # The default meth.signature assumes ruby invocation (e.g. def meth(...)) but this doesn't make sense for a
  # Puppet Data Type function invocation. So instead we derive a signature from the method definition.
  object.meths.each do |meth|
    params = ''
    params += "(#{meth.docstring.tags(:param).map(&:name).join(', ')})" unless meth.docstring.tags(:param).empty?
    meth.signature = "#{object.name}.#{meth.name}" + params
  end

  nil
end
validate_param_tags!(object, actual_params_hash) click to toggle source

Validates and automatically fixes yard @param tags for the data type

# File lib/puppet-strings/yard/handlers/ruby/data_type_handler.rb, line 273
def validate_param_tags!(object, actual_params_hash)
  actual_param_names = actual_params_hash.keys
  tagged_param_names = object.tags(:param).map(&:name)
  # Log any errors
  # Find attributes which are not documented
  (actual_param_names - tagged_param_names).each do |item|
    log.warn "Missing @param tag for attribute '#{item}' near #{object.file}:#{object.line}."
  end
  # Find param tags with no matching attribute
  (tagged_param_names - actual_param_names).each do |item|
    log.warn "The @param tag for '#{item}' has no matching attribute near #{object.file}:#{object.line}."
  end
  # Find param tags with a type that is different from the actual definition
  object.tags(:param).reject { |tag| tag.types.nil? }.each do |tag|
    next if actual_params_hash[tag.name].nil?

    actual_data_type = actual_params_hash[tag.name][:types]
    next if actual_data_type.nil?

    log.warn "The @param tag for '#{tag.name}' has a different type definition than the actual attribute near #{object.file}:#{object.line}." if tag.types != actual_data_type
  end

  # Automatically fix missing @param tags
  (actual_param_names - tagged_param_names).each do |name|
    object.add_parameter(name, actual_params_hash[name][:types], actual_params_hash[name][:default])
  end
  # Remove extra param tags
  object.docstring.delete_tag_if { |item| item.tag_name == 'param' && !actual_param_names.include?(item.name) }

  # Set the type in the param tag
  object.tags(:param).each do |tag|
    next if actual_params_hash[tag.name].nil?

    tag.types = actual_params_hash[tag.name][:types]
  end
end