summaryrefslogtreecommitdiff
path: root/lib/bundler/vendor/pub_grub/lib/pub_grub/incompatibility.rb
blob: 239eaf3401c0640eea70be4b64ae63be5364d61a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
module Bundler::PubGrub
  class Incompatibility
    ConflictCause = Struct.new(:incompatibility, :satisfier) do
      alias_method :conflict, :incompatibility
      alias_method :other, :satisfier
    end

    InvalidDependency = Struct.new(:package, :constraint) do
    end

    NoVersions = Struct.new(:constraint) do
    end

    attr_reader :terms, :cause

    def initialize(terms, cause:, custom_explanation: nil)
      @cause = cause
      @terms = cleanup_terms(terms)
      @custom_explanation = custom_explanation

      if cause == :dependency && @terms.length != 2
        raise ArgumentError, "a dependency Incompatibility must have exactly two terms. Got #{@terms.inspect}"
      end
    end

    def hash
      cause.hash ^ terms.hash
    end

    def eql?(other)
      cause.eql?(other.cause) &&
        terms.eql?(other.terms)
    end

    def failure?
      terms.empty? || (terms.length == 1 && Package.root?(terms[0].package) && terms[0].positive?)
    end

    def conflict?
      ConflictCause === cause
    end

    # Returns all external incompatibilities in this incompatibility's
    # derivation graph
    def external_incompatibilities
      if conflict?
        [
          cause.conflict,
          cause.other
        ].flat_map(&:external_incompatibilities)
      else
        [this]
      end
    end

    def to_s
      return @custom_explanation if @custom_explanation

      case cause
      when :root
        "(root dependency)"
      when :dependency
        "#{terms[0].to_s(allow_every: true)} depends on #{terms[1].invert}"
      when Bundler::PubGrub::Incompatibility::InvalidDependency
        "#{terms[0].to_s(allow_every: true)} depends on unknown package #{cause.package}"
      when Bundler::PubGrub::Incompatibility::NoVersions
        "no versions satisfy #{cause.constraint}"
      when Bundler::PubGrub::Incompatibility::ConflictCause
        if failure?
          "version solving has failed"
        elsif terms.length == 1
          term = terms[0]
          if term.positive?
            if term.constraint.any?
              "#{term.package} cannot be used"
            else
              "#{term.to_s(allow_every: true)} cannot be used"
            end
          else
            "#{term.invert} is required"
          end
        else
          if terms.all?(&:positive?)
            if terms.length == 2
              "#{terms[0].to_s(allow_every: true)} is incompatible with #{terms[1]}"
            else
              "one of #{terms.map(&:to_s).join(" or ")} must be false"
            end
          elsif terms.all?(&:negative?)
            if terms.length == 2
              "either #{terms[0].invert} or #{terms[1].invert}"
            else
              "one of #{terms.map(&:invert).join(" or ")} must be true";
            end
          else
            positive = terms.select(&:positive?)
            negative = terms.select(&:negative?).map(&:invert)

            if positive.length == 1
              "#{positive[0].to_s(allow_every: true)} requires #{negative.join(" or ")}"
            else
              "if #{positive.join(" and ")} then #{negative.join(" or ")}"
            end
          end
        end
      else
        raise "unhandled cause: #{cause.inspect}"
      end
    end

    def inspect
      "#<#{self.class} #{to_s}>"
    end

    def pretty_print(q)
      q.group 2, "#<#{self.class}", ">" do
        q.breakable
        q.text to_s

        q.breakable
        q.text " caused by "
        q.pp @cause
      end
    end

    private

    def cleanup_terms(terms)
      terms.each do |term|
        raise "#{term.inspect} must be a term" unless term.is_a?(Term)
      end

      if terms.length != 1 && ConflictCause === cause
        terms = terms.reject do |term|
          term.positive? && Package.root?(term.package)
        end
      end

      # Optimized simple cases
      return terms if terms.length <= 1
      return terms if terms.length == 2 && terms[0].package != terms[1].package

      terms.group_by(&:package).map do |package, common_terms|
        common_terms.inject do |acc, term|
          acc.intersect(term)
        end
      end
    end
  end
end