Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Strict type Dependabot::NpmAndYarn::FileUpdater::PackageJsonUpdater #9800

Merged
merged 1 commit into from
May 27, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,17 +1,28 @@
# typed: false
# typed: strict
# frozen_string_literal: true

require "sorbet-runtime"

require "dependabot/npm_and_yarn/file_updater"

module Dependabot
module NpmAndYarn
class FileUpdater < Dependabot::FileUpdaters::Base
class PackageJsonUpdater
extend T::Sig

sig do
params(
package_json: Dependabot::DependencyFile,
dependencies: T::Array[Dependabot::Dependency]
) .void
end
def initialize(package_json:, dependencies:)
@package_json = package_json
@dependencies = dependencies
end

sig { returns(Dependabot::DependencyFile) }
def updated_package_json
updated_file = package_json.dup
updated_file.content = updated_package_json_content
Expand All @@ -20,16 +31,20 @@ def updated_package_json

private

sig { returns(Dependabot::DependencyFile) }
attr_reader :package_json

sig { returns(T::Array[Dependabot::Dependency]) }
attr_reader :dependencies

sig { returns(T.nilable(String)) }
def updated_package_json_content
dependencies.reduce(package_json.content.dup) do |content, dep|
updated_requirements(dep).each do |new_req|
updated_requirements(dep)&.each do |new_req|
old_req = old_requirement(dep, new_req)

new_content = update_package_json_declaration(
package_json_content: content,
package_json_content: T.must(content),
dependency_name: dep.name,
old_req: old_req,
new_req: new_req
Expand All @@ -44,7 +59,7 @@ def updated_package_json_content
old_req = old_requirement(dep, new_req)

content = update_package_json_resolutions(
package_json_content: content,
package_json_content: T.must(content),
new_req: new_req,
dependency: dep,
old_req: old_req
Expand All @@ -55,33 +70,52 @@ def updated_package_json_content
end
end

sig do
params(
dependency: Dependabot::Dependency,
new_requirement: T::Hash[Symbol, T.untyped]
)
.returns(T.nilable(T::Hash[Symbol, T.untyped]))
end
def old_requirement(dependency, new_requirement)
dependency.previous_requirements
.select { |r| r[:file] == package_json.name }
.find { |r| r[:groups] == new_requirement[:groups] }
T.must(dependency.previous_requirements)
.select { |r| r[:file] == package_json.name }
.find { |r| r[:groups] == new_requirement[:groups] }
end

sig { params(dependency: Dependabot::Dependency).returns(T::Array[T::Hash[Symbol, T.untyped]]) }
def new_requirements(dependency)
dependency.requirements.select { |r| r[:file] == package_json.name }
end

sig { params(dependency: Dependabot::Dependency).returns(T.nilable(T::Array[T::Hash[Symbol, T.untyped]])) }
def updated_requirements(dependency)
return unless dependency.previous_requirements

updated_requirement_pairs =
dependency.requirements.zip(dependency.previous_requirements)
dependency.requirements.zip(T.must(dependency.previous_requirements))
.reject do |new_req, old_req|
next true if new_req == old_req
next false unless old_req[:source].nil?
next false unless old_req&.fetch(:source).nil?

new_req[:requirement] == old_req[:requirement]
new_req[:requirement] == old_req&.fetch(:requirement)
end

updated_requirement_pairs
.map(&:first)
.select { |r| r[:file] == package_json.name }
end

def update_package_json_declaration(package_json_content:, new_req:,
dependency_name:, old_req:)
sig do
params(
package_json_content: String,
new_req: T::Hash[Symbol, T.untyped],
dependency_name: String,
old_req: T.nilable(T::Hash[Symbol, T.untyped])
)
.returns(String)
end
def update_package_json_declaration(package_json_content:, new_req:, dependency_name:, old_req:)
original_line = declaration_line(
dependency_name: dependency_name,
dependency_req: old_req,
Expand All @@ -107,8 +141,16 @@ def update_package_json_declaration(package_json_content:, new_req:,
# For full details on how Yarn resolutions work, see
# https://github.com/yarnpkg/rfcs/blob/master/implemented/
# 0000-selective-versions-resolutions.md
def update_package_json_resolutions(package_json_content:, new_req:,
dependency:, old_req:)
sig do
params(
package_json_content: String,
new_req: T::Hash[Symbol, T.untyped],
dependency: Dependabot::Dependency,
old_req: T.nilable(T::Hash[Symbol, T.untyped])
)
.returns(String)
end
def update_package_json_resolutions(package_json_content:, new_req:, dependency:, old_req:)
dep = dependency
parsed_json_content = JSON.parse(package_json_content)
resolutions =
Expand Down Expand Up @@ -141,31 +183,47 @@ def update_package_json_resolutions(package_json_content:, new_req:,
content
end

sig do
params(
dependency_name: String,
dependency_req: T.nilable(T::Hash[Symbol, T.untyped]),
content: String
)
.returns(String)
end
def declaration_line(dependency_name:, dependency_req:, content:)
git_dependency = dependency_req.dig(:source, :type) == "git"
git_dependency = dependency_req&.dig(:source, :type) == "git"

unless git_dependency
requirement = dependency_req.fetch(:requirement)
requirement = dependency_req&.fetch(:requirement)
return content.match(/"#{Regexp.escape(dependency_name)}"\s*:\s*
"#{Regexp.escape(requirement)}"/x).to_s
end

username, repo =
dependency_req.dig(:source, :url).split("/").last(2)
dependency_req&.dig(:source, :url)&.split("/")&.last(2)

content.match(
%r{"#{Regexp.escape(dependency_name)}"\s*:\s*
".*?#{Regexp.escape(username)}/#{Regexp.escape(repo)}.*"}x
).to_s
end

sig do
params(
original_line: String,
old_req: T.nilable(T::Hash[Symbol, T.untyped]),
new_req: T::Hash[Symbol, T.untyped]
)
.returns(String)
end
def replacement_declaration_line(original_line:, old_req:, new_req:)
was_git_dependency = old_req.dig(:source, :type) == "git"
was_git_dependency = old_req&.dig(:source, :type) == "git"
now_git_dependency = new_req.dig(:source, :type) == "git"

unless was_git_dependency
return original_line.gsub(
%("#{old_req.fetch(:requirement)}"),
%("#{old_req&.fetch(:requirement)}"),
%("#{new_req.fetch(:requirement)}")
)
end
Expand All @@ -186,29 +244,45 @@ def replacement_declaration_line(original_line:, old_req:, new_req:)
end

original_line.gsub(
%(##{old_req.dig(:source, :ref)}"),
%(##{old_req&.dig(:source, :ref)}"),
%(##{new_req.dig(:source, :ref)}")
)
end

sig do
params(
original_line: String,
old_req: T.nilable(T::Hash[Symbol, String]),
new_req: T::Hash[Symbol, String]
)
.returns(String)
end
def update_git_semver_requirement(original_line:, old_req:, new_req:)
if original_line.include?("semver:")
return original_line.gsub(
%(semver:#{old_req.fetch(:requirement)}"),
%(semver:#{old_req&.fetch(:requirement)}"),
%(semver:#{new_req.fetch(:requirement)}")
)
end

raise "Not a semver req!" unless original_line.match?(/#[\^~=<>]/)

original_line.gsub(
%(##{old_req.fetch(:requirement)}"),
%(##{old_req&.fetch(:requirement)}"),
%(##{new_req.fetch(:requirement)}")
)
end

def update_package_json_sections(sections, content, old_line,
new_line)
sig do
params(
sections: T::Array[String],
content: String,
old_line: String,
new_line: String
)
.returns(String)
end
def update_package_json_sections(sections, content, old_line, new_line)
# Currently, Dependabot doesn't update peerDependencies. However,
# if a development dependency is being updated and its requirement
# matches the requirement on a peer dependency we probably want to
Expand All @@ -218,21 +292,21 @@ def update_package_json_sections(sections, content, old_line,
sections += ["peerDependencies"]
sections_regex = /#{sections.join('|')}/

declaration_blocks = []
declaration_blocks = T.let([], T::Array[String])

content.scan(/['"]#{sections_regex}['"]\s*:\s*\{/m) do
mtch = Regexp.last_match
mtch = T.must(Regexp.last_match)
declaration_blocks <<
(mtch.to_s +
mtch.post_match[0..closing_bracket_index(mtch.post_match)])
(mtch.to_s + T.must(mtch.post_match[0..closing_bracket_index(mtch.post_match)]))
end

declaration_blocks.reduce(content.dup) do |new_content, block|
updated_block = block.sub(old_line, new_line)
new_content.sub!(block, updated_block)
new_content.sub(block, updated_block)
end
end

sig { params(string: String).returns(Integer) }
def closing_bracket_index(string)
closes_required = 1

Expand Down
Loading