diff --git a/bundler/helpers/v2/lib/functions.rb b/bundler/helpers/v2/lib/functions.rb index dbf99c12352..ee90f72241e 100644 --- a/bundler/helpers/v2/lib/functions.rb +++ b/bundler/helpers/v2/lib/functions.rb @@ -173,4 +173,12 @@ def self.git_source_credentials(credentials) credentials .select { |cred| cred["type"] == "git_source" } end + + def self.bundler_raw_version + Bundler::VERSION + end + + def self.ruby_raw_version + RUBY_VERSION + end end diff --git a/bundler/lib/dependabot/bundler.rb b/bundler/lib/dependabot/bundler.rb index bff5d467dee..712dcaeb155 100644 --- a/bundler/lib/dependabot/bundler.rb +++ b/bundler/lib/dependabot/bundler.rb @@ -3,6 +3,8 @@ # These all need to be required so the various classes can be registered in a # lookup table of package manager names to concrete classes. +require "dependabot/bundler/language" +require "dependabot/bundler/package_manager" require "dependabot/bundler/file_fetcher" require "dependabot/bundler/file_parser" require "dependabot/bundler/update_checker" @@ -10,7 +12,6 @@ require "dependabot/bundler/metadata_finder" require "dependabot/bundler/requirement" require "dependabot/bundler/version" -require "dependabot/bundler/package_manager" require "dependabot/pull_request_creator/labeler" Dependabot::PullRequestCreator::Labeler diff --git a/bundler/lib/dependabot/bundler/file_parser.rb b/bundler/lib/dependabot/bundler/file_parser.rb index a0e9461bcfa..7b8ae98bfc9 100644 --- a/bundler/lib/dependabot/bundler/file_parser.rb +++ b/bundler/lib/dependabot/bundler/file_parser.rb @@ -2,6 +2,8 @@ # frozen_string_literal: true require "parallel" +require "dependabot/bundler/language" +require "dependabot/bundler/package_manager" require "dependabot/dependency" require "dependabot/file_parsers" require "dependabot/file_parsers/base" @@ -37,7 +39,8 @@ def ecosystem @ecosystem ||= T.let( Ecosystem.new( name: ECOSYSTEM, - package_manager: package_manager + package_manager: package_manager, + language: language ), T.nilable(Ecosystem) ) @@ -47,7 +50,16 @@ def ecosystem sig { returns(Ecosystem::VersionManager) } def package_manager - PackageManager.new(bundler_version) + @package_manager ||= PackageManager.new(bundler_raw_version) + end + + sig { returns(T.nilable(Ecosystem::VersionManager)) } + def language + return @language if defined?(@language) + + return nil if package_manager.unsupported? + + Language.new(ruby_raw_version) end def check_external_code(dependencies) @@ -327,6 +339,51 @@ def imported_ruby_files .reject { |f| f.name == "gems.rb" } end + sig { returns(String) } + def bundler_raw_version + return bundler_raw_version if defined?(@bundler_raw_version) + + package_manager = PackageManager.new(bundler_version) + + # If the selected version is unsupported, an unsupported error will be raised, + # so there’s no need to attempt retrieving the raw version. + return bundler_version if package_manager.unsupported? + + # read raw version directly from the ecosystem environment + bundler_raw_version = SharedHelpers.in_a_temporary_repo_directory( + base_directory, + repo_contents_path + ) do + write_temporary_dependency_files + NativeHelpers.run_bundler_subprocess( + function: "bundler_raw_version", + args: {}, + bundler_version: bundler_version, + options: { timeout_per_operation_seconds: 10 } + ) + end + bundler_raw_version || ::Bundler::VERSION + end + + sig { returns(String) } + def ruby_raw_version + return @ruby_raw_version if defined?(@ruby_raw_version) + + ruby_raw_version = SharedHelpers.in_a_temporary_repo_directory( + base_directory, + repo_contents_path + ) do + write_temporary_dependency_files + NativeHelpers.run_bundler_subprocess( + function: "ruby_raw_version", + args: {}, + bundler_version: bundler_version, + options: { timeout_per_operation_seconds: 10 } + ) + end + ruby_raw_version || RUBY_VERSION + end + sig { returns(String) } def bundler_version @bundler_version ||= Helpers.bundler_version(lockfile) diff --git a/bundler/lib/dependabot/bundler/language.rb b/bundler/lib/dependabot/bundler/language.rb new file mode 100644 index 00000000000..dcd5caf1311 --- /dev/null +++ b/bundler/lib/dependabot/bundler/language.rb @@ -0,0 +1,24 @@ +# typed: strong +# frozen_string_literal: true + +require "sorbet-runtime" +require "dependabot/bundler/version" +require "dependabot/ecosystem" + +module Dependabot + module Bundler + LANGUAGE = "ruby" + + class Language < Dependabot::Ecosystem::VersionManager + extend T::Sig + + sig { params(raw_version: String).void } + def initialize(raw_version) + super( + LANGUAGE, + Version.new(raw_version) + ) + end + end + end +end diff --git a/bundler/spec/dependabot/bundler/helper_spec.rb b/bundler/spec/dependabot/bundler/helper_spec.rb index 247ae784f57..be458abd8e8 100644 --- a/bundler/spec/dependabot/bundler/helper_spec.rb +++ b/bundler/spec/dependabot/bundler/helper_spec.rb @@ -2,22 +2,30 @@ # frozen_string_literal: true require "spec_helper" - require "dependabot/bundler/helpers" RSpec.describe Dependabot::Bundler::Helpers do let(:no_lockfile) { nil } + let(:no_gemfile) { nil } + let(:no_ruby_version_file) { nil } + + let(:gemfile_with_ruby_version) do + Dependabot::DependencyFile.new(name: "Gemfile", content: <<~GEMFILE) + source 'https://rubygems.org' + ruby '3.0.0' + gem 'rails' + GEMFILE + end - let(:lockfile_bundled_with_missing) do + let(:lockfile_with_ruby_version) do Dependabot::DependencyFile.new(name: "Gemfile.lock", content: <<~LOCKFILE) - Mock Gemfile.lock Content Goes Here + RUBY VERSION + ruby 2.7.2 LOCKFILE end let(:lockfile_bundled_with_v1) do Dependabot::DependencyFile.new(name: "Gemfile.lock", content: <<~LOCKFILE) - Mock Gemfile.lock Content Goes Here - BUNDLED WITH 1.17.3 LOCKFILE @@ -25,8 +33,6 @@ let(:lockfile_bundled_with_v2) do Dependabot::DependencyFile.new(name: "Gemfile.lock", content: <<~LOCKFILE) - Mock Gemfile.lock Content Goes Here - BUNDLED WITH 2.2.11 LOCKFILE @@ -34,13 +40,21 @@ let(:lockfile_bundled_with_future_version) do Dependabot::DependencyFile.new(name: "Gemfile.lock", content: <<~LOCKFILE) - Mock Gemfile.lock Content Goes Here - BUNDLED WITH 3.9.99 LOCKFILE end + let(:lockfile_bundled_with_missing) do + Dependabot::DependencyFile.new(name: "Gemfile.lock", content: <<~LOCKFILE) + Mock Gemfile.lock Content Goes Here + LOCKFILE + end + + let(:ruby_version_file) do + Dependabot::DependencyFile.new(name: ".ruby-version", content: "ruby-2.7.1") + end + describe "#bundler_version" do def described_method(lockfile) described_class.bundler_version(lockfile) diff --git a/bundler/spec/dependabot/bundler/language_spec.rb b/bundler/spec/dependabot/bundler/language_spec.rb new file mode 100644 index 00000000000..eefe123a382 --- /dev/null +++ b/bundler/spec/dependabot/bundler/language_spec.rb @@ -0,0 +1,49 @@ +# typed: false +# frozen_string_literal: true + +require "dependabot/bundler/language" +require "dependabot/ecosystem" +require "spec_helper" + +RSpec.describe Dependabot::Bundler::Language do + let(:language) { described_class.new(version) } + let(:version) { "3.0.0" } + + describe "#initialize" do + context "when version is a String" do + let(:version) { "3.0.0" } + + it "sets the version correctly" do + expect(language.version).to eq(Dependabot::Bundler::Version.new(version)) + end + + it "sets the name correctly" do + expect(language.name).to eq(Dependabot::Bundler::LANGUAGE) + end + end + + context "when version is a Dependabot::Bundler::Version" do + let(:version) { "3.0.0" } + + it "sets the version correctly" do + expect(language.version).to eq(version) + end + + it "sets the name correctly" do + expect(language.name).to eq(Dependabot::Bundler::LANGUAGE) + end + end + end + + describe "#unsupported?" do + it "returns false by default as no specific support or deprecation for languages is currently defined" do + expect(language.unsupported?).to be false + end + end + + describe "#deprecated?" do + it "returns false by default as no specific deprecation for languages is currently defined" do + expect(language.deprecated?).to be false + end + end +end diff --git a/common/lib/dependabot/ecosystem.rb b/common/lib/dependabot/ecosystem.rb index 13f53c549d6..c53ae172ece 100644 --- a/common/lib/dependabot/ecosystem.rb +++ b/common/lib/dependabot/ecosystem.rb @@ -12,13 +12,13 @@ class VersionManager extend T::Helpers abstract! - # Initialize version information with optional requirement - # @param name [String] the name for the package manager (e.g., "bundler", "npm"). + # Initialize version information for a package manager or language. + # @param name [String] the name of the package manager or language (e.g., "bundler", "ruby"). # @param version [Dependabot::Version] the parsed current version. # @param deprecated_versions [Array] an array of deprecated versions. # @param supported_versions [Array] an array of supported versions. # @example - # VersionManager.new("bundler", "2.1.4", Dependabot::Version.new("2.1.4"), nil) + # VersionManager.new("bundler", "2.1.4", nil) sig do params( name: String, @@ -46,7 +46,7 @@ def initialize( sig { returns(String) } attr_reader :name - # The current version of the package manager. + # The current version of the package manager or language. # @example # version #=> Dependabot::Version.new("2.1.4") sig { returns(Dependabot::Version) } @@ -68,6 +68,7 @@ def initialize( # deprecated? #=> true sig { returns(T::Boolean) } def deprecated? + # If the version is unsupported, the unsupported error is getting raised separately. return false if unsupported? deprecated_versions.include?(version) @@ -112,19 +113,23 @@ def support_later_versions? # Initialize with mandatory name and optional language information. # @param name [String] the name of the ecosystem (e.g., "bundler", "npm_and_yarn"). - # @param package_manager [VersionManager] the package manager. + # @param package_manager [VersionManager] the package manager (mandatory). + # @param language [VersionManager] the language (optional). sig do params( name: String, - package_manager: VersionManager + package_manager: VersionManager, + language: T.nilable(VersionManager) ).void end def initialize( name:, - package_manager: + package_manager:, + language: nil ) @name = T.let(name, String) @package_manager = T.let(package_manager, VersionManager) + @language = T.let(language, T.nilable(VersionManager)) end # The name of the ecosystem (mandatory). @@ -135,10 +140,16 @@ def initialize( # The information related to the package manager (mandatory). # @example - # package_manager #=> VersionManager.new("bundler", "2.1.4", Version.new("2.1.4"), nil) + # package_manager #=> VersionManager.new("bundler", "2.1.4", deprecated_versions, supported_versions) sig { returns(VersionManager) } attr_reader :package_manager + # The information related to the language (optional). + # @example + # language #=> VersionManager.new("ruby", "3.9", deprecated_versions, supported_versions) + sig { returns(T.nilable(VersionManager)) } + attr_reader :language + # Checks if the current version is deprecated. # Returns true if the version is in the deprecated_versions array; false otherwise. sig { returns(T::Boolean) } diff --git a/common/spec/dependabot/ecosystem_spec.rb b/common/spec/dependabot/ecosystem_spec.rb index 82448c077e1..ba6d13af183 100644 --- a/common/spec/dependabot/ecosystem_spec.rb +++ b/common/spec/dependabot/ecosystem_spec.rb @@ -24,12 +24,26 @@ def initialize(raw_version, deprecated_versions, supported_versions) end.new(package_manager_raw_version, deprecated_versions, supported_versions) end + let(:language) do + Class.new(Dependabot::Ecosystem::VersionManager) do + def initialize(raw_version) + super( + "ruby", # name + Dependabot::Version.new(raw_version), # version + [], # deprecated_versions + [] # supported_versions + ) + end + end.new(language_raw_version) + end + describe "#initialize" do it "sets the correct attributes" do - ecosystem = described_class.new(name: "bundler", package_manager: package_manager) + ecosystem = described_class.new(name: "bundler", package_manager: package_manager, language: language) expect(ecosystem.name).to eq("bundler") expect(ecosystem.package_manager.name).to eq("bundler") + expect(ecosystem.language.name).to eq("ruby") end end @@ -38,7 +52,7 @@ def initialize(raw_version, deprecated_versions, supported_versions) let(:package_manager_raw_version) { "1" } it "returns true" do - ecosystem = described_class.new(name: "bundler", package_manager: package_manager) + ecosystem = described_class.new(name: "bundler", package_manager: package_manager, language: language) expect(ecosystem.deprecated?).to be true end end @@ -47,7 +61,7 @@ def initialize(raw_version, deprecated_versions, supported_versions) let(:package_manager_raw_version) { "2.0.0" } it "returns false" do - ecosystem = described_class.new(name: "bundler", package_manager: package_manager) + ecosystem = described_class.new(name: "bundler", package_manager: package_manager, language: language) expect(ecosystem.deprecated?).to be false end end @@ -58,7 +72,7 @@ def initialize(raw_version, deprecated_versions, supported_versions) let(:package_manager_raw_version) { "0.8.0" } it "returns true" do - ecosystem = described_class.new(name: "bundler", package_manager: package_manager) + ecosystem = described_class.new(name: "bundler", package_manager: package_manager, language: language) expect(ecosystem.unsupported?).to be true end end @@ -67,7 +81,7 @@ def initialize(raw_version, deprecated_versions, supported_versions) let(:package_manager_raw_version) { "2.0.0" } it "returns false" do - ecosystem = described_class.new(name: "bundler", package_manager: package_manager) + ecosystem = described_class.new(name: "bundler", package_manager: package_manager, language: language) expect(ecosystem.unsupported?).to be false end end @@ -78,7 +92,7 @@ def initialize(raw_version, deprecated_versions, supported_versions) let(:package_manager_raw_version) { "0.8.0" } it "raises a ToolVersionNotSupported error" do - ecosystem = described_class.new(name: "bundler", package_manager: package_manager) + ecosystem = described_class.new(name: "bundler", package_manager: package_manager, language: language) expect { ecosystem.raise_if_unsupported! }.to raise_error(Dependabot::ToolVersionNotSupported) end end @@ -87,7 +101,7 @@ def initialize(raw_version, deprecated_versions, supported_versions) let(:package_manager_raw_version) { "2.0.0" } it "does not raise an error" do - ecosystem = described_class.new(name: "bundler", package_manager: package_manager) + ecosystem = described_class.new(name: "bundler", package_manager: package_manager, language: language) expect { ecosystem.raise_if_unsupported! }.not_to raise_error end end