diff --git a/npm_and_yarn/lib/dependabot/npm_and_yarn/file_updater/package_json_updater.rb b/npm_and_yarn/lib/dependabot/npm_and_yarn/file_updater/package_json_updater.rb index 536b8c7d0a..332dcaad6c 100644 --- a/npm_and_yarn/lib/dependabot/npm_and_yarn/file_updater/package_json_updater.rb +++ b/npm_and_yarn/lib/dependabot/npm_and_yarn/file_updater/package_json_updater.rb @@ -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 @@ -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 @@ -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 @@ -55,24 +70,35 @@ 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 @@ -80,8 +106,16 @@ def updated_requirements(dependency) .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, @@ -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 = @@ -141,17 +183,25 @@ 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* @@ -159,13 +209,21 @@ def declaration_line(dependency_name:, dependency_req:, content:) ).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 @@ -186,15 +244,23 @@ 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 @@ -202,13 +268,21 @@ def update_git_semver_requirement(original_line:, old_req:, new_req:) 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 @@ -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