Class: RuboCop::Cop::Gusto::Graphql::ResolverIgnoresObject

Inherits:
Base
  • Object
show all
Defined in:
lib/rubocop/cop/gusto/graphql/resolver_ignores_object.rb

Overview

Flags a field whose resolver cannot reach the node it hangs off: the method body never reads object or @object, and no helper it calls in the same class does either. The value is then identical for every node, so the field does not belong on this type.

Examples:

Bad - the value does not depend on the node

field :eor_coming_soon_countries, [String], null: false
def eor_coming_soon_countries
  BusinessValues::BusinessValueService.get_value_for("eor.coming_soon_countries", nil)
end

Good - the value depends on the node

field :eor_coming_soon_countries, [String], null: false
def eor_coming_soon_countries
  object.eor_coming_soon_countries
end

Constant Summary collapse

MSG =
"Field `:%{name}` has a resolver that never reads `object`, so its value is the same " \
"for every node. Re-home it, or, if it really does depend on the node, resolve the " \
"indirect dependency."
FIELD_NAME_NODE =
RuboCop::AST::NodePattern.new("(send nil? :field $(sym _) ...)")
RESOLUTION_ELSEWHERE =
RuboCop::AST::NodePattern.new("(send nil? :field ... (hash <(pair (sym {:resolver :hash_key}) _) ...>))\n")
RESOLVER_METHOD =

resolver_method: redirects the lookup on the type instance; method: names a method on the backing object, which this cop does not read.

RuboCop::AST::NodePattern.new("(send nil? :field ... (hash <(pair (sym :resolver_method) (sym $_)) ...>))\n")
EPHEMERAL_BACKING =
RuboCop::AST::NodePattern.new("(send nil? :set_backing_type (const ... :EphemeralObject))\n")
DELEGATE_TO_OBJECT =
RuboCop::AST::NodePattern.new("(send nil? :delegate ... (hash <(pair (sym :to) (sym :object)) ...>))\n")
NODE_READ_THROUGH_RECEIVER =

A concern that requires_ancestor reaches the node through T.bind(self, ...).object, since Sorbet cannot see object on the module itself. That reads the node exactly as a bare object does, so the receiver form has to count too.

RuboCop::AST::NodePattern.new("(send {self (send (const _ :T) :bind self ...)} :object)\n")
SCOPE_BOUNDARY =

A nested class gets its own on_class, so the walk must not cross into one.

i(class module sclass).freeze
BODY_BOUNDARY =

Fields are never declared inside a def, so stopping there loses nothing.

i(class module sclass def).freeze

Instance Method Summary collapse

Instance Method Details

#on_class(node) ⇒ Object Also known as: on_module



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
# File 'lib/rubocop/cop/gusto/graphql/resolver_ignores_object.rb', line 61

def on_class(node)
  return unless node.body

  fields = []
  definitions = []
  delegated = []
  ephemeral = T.let(false, T::Boolean)

  walk(node.body, BODY_BOUNDARY) do |child|
    case child.type
    when :send
      next unless child.receiver.nil?

      case child.method_name
      when :field
        name_node = FIELD_NAME_NODE.match(child)
        next if name_node.nil? || RESOLUTION_ELSEWHERE.match(child)

        fields << [name_node, RESOLVER_METHOD.match(child) || name_node.value]
      when :set_backing_type
        ephemeral = true if EPHEMERAL_BACKING.match(child)
      when :delegate
        delegated.concat(delegated_names(child))
      end
    when :def
      definitions << child
    end
  end
  return if ephemeral || fields.empty?

  accessors = node_accessors | delegated
  resolvers = {}
  direct = []
  callers_of = {}
  definitions.each { |d| scan_resolver(d, accessors, resolvers, direct, callers_of) }

  reaching = propagate(direct, callers_of)
  fields.each do |name_node, resolver_name|
    next unless resolvers.key?(resolver_name)
    next if reaching.key?(resolver_name)

    add_offense(name_node, message: format(MSG, name: name_node.value))
  end
end