{"record":{"id":"97ac5ad63c71a2f6","repo":"puppetlabs/puppet","slug":"given-argument-must-be-a-hash","errorCode":null,"errorMessage":"Given argument must be a Hash","messagePattern":"Given argument must be a Hash","errorType":"validation","errorClass":"ArgumentError","httpStatus":null,"severity":"error","filePath":"lib/puppet/info_service/class_information_service.rb","lineNumber":19,"sourceCode":"# frozen_string_literal: true\n\nrequire_relative '../../puppet'\nrequire_relative '../../puppet/pops'\nrequire_relative '../../puppet/pops/evaluator/json_strict_literal_evaluator'\n\nclass Puppet::InfoService::ClassInformationService\n  def initialize\n    @file_to_result = {}\n    @parser = Puppet::Pops::Parser::EvaluatingParser.new()\n  end\n\n  def classes_per_environment(env_file_hash)\n    # In this version of puppet there is only one way to parse manifests, as feature switches per environment\n    # are added or removed, this logic needs to change to compute the result per environment with the correct\n    # feature flags in effect.\n\n    unless env_file_hash.is_a?(Hash)\n      raise ArgumentError, _('Given argument must be a Hash')\n    end\n\n    result = {}\n\n    # for each environment\n    #   for each file\n    #     if file already processed, use last result or error\n    #\n    env_file_hash.each do |env, files|\n      env_result = result[env] = {}\n      files.each do |f|\n        env_result[f] = result_of(f)\n      end\n    end\n    result\n  end\n\n  private","sourceCodeStart":1,"sourceCodeEnd":37,"githubUrl":"https://github.com/puppetlabs/puppet/blob/e227c27540975c25aa22d533a52424a9d2fc886a/lib/puppet/info_service/class_information_service.rb#L1-L37","documentation":"Puppet::InfoService::ClassInformationService#classes_per_environment expects a Hash mapping environment name(s) to a list of manifest files. Any other top-level type (Array, String, nil) raises ArgumentError immediately. This service backs 'puppet parser' / face APIs and PuppetDB/Puppet Server class-info queries, so callers marshalling parameters incorrectly (e.g., a bare file list) hit this guard.","triggerScenarios":"Calling Puppet::InfoService::ClassInformationService.new.classes_per_environment(['/path/site.pp']) or (nil) or ('production') instead of { 'production' => ['/path/site.pp'] }; JSON API wrappers that unwrap the payload one level too many and forward an array.","commonSituations":"Custom tooling or puppetserver routing wrappers that construct the argument from JSON and assume a list; refactorings that change the service signature's shape; exploratory scripts iterating files directly.","solutions":["Pass a Hash of environment => file list: { 'production' => ['/etc/puppetlabs/code/environments/production/site.pp'] }","If you have a flat file list, wrap it: files.each_with_object({}) { |f,h| (h['production'] ||= []) << f }","Add a type check at the caller boundary and log the received class for debugging","See the face/tool docs for classes_per_environment for the canonical payload shape"],"exampleFix":"# before\nsvc.classes_per_environment(['/etc/puppetlabs/code/site.pp'])\n# => ArgumentError: Given argument must be a Hash\n\n# after\nsvc.classes_per_environment(\n  'production' => ['/etc/puppetlabs/code/environments/production/site.pp']\n)","handlingStrategy":"type-guard","validationCode":"raise ArgumentError, \"expected Hash, got #{arg.class}\" unless arg.is_a?(Hash)\narg.each_value { |files| raise ArgumentError, 'values must be file lists' unless files.is_a?(Array) }","typeGuard":"def env_file_hash?(arg)\n  arg.is_a?(Hash) && arg.values.all? { |v| v.is_a?(Array) && v.all? { |f| f.is_a?(String) } }\nend","tryCatchPattern":null,"preventionTips":["Document the {env => [files]} shape at every API boundary and validate at ingress","Write a small contract test that feeds wrong shapes and asserts ArgumentError","When forwarding JSON payloads, validate with a schema before calling the service"],"tags":["puppet","info-service","argument-validation","api-contract"],"backgroundTag":"invalid-argument-type","analyzedSha":"e227c27540975c25aa22d533a52424a9d2fc886a","analyzedAt":"2026-08-21T20:49:46.650Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}