Skip to content

Commit 119fcb0

Browse files
authored
Merge pull request #5873 from dependabot/mctofu/optimize-transitive-update
[npm] Flag indirect transitive updates to be ignored by the FileUpdater
2 parents 7b19233 + ee24f9f commit 119fcb0

File tree

5 files changed

+52
-19
lines changed

5 files changed

+52
-19
lines changed

common/lib/dependabot/dependency.rb

+7
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,13 @@ def all_versions
115115
all_versions.filter_map(&:version)
116116
end
117117

118+
# This dependency is being indirectly updated by an update to another
119+
# dependency. We don't need to try and update it ourselves but want to
120+
# surface it to the user in the PR.
121+
def informational_only?
122+
metadata[:information_only]
123+
end
124+
118125
def ==(other)
119126
other.instance_of?(self.class) && to_h == other.to_h
120127
end

hex/spec/dependabot/hex/update_checker_spec.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,7 @@
228228
[{ file: "mix.exs", requirement: "~> 1.2.1", groups: [], source: nil }]
229229
end
230230

231-
it { is_expected.to eq(Gem::Version.new("1.3.4")) }
231+
it { is_expected.to eq(Gem::Version.new("1.3.5")) }
232232
end
233233

234234
context "when the user is ignoring the latest version" do

npm_and_yarn/lib/dependabot/npm_and_yarn/update_checker.rb

+9-5
Original file line numberDiff line numberDiff line change
@@ -190,16 +190,18 @@ def conflicting_updated_dependencies
190190
end
191191
# rubocop:enable Metrics/AbcSize
192192

193-
# We don't need to update this but need to include it so it's described
194-
# in the PR and we'll pass validation that this dependency is at a
195-
# non-vulnerable version.
193+
# We don't need to directly update the target dependency if it will
194+
# be updated as a side effect of updating the parent. However, we need
195+
# to include it so it's described in the PR and we'll pass validation
196+
# that this dependency is at a non-vulnerable version.
196197
if updated_deps.none? { |dep| dep.name == dependency.name }
197198
target_version = vulnerability_audit["target_version"]
198199
updated_deps << build_updated_dependency(
199200
dependency: dependency,
200201
version: target_version,
201202
previous_version: dependency.version,
202-
removed: target_version.nil?
203+
removed: target_version.nil?,
204+
metadata: { information_only: true } # Instruct updater to not directly update this dependency
203205
)
204206
end
205207

@@ -223,6 +225,7 @@ def build_updated_dependency(update_details)
223225
removed = update_details.fetch(:removed, false)
224226
version = update_details.fetch(:version).to_s unless removed
225227
previous_version = update_details.fetch(:previous_version)&.to_s
228+
metadata = update_details.fetch(:metadata, {})
226229

227230
Dependency.new(
228231
name: original_dep.name,
@@ -236,7 +239,8 @@ def build_updated_dependency(update_details)
236239
previous_version: previous_version,
237240
previous_requirements: original_dep.requirements,
238241
package_manager: original_dep.package_manager,
239-
removed: removed
242+
removed: removed,
243+
metadata: metadata
240244
)
241245
end
242246

npm_and_yarn/spec/dependabot/npm_and_yarn/update_checker_spec.rb

+31-10
Original file line numberDiff line numberDiff line change
@@ -1361,6 +1361,22 @@
13611361
)
13621362
end
13631363

1364+
# Dependency doesn't consider metadata as part of equality checks
1365+
# so this allows us to check that the metadata is updated in tests.
1366+
RSpec::Matchers.define :including_metadata do |expected|
1367+
match do |actual|
1368+
actual == expected && actual.metadata == expected.metadata
1369+
end
1370+
end
1371+
1372+
def contain_exactly_including_metadata(*expected)
1373+
contain_exactly(*expected.map { |e| including_metadata(e) })
1374+
end
1375+
1376+
def eq_including_metadata(expected_array)
1377+
eq(expected_array).and contain_exactly_including_metadata(*expected_array)
1378+
end
1379+
13641380
context "for a security update for a locked transitive dependency" do
13651381
let(:dependency_files) { project_dependency_files("npm8/locked_transitive_dependency") }
13661382
let(:registry_listing_url) { "https://registry.npmjs.org/locked-transitive-dependency" }
@@ -1385,14 +1401,15 @@
13851401

13861402
it "correctly updates the transitive dependency" do
13871403
expect(checker.send(:updated_dependencies_after_full_unlock)).
1388-
to eq([
1404+
to eq_including_metadata([
13891405
Dependabot::Dependency.new(
13901406
name: "@dependabot-fixtures/npm-transitive-dependency",
13911407
version: "1.0.1",
13921408
package_manager: "npm_and_yarn",
13931409
previous_version: "1.0.0",
13941410
requirements: [],
1395-
previous_requirements: []
1411+
previous_requirements: [],
1412+
metadata: { information_only: true }
13961413
),
13971414
Dependabot::Dependency.new(
13981415
name: "@dependabot-fixtures/npm-parent-dependency",
@@ -1426,14 +1443,15 @@
14261443
let(:registry_listing_url) { "https://registry.npmjs.org/transitive-dependency-locked-by-intermediate" }
14271444

14281445
it "correctly updates the transitive dependency" do
1429-
expect(checker.send(:updated_dependencies_after_full_unlock)).to eq([
1446+
expect(checker.send(:updated_dependencies_after_full_unlock)).to eq_including_metadata([
14301447
Dependabot::Dependency.new(
14311448
name: "@dependabot-fixtures/npm-transitive-dependency",
14321449
package_manager: "npm_and_yarn",
14331450
previous_requirements: [],
14341451
previous_version: "1.0.0",
14351452
requirements: [],
1436-
version: "1.0.1"
1453+
version: "1.0.1",
1454+
metadata: { information_only: true }
14371455
),
14381456
Dependabot::Dependency.new(
14391457
name: "@dependabot-fixtures/npm-intermediate-dependency",
@@ -1452,7 +1470,7 @@
14521470
let(:registry_listing_url) { "https://registry.npmjs.org/transitive-dependency-locked-by-multiple" }
14531471

14541472
it "correctly updates the transitive dependency" do
1455-
expect(checker.send(:updated_dependencies_after_full_unlock)).to contain_exactly(
1473+
expect(checker.send(:updated_dependencies_after_full_unlock)).to contain_exactly_including_metadata(
14561474
Dependabot::Dependency.new(
14571475
name: "@dependabot-fixtures/npm-parent-dependency",
14581476
package_manager: "npm_and_yarn",
@@ -1531,7 +1549,8 @@
15311549
previous_requirements: [],
15321550
previous_version: "1.0.0",
15331551
requirements: [],
1534-
version: "1.0.1"
1552+
version: "1.0.1",
1553+
metadata: { information_only: true }
15351554
)
15361555
)
15371556
end
@@ -1547,14 +1566,15 @@
15471566
end
15481567

15491568
it "correctly updates the parent dependency and removes the transitive because removal is enabled" do
1550-
expect(checker.send(:updated_dependencies_after_full_unlock)).to contain_exactly(
1569+
expect(checker.send(:updated_dependencies_after_full_unlock)).to contain_exactly_including_metadata(
15511570
Dependabot::Dependency.new(
15521571
name: "@dependabot-fixtures/npm-transitive-dependency",
15531572
package_manager: "npm_and_yarn",
15541573
previous_requirements: [],
15551574
previous_version: "1.0.0",
15561575
requirements: [],
1557-
removed: true
1576+
removed: true,
1577+
metadata: { information_only: true }
15581578
),
15591579
Dependabot::Dependency.new(
15601580
name: "@dependabot-fixtures/npm-remove-dependency",
@@ -1608,14 +1628,15 @@
16081628
end
16091629

16101630
it "correctly updates the transitive dependency by unlocking the parent" do
1611-
expect(checker.send(:updated_dependencies_after_full_unlock)).to eq([
1631+
expect(checker.send(:updated_dependencies_after_full_unlock)).to eq_including_metadata([
16121632
Dependabot::Dependency.new(
16131633
name: "@dependabot-fixtures/npm-transitive-dependency-with-more-versions",
16141634
package_manager: "npm_and_yarn",
16151635
previous_requirements: [],
16161636
previous_version: "1.0.0",
16171637
requirements: [],
1618-
version: "2.0.0"
1638+
version: "2.0.0",
1639+
metadata: { information_only: true }
16191640
),
16201641
Dependabot::Dependency.new(
16211642
name: "@dependabot-fixtures/npm-parent-dependency-with-more-versions",

updater/lib/dependabot/updater.rb

+4-3
Original file line numberDiff line numberDiff line change
@@ -733,9 +733,10 @@ def generate_dependency_files_for(updated_dependencies)
733733
logger_info("Updating #{dependency_names.join(', ')}")
734734
end
735735

736-
# Removal is only supported for transitive dependencies which are removed as a
737-
# side effect of the parent update
738-
deps_to_update = updated_dependencies.reject(&:removed?)
736+
# Ignore dependencies that are tagged as information_only. These will be
737+
# updated indirectly as a result of a parent dependency update and are
738+
# only included here to be included in the PR info.
739+
deps_to_update = updated_dependencies.reject(&:informational_only?)
739740
updater = file_updater_for(deps_to_update)
740741
updater.updated_dependency_files
741742
end

0 commit comments

Comments
 (0)