Skip to content
69 changes: 67 additions & 2 deletions npm_and_yarn/lib/dependabot/npm_and_yarn/package_manager.rb
Original file line number Diff line number Diff line change
Expand Up @@ -207,8 +207,9 @@ def find_engine_constraints_as_requirement(name)
end

if constraints && !constraints.empty?
Dependabot.logger.info("Parsed constraints for #{name}: #{constraints.join(', ')}")
Requirement.new(constraints)
normalized_constraints = split_compound_constraints(constraints)
Dependabot.logger.info("Parsed constraints for #{name}: #{normalized_constraints.join(', ')}")
Requirement.new(normalized_constraints)
end
rescue StandardError => e
Dependabot.logger.error("Error processing constraints for #{name}: #{e.message}")
Expand Down Expand Up @@ -374,6 +375,70 @@ def installed_version(name)

private

sig { params(constraints: T::Array[String]).returns(T::Array[String]) }
def split_compound_constraints(constraints)
constraints.flat_map { |constraint| split_compound_constraint(constraint) }
end

sig { params(constraint: String).returns(T::Array[String]) }
def split_compound_constraint(constraint)
tokens = T.let([], T::Array[String])
current = +""
index = 0

while index < constraint.length
character = T.must(constraint[index])

if whitespace_character?(character)
index = skip_whitespace_characters(constraint, index + 1)
normalize_boundary_after_whitespace(constraint, index, current, tokens)

next
end

current << character
index += 1
end

tokens << current unless current.empty?
tokens
end

sig { params(constraint: String, index: Integer).returns(Integer) }
def skip_whitespace_characters(constraint, index)
index += 1 while index < constraint.length && whitespace_character?(T.must(constraint[index]))
index
end

sig do
params(
constraint: String,
index: Integer,
current: String,
tokens: T::Array[String]
).void
end
def normalize_boundary_after_whitespace(constraint, index, current, tokens)
return if current.empty? || index >= constraint.length

if comparator_start?(T.must(constraint[index]))
tokens << current.dup
current.clear
else
current << " "
end
end

sig { params(character: String).returns(T::Boolean) }
def whitespace_character?(character)
character == " " || character == "\t" || character == "\n" || character == "\r" || character == "\f"
end

sig { params(character: String).returns(T::Boolean) }
def comparator_start?(character)
character == ">" || character == "<" || character == "="
end

sig { params(name: String, version: String).void }
def raise_if_unsupported!(name, version)
return unless name == PNPMPackageManager::NAME
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,24 @@
end
end

context "when node engines include a compound range and an alternative comparator" do
let(:package_json) do
{
"name" => "example",
"version" => "1.0.0",
"engines" => {
"node" => "^22.0.0 || >=24"
}
}
end

it "returns a requirement without raising illformed requirement errors" do
requirement = helper.find_engine_constraints_as_requirement("node")
expect(requirement).to be_a(Dependabot::NpmAndYarn::Requirement)
expect(requirement.constraints).to eq([">= 22.0.0", "< 23.0.0", ">= 24"])
end
end

context "when the engines field contains npm >=11.0.0 constraint" do
let(:package_json) do
{
Expand Down