summaryrefslogtreecommitdiff
path: root/lib/bundler/vendor/pub_grub/lib/pub_grub/failure_writer.rb
blob: ee099b23f446d838b00f76fad0993627b2e06cef (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
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
module Bundler::PubGrub
  class FailureWriter
    def initialize(root)
      @root = root

      # { Incompatibility => Integer }
      @derivations = {}

      # [ [ String, Integer or nil ] ]
      @lines = []

      # { Incompatibility => Integer }
      @line_numbers = {}

      count_derivations(root)
    end

    def write
      return @root.to_s unless @root.conflict?

      visit(@root)

      padding = @line_numbers.empty? ? 0 : "(#{@line_numbers.values.last}) ".length

      @lines.map do |message, number|
        next "" if message.empty?

        lead = number ? "(#{number}) " : ""
        lead = lead.ljust(padding)
        message = message.gsub("\n", "\n" + " " * (padding + 2))
        "#{lead}#{message}"
      end.join("\n")
    end

    private

    def write_line(incompatibility, message, numbered:)
      if numbered
        number = @line_numbers.length + 1
        @line_numbers[incompatibility] = number
      end

      @lines << [message, number]
    end

    def visit(incompatibility, conclusion: false)
      raise unless incompatibility.conflict?

      numbered = conclusion || @derivations[incompatibility] > 1;
      conjunction = conclusion || incompatibility == @root ? "So," : "And"

      cause = incompatibility.cause

      if cause.conflict.conflict? && cause.other.conflict?
        conflict_line = @line_numbers[cause.conflict]
        other_line = @line_numbers[cause.other]

        if conflict_line && other_line
          write_line(
            incompatibility,
            "Because #{cause.conflict} (#{conflict_line})\nand #{cause.other} (#{other_line}),\n#{incompatibility}.",
            numbered: numbered
          )
        elsif conflict_line || other_line
          with_line    = conflict_line ? cause.conflict : cause.other
          without_line = conflict_line ? cause.other : cause.conflict
          line = @line_numbers[with_line]

          visit(without_line);
          write_line(
            incompatibility,
            "#{conjunction} because #{with_line} (#{line}),\n#{incompatibility}.",
            numbered: numbered
          )
        else
          single_line_conflict = single_line?(cause.conflict.cause)
          single_line_other    = single_line?(cause.other.cause)

          if single_line_conflict || single_line_other
            first  = single_line_other ? cause.conflict : cause.other
            second = single_line_other ? cause.other : cause.conflict
            visit(first)
            visit(second)
            write_line(
              incompatibility,
              "Thus, #{incompatibility}.",
              numbered: numbered
            )
          else
            visit(cause.conflict, conclusion: true)
            @lines << ["", nil]
            visit(cause.other)

            write_line(
              incompatibility,
              "#{conjunction} because #{cause.conflict} (#{@line_numbers[cause.conflict]}),\n#{incompatibility}.",
              numbered: numbered
            )
          end
        end
      elsif cause.conflict.conflict? || cause.other.conflict?
        derived = cause.conflict.conflict? ? cause.conflict : cause.other
        ext     = cause.conflict.conflict? ? cause.other : cause.conflict

        derived_line = @line_numbers[derived]
        if derived_line
          write_line(
            incompatibility,
            "Because #{ext}\nand #{derived} (#{derived_line}),\n#{incompatibility}.",
            numbered: numbered
          )
        elsif collapsible?(derived)
          derived_cause = derived.cause
          if derived_cause.conflict.conflict?
            collapsed_derived = derived_cause.conflict
            collapsed_ext = derived_cause.other
          else
            collapsed_derived = derived_cause.other
            collapsed_ext = derived_cause.conflict
          end

          visit(collapsed_derived)

          write_line(
            incompatibility,
            "#{conjunction} because #{collapsed_ext}\nand #{ext},\n#{incompatibility}.",
            numbered: numbered
          )
        else
          visit(derived)
          write_line(
            incompatibility,
            "#{conjunction} because #{ext},\n#{incompatibility}.",
            numbered: numbered
          )
        end
      else
        write_line(
          incompatibility,
          "Because #{cause.conflict}\nand #{cause.other},\n#{incompatibility}.",
          numbered: numbered
        )
      end
    end

    def single_line?(cause)
      !cause.conflict.conflict? && !cause.other.conflict?
    end

    def collapsible?(incompatibility)
      return false if @derivations[incompatibility] > 1

      cause = incompatibility.cause
      # If incompatibility is derived from two derived incompatibilities,
      # there are too many transitive causes to display concisely.
      return false if cause.conflict.conflict? && cause.other.conflict?

      # If incompatibility is derived from two external incompatibilities, it
      # tends to be confusing to collapse it.
      return false unless cause.conflict.conflict? || cause.other.conflict?

      # If incompatibility's internal cause is numbered, collapsing it would
      # get too noisy.
      complex = cause.conflict.conflict? ? cause.conflict : cause.other

      !@line_numbers.has_key?(complex)
    end

    def count_derivations(incompatibility)
      if @derivations.has_key?(incompatibility)
        @derivations[incompatibility] += 1
      else
        @derivations[incompatibility] = 1
        if incompatibility.conflict?
          cause = incompatibility.cause
          count_derivations(cause.conflict)
          count_derivations(cause.other)
        end
      end
    end
  end
end