Module: Bundler::Overrule::DependencyRewriter

Defined in:
lib/bundler/overrule/dependency_rewriter.rb

Overview

The heart of the gem: given a list of Gem::Dependency objects and a set of rules, return the rewritten list plus the edges that changed.

Deliberately pure — no Bundler objects, no global state, no I/O — so the behavior contract can be unit-tested without running Bundler at all. Both the resolution seam and the installation seam call this same function, which is what keeps the lockfile and the installed gems from ever disagreeing about what a dependency list contains.

Constant Summary collapse

LOCATOR_OPTIONS =

Options on a Gemfile dependency that describe which gem, as opposed to how it is used. A swap points at a different gem, so these must not be carried over — gem 'httpclient', git: '...' swapped to a fork would otherwise fetch the fork's name from the old gem's git remote.

%w[
  source git github gist bitbucket path branch ref tag glob submodules
].freeze

Class Method Summary collapse

Class Method Details

.apply_rule(dependency, rule, owner) ⇒ Array(Gem::Dependency, Edge)

Decide what one dependency becomes under one rule.

Returns:

  • (Array(Gem::Dependency, Edge))

    the replacement (nil to drop the dependency entirely) and the edge to record (nil if nothing changed)



57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
# File 'lib/bundler/overrule/dependency_rewriter.rb', line 57

def apply_rule(dependency, rule, owner)
  # Development dependencies never reach the resolver, but spec objects
  # still report them. Rewriting or dropping one would be a lie about
  # what the gem declares, for no benefit.
  return [dependency, nil] if rule.nil? || development?(dependency)

  case rule.type
  when :ban
    [nil, Edge.new(rule, owner, requirement_string(dependency), nil)]
  when :force
    return [dependency, nil] if dependency.requirement == rule.requirement

    [replace(dependency, rule.requirement),
     Edge.new(rule, owner, requirement_string(dependency), rule.requirement.to_s)]
  when :swap
    [substitute(dependency, rule.replacement, rule.requirement),
     Edge.new(rule, owner, requirement_string(dependency),
              "#{rule.replacement} #{rule.requirement}")]
  else
    [dependency, nil]
  end
end

.development?(dependency) ⇒ Boolean

Returns:

  • (Boolean)


80
81
82
# File 'lib/bundler/overrule/dependency_rewriter.rb', line 80

def development?(dependency)
  dependency.respond_to?(:type) && dependency.type == :development
end

.replace(dependency, requirement) ⇒ Object

Swap in a new requirement while keeping everything else about the dependency intact.

We copy-and-mutate rather than construct a fresh object on purpose. Bundler::Dependency derives #groups, #source, #git, #path, #github, #branch and #ref lazily from an options hash it keeps privately, and the shape of that hash has changed across Bundler versions. Rebuilding from public readers silently drops whichever of those we forgot — a force on a gem declared with git: would quietly lose its source. Duplicating preserves the exact class and every option, forever.



98
99
100
101
102
# File 'lib/bundler/overrule/dependency_rewriter.rb', line 98

def replace(dependency, requirement)
  replacement = dependency.dup
  replacement.instance_variable_set(:@requirement, requirement)
  replacement
end

.requirement_string(dependency) ⇒ Object



84
85
86
# File 'lib/bundler/overrule/dependency_rewriter.rb', line 84

def requirement_string(dependency)
  dependency.requirement.to_s
end

.rewrite(dependencies, rules_by_name, owner: nil) ⇒ Array(Array<Gem::Dependency>, Array<Edge>)

Parameters:

  • dependencies (Array<Gem::Dependency>)
  • rules_by_name (Hash{String => Rule})
  • owner (String, nil) (defaults to: nil)

    gem name that declared these dependencies

Returns:

  • (Array(Array<Gem::Dependency>, Array<Edge>))


36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# File 'lib/bundler/overrule/dependency_rewriter.rb', line 36

def rewrite(dependencies, rules_by_name, owner: nil)
  return [dependencies, []] if rules_by_name.empty? || dependencies.nil? || dependencies.empty?

  edges = []
  rewritten = []

  dependencies.each do |dependency|
    rule = rules_by_name[dependency.name]
    replacement, edge = apply_rule(dependency, rule, owner)

    rewritten << replacement unless replacement.nil?
    edges << edge if edge
  end

  [rewritten, edges]
end

.substitute(dependency, name, requirement) ⇒ Object

Point a dependency at a different gem entirely, keeping the options that still make sense (groups, platforms, the require path).



114
115
116
117
118
119
120
121
122
# File 'lib/bundler/overrule/dependency_rewriter.rb', line 114

def substitute(dependency, name, requirement)
  unless dependency.is_a?(::Bundler::Dependency)
    return ::Gem::Dependency.new(name, requirement, dependency.type)
  end

  options = dependency.instance_variable_get(:@options) || {}
  kept = options.reject { |key, _| LOCATOR_OPTIONS.include?(key.to_s) }
  ::Bundler::Dependency.new(name, requirement.as_list, kept)
end