@@ -200,6 +200,12 @@ def github(repo, options = {})
200200
201201 def override ( target , **operations )
202202 validate_override_target! ( target )
203+ source_location = caller_locations ( 1 , 1 ) &.first
204+
205+ if operations . key? ( :from ) || operations . key? ( :to )
206+ add_dependency_override ( target , operations , source_location )
207+ return
208+ end
203209
204210 if target == :all && operations . key? ( :version )
205211 raise ArgumentError , "`override :all, version:` is not allowed; version requirements are per-gem"
@@ -211,14 +217,14 @@ def override(target, **operations)
211217 validate_override_uniqueness! ( target , field )
212218 end
213219
214- source_location = caller_locations ( 1 , 1 ) &.first
215220 operations . each do |field , operation |
216221 @overrides << Override . new ( target , field , operation , source_location : source_location )
217222 end
218223 end
219224
220225 def to_definition ( lockfile , unlock )
221226 check_primary_source_safety
227+ check_dropped_dependencies
222228 lockfile = @lockfile unless @lockfile . nil?
223229 Definition . new ( lockfile , @dependencies , @sources , unlock , @ruby_version , @optional_groups , @gemfiles , @overrides )
224230 end
@@ -317,6 +323,54 @@ def validate_override_uniqueness!(target, field)
317323 raise ArgumentError , "duplicate override for #{ target . inspect } `#{ field } :`"
318324 end
319325
326+ def add_dependency_override ( target , operations , source_location )
327+ unless operations . key? ( :from )
328+ raise ArgumentError , "`override #{ target . inspect } , to:` requires `from:` naming the gem whose dependency is rewritten"
329+ end
330+ unless operations . key? ( :to )
331+ raise ArgumentError , "`override #{ target . inspect } , from:` requires `to:`; use `to: nil` to drop the dependency or `to: \" <gem>\" ` to replace it"
332+ end
333+
334+ from = operations [ :from ]
335+ to = operations [ :to ]
336+ requirement = operations [ :version ]
337+
338+ raise ArgumentError , "`override :all` does not accept `from:`" if target == :all
339+ raise ArgumentError , "`from:` must be a gem name string, got #{ from . inspect } " unless from . is_a? ( String )
340+ raise ArgumentError , "`to:` must be a gem name string or nil, got #{ to . inspect } " unless to . nil? || to . is_a? ( String )
341+ raise ArgumentError , "`to:` must name a gem other than #{ target . inspect } and #{ from . inspect } " if to == target || to == from
342+ validate_gem_name! ( from )
343+ validate_gem_name! ( to ) if to
344+
345+ unsupported = operations . keys - [ :from , :to , :version ]
346+ if unsupported . any?
347+ raise ArgumentError , "unsupported override field `#{ unsupported . first } :` with `from:`; supported fields: `to:`, `version:`"
348+ end
349+
350+ if operations . key? ( :version )
351+ raise ArgumentError , "`version:` with `from:` requires `to:` to name a replacement gem" if to . nil?
352+ raise ArgumentError , "`version:` with `from:` must be a version requirement string, got #{ requirement . inspect } " unless requirement . is_a? ( String )
353+ validate_override_operation! ( requirement )
354+ end
355+
356+ if @overrides . any? { |o | o . target == target && o . from == from }
357+ raise ArgumentError , "duplicate override for #{ target . inspect } `from: #{ from . inspect } `"
358+ end
359+
360+ @overrides << Override . new ( target , :to , to , from : from , requirement : requirement , source_location : source_location )
361+ end
362+
363+ # Dropping a gem the Gemfile requires directly would leave it installed
364+ # anyway, with only the parent's requirement gone.
365+ def check_dropped_dependencies
366+ @overrides . each do |override |
367+ next unless override . from && override . operation . nil?
368+ next unless @dependencies . any? { |d | d . name == override . target }
369+
370+ raise GemfileError , "#{ override } cannot drop #{ override . target } because the Gemfile depends on it directly"
371+ end
372+ end
373+
320374 def add_dependency ( name , version = nil , options = { } )
321375 options [ "gemfile" ] = @gemfile
322376 options [ "source" ] ||= @source
@@ -460,14 +514,18 @@ def valid_keys
460514 @valid_keys ||= VALID_KEYS
461515 end
462516
463- def normalize_options ( name , version , opts )
517+ def validate_gem_name! ( name )
464518 if name . is_a? ( Symbol )
465519 raise GemfileError , %(You need to specify gem names as Strings. Use 'gem "#{ name } "' instead)
466520 end
467521 if /\s / . match? ( name )
468522 raise GemfileError , %('#{ name } ' is not a valid gem name because it contains whitespace)
469523 end
470524 raise GemfileError , %(an empty gem name is not valid) if name . empty?
525+ end
526+
527+ def normalize_options ( name , version , opts )
528+ validate_gem_name! ( name )
471529
472530 normalize_hash ( opts )
473531
0 commit comments