class Dependabot::Bundler::FileParser

Public Instance Methods

parse() click to toggle source
# File lib/dependabot/bundler/file_parser.rb, line 20
def parse
  dependency_set = DependencySet.new
  dependency_set += gemfile_dependencies
  dependency_set += gemspec_dependencies
  dependency_set += lockfile_dependencies
  check_external_code(dependency_set.dependencies)
  instrument_package_manager_version
  dependency_set.dependencies
end

Private Instance Methods

base_directory() click to toggle source
# File lib/dependabot/bundler/file_parser.rb, line 190
def base_directory
  dependency_files.first.directory
end
bundler_version() click to toggle source
# File lib/dependabot/bundler/file_parser.rb, line 315
def bundler_version
  @bundler_version ||= Helpers.bundler_version(lockfile)
end
check_external_code(dependencies) click to toggle source
# File lib/dependabot/bundler/file_parser.rb, line 32
def check_external_code(dependencies)
  return unless @reject_external_code
  return unless git_source?(dependencies)

  # A git source dependency might contain a .gemspec that is evaluated
  raise ::Dependabot::UnexpectedExternalCode
end
check_required_files() click to toggle source
# File lib/dependabot/bundler/file_parser.rb, line 210
def check_required_files
  file_names = dependency_files.map(&:name)

  return if file_names.any? do |name|
    name.end_with?(".gemspec") && !name.include?("/")
  end

  return if gemfile

  raise "A gemspec or Gemfile must be provided!"
end
dependency_version(dependency_name) click to toggle source
# File lib/dependabot/bundler/file_parser.rb, line 222
def dependency_version(dependency_name)
  return unless lockfile

  spec = parsed_lockfile.specs.find { |s| s.name == dependency_name }

  # Not all files in the Gemfile will appear in the Gemfile.lock. For
  # instance, if a gem specifies `platform: [:windows]`, and the
  # Gemfile.lock is generated on a Linux machine, the gem will be not
  # appear in the lockfile.
  return unless spec

  # If the source is Git we're better off knowing the SHA-1 than the
  # version.
  return spec.source.revision if spec.source.instance_of?(::Bundler::Source::Git)

  spec.version
end
evaled_gemfiles() click to toggle source
# File lib/dependabot/bundler/file_parser.rb, line 245
def evaled_gemfiles
  dependency_files.
    reject { |f| f.name.end_with?(".gemspec") }.
    reject { |f| f.name.end_with?(".specification") }.
    reject { |f| f.name.end_with?(".lock") }.
    reject { |f| f.name.end_with?(".ruby-version") }.
    reject { |f| f.name == "Gemfile" }.
    reject { |f| f.name == "gems.rb" }.
    reject { |f| f.name == "gems.locked" }
end
expanded_dependency_names(dep) click to toggle source
# File lib/dependabot/bundler/file_parser.rb, line 274
def expanded_dependency_names(dep)
  spec = parsed_lockfile.specs.find { |s| s.name == dep.name }
  return [dep.name] unless spec

  [
    dep.name,
    *spec.dependencies.flat_map { |d| expanded_dependency_names(d) }
  ]
end
gemfile() click to toggle source
# File lib/dependabot/bundler/file_parser.rb, line 240
def gemfile
  @gemfile ||= get_original_file("Gemfile") ||
               get_original_file("gems.rb")
end
gemfile_dependencies() click to toggle source
# File lib/dependabot/bundler/file_parser.rb, line 57
def gemfile_dependencies
  dependencies = DependencySet.new

  return dependencies unless gemfile

  [gemfile, *evaled_gemfiles].each do |file|
    parsed_gemfile.each do |dep|
      gemfile_declaration_finder =
        GemfileDeclarationFinder.new(dependency: dep, gemfile: file)
      next unless gemfile_declaration_finder.gemfile_includes_dependency?

      dependencies <<
        Dependency.new(
          name: dep.fetch("name"),
          version: dependency_version(dep.fetch("name"))&.to_s,
          requirements: [{
            requirement: gemfile_declaration_finder.enhanced_req_string,
            groups: dep.fetch("groups").map(&:to_sym),
            source: dep.fetch("source")&.transform_keys(&:to_sym),
            file: file.name
          }],
          package_manager: "bundler"
        )
    end
  end

  dependencies
end
gemspec_dependencies() click to toggle source
# File lib/dependabot/bundler/file_parser.rb, line 86
def gemspec_dependencies
  dependencies = DependencySet.new

  gemspecs.each do |gemspec|
    parsed_gemspec(gemspec).each do |dependency|
      dependencies <<
        Dependency.new(
          name: dependency.fetch("name"),
          version: dependency_version(dependency.fetch("name"))&.to_s,
          requirements: [{
            requirement: dependency.fetch("requirement").to_s,
            groups: if dependency.fetch("type") == "runtime"
                      ["runtime"]
                    else
                      ["development"]
                    end,
            source: dependency.fetch("source")&.transform_keys(&:to_sym),
            file: gemspec.name
          }],
          package_manager: "bundler"
        )
    end
  end

  dependencies
end
gemspecs() click to toggle source
# File lib/dependabot/bundler/file_parser.rb, line 302
def gemspecs
  # Path gemspecs are excluded (they're supporting files)
  @gemspecs ||= prepared_dependency_files.
                select { |file| file.name.end_with?(".gemspec") }.
                reject(&:support_file?)
end
git_source?(dependencies) click to toggle source
# File lib/dependabot/bundler/file_parser.rb, line 40
def git_source?(dependencies)
  dependencies.any? do |dep|
    dep.requirements.any? { |req| req.fetch(:source)&.fetch(:type) == "git" }
  end
end
handle_eval_error(err) click to toggle source
# File lib/dependabot/bundler/file_parser.rb, line 163
def handle_eval_error(err)
  msg = "Error evaluating your dependency files: #{err.message}"
  raise Dependabot::DependencyFileNotEvaluatable, msg
end
imported_ruby_files() click to toggle source
# File lib/dependabot/bundler/file_parser.rb, line 309
def imported_ruby_files
  dependency_files.
    select { |f| f.name.end_with?(".rb") }.
    reject { |f| f.name == "gems.rb" }
end
instrument_package_manager_version() click to toggle source
# File lib/dependabot/bundler/file_parser.rb, line 46
def instrument_package_manager_version
  version = Helpers.detected_bundler_version(lockfile)
  Dependabot.instrument(
    Notifications::FILE_PARSER_PACKAGE_MANAGER_VERSION_PARSED,
    ecosystem: "bundler",
    package_managers: {
      "bundler" => version
    }
  )
end
lockfile() click to toggle source
# File lib/dependabot/bundler/file_parser.rb, line 256
def lockfile
  @lockfile ||= get_original_file("Gemfile.lock") ||
                get_original_file("gems.locked")
end
lockfile_dependencies() click to toggle source
# File lib/dependabot/bundler/file_parser.rb, line 113
def lockfile_dependencies
  dependencies = DependencySet.new

  return dependencies unless lockfile

  # Create a DependencySet where each element has no requirement. Any
  # requirements will be added when combining the DependencySet with
  # other DependencySets.
  parsed_lockfile.specs.each do |dependency|
    next if dependency.source.is_a?(::Bundler::Source::Path)

    dependencies <<
      Dependency.new(
        name: dependency.name,
        version: dependency_version(dependency.name)&.to_s,
        requirements: [],
        package_manager: "bundler",
        subdependency_metadata: [{
          production: production_dep_names.include?(dependency.name)
        }]
      )
  end

  dependencies
end
parsed_gemfile() click to toggle source
# File lib/dependabot/bundler/file_parser.rb, line 139
def parsed_gemfile
  @parsed_gemfile ||=
    SharedHelpers.in_a_temporary_repo_directory(base_directory,
                                                repo_contents_path) do
      write_temporary_dependency_files

      NativeHelpers.run_bundler_subprocess(
        bundler_version: bundler_version,
        function: "parsed_gemfile",
        args: {
          gemfile_name: gemfile.name,
          lockfile_name: lockfile&.name,
          dir: Dir.pwd
        }
      )
    end
rescue SharedHelpers::HelperSubprocessFailed => e
  handle_eval_error(e) if e.error_class == "JSON::ParserError"

  msg = e.error_class + " with message: " +
        e.message.force_encoding("UTF-8").encode
  raise Dependabot::DependencyFileNotEvaluatable, msg
end
parsed_gemspec(file) click to toggle source
# File lib/dependabot/bundler/file_parser.rb, line 168
def parsed_gemspec(file)
  @parsed_gemspecs ||= {}
  @parsed_gemspecs[file.name] ||=
    SharedHelpers.in_a_temporary_repo_directory(base_directory,
                                                repo_contents_path) do
      write_temporary_dependency_files

      NativeHelpers.run_bundler_subprocess(
        bundler_version: bundler_version,
        function: "parsed_gemspec",
        args: {
          gemspec_name: file.name,
          lockfile_name: lockfile&.name,
          dir: Dir.pwd
        }
      )
    end
rescue SharedHelpers::HelperSubprocessFailed => e
  msg = e.error_class + " with message: " + e.message
  raise Dependabot::DependencyFileNotEvaluatable, msg
end
parsed_lockfile() click to toggle source
# File lib/dependabot/bundler/file_parser.rb, line 261
def parsed_lockfile
  @parsed_lockfile ||=
    ::Bundler::LockfileParser.new(sanitized_lockfile_content)
end
prepared_dependency_files() click to toggle source
# File lib/dependabot/bundler/file_parser.rb, line 194
def prepared_dependency_files
  @prepared_dependency_files ||=
    FilePreparer.new(dependency_files: dependency_files).
    prepared_dependency_files
end
production?(dependency) click to toggle source
# File lib/dependabot/bundler/file_parser.rb, line 284
def production?(dependency)
  groups = dependency.requirements.
           flat_map { |r| r.fetch(:groups) }.
           map(&:to_s)

  return true if groups.empty?
  return true if groups.include?("runtime")
  return true if groups.include?("default")

  groups.any? { |g| g.include?("prod") }
end
production_dep_names() click to toggle source
# File lib/dependabot/bundler/file_parser.rb, line 266
def production_dep_names
  @production_dep_names ||=
    (gemfile_dependencies + gemspec_dependencies).dependencies.
    select { |dep| production?(dep) }.
    flat_map { |dep| expanded_dependency_names(dep) }.
    uniq
end
sanitized_lockfile_content() click to toggle source

TODO: Stop sanitizing the lockfile once we have bundler 2 installed

# File lib/dependabot/bundler/file_parser.rb, line 297
def sanitized_lockfile_content
  regex = FileUpdater::LockfileUpdater::LOCKFILE_ENDING
  lockfile.content.gsub(regex, "")
end
write_temporary_dependency_files() click to toggle source
# File lib/dependabot/bundler/file_parser.rb, line 200
def write_temporary_dependency_files
  prepared_dependency_files.each do |file|
    path = file.name
    FileUtils.mkdir_p(Pathname.new(path).dirname)
    File.write(path, file.content)
  end

  File.write(lockfile.name, sanitized_lockfile_content) if lockfile
end