Skip to content

Commit

Permalink
Add package_manager for Composer v1 deprecation warning and unsupport…
Browse files Browse the repository at this point in the history
…ed error (#10716)

* add package_manager for composer.
* set composer v1 as deprecated
* set composer v1 as unsupported
* change fallback version from v1 to v2 when composer v1 is not supported
* refactor composer helper improve version determination logic.
* regex refined to remove backtracking for performance reasons
  • Loading branch information
kbukum1 authored Oct 4, 2024
1 parent 9e5838b commit f38ac2f
Show file tree
Hide file tree
Showing 6 changed files with 274 additions and 6 deletions.
2 changes: 2 additions & 0 deletions composer/lib/dependabot/composer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
require "dependabot/composer/metadata_finder"
require "dependabot/composer/requirement"
require "dependabot/composer/version"
require "dependabot/composer/helpers"
require "dependabot/composer/package_manager"

require "dependabot/pull_request_creator/labeler"
Dependabot::PullRequestCreator::Labeler
Expand Down
10 changes: 10 additions & 0 deletions composer/lib/dependabot/composer/file_parser.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# typed: true
# frozen_string_literal: true

require "dependabot/composer"
require "dependabot/dependency"
require "dependabot/composer/version"
require "dependabot/file_parsers"
Expand Down Expand Up @@ -33,6 +34,11 @@ def parse
dependency_set.dependencies
end

sig { returns(PackageManagerBase) }
def package_manager
PackageManager.new(composer_version)
end

private

def manifest_dependencies
Expand Down Expand Up @@ -208,6 +214,10 @@ def composer_json
def lockfile
@lockfile ||= get_original_file("composer.lock")
end

def composer_version
@composer_version ||= Helpers.composer_version(parsed_composer_json, parsed_lockfile)
end
end
end
end
Expand Down
40 changes: 34 additions & 6 deletions composer/lib/dependabot/composer/helpers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,13 @@
module Dependabot
module Composer
module Helpers
V1 = "1"
V2 = "2"
# If we are updating a project with no lock file then the default should be the newest version
DEFAULT = V2

# From composers json-schema: https://getcomposer.org/schema.json
COMPOSER_V2_NAME_REGEX = %r{^[a-z0-9]([_.-]?[a-z0-9]+)*/[a-z0-9](([_.]?|-{0,2})[a-z0-9]+)*$}
COMPOSER_V2_NAME_REGEX = %r{^[a-z0-9]([_.-]?[a-z0-9]++)*/[a-z0-9](([_.]?|-{0,2})[a-z0-9]++)*$}
# From https://github.com/composer/composer/blob/b7d770659b4e3ef21423bd67ade935572913a4c1/src/Composer/Repository/PlatformRepository.php#L33
PLATFORM_PACKAGE_REGEX = /
^(?:php(?:-64bit|-ipv6|-zts|-debug)?|hhvm|(?:ext|lib)-[a-z0-9](?:[_.-]?[a-z0-9]+)*
Expand All @@ -18,15 +23,35 @@ module Helpers
FAILED_GIT_CLONE = /^Failed to clone (?<url>.*?)/

def self.composer_version(composer_json, parsed_lockfile = nil)
v1_unsupported = Dependabot::Experiments.enabled?(:composer_v1_unsupported_error)

# If the parsed lockfile has a plugin API version, we return either V1 or V2
# based on the major version of the lockfile.
if parsed_lockfile && parsed_lockfile["plugin-api-version"]
version = Composer::Version.new(parsed_lockfile["plugin-api-version"])
return version.canonical_segments.first == 1 ? "1" : "2"
else
return "1" if composer_json["name"] && composer_json["name"] !~ COMPOSER_V2_NAME_REGEX
return "1" if invalid_v2_requirement?(composer_json)
return version.canonical_segments.first == 1 ? V1 : V2
end

# Check if the composer name does not follow the Composer V2 naming conventions.
# This happens if "name" is present in composer.json but doesn't match the required pattern.
composer_name_invalid = composer_json["name"] && composer_json["name"] !~ COMPOSER_V2_NAME_REGEX

# If the name is invalid returns the fallback version.
if composer_name_invalid
return v1_unsupported ? V2 : V1
end

# Check if the composer.json file contains "require" entries that don't follow
# either the platform package naming conventions or the Composer V2 name conventions.
invalid_v2 = invalid_v2_requirement?(composer_json)

# If there are invalid requirements returns fallback version.
if invalid_v2
return v1_unsupported ? V2 : V1
end

"2"
# If no conditions are met return V2 by default.
V2
end

def self.dependency_url_from_git_clone_error(message)
Expand All @@ -43,6 +68,8 @@ def self.dependency_url_from_git_clone_error(message)
end
end

# Checks if the "require" key in composer.json contains invalid packages
# that don't match either platform package patterns or Composer V2 naming rules.
def self.invalid_v2_requirement?(composer_json)
return false unless composer_json.key?("require")

Expand All @@ -52,6 +79,7 @@ def self.invalid_v2_requirement?(composer_json)
end
private_class_method :invalid_v2_requirement?

# Removes user credentials from a given dependency URL for security reasons.
def self.clean_dependency_url(dependency_url)
return dependency_url unless URI::DEFAULT_PARSER.regexp[:ABS_URI].match?(dependency_url)

Expand Down
61 changes: 61 additions & 0 deletions composer/lib/dependabot/composer/package_manager.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# typed: strong
# frozen_string_literal: true

require "sorbet-runtime"
require "dependabot/package_manager"
require "dependabot/composer/version"

module Dependabot
module Composer
PACKAGE_MANAGER = "composer"

# Keep versions in ascending order
SUPPORTED_COMPOSER_VERSIONS = T.let([Version.new("2")].freeze, T::Array[Dependabot::Version])

DEPRECATED_COMPOSER_VERSIONS = T.let([
Version.new("1")
].freeze, T::Array[Dependabot::Version])

class PackageManager < PackageManagerBase
extend T::Sig

sig { params(version: T.any(String, Dependabot::Version)).void }
def initialize(version)
@version = T.let(Version.new(version), Dependabot::Version)
@name = T.let(PACKAGE_MANAGER, String)
@deprecated_versions = T.let(DEPRECATED_COMPOSER_VERSIONS, T::Array[Dependabot::Version])
@supported_versions = T.let(SUPPORTED_COMPOSER_VERSIONS, T::Array[Dependabot::Version])
end

sig { override.returns(String) }
attr_reader :name

sig { override.returns(Dependabot::Version) }
attr_reader :version

sig { override.returns(T::Array[Dependabot::Version]) }
attr_reader :deprecated_versions

sig { override.returns(T::Array[Dependabot::Version]) }
attr_reader :supported_versions

sig { override.returns(T::Boolean) }
def deprecated?
return false if unsupported?

# Check if the feature flag for Composer v1 deprecation warning is enabled.
return false unless Dependabot::Experiments.enabled?(:composer_v1_deprecation_warning)

deprecated_versions.include?(version)
end

sig { override.returns(T::Boolean) }
def unsupported?
# Check if the feature flag for Composer v1 unsupported error is enabled.
return false unless Dependabot::Experiments.enabled?(:composer_v1_unsupported_error)

supported_versions.all? { |supported| supported > version }
end
end
end
end
6 changes: 6 additions & 0 deletions composer/spec/dependabot/composer/file_parser_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -409,4 +409,10 @@
end
end
end

describe "#package_manager" do
it "returns the correct package manager" do
expect(parser.package_manager).to be_a(Dependabot::Composer::PackageManager)
end
end
end
161 changes: 161 additions & 0 deletions composer/spec/dependabot/composer/package_manager_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
# typed: false
# frozen_string_literal: true

require "dependabot/composer/package_manager"
require "dependabot/package_manager"
require "spec_helper"

RSpec.describe Dependabot::Composer::PackageManager do
let(:package_manager) { described_class.new(version) }

describe "#initialize" do
context "when version is a String" do
let(:version) { "2" }

it "sets the version correctly" do
expect(package_manager.version).to eq(Dependabot::Version.new(version))
end

it "sets the name correctly" do
expect(package_manager.name).to eq(Dependabot::Composer::PACKAGE_MANAGER)
end

it "sets the deprecated_versions correctly" do
expect(package_manager.deprecated_versions).to eq(Dependabot::Composer::DEPRECATED_COMPOSER_VERSIONS)
end

it "sets the supported_versions correctly" do
expect(package_manager.supported_versions).to eq(Dependabot::Composer::SUPPORTED_COMPOSER_VERSIONS)
end
end

context "when version is a Dependabot::Version" do
let(:version) { Dependabot::Version.new("2") }

it "sets the version correctly" do
expect(package_manager.version).to eq(version)
end

it "sets the name correctly" do
expect(package_manager.name).to eq(Dependabot::Composer::PACKAGE_MANAGER)
end

it "sets the deprecated_versions correctly" do
expect(package_manager.deprecated_versions).to eq(Dependabot::Composer::DEPRECATED_COMPOSER_VERSIONS)
end

it "sets the supported_versions correctly" do
expect(package_manager.supported_versions).to eq(Dependabot::Composer::SUPPORTED_COMPOSER_VERSIONS)
end
end
end

describe "SUPPORTED_COMPOSER_VERSIONS" do
it "is in ascending order" do
expect(Dependabot::Composer::SUPPORTED_COMPOSER_VERSIONS)
.to eq(Dependabot::Composer::SUPPORTED_COMPOSER_VERSIONS.sort)
end
end

describe "#deprecated?" do
before do
allow(Dependabot::Experiments).to receive(:enabled?)
.with(:composer_v1_deprecation_warning)
.and_return(feature_flag_deprecation_enabled)
allow(Dependabot::Experiments).to receive(:enabled?)
.with(:composer_v1_unsupported_error)
.and_return(feature_flag_unsupported_enabled)
end

context "when feature flag `composer_v1_deprecation_warning` is enabled and version is deprecated" do
let(:version) { "1" }
let(:feature_flag_deprecation_enabled) { true }
let(:feature_flag_unsupported_enabled) { false }

it "returns true" do
expect(package_manager.deprecated?).to be true
end
end

context "when feature flag `composer_v1_deprecation_warning` is disabled" do
let(:version) { "1" }
let(:feature_flag_deprecation_enabled) { false }
let(:feature_flag_unsupported_enabled) { false }

it "returns false" do
expect(package_manager.deprecated?).to be false
end
end

context "when version is unsupported and takes precedence" do
let(:version) { "0.9" }
let(:feature_flag_deprecation_enabled) { true }
let(:feature_flag_unsupported_enabled) { true }

it "returns false, as unsupported takes precedence" do
expect(package_manager.deprecated?).to be false
end
end
end

describe "#unsupported?" do
before do
allow(Dependabot::Experiments).to receive(:enabled?)
.with(:composer_v1_unsupported_error)
.and_return(feature_flag_unsupported_enabled)
end

context "when feature flag `composer_v1_unsupported_error` is enabled and version is unsupported" do
let(:version) { "0.9" }
let(:feature_flag_unsupported_enabled) { true }

it "returns true" do
expect(package_manager.unsupported?).to be true
end
end

context "when feature flag `composer_v1_unsupported_error` is disabled" do
let(:version) { "0.9" }
let(:feature_flag_unsupported_enabled) { false }

it "returns false" do
expect(package_manager.unsupported?).to be false
end
end

context "when feature flag is enabled and version is supported" do
let(:version) { "2" }
let(:feature_flag_unsupported_enabled) { true }

it "returns false" do
expect(package_manager.unsupported?).to be false
end
end
end

describe "#raise_if_unsupported!" do
before do
allow(Dependabot::Experiments).to receive(:enabled?)
.with(:composer_v1_unsupported_error)
.and_return(feature_flag_enabled)
end

context "when feature flag is enabled and version is unsupported" do
let(:version) { "0.9" }
let(:feature_flag_enabled) { true }

it "raises a ToolVersionNotSupported error" do
expect { package_manager.raise_if_unsupported! }.to raise_error(Dependabot::ToolVersionNotSupported)
end
end

context "when feature flag is disabled" do
let(:version) { "0.9" }
let(:feature_flag_enabled) { false }

it "does not raise an error" do
expect { package_manager.raise_if_unsupported! }.not_to raise_error
end
end
end
end

0 comments on commit f38ac2f

Please sign in to comment.