|
| 1 | +# typed: strict |
| 2 | +# frozen_string_literal: true |
| 3 | + |
| 4 | +require "excon" |
| 5 | +require "json" |
| 6 | +require "sorbet-runtime" |
| 7 | +require "dependabot/dependency" |
| 8 | +require "dependabot/update_checkers" |
| 9 | +require "dependabot/pre_commit/additional_dependency_checkers" |
| 10 | +require "dependabot/pre_commit/additional_dependency_checkers/base" |
| 11 | + |
| 12 | +module Dependabot |
| 13 | + module PreCommit |
| 14 | + module AdditionalDependencyCheckers |
| 15 | + class Ruby < Base |
| 16 | + extend T::Sig |
| 17 | + |
| 18 | + sig { override.returns(T.nilable(String)) } |
| 19 | + def latest_version |
| 20 | + return nil unless package_name |
| 21 | + |
| 22 | + @latest_version ||= T.let( |
| 23 | + fetch_latest_version_via_bundler_checker, |
| 24 | + T.nilable(String) |
| 25 | + ) |
| 26 | + end |
| 27 | + |
| 28 | + sig { override.params(latest_version: String).returns(T::Array[T::Hash[Symbol, T.untyped]]) } |
| 29 | + def updated_requirements(latest_version) |
| 30 | + requirements.map do |original_req| |
| 31 | + original_source = original_req[:source] |
| 32 | + next original_req unless original_source.is_a?(Hash) |
| 33 | + next original_req unless original_source[:type] == "additional_dependency" |
| 34 | + |
| 35 | + original_requirement = original_req[:requirement] |
| 36 | + new_requirement = build_updated_requirement(original_requirement, latest_version) |
| 37 | + |
| 38 | + new_original_string = build_original_string( |
| 39 | + package_name: original_source[:original_name] || original_source[:package_name], |
| 40 | + requirement: new_requirement |
| 41 | + ) |
| 42 | + |
| 43 | + new_source = original_source.merge(original_string: new_original_string) |
| 44 | + |
| 45 | + original_req.merge( |
| 46 | + requirement: new_requirement, |
| 47 | + source: new_source |
| 48 | + ) |
| 49 | + end |
| 50 | + end |
| 51 | + |
| 52 | + private |
| 53 | + |
| 54 | + sig { returns(T.nilable(String)) } |
| 55 | + def fetch_latest_version_via_bundler_checker |
| 56 | + bundler_checker = bundler_update_checker |
| 57 | + return nil unless bundler_checker |
| 58 | + |
| 59 | + latest = bundler_checker.latest_version |
| 60 | + Dependabot.logger.info("Ruby UpdateChecker found latest version: #{latest || 'none'}") |
| 61 | + |
| 62 | + latest&.to_s |
| 63 | + rescue Dependabot::DependabotError, Excon::Error, JSON::ParserError => e |
| 64 | + Dependabot.logger.debug("Error checking Ruby gem #{package_name}: #{e.message}") |
| 65 | + nil |
| 66 | + end |
| 67 | + |
| 68 | + sig { returns(T.nilable(Dependabot::UpdateCheckers::Base)) } |
| 69 | + def bundler_update_checker |
| 70 | + @bundler_update_checker ||= T.let( |
| 71 | + build_bundler_update_checker, |
| 72 | + T.nilable(Dependabot::UpdateCheckers::Base) |
| 73 | + ) |
| 74 | + end |
| 75 | + |
| 76 | + sig { returns(T.nilable(Dependabot::UpdateCheckers::Base)) } |
| 77 | + def build_bundler_update_checker |
| 78 | + bundler_dependency = build_bundler_dependency |
| 79 | + return nil unless bundler_dependency |
| 80 | + |
| 81 | + Dependabot.logger.info("Delegating to bundler UpdateChecker for gem: #{bundler_dependency.name}") |
| 82 | + |
| 83 | + Dependabot::UpdateCheckers.for_package_manager("bundler").new( |
| 84 | + dependency: bundler_dependency, |
| 85 | + dependency_files: build_bundler_dependency_files, |
| 86 | + credentials: credentials, |
| 87 | + ignored_versions: [], |
| 88 | + security_advisories: [], |
| 89 | + raise_on_ignored: false |
| 90 | + ) |
| 91 | + end |
| 92 | + |
| 93 | + sig { returns(T.nilable(Dependabot::Dependency)) } |
| 94 | + def build_bundler_dependency |
| 95 | + return nil unless package_name |
| 96 | + |
| 97 | + version = current_version || extract_version_from_requirement |
| 98 | + |
| 99 | + Dependabot::Dependency.new( |
| 100 | + name: T.must(package_name), |
| 101 | + version: version, |
| 102 | + requirements: [{ |
| 103 | + requirement: version ? "= #{version}" : nil, |
| 104 | + groups: [], |
| 105 | + file: "Gemfile", |
| 106 | + source: nil |
| 107 | + }], |
| 108 | + package_manager: "bundler" |
| 109 | + ) |
| 110 | + end |
| 111 | + |
| 112 | + sig { returns(T.nilable(String)) } |
| 113 | + def extract_version_from_requirement |
| 114 | + req_string = requirements.first&.dig(:requirement) |
| 115 | + return nil unless req_string |
| 116 | + |
| 117 | + version_part = req_string.sub(/\A[~><=!]+\s*/, "").strip |
| 118 | + return version_part if version_part.match?(/\A\d+(?:\.\d+)*(?:\.\w+)?\z/) |
| 119 | + |
| 120 | + nil |
| 121 | + end |
| 122 | + |
| 123 | + sig { returns(T::Array[Dependabot::DependencyFile]) } |
| 124 | + def build_bundler_dependency_files |
| 125 | + version = current_version || extract_version_from_requirement |
| 126 | + version_constraint = version ? ", \"#{version}\"" : "" |
| 127 | + gemfile_content = "source 'https://rubygems.org'\ngem '#{package_name}'#{version_constraint}\n" |
| 128 | + |
| 129 | + [ |
| 130 | + Dependabot::DependencyFile.new( |
| 131 | + name: "Gemfile", |
| 132 | + content: gemfile_content |
| 133 | + ) |
| 134 | + ] |
| 135 | + end |
| 136 | + |
| 137 | + sig do |
| 138 | + params( |
| 139 | + package_name: T.nilable(String), |
| 140 | + requirement: T.nilable(String) |
| 141 | + ).returns(String) |
| 142 | + end |
| 143 | + def build_original_string(package_name:, requirement:) |
| 144 | + base = package_name.to_s |
| 145 | + base = "#{base}:#{requirement}" if requirement |
| 146 | + base |
| 147 | + end |
| 148 | + |
| 149 | + sig { params(original_requirement: T.nilable(String), new_version: String).returns(String) } |
| 150 | + def build_updated_requirement(original_requirement, new_version) |
| 151 | + return new_version unless original_requirement |
| 152 | + |
| 153 | + # Handle Ruby gem version operators: ~>, >=, >, <=, <, =, != |
| 154 | + operator_match = original_requirement.match(/\A(?<op>[~><=!]+)\s*/) |
| 155 | + if operator_match |
| 156 | + "#{operator_match[:op]} #{new_version}" |
| 157 | + else |
| 158 | + new_version |
| 159 | + end |
| 160 | + end |
| 161 | + end |
| 162 | + end |
| 163 | + end |
| 164 | +end |
| 165 | + |
| 166 | +Dependabot::PreCommit::AdditionalDependencyCheckers.register( |
| 167 | + "ruby", |
| 168 | + Dependabot::PreCommit::AdditionalDependencyCheckers::Ruby |
| 169 | +) |
0 commit comments