Class: Parlour::ConflictResolver

Inherits:
Object
  • Object
show all
Extended by:
T::Sig
Defined in:
lib/parlour/conflict_resolver.rb

Overview

Responsible for resolving conflicts (that is, multiple definitions with the same name) between objects defined in the same namespace.

Instance Method Summary collapse

Instance Method Details

#resolve_conflicts(namespace) {|message, candidates| ... } ⇒ void

This method returns an undefined value.

Given a namespace, attempts to automatically resolve conflicts in the namespace’s definitions. (A conflict occurs when multiple objects share the same name.)

All children of the given namespace which are also namespaces are processed recursively, so passing RbiGenerator#root will eliminate all conflicts in the entire object tree.

If automatic resolution is not possible, the block passed to this method is invoked and passed two arguments: a message on what the conflict is, and an array of candidate objects. The block should return one of these candidate objects, which will be kept, and all other definitions are deleted. Alternatively, the block may return nil, which will delete all definitions. The block may be invoked many times from one call to #resolve_conflicts, one for each unresolvable conflict.

Parameters:

Yield Parameters:

  • message (String)

    A descriptional message on what the conflict is.

  • candidates (Array<RbiGenerator::RbiObject>)

    The objects for which there is a conflict.

Yield Returns:



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
183
184
185
186
187
188
189
190
191
192
193
# File 'lib/parlour/conflict_resolver.rb', line 43

def resolve_conflicts(namespace, &resolver)
  Debugging.debug_puts(self, Debugging::Tree.begin("Resolving conflicts for #{namespace.name}..."))

  # Check for multiple definitions with the same name
  # (Special case here: writer attributes get an "=" appended to their name)
  grouped_by_name_children = namespace.children.group_by do |child|
    if RbiGenerator::Attribute === child && child.kind == :writer
      "#{child.name}=" unless child.name.end_with?('=')
    else
      child.name
    end
  end

  grouped_by_name_children.each do |name, children|
    Debugging.debug_puts(self, Debugging::Tree.begin("Checking children named #{name}..."))

    if children.length > 1
      Debugging.debug_puts(self, Debugging::Tree.here("Possible conflict between #{children.length} objects"))

      # Special case: do we have two methods, one of which is a class method
      # and the other isn't? If so, do nothing - this is fine
      if children.length == 2 &&
        children.all? { |c| c.is_a?(RbiGenerator::Method) } &&
        children.count { |c| T.cast(c, RbiGenerator::Method).class_method } == 1

        Debugging.debug_puts(self, Debugging::Tree.end("One is an instance method and one is a class method; no resolution required"))
        next
      end

      # Special case: if we remove the namespaces, is everything either an
      # include or an extend? If so, do nothing - this is fine
      if children \
        .reject { |c| c.is_a?(RbiGenerator::Namespace) }
        .then do |x|
          !x.empty? && x.all? do |c|
            c.is_a?(RbiGenerator::Include) || c.is_a?(RbiGenerator::Extend)
          end
        end
        deduplicate_mixins_of_name(namespace, name)

        Debugging.debug_puts(self, Debugging::Tree.end("Includes/extends do not conflict with namespaces; no resolution required"))
        next
      end

      # Special case: do we have two attributes, one of which is a class
      # attribute and the other isn't? If so, do nothing - this is fine
      if children.length == 2 &&
        children.all? { |c| c.is_a?(RbiGenerator::Attribute) } &&
        children.count { |c| T.cast(c, RbiGenerator::Attribute).class_attribute } == 1

        Debugging.debug_puts(self, Debugging::Tree.end("One is an instance attribute and one is a class attribute; no resolution required"))
        next
      end

      # Optimization for Special case: are they all clearly equal? If so, remove all but one
      if all_eql?(children)
        Debugging.debug_puts(self, Debugging::Tree.end("All children are identical"))

        # All of the children are the same, so this deletes all of them
        namespace.children.delete(T.must(children.first))

        # Re-add one child
        namespace.children << T.must(children.first)
        next
      end

      # We found a conflict!
      # Start by removing all the conflicting items
      children.each do |c|
        namespace.children.delete(c)
      end

      # Check that the types of the given objects allow them to be merged,
      # and get the strategy to use
      strategy = merge_strategy(children)
      unless strategy
        Debugging.debug_puts(self, Debugging::Tree.end("Children are unmergeable types; requesting manual resolution"))
        # The types aren't the same, so ask the resolver what to do, and
        # insert that (if not nil)
        choice = resolver.call("Different kinds of definition for the same name", children)
        namespace.children << choice if choice
        next
      end

      case strategy
      when :normal
        first, *rest = children
      when :differing_namespaces
        # Let the namespaces be merged normally, but handle the method here
        namespaces, non_namespaces = children.partition { |x| RbiGenerator::Namespace === x }

        # If there is any non-namespace item in this conflict, it should be
        # a single method
        if non_namespaces.length != 0
          unless non_namespaces.length == 1 && RbiGenerator::Method === non_namespaces.first
            Debugging.debug_puts(self, Debugging::Tree.end("Non-namespace item in a differing namespace conflict is not a single method; requesting manual resolution"))
            # The types aren't the same, so ask the resolver what to do, and
            # insert that (if not nil)
            choice = resolver.call("Non-namespace item in a differing namespace conflict is not a single method", non_namespaces)
            non_namespaces = []
            non_namespaces << choice if choice
          end
        end

        non_namespaces.each do |x|
          namespace.children << x
        end

        # For certain namespace types the order matters. For example, if there's
        # both a `Namespace` and `ModuleNamespace` then merging the two would
        # produce different results depending on which is first.
        first_index = (
          namespaces.find_index { |x| RbiGenerator::EnumClassNamespace === x || RbiGenerator::StructClassNamespace === x } ||
          namespaces.find_index { |x| RbiGenerator::ClassNamespace === x } ||
          namespaces.find_index { |x| RbiGenerator::ModuleNamespace === x } ||
          0
        )

        first = namespaces.delete_at(first_index)
        rest = namespaces
      else
        raise 'unknown merge strategy; this is a Parlour bug'
      end

      # Can the children merge themselves automatically? If so, let them
      first, rest = T.must(first), T.must(rest)
      if T.must(first).mergeable?(T.must(rest))
        Debugging.debug_puts(self, Debugging::Tree.end("Children are all mergeable; resolving automatically"))
        first.merge_into_self(rest)
        namespace.children << first
        next
      end

      # I give up! Let it be resolved manually somehow
      Debugging.debug_puts(self, Debugging::Tree.end("Unable to resolve automatically; requesting manual resolution"))
      choice = resolver.call("Can't automatically resolve", children)
      namespace.children << choice if choice
    else
      Debugging.debug_puts(self, Debugging::Tree.end("No conflicts"))
    end
  end

  Debugging.debug_puts(self, Debugging::Tree.here("Resolving children..."))

  # Recurse to child namespaces
  namespace.children.each do |child|
    resolve_conflicts(child, &resolver) if RbiGenerator::Namespace === child
  end

  Debugging.debug_puts(self, Debugging::Tree.end("All children done"))
end