{"record":{"id":"9a47bb86a654426f","repo":"puppetlabs/puppet","slug":"illegal-legacy-function-definition-the-code-loade","errorCode":null,"errorMessage":"Illegal legacy function definition! The code loaded from %{source_ref} did not return the result of calling 'newfunction'. Got '%{klass}'","messagePattern":"Illegal legacy function definition! The code loaded from %(.+?) did not return the result of calling 'newfunction'\\. Got '%(.+?)'","errorType":"exception","errorClass":"ArgumentError","httpStatus":null,"severity":"error","filePath":"lib/puppet/pops/loader/ruby_legacy_function_instantiator.rb","lineNumber":47,"sourceCode":"    # make the private loader available in a binding to allow it to be passed on\n    loader_for_function = loader.private_loader\n    here = get_binding(loader_for_function)\n\n    # Avoid reloading the function if already loaded via one of the APIs that trigger 3x function loading\n    # Check if function is already loaded the 3x way (and obviously not the 4x way since we would not be here in the\n    # first place.\n    environment = Puppet.lookup(:current_environment)\n    func_info = Puppet::Parser::Functions.environment_module(environment).get_function_info(typed_name.name.to_sym)\n    if func_info.nil?\n      # This will do the 3x loading and define the \"function_<name>\" and \"real_function_<name>\" methods\n      # in the anonymous module used to hold function definitions.\n      #\n      func_info = eval(ruby_code_string, here, source_ref, 1) # rubocop:disable Security/Eval\n\n      # Validate what was loaded\n      unless func_info.is_a?(Hash)\n        # TRANSLATORS - the word 'newfunction' should not be translated as it is a method name.\n        raise ArgumentError, _(\"Illegal legacy function definition! The code loaded from %{source_ref} did not return the result of calling 'newfunction'. Got '%{klass}'\") % { source_ref: source_ref, klass: func_info.class }\n      end\n\n      unless func_info[:name] == \"function_#{typed_name.name()}\"\n        raise ArgumentError, _(\"The code loaded from %{source_ref} produced mis-matched name, expected 'function_%{type_name}', got '%{created_name}'\") % {\n          source_ref: source_ref, type_name: typed_name.name, created_name: func_info[:name]\n        }\n      end\n    end\n\n    created = Puppet::Functions::Function3x.create_function(typed_name.name(), func_info, loader_for_function)\n\n    # create the function instance - it needs closure (scope), and loader (i.e. where it should start searching for things\n    # when calling functions etc.\n    # It should be bound to global scope\n\n    # Sets closure scope to nil, to let it be picked up at runtime from Puppet.lookup(:global_scope)\n    # If function definition used the loader from the binding to create a new loader, that loader wins\n    created.new(nil, loader_for_function)","sourceCodeStart":29,"sourceCodeEnd":65,"githubUrl":"https://github.com/puppetlabs/puppet/blob/e227c27540975c25aa22d533a52424a9d2fc886a/lib/puppet/pops/loader/ruby_legacy_function_instantiator.rb#L29-L65","documentation":"Raised by RubyLegacyFunctionInstantiator after eval'ing legacy function code when the eval result is not a Hash. newfunction returns a Hash of function info (name, arity, type, block), so the file's last expression must be the newfunction call itself; any other final value fails here. This runs only when the function was not already loaded through another 3x API (func_info was nil).","triggerScenarios":"`eval(ruby_code_string, ...)` does not return a Hash: statements after the newfunction call (requires, logging, assignments) change the file's value, or the newfunction call is inside a conditional.","commonSituations":"Trailing require/load statements or debug output after the definition; refactors that leave a different final expression; partially converted files.","solutions":["Make Puppet::Parser::Functions.newfunction(...) the last expression in the file","Move requires and other statements above the newfunction call","Use the reported 'Got' class to identify what the file now returns"],"exampleFix":"# before\nmodule Puppet::Parser::Functions\n  newfunction(:double, :type => :rvalue) { |a| a[0] * 2 }\nend\nrequire 'English'\n\n# after\nrequire 'English'\nmodule Puppet::Parser::Functions\n  newfunction(:double, :type => :rvalue) { |a| a[0] * 2 }\nend","handlingStrategy":"try-catch","validationCode":null,"typeGuard":"def legacy_function_info?(obj)\n  obj.is_a?(Hash) && obj.key?(:name) && obj.key?(:arity)\nend","tryCatchPattern":"begin\n  fn = loader.load_typed(typed_name)\nrescue ArgumentError => e\n  raise unless e.message.include?(\"did not return the result of calling 'newfunction'\")\n  warn \"legacy file does not end with newfunction call: #{e.message}\"\nend","preventionTips":["Make newfunction the final expression of the file","Move requires and logging above the definition block","Re-test legacy functions after any edit, since they are eval'd once per process"],"tags":["puppet","legacy-function","ruby","loader","eval"],"backgroundTag":"puppet-legacy-function-load-validation","analyzedSha":"e227c27540975c25aa22d533a52424a9d2fc886a","analyzedAt":"2026-08-21T20:49:46.650Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}