From d7ff4a748be6353aa61ba15fb0c9f127eadeffff Mon Sep 17 00:00:00 2001 From: Hariharan Thavachelvam <164553783+thavaahariharangit@users.noreply.github.com> Date: Fri, 22 May 2026 13:22:51 +0000 Subject: [PATCH 1/3] fix(npm_and_yarn): split compound engine constraints before Requirement parsing --- .../dependabot/npm_and_yarn/package_manager.rb | 12 ++++++++++-- .../package_manager_helper_spec.rb | 18 ++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/npm_and_yarn/lib/dependabot/npm_and_yarn/package_manager.rb b/npm_and_yarn/lib/dependabot/npm_and_yarn/package_manager.rb index d35b6520379..ead69395824 100644 --- a/npm_and_yarn/lib/dependabot/npm_and_yarn/package_manager.rb +++ b/npm_and_yarn/lib/dependabot/npm_and_yarn/package_manager.rb @@ -207,14 +207,22 @@ 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}") nil end + sig { params(constraints: T::Array[String]).returns(T::Array[String]) } + def split_compound_constraints(constraints) + constraints.flat_map do |constraint| + constraint.strip.split(/\s+(?=[<>]=?|=)/) + end + end + # rubocop:disable Metrics/CyclomaticComplexity # rubocop:disable Metrics/AbcSize # rubocop:disable Metrics/PerceivedComplexity diff --git a/npm_and_yarn/spec/dependabot/npm_and_yarn/package_manager_helper_spec.rb b/npm_and_yarn/spec/dependabot/npm_and_yarn/package_manager_helper_spec.rb index 51fc3a5ee66..9bf5e35d02e 100644 --- a/npm_and_yarn/spec/dependabot/npm_and_yarn/package_manager_helper_spec.rb +++ b/npm_and_yarn/spec/dependabot/npm_and_yarn/package_manager_helper_spec.rb @@ -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 { From 8b1868f78485955b36e748f1c204d681becd613a Mon Sep 17 00:00:00 2001 From: Hariharan Thavachelvam <164553783+thavaahariharangit@users.noreply.github.com> Date: Fri, 22 May 2026 13:43:54 +0000 Subject: [PATCH 2/3] fix(npm_and_yarn): avoid regex split for compound constraints --- .../npm_and_yarn/package_manager.rb | 53 ++++++++++++++++--- 1 file changed, 46 insertions(+), 7 deletions(-) diff --git a/npm_and_yarn/lib/dependabot/npm_and_yarn/package_manager.rb b/npm_and_yarn/lib/dependabot/npm_and_yarn/package_manager.rb index ead69395824..58ffbc5be62 100644 --- a/npm_and_yarn/lib/dependabot/npm_and_yarn/package_manager.rb +++ b/npm_and_yarn/lib/dependabot/npm_and_yarn/package_manager.rb @@ -216,13 +216,6 @@ def find_engine_constraints_as_requirement(name) nil end - sig { params(constraints: T::Array[String]).returns(T::Array[String]) } - def split_compound_constraints(constraints) - constraints.flat_map do |constraint| - constraint.strip.split(/\s+(?=[<>]=?|=)/) - end - end - # rubocop:disable Metrics/CyclomaticComplexity # rubocop:disable Metrics/AbcSize # rubocop:disable Metrics/PerceivedComplexity @@ -382,6 +375,52 @@ 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 = constraint[index] + + if whitespace_character?(character) + index += 1 + index += 1 while index < constraint.length && whitespace_character?(constraint[index]) + + if !current.empty? && index < constraint.length && comparator_start?(constraint[index]) + tokens << current + current = +"" + elsif !current.empty? && index < constraint.length + current << " " + end + + next + end + + current << character + index += 1 + end + + tokens << current unless current.empty? + tokens + 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 From 1079c07a94d38f0484f181864c014b4679cabbbe Mon Sep 17 00:00:00 2001 From: Hariharan Thavachelvam <164553783+thavaahariharangit@users.noreply.github.com> Date: Fri, 22 May 2026 13:56:17 +0000 Subject: [PATCH 3/3] refactor(npm_and_yarn): reduce constraint tokenizer complexity --- .../npm_and_yarn/package_manager.rb | 38 ++++++++++++++----- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/npm_and_yarn/lib/dependabot/npm_and_yarn/package_manager.rb b/npm_and_yarn/lib/dependabot/npm_and_yarn/package_manager.rb index 58ffbc5be62..87261f8b197 100644 --- a/npm_and_yarn/lib/dependabot/npm_and_yarn/package_manager.rb +++ b/npm_and_yarn/lib/dependabot/npm_and_yarn/package_manager.rb @@ -387,18 +387,11 @@ def split_compound_constraint(constraint) index = 0 while index < constraint.length - character = constraint[index] + character = T.must(constraint[index]) if whitespace_character?(character) - index += 1 - index += 1 while index < constraint.length && whitespace_character?(constraint[index]) - - if !current.empty? && index < constraint.length && comparator_start?(constraint[index]) - tokens << current - current = +"" - elsif !current.empty? && index < constraint.length - current << " " - end + index = skip_whitespace_characters(constraint, index + 1) + normalize_boundary_after_whitespace(constraint, index, current, tokens) next end @@ -411,6 +404,31 @@ def split_compound_constraint(constraint) 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"