class Dependabot::NpmAndYarn::UpdateChecker::VersionResolver

Constants

NPM6_PEER_DEP_ERROR_REGEX

Error message from npm install: react-dom@15.2.0 requires a peer of react@^15.2.0 \ but none is installed. You must install peer dependencies yourself.

NPM7_PEER_DEP_ERROR_REGEX

Error message from npm install: npm ERR! Could not resolve dependency: npm ERR! peer react@“^16.14.0” from react-dom@16.14.0

TIGHTLY_COUPLED_MONOREPOS
YARN_PEER_DEP_ERROR_REGEX

Error message from yarn add: “ > @reach/router@1.2.1” has incorrect \ peer dependency “react@15.x || 16.x || 16.4.0-alpha.0911da3” “ > react-burger-menu@1.9.9” has unmet \ peer dependency “react@>=0.14.0 <16.0.0”.

Attributes

credentials[R]
dependency[R]
dependency_files[R]
latest_allowable_version[R]

Public Class Methods

new(dependency:, credentials:, dependency_files:, latest_allowable_version:, latest_version_finder:) click to toggle source
# File lib/dependabot/npm_and_yarn/update_checker/version_resolver.rb, line 59
def initialize(dependency:, credentials:, dependency_files:,
               latest_allowable_version:, latest_version_finder:)
  @dependency               = dependency
  @credentials              = credentials
  @dependency_files         = dependency_files
  @latest_allowable_version = latest_allowable_version

  @latest_version_finder = {}
  @latest_version_finder[dependency] = latest_version_finder
end

Public Instance Methods

dependency_updates_from_full_unlock() click to toggle source
# File lib/dependabot/npm_and_yarn/update_checker/version_resolver.rb, line 89
def dependency_updates_from_full_unlock
  return if git_dependency?(dependency)
  return updated_monorepo_dependencies if part_of_tightly_locked_monorepo?
  return if newly_broken_peer_reqs_from_dep.any?

  updates = [{
    dependency: dependency,
    version: latest_allowable_version,
    previous_version: latest_resolvable_previous_version(
      latest_allowable_version
    )
  }]
  newly_broken_peer_reqs_on_dep.each do |peer_req|
    dep_name = peer_req.fetch(:requiring_dep_name)
    dep = top_level_dependencies.find { |d| d.name == dep_name }

    # Can't handle reqs from sub-deps or git source deps (yet)
    return nil if dep.nil?
    return nil if git_dependency?(dep)

    updated_version =
      latest_version_of_dep_with_satisfied_peer_reqs(dep)
    return nil unless updated_version

    updates << {
      dependency: dep,
      version: updated_version,
      previous_version: resolve_latest_previous_version(
        dep, updated_version
      )
    }
  end
  updates.uniq
end
latest_resolvable_previous_version(updated_version) click to toggle source
# File lib/dependabot/npm_and_yarn/update_checker/version_resolver.rb, line 85
def latest_resolvable_previous_version(updated_version)
  resolve_latest_previous_version(dependency, updated_version)
end
latest_resolvable_version() click to toggle source
# File lib/dependabot/npm_and_yarn/update_checker/version_resolver.rb, line 70
def latest_resolvable_version
  return latest_allowable_version if git_dependency?(dependency)
  return if part_of_tightly_locked_monorepo?

  return latest_allowable_version unless relevant_unmet_peer_dependencies.any?

  satisfying_versions.first
end
latest_version_resolvable_with_full_unlock?() click to toggle source
# File lib/dependabot/npm_and_yarn/update_checker/version_resolver.rb, line 79
def latest_version_resolvable_with_full_unlock?
  return false if dependency_updates_from_full_unlock.nil?

  true
end

Private Instance Methods

dependency_files_builder() click to toggle source
# File lib/dependabot/npm_and_yarn/update_checker/version_resolver.rb, line 503
def dependency_files_builder
  @dependency_files_builder ||=
    DependencyFilesBuilder.new(
      dependency: dependency,
      dependency_files: dependency_files,
      credentials: credentials
    )
end
error_details_from_captures(captures) click to toggle source
# File lib/dependabot/npm_and_yarn/update_checker/version_resolver.rb, line 287
def error_details_from_captures(captures)
  {
    requirement_name:
      captures.fetch("required_dep").sub(/@[^@]+$/, ""),
    requirement_version:
      captures.fetch("required_dep").split("@").last.gsub('"', ""),
    requiring_dep_name:
      captures.fetch("requiring_dep").sub(/@[^@]+$/, "")
  }
end
fetch_peer_dependency_errors(version:) click to toggle source
# File lib/dependabot/npm_and_yarn/update_checker/version_resolver.rb, line 241
def fetch_peer_dependency_errors(version:)
  # TODO: Add all of the error handling that the FileUpdater does
  # here (since problematic repos will be resolved here before they're
  # seen by the FileUpdater)
  SharedHelpers.in_a_temporary_directory do
    dependency_files_builder.write_temporary_dependency_files

    filtered_package_files.flat_map do |file|
      path = Pathname.new(file.name).dirname
      run_checker(path: path, version: version)
    rescue SharedHelpers::HelperSubprocessFailed => e
      errors = []
      if e.message.match?(NPM6_PEER_DEP_ERROR_REGEX)
        e.message.scan(NPM6_PEER_DEP_ERROR_REGEX) do
          errors << Regexp.last_match.named_captures
        end
      elsif e.message.match?(NPM7_PEER_DEP_ERROR_REGEX)
        e.message.scan(NPM7_PEER_DEP_ERROR_REGEX) do
          errors << Regexp.last_match.named_captures
        end
      elsif e.message.match?(YARN_PEER_DEP_ERROR_REGEX)
        e.message.scan(YARN_PEER_DEP_ERROR_REGEX) do
          errors << Regexp.last_match.named_captures
        end
      else raise
      end
      errors
    end.compact
  end
rescue SharedHelpers::HelperSubprocessFailed
  # Fall back to allowing the version through. Whatever error
  # occurred should be properly handled by the FileUpdater. We
  # can slowly migrate error handling to this class over time.
  []
end
filtered_package_files() click to toggle source
# File lib/dependabot/npm_and_yarn/update_checker/version_resolver.rb, line 495
def filtered_package_files
  @filtered_package_files ||=
    DependencyFilesFilterer.new(
      dependency_files: dependency_files,
      updated_dependencies: [dependency]
    ).package_files_requiring_update
end
git_dependency?(dep) click to toggle source
# File lib/dependabot/npm_and_yarn/update_checker/version_resolver.rb, line 379
def git_dependency?(dep)
  # ignored_version/raise_on_ignored are irrelevant.
  GitCommitChecker.
    new(dependency: dep, credentials: credentials).
    git_dependency?
end
latest_version_finder(dep) click to toggle source
# File lib/dependabot/npm_and_yarn/update_checker/version_resolver.rb, line 129
def latest_version_finder(dep)
  @latest_version_finder[dep] ||=
    LatestVersionFinder.new(
      dependency: dep,
      credentials: credentials,
      dependency_files: dependency_files,
      ignored_versions: [],
      security_advisories: []
    )
end
latest_version_of_dep_with_satisfied_peer_reqs(dep) click to toggle source
# File lib/dependabot/npm_and_yarn/update_checker/version_resolver.rb, line 357
def latest_version_of_dep_with_satisfied_peer_reqs(dep)
  latest_version_finder(dep).
    possible_versions_with_details.
    find do |version, details|
      next false unless version > version_for_dependency(dep)
      next true unless details["peerDependencies"]

      details["peerDependencies"].all? do |peer_dep_name, req|
        # Can't handle multiple peer dependencies
        next false unless peer_dep_name == dependency.name
        next git_dependency?(dependency) if req.include?("/")

        reqs = requirement_class.requirements_array(req)

        reqs.any? { |r| r.satisfied_by?(latest_allowable_version) }
      rescue Gem::Requirement::BadRequirementError
        false
      end
    end&.
    first
end
lockfiles_for_path(lockfiles:, path:) click to toggle source
# File lib/dependabot/npm_and_yarn/update_checker/version_resolver.rb, line 396
def lockfiles_for_path(lockfiles:, path:)
  lockfiles.select do |lockfile|
    File.dirname(lockfile.name) == File.dirname(path)
  end
end
newly_broken_peer_reqs_from_dep() click to toggle source
# File lib/dependabot/npm_and_yarn/update_checker/version_resolver.rb, line 391
def newly_broken_peer_reqs_from_dep
  relevant_unmet_peer_dependencies.
    select { |dep| dep[:requiring_dep_name] == dependency.name }
end
newly_broken_peer_reqs_on_dep() click to toggle source
# File lib/dependabot/npm_and_yarn/update_checker/version_resolver.rb, line 386
def newly_broken_peer_reqs_on_dep
  relevant_unmet_peer_dependencies.
    select { |dep| dep[:requirement_name] == dependency.name }
end
old_peer_dependency_errors() click to toggle source
# File lib/dependabot/npm_and_yarn/update_checker/version_resolver.rb, line 230
def old_peer_dependency_errors
  return @old_peer_dependency_errors if @old_peer_dependency_errors_checked

  @old_peer_dependency_errors_checked = true

  version = version_for_dependency(dependency)

  @old_peer_dependency_errors =
    fetch_peer_dependency_errors(version: version)
end
old_unmet_peer_dependencies() click to toggle source
# File lib/dependabot/npm_and_yarn/update_checker/version_resolver.rb, line 282
def old_unmet_peer_dependencies
  old_peer_dependency_errors.
    map { |captures| error_details_from_captures(captures) }
end
part_of_tightly_locked_monorepo?() click to toggle source

rubocop:enable Metrics/PerceivedComplexity

# File lib/dependabot/npm_and_yarn/update_checker/version_resolver.rb, line 175
def part_of_tightly_locked_monorepo?
  monorepo_dep_names =
    TIGHTLY_COUPLED_MONOREPOS.values.
    find { |deps| deps.include?(dependency.name) }
  return false unless monorepo_dep_names

  deps_to_update =
    top_level_dependencies.
    select { |d| monorepo_dep_names.include?(d.name) }

  deps_to_update.count > 1
end
peer_dependency_errors() click to toggle source
# File lib/dependabot/npm_and_yarn/update_checker/version_resolver.rb, line 221
def peer_dependency_errors
  return @peer_dependency_errors if @peer_dependency_errors_checked

  @peer_dependency_errors_checked = true

  @peer_dependency_errors =
    fetch_peer_dependency_errors(version: latest_allowable_version)
end
relevant_unmet_peer_dependencies() click to toggle source
# File lib/dependabot/npm_and_yarn/update_checker/version_resolver.rb, line 298
def relevant_unmet_peer_dependencies
  relevant_unmet_peer_dependencies =
    unmet_peer_dependencies.select do |dep|
      dep[:requirement_name] == dependency.name ||
        dep[:requiring_dep_name] == dependency.name
    end

  return [] if relevant_unmet_peer_dependencies.empty?

  # Prune out any pre-existing warnings
  relevant_unmet_peer_dependencies.reject do |issue|
    old_unmet_peer_dependencies.any? do |old_issue|
      old_issue.slice(:requirement_name, :requiring_dep_name) ==
        issue.slice(:requirement_name, :requiring_dep_name)
    end
  end
end
requirement_class() click to toggle source
# File lib/dependabot/npm_and_yarn/update_checker/version_resolver.rb, line 528
def requirement_class
  NpmAndYarn::Requirement
end
requirements_for_path(requirements, path) click to toggle source
# File lib/dependabot/npm_and_yarn/update_checker/version_resolver.rb, line 474
def requirements_for_path(requirements, path)
  return requirements if path.to_s == "."

  requirements.map do |r|
    next unless r[:file].start_with?("#{path}/")

    r.merge(file: r[:file].gsub(/^#{Regexp.quote("#{path}/")}/, ""))
  end.compact
end
resolve_latest_previous_version(dep, updated_version) click to toggle source

rubocop:disable Metrics/PerceivedComplexity

# File lib/dependabot/npm_and_yarn/update_checker/version_resolver.rb, line 141
def resolve_latest_previous_version(dep, updated_version)
  return dep.version if dep.version

  @resolve_latest_previous_version ||= {}
  @resolve_latest_previous_version[dep] ||= begin
    relevant_versions = latest_version_finder(dependency).
                        possible_previous_versions_with_details.
                        map(&:first)
    reqs = dep.requirements.map { |r| r[:requirement] }.compact.
           map { |r| requirement_class.requirements_array(r) }

    # Pick the lowest version from the max possible version from all
    # requirements. This matches the logic when combining the same
    # dependency in DependencySet from multiple manifest files where we
    # pick the lowest version from the duplicates.
    latest_previous_version = reqs.flat_map do |req|
      relevant_versions.select do |version|
        req.any? { |r| r.satisfied_by?(version) }
      end.max
    end.min&.to_s

    # Handle cases where the latest resolvable previous version is the
    # latest version. This often happens if you don't have lockfiles and
    # have requirements update strategy set to bump_versions, where an
    # update might go from ^1.1.1 to ^1.1.2 (both resolve to 1.1.2).
    if updated_version.to_s == latest_previous_version
      nil
    else
      latest_previous_version
    end
  end
end
run_checker(path:, version:) click to toggle source
# File lib/dependabot/npm_and_yarn/update_checker/version_resolver.rb, line 402
def run_checker(path:, version:)
  # If there are both yarn lockfiles and npm lockfiles only run the
  # yarn updater
  if lockfiles_for_path(lockfiles: dependency_files_builder.yarn_locks, path: path).any?
    return run_yarn_checker(path: path, version: version)
  end

  run_npm_checker(path: path, version: version)
end
run_npm7_checker(version:) click to toggle source
# File lib/dependabot/npm_and_yarn/update_checker/version_resolver.rb, line 455
def run_npm7_checker(version:)
  SharedHelpers.run_shell_command(
    "npm install #{version_install_arg(version: version)} --package-lock-only --dry-run=true --ignore-scripts"
  )
  nil
rescue SharedHelpers::HelperSubprocessFailed => e
  raise if e.message.match?(NPM7_PEER_DEP_ERROR_REGEX)
end
run_npm_checker(path:, version:) click to toggle source
# File lib/dependabot/npm_and_yarn/update_checker/version_resolver.rb, line 429
def run_npm_checker(path:, version:)
  SharedHelpers.with_git_configured(credentials: credentials) do
    Dir.chdir(path) do
      package_lock = dependency_files_builder.package_locks.find do |f|
        # Find the lockfile that's in the current directory
        f.name == [path, "package-lock.json"].join("/").sub(%r{\A.?\/}, "")
      end
      npm_version = Dependabot::NpmAndYarn::Helpers.npm_version(package_lock&.content)

      return run_npm7_checker(version: version) if npm_version == "npm7"

      SharedHelpers.run_helper_subprocess(
        command: NativeHelpers.helper_path,
        function: "npm6:checkPeerDependencies",
        args: [
          Dir.pwd,
          dependency.name,
          version,
          requirements_for_path(dependency.requirements, path),
          top_level_dependencies.map(&:to_h)
        ]
      )
    end
  end
end
run_yarn_checker(path:, version:) click to toggle source
# File lib/dependabot/npm_and_yarn/update_checker/version_resolver.rb, line 412
def run_yarn_checker(path:, version:)
  SharedHelpers.with_git_configured(credentials: credentials) do
    Dir.chdir(path) do
      SharedHelpers.run_helper_subprocess(
        command: NativeHelpers.helper_path,
        function: "yarn:checkPeerDependencies",
        args: [
          Dir.pwd,
          dependency.name,
          version,
          requirements_for_path(dependency.requirements, path)
        ]
      )
    end
  end
end
satisfies_peer_reqs_on_dep?(version) click to toggle source

rubocop:enable Metrics/PerceivedComplexity

# File lib/dependabot/npm_and_yarn/update_checker/version_resolver.rb, line 343
def satisfies_peer_reqs_on_dep?(version)
  newly_broken_peer_reqs_on_dep.all? do |peer_req|
    req = peer_req.fetch(:requirement_version)

    # Git requirements can't be satisfied by a version
    next false if req.include?("/")

    reqs = requirement_class.requirements_array(req)
    reqs.any? { |r| r.satisfied_by?(version) }
  rescue Gem::Requirement::BadRequirementError
    false
  end
end
satisfying_versions() click to toggle source

rubocop:disable Metrics/PerceivedComplexity

# File lib/dependabot/npm_and_yarn/update_checker/version_resolver.rb, line 317
def satisfying_versions
  latest_version_finder(dependency).
    possible_versions_with_details.
    select do |version, details|
      next false unless satisfies_peer_reqs_on_dep?(version)
      next true unless details["peerDependencies"]
      next true if version == version_for_dependency(dependency)

      details["peerDependencies"].all? do |dep, req|
        dep = top_level_dependencies.find { |d| d.name == dep }
        next false unless dep
        next git_dependency?(dep) if req.include?("/")

        reqs = requirement_class.requirements_array(req)
        next false unless version_for_dependency(dep)

        reqs.any? { |r| r.satisfied_by?(version_for_dependency(dep)) }
      rescue Gem::Requirement::BadRequirementError
        false
      end
    end.
    map(&:first)
end
top_level_dependencies() click to toggle source

Top level dependencies are required in the peer dep checker to fetch the manifests for all top level deps which may contain “peerDependency” requirements

# File lib/dependabot/npm_and_yarn/update_checker/version_resolver.rb, line 487
def top_level_dependencies
  @top_level_dependencies ||= NpmAndYarn::FileParser.new(
    dependency_files: dependency_files,
    source: nil,
    credentials: credentials
  ).parse.select(&:top_level?)
end
unmet_peer_dependencies() click to toggle source
# File lib/dependabot/npm_and_yarn/update_checker/version_resolver.rb, line 277
def unmet_peer_dependencies
  peer_dependency_errors.
    map { |captures| error_details_from_captures(captures) }
end
updated_monorepo_dependencies() click to toggle source
# File lib/dependabot/npm_and_yarn/update_checker/version_resolver.rb, line 188
def updated_monorepo_dependencies
  monorepo_dep_names =
    TIGHTLY_COUPLED_MONOREPOS.values.
    find { |deps| deps.include?(dependency.name) }

  deps_to_update =
    top_level_dependencies.
    select { |d| monorepo_dep_names.include?(d.name) }

  updates = []
  deps_to_update.each do |dep|
    next if git_dependency?(dep)
    next if dep.version &&
            version_class.new(dep.version) >= latest_allowable_version

    updated_version =
      latest_version_finder(dep).
      possible_versions.
      find { |v| v == latest_allowable_version }
    next unless updated_version

    updates << {
      dependency: dep,
      version: updated_version,
      previous_version: resolve_latest_previous_version(
        dep, updated_version
      )
    }
  end

  updates
end
version_class() click to toggle source
# File lib/dependabot/npm_and_yarn/update_checker/version_resolver.rb, line 524
def version_class
  NpmAndYarn::Version
end
version_for_dependency(dep) click to toggle source
# File lib/dependabot/npm_and_yarn/update_checker/version_resolver.rb, line 512
def version_for_dependency(dep)
  return version_class.new(dep.version) if dep.version && version_class.correct?(dep.version)

  dep.requirements.map { |r| r[:requirement] }.compact.
    reject { |req_string| req_string.start_with?("<") }.
    select { |req_string| req_string.match?(version_regex) }.
    map { |req_string| req_string.match(version_regex) }.
    select { |version| version_class.correct?(version.to_s) }.
    map { |version| version_class.new(version.to_s) }.
    max
end
version_install_arg(version:) click to toggle source
# File lib/dependabot/npm_and_yarn/update_checker/version_resolver.rb, line 464
def version_install_arg(version:)
  git_source = dependency.requirements.find { |req| req[:source] && req[:source][:type] == "git" }

  if git_source
    "#{dependency.name}@#{git_req[:source][:url]}##{version}"
  else
    "#{dependency.name}@#{version}"
  end
end
version_regex() click to toggle source
# File lib/dependabot/npm_and_yarn/update_checker/version_resolver.rb, line 532
def version_regex
  version_class::VERSION_PATTERN
end