(PDOC-21) Typecheck documented parameters
The documented parameter types should be compared with the actual types when possible.
This commit is contained in:
parent
2e3821c2af
commit
4653a5f9f0
|
@ -2,4 +2,6 @@ class PuppetX::PuppetLabs::Strings::YARD::CodeObjects::DefinedTypeObject < Puppe
|
|||
# A list of parameters attached to this class.
|
||||
# @return [Array<Array(String, String)>]
|
||||
attr_accessor :parameters
|
||||
attr_accessor :type_info
|
||||
|
||||
end
|
||||
|
|
|
@ -2,6 +2,8 @@ class PuppetX::PuppetLabs::Strings::YARD::CodeObjects::HostClassObject < PuppetX
|
|||
# The {HostClassObject} that this class inherits from, if any.
|
||||
# @return [HostClassObject, Proxy, nil]
|
||||
attr_accessor :parent_class
|
||||
attr_accessor :type_info
|
||||
|
||||
|
||||
# NOTE: `include_mods` is never used as it makes no sense for Puppet, but
|
||||
# this is called by `YARD::Registry` and it will pass a parameter.
|
||||
|
|
|
@ -5,6 +5,8 @@ class PuppetX::PuppetLabs::Strings::YARD::CodeObjects::PuppetNamespaceObject < Y
|
|||
[self]
|
||||
end
|
||||
|
||||
attr_accessor :type_info
|
||||
|
||||
# FIXME: We used to override `self.new` to ensure no YARD proxies were
|
||||
# created for namespaces segments that did not map to a host class or
|
||||
# defined type. Fighting the system in this way turned out to be
|
||||
|
|
|
@ -8,6 +8,23 @@ class PuppetX::PuppetLabs::Strings::YARD::Handlers::HostClassHandler < PuppetX::
|
|||
param_tuple << ( a[1].nil? ? nil : a[1].source )
|
||||
end
|
||||
end
|
||||
tp = Puppet::Pops::Types::TypeParser.new
|
||||
param_type_info = {}
|
||||
statement.pops_obj.parameters.each do |pop_param|
|
||||
# If the parameter's type expression is nil, default to Any
|
||||
if pop_param.type_expr == nil
|
||||
param_type_info[pop_param.name] = Puppet::Pops::Types::TypeFactory.any()
|
||||
else
|
||||
begin
|
||||
param_type_info[pop_param.name] = tp.interpret_any(pop_param.type_expr)
|
||||
rescue Puppet::ParseError
|
||||
# If the type could not be interpreted, default to type Any
|
||||
param_type_info[pop_param.name] = "TypeError - " +
|
||||
"#{pop_param.type_expr} isn't a valid type."
|
||||
end
|
||||
end
|
||||
end
|
||||
obj.type_info = [param_type_info]
|
||||
|
||||
statement.pops_obj.tap do |o|
|
||||
if o.parent_class
|
||||
|
|
|
@ -52,19 +52,24 @@ class Puppet4xFunctionHandler < YARD::Handlers::Ruby::Base
|
|||
obj = MethodObject.new(function_namespace, name) do |o|
|
||||
end
|
||||
|
||||
# overload_signatures is a array of arrays of tuples or empty:
|
||||
# The data structure for overload_signatures is an array of hashes. Each
|
||||
# hash represents the arguments a single function dispatch (aka overload)
|
||||
# can take.
|
||||
# overload_signatures = [
|
||||
# [ # First function dispatch arguments
|
||||
# { # First function dispatch arguments
|
||||
# # argument name, argument type
|
||||
# ['arg0', 'Variant[String,Array[String]]'
|
||||
# ['arg1', 'Optional[Type]']
|
||||
# ],
|
||||
# [ # Second function dispatch arguments
|
||||
# ['arg0', 'Variant[String,Array[String]]'
|
||||
# ['arg1', 'Optional[Type]']
|
||||
# ['arg2', 'Any']
|
||||
# ]
|
||||
# 'arg0': 'Variant[String,Array[String]]',
|
||||
# 'arg1': 'Optional[Type]'
|
||||
# },
|
||||
# { # Second function dispatch arguments
|
||||
# 'arg0': 'Variant[String,Array[String]]',
|
||||
# 'arg1': 'Optional[Type]',
|
||||
# 'arg2': 'Any'
|
||||
# }
|
||||
# ]
|
||||
# Note that the order for arguments to a function doesn't actually matter
|
||||
# because we allow users flexibility when listing their arguments in the
|
||||
# comments.
|
||||
overload_signatures = []
|
||||
statement.traverse do |node|
|
||||
# Find all of the dispatch methods
|
||||
|
@ -75,16 +80,23 @@ class Puppet4xFunctionHandler < YARD::Handlers::Ruby::Base
|
|||
if do_block == command
|
||||
next
|
||||
end
|
||||
signature = {}
|
||||
# Iterate through each of the children of the do block and build
|
||||
# tuples of parameter names and parameter type signatures
|
||||
do_block.children.first.children.each do |child|
|
||||
overload_signatures <<= [extract_type_from_command(child)]
|
||||
name, type = extract_type_from_command(child)
|
||||
# This can happen if there is a function or something we aren't
|
||||
# expecting.
|
||||
if name != nil and type != nil
|
||||
signature[name] = type
|
||||
end
|
||||
end
|
||||
overload_signatures <<= signature
|
||||
end
|
||||
end
|
||||
|
||||
# If the overload_signatures array is empty because we couldn't find any
|
||||
# dispatch blocks, then there must be one method named the same as the
|
||||
# If the overload_signatures list is empty because we couldn't find any
|
||||
# dispatch blocks, then there must be one function named the same as the
|
||||
# name of the function being created.
|
||||
if overload_signatures.length == 0
|
||||
statement.traverse do |node|
|
||||
|
@ -92,7 +104,7 @@ class Puppet4xFunctionHandler < YARD::Handlers::Ruby::Base
|
|||
# function being created.
|
||||
if (node.type == :def and node.children.first.type == :ident and
|
||||
node.children.first.source == obj.name.to_s)
|
||||
signature = []
|
||||
signature = {}
|
||||
# Find its parameters. If they don't exist, fine
|
||||
params = node.jump :params
|
||||
break if params == node
|
||||
|
@ -100,7 +112,7 @@ class Puppet4xFunctionHandler < YARD::Handlers::Ruby::Base
|
|||
if param.type == :ident
|
||||
# The parameters of Puppet functions with no defined dispatch are
|
||||
# as though they are Any type.
|
||||
signature << [param[0], 'Any']
|
||||
signature[param[0]] = 'Any'
|
||||
end
|
||||
end
|
||||
overload_signatures <<= signature
|
||||
|
@ -114,9 +126,9 @@ class Puppet4xFunctionHandler < YARD::Handlers::Ruby::Base
|
|||
# at the docstring.
|
||||
obj.type_info = overload_signatures
|
||||
|
||||
# The yard docstring parser expects a array of arrays, not a array of
|
||||
# arrays of arrays.
|
||||
obj.parameters = overload_signatures.flatten(1)
|
||||
# The yard docstring parser expects a list of lists, not a list of lists of
|
||||
# lists.
|
||||
obj.parameters = overload_signatures.map { |sig| sig.to_a }.flatten(1)
|
||||
|
||||
obj['puppet_4x_function'] = true
|
||||
|
||||
|
|
|
@ -6,6 +6,10 @@ def init
|
|||
|
||||
@template_helper = TemplateHelper.new
|
||||
@template_helper.check_parameters_match_docs object
|
||||
params = object.parameters.map { |param| param.first }
|
||||
param_tags = object.tags.find_all{ |tag| tag.tag_name == "param"}
|
||||
param_details = @template_helper.extract_param_details(params, param_tags) unless params.nil?
|
||||
@template_helper.check_types_match_docs object, param_details
|
||||
end
|
||||
|
||||
def subclasses
|
||||
|
|
|
@ -76,6 +76,7 @@ def method_details_list
|
|||
params = parameters.nil? ? nil : parameters[1].split(/\s*,\s*/)
|
||||
|
||||
param_details = @template_helper.extract_param_details(params, param_tags) unless params.nil?
|
||||
@template_helper.check_types_match_docs object, param_details
|
||||
else
|
||||
param_details = @template_helper.comment_only_param_details(param_tags)
|
||||
end
|
||||
|
|
|
@ -105,6 +105,63 @@ class TemplateHelper
|
|||
parameter_info
|
||||
end
|
||||
|
||||
# Check that any types specified in the docstrings match the actual method
|
||||
# types. This is used by puppet 4x functions and defined types.
|
||||
# @param object the code object to examine for parameters names
|
||||
def check_types_match_docs(object, params_hash)
|
||||
# We'll need this to extract type info from the type specified by the
|
||||
# docstring.
|
||||
type_parser = Puppet::Pops::Types::TypeParser.new
|
||||
type_calculator = Puppet::Pops::Types::TypeCalculator.new
|
||||
|
||||
object.type_info.each do |function|
|
||||
function.keys.each do |key|
|
||||
if function[key].class == String
|
||||
begin
|
||||
instantiated = type_parser.parse function[key].gsub(/'/, '').gsub(/"/, "")
|
||||
rescue Puppet::ParseError
|
||||
# Likely the result of a malformed type
|
||||
next
|
||||
end
|
||||
else
|
||||
instantiated = function[key]
|
||||
end
|
||||
params_hash.each do |param|
|
||||
if param[:name] == key and param[:types] != nil
|
||||
param[:types].each do |type|
|
||||
param_instantiated = type_parser.parse type
|
||||
if not type_calculator.assignable? instantiated, param_instantiated
|
||||
actual_types = object.type_info.map do |sig|
|
||||
sig[key]
|
||||
end
|
||||
# Get the locations where the object can be found. We only care about
|
||||
# the first one.
|
||||
locations = object.files
|
||||
# If the locations aren't in the shape we expect then report that
|
||||
# the file number couldn't be determined.
|
||||
if locations.length >= 1 and locations[0].length == 2
|
||||
file = locations[0][0]
|
||||
line = locations[0][1]
|
||||
warning = "@param tag types do not match the code. The " +
|
||||
"#{param[:name]} parameter is declared as types #{param[:types]} in " +
|
||||
"the docstring, but the code specifies the types " +
|
||||
"#{actual_types.inspect} in file #{file} near line #{line}"
|
||||
else
|
||||
warning = "@param tag types do not match the code. The " +
|
||||
"#{param[:name]} parameter is declared as types #{param[:types]} in " +
|
||||
"the docstring, but the code specifies the types " +
|
||||
"#{actual_types.inspect} Sorry, the file and line number could" +
|
||||
"not be determined."
|
||||
end
|
||||
log.warn warning
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Check that the actual function parameters match what is stated in the docs.
|
||||
# If there is a mismatch, print a warning to stderr.
|
||||
# This is necessary for puppet classes and defined types. This type of
|
||||
|
|
|
@ -129,8 +129,8 @@ describe PuppetX::PuppetLabs::Strings::YARD::Handlers::Puppet4xFunctionHandler d
|
|||
"parameter name: also_not_a_param \n in file `(stdin)' near line 3"
|
||||
expect {
|
||||
parse <<-RUBY
|
||||
# @param not_a_param Integer the first number to be compared
|
||||
# @param also_not_a_param Integer the second number to be compared
|
||||
# @param not_a_param [Integer] the first number to be compared
|
||||
# @param also_not_a_param [Integer] the second number to be compared
|
||||
Puppet::Functions.create_function(:max) do
|
||||
dispatch max_1 do
|
||||
param 'Integer[1,2]', :num_a
|
||||
|
@ -158,8 +158,8 @@ describe PuppetX::PuppetLabs::Strings::YARD::Handlers::Puppet4xFunctionHandler d
|
|||
"docstring in dispatch method" do
|
||||
expect {
|
||||
parse <<-RUBY
|
||||
# @param num_a Integer the first number to be compared
|
||||
# @param num_b Integer the second number to be compared
|
||||
# @param [Integer] num_a the first number to be compared
|
||||
# @param num_b [Integer] the second number to be compared
|
||||
Puppet::Functions.create_function(:max) do
|
||||
dispatch max_1 do
|
||||
param 'Integer', :num_a
|
||||
|
|
|
@ -56,4 +56,70 @@ describe TemplateHelper do
|
|||
obj2.docstring.instance_variable_set("@tags", [])
|
||||
end
|
||||
|
||||
it "should issue a warning if the parameter types do not match the docstring in dispatch method" do
|
||||
expected_output_not_a_param = "[warn]: @param tag types do not match the" +
|
||||
" code. The arg1 parameter is declared as types [\"Integer\"] in the " +
|
||||
"docstring, but the code specifies the types [\"Optional[String]\"] " +
|
||||
"in file test near line 0\n"
|
||||
object = PuppetX::PuppetLabs::Strings::YARD::CodeObjects::PuppetNamespaceObject.new(:root, 'Puppet4xFunctions')
|
||||
object.files = [['test', 0]]
|
||||
object.type_info = [{
|
||||
'arg0' => 'Variant[String,Array[String]]',
|
||||
'arg1' => 'Optional[String]'
|
||||
}]
|
||||
param_details = [{
|
||||
:name => 'arg0',
|
||||
:types => ['Variant[String,Array[String]]']
|
||||
},
|
||||
{
|
||||
:name => 'arg1',
|
||||
:types => ['Integer']
|
||||
}]
|
||||
template_helper = TemplateHelper.new
|
||||
expect {
|
||||
template_helper.check_types_match_docs(object, param_details)
|
||||
}.to output(expected_output_not_a_param).to_stdout_from_any_process
|
||||
end
|
||||
|
||||
it "should not issue a warning if the parameter types do match the docstring in dispatch method" do
|
||||
object = PuppetX::PuppetLabs::Strings::YARD::CodeObjects::PuppetNamespaceObject.new(:root, 'Puppet4xFunctions')
|
||||
object.files = [['test', 0]]
|
||||
object.type_info = [{
|
||||
'arg0' => 'Variant[String,Array[String]]',
|
||||
'arg1' => 'Optional[String]'
|
||||
}]
|
||||
param_details = [{
|
||||
:name => 'arg0',
|
||||
:types => ['Variant[String,Array[String]]']
|
||||
},
|
||||
{
|
||||
:name => 'arg1',
|
||||
:types => ['Optional[String]']
|
||||
}]
|
||||
template_helper = TemplateHelper.new
|
||||
expect {
|
||||
template_helper.check_types_match_docs(object, param_details)
|
||||
}.to output("").to_stdout_from_any_process
|
||||
end
|
||||
|
||||
it "should not issue a warning if the types in the docstring in dispatch method are assignable to parameter types" do
|
||||
object = PuppetX::PuppetLabs::Strings::YARD::CodeObjects::PuppetNamespaceObject.new(:root, 'Puppet4xFunctions')
|
||||
object.files = [['test', 0]]
|
||||
object.type_info = [{
|
||||
'arg0' => 'Variant[String,Array[String]]',
|
||||
'arg1' => 'Optional[String]'
|
||||
}]
|
||||
param_details = [{
|
||||
:name => 'arg0',
|
||||
:types => ['Variant[String,Array[String]]']
|
||||
},
|
||||
{
|
||||
:name => 'arg1',
|
||||
:types => ['String']
|
||||
}]
|
||||
template_helper = TemplateHelper.new
|
||||
expect {
|
||||
template_helper.check_types_match_docs(object, param_details)
|
||||
}.to output("").to_stdout_from_any_process
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue