diff --git a/lib/puppet_x/puppetlabs/strings/yard/handlers/puppet_4x_function_handler.rb b/lib/puppet_x/puppetlabs/strings/yard/handlers/puppet_4x_function_handler.rb index 27d6ebf..cd373d9 100644 --- a/lib/puppet_x/puppetlabs/strings/yard/handlers/puppet_4x_function_handler.rb +++ b/lib/puppet_x/puppetlabs/strings/yard/handlers/puppet_4x_function_handler.rb @@ -23,32 +23,100 @@ class Puppet4xFunctionHandler < YARD::Handlers::Ruby::Base handles method_call(:create_function) + # Given a command node which represents code like this: + # param 'Optional[Type]', :value_type + # Extract the type name and type signature and return them as a array. + def extract_type_from_command command + return [] if command.children.length < 2 or command.children[1].children.length < 2 + type_specifier = command.children[1] + # the parameter signature is the first child of the specifier and an + # identifier. Convert it to a string. + param_signature = type_specifier.children[0].source + # The parameter name is the second child of the specifier and a symbol. + # convert it to a string. + param_name_ident = type_specifier.jump :ident + return [] if param_name_ident == type_specifier + param_name = param_name_ident.source + [param_name, param_signature] + end + process do name = process_parameters method_arguments = [] # To attach the method parameters to the new code object, traverse the - # ruby AST until a node is found which defines a list of parameters. + # ruby AST until a node is found which defines a array of parameters. # Then, traverse the children of the parameters, storing each identifier - # in the list of method arguments. + # in the array of method arguments. obj = MethodObject.new(function_namespace, name) do |o| + end + + # overload_signatures is a array of arrays of tuples or empty: + # overload_signatures = [ + # [ # 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'] + # ] + # ] + overload_signatures = [] + statement.traverse do |node| + # Find all of the dispatch methods + if node.type == :ident and node.source == 'dispatch' + command = node.parent + do_block = command.jump :do_block + # If the command doesn't have a do_block we can't extract type info + if do_block == command + next + end + # 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)] + end + 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 + # name of the function being created. + if overload_signatures.length == 0 statement.traverse do |node| - if node.type == :params - node.traverse do |params| - if params.type == :ident - # FIXME: Replace nil with parameter types - method_arguments << [params[0], nil] + # Find the function definition with the same name as the puppet + # function being created. + if (node.type == :def and node.children.first.type == :ident and + node.children.first.source == obj.name.to_s) + signature = [] + # Find its parameters. If they don't exist, fine + params = node.jump :params + break if params == node + params.traverse do |param| + if param.type == :ident + # The parameters of Puppet functions with no defined dispatch are + # as though they are Any type. + signature << [param[0], 'Any'] end end - + overload_signatures <<= signature # Now that the parameters have been found, break out of the traversal break end end end - obj.parameters = method_arguments + # Preserve this type information. We'll need it later when we look + # 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) obj['puppet_4x_function'] = true @@ -102,7 +170,7 @@ class Puppet4xFunctionHandler < YARD::Handlers::Ruby::Base # @return [(String, Hash{String => String})] def process_parameters # Passing `false` to parameters excludes the block param from the returned - # list. + # array. name, _ = statement.parameters(false).compact name = process_element(name) @@ -114,7 +182,7 @@ class Puppet4xFunctionHandler < YARD::Handlers::Ruby::Base # instead of `<<-`. HEREDOC_START = /^= num_b ? num_a : num_b + end + end + RUBY + }.to output("").to_stdout_from_any_process + end + + it "should issue a warning when there are parametarized types and parameter names differ" do + expected_output_not_num_a = "[warn]: @param tag has unknown parameter" + + " name: not_num_a \n in file `(stdin)' near line 3" + expect { + parse <<-RUBY + # @param not_num_a Integer[1,2] the first number to be compared + # @param num_b Integer[1,2] the second number to be compared + Puppet::Functions.create_function(:max) do + dispatch max_1 do + param 'Integer[1,2]', :num_a + param 'Integer[1,2]', :num_b + end + + def max_1(num_a, num_b) + num_a >= num_b ? num_a : num_b + end + end + RUBY + }.to output("#{expected_output_not_num_a}\n").to_stdout_from_any_process + end - it "should issue a warning if the parameter names do not match the " + - "docstring in dispatch method" do + it "should issue a warning if the parameter names do not match the docstring in dispatch method" do expected_output_not_a_param = "[warn]: @param tag has unknown parameter" + " name: not_a_param \n in file `(stdin)' near line 3" expected_output_also_not_a_param = "[warn]: @param tag has unknown " + @@ -101,13 +133,13 @@ describe PuppetX::PuppetLabs::Strings::YARD::Handlers::Puppet4xFunctionHandler d # @param also_not_a_param Integer the second number to be compared Puppet::Functions.create_function(:max) do dispatch max_1 do - param 'Integer', :num_a + param 'Integer[1,2]', :num_a param 'Integer', :num_b end dispatch max_2 { param 'String', :num_c - param 'String', :num_d + param 'String[1,2]', :num_d } def max_1(num_a, num_b)