rubocop-graphql 1.6.0 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,6 +6,19 @@ module RuboCop
6
6
  # This cop checks if a type (object, input, interface, scalar, union,
7
7
  # mutation, subscription, and resolver) has a description.
8
8
  #
9
+ # Only classes that actually declare a GraphQL type are checked: those whose
10
+ # superclass resolves to a GraphQL base (`Object`, `InputObject`, `Union`,
11
+ # `Enum`, `Scalar`, or anything ending in `Mutation`, `Subscription` or
12
+ # `Resolver`), plus modules that `include` an `*Interface` base. Plain Ruby
13
+ # classes that happen to live alongside types - error classes, analyzers,
14
+ # validators, loaders, generators - are skipped, so they no longer need to be
15
+ # silenced one by one.
16
+ #
17
+ # Two kinds of real type declarations are also skipped, because neither
18
+ # surfaces a description in the schema: abstract `Base*` types that other
19
+ # types inherit from, and (unless `IgnoreRootTypes` is disabled) the root
20
+ # operation types `Query`, `Mutation` and `Subscription`.
21
+ #
9
22
  # @example
10
23
  # # good
11
24
  #
@@ -20,44 +33,185 @@ module RuboCop
20
33
  # # ...
21
34
  # end
22
35
  #
36
+ # @example
37
+ # # good - not a GraphQL type, so no description is expected
38
+ #
39
+ # class TrackingInfoNotAvailable < StandardError; end
40
+ # class UserLoader < GraphQL::Batch::Loader; end
41
+ #
42
+ # @example
43
+ # # good - abstract base and root operation types carry no description
44
+ #
45
+ # class Types::BaseObject < GraphQL::Schema::Object; end
46
+ # class Types::Query < Types::BaseObject; end
47
+ #
48
+ # @example AdditionalTypeBases: [] (default)
49
+ # # good - `ApplicationType` is not a recognized GraphQL base, so this
50
+ # # class is not checked
51
+ #
52
+ # class Types::UserType < ApplicationType
53
+ # end
54
+ #
55
+ # @example AdditionalTypeBases: ['ApplicationType']
56
+ # # bad - `ApplicationType` is now treated as a GraphQL base
57
+ #
58
+ # class Types::UserType < ApplicationType
59
+ # end
60
+ #
23
61
  class ObjectDescription < Base
24
- include RuboCop::GraphQL::NodePattern
25
62
  include RuboCop::GraphQL::DescriptionMethod
26
63
 
27
64
  MSG = "Missing type description"
28
65
 
29
- # @!method interface?(node)
30
- def_node_matcher :interface?, <<~PATTERN
31
- (send nil? :include (const ...))
32
- PATTERN
66
+ # Base class names that mark a GraphQL type when they match exactly, or with
67
+ # a `Base` prefix (`GraphQL::Schema::Object`, `Types::BaseObject`).
68
+ # Ambiguous words are deliberately not matched as suffixes, so a plain Ruby
69
+ # `ValueObject` base is not mistaken for a GraphQL one. `Interface` is absent
70
+ # on purpose: graphql-ruby interfaces are modules, so a *class* inheriting an
71
+ # `*::Interface` constant is always some other abstract base.
72
+ EXACT_BASES = %w[Object InputObject Union Enum Scalar].freeze
73
+
74
+ # These read unambiguously as GraphQL even inside a longer name, so they are
75
+ # matched as suffixes (`RelayClassicMutation`, `Base::PermissionedMutation`).
76
+ SUFFIX_BASES = %w[Mutation Subscription Resolver].freeze
77
+
78
+ # A base named exactly `Base` (`Resolvers::Base`) is only a GraphQL base when
79
+ # it sits in a namespace that says so.
80
+ GRAPHQL_NAMESPACES = %w[
81
+ Types Mutations Subscriptions Resolvers Inputs Interfaces Unions Enums Scalars
82
+ ].freeze
83
+
84
+ # GraphQL reserves these names for the root operation types.
85
+ ROOT_TYPE_NAMES = %w[Query Mutation Subscription].freeze
86
+
87
+ # `Base`, or `Base` followed by another word - the naming convention for the
88
+ # abstract types that other types inherit from (`BaseObject`, `BaseInterface`).
89
+ ABSTRACT_BASE_NAME = /\ABase(?:[A-Z]|\z)/
90
+
91
+ # Sorbet's `T` namespace is reserved, and `T::Enum` / `T::Struct` collide with
92
+ # the base names above.
93
+ SORBET_NAMESPACE = :T
33
94
 
34
95
  def on_class(node)
35
- return if child_nodes(node).find { |child_node| has_description?(child_node) }
96
+ return unless graphql_type_class?(node)
97
+ return if exempt_type?(node)
98
+ return if described_or_root_named?(node)
36
99
 
37
100
  add_offense(node.identifier)
38
101
  end
39
102
 
40
103
  def on_module(node)
41
- return if child_nodes(node).none? { |child_node| interface?(child_node) }
104
+ return if abstract_base?(node)
105
+ return unless undescribed_interface_module?(node)
42
106
 
43
- if child_nodes(node).none? { |child_node| has_description?(child_node) }
44
- add_offense(node.identifier)
45
- end
107
+ add_offense(node.identifier)
46
108
  end
47
109
 
48
110
  private
49
111
 
50
- def has_description?(node)
51
- description_method_call?(node)
112
+ # Abstract bases and the root operation types are real GraphQL declarations,
113
+ # but neither surfaces a description in the schema.
114
+ def exempt_type?(node)
115
+ return true if abstract_base?(node)
116
+
117
+ ignore_root_types? && ROOT_TYPE_NAMES.include?(type_name(node))
118
+ end
119
+
120
+ def abstract_base?(node)
121
+ ABSTRACT_BASE_NAME.match?(type_name(node))
122
+ end
123
+
124
+ def type_name(node)
125
+ node.identifier.short_name.to_s
126
+ end
127
+
128
+ def graphql_type_class?(node)
129
+ superclass = node.parent_class
130
+ return false if superclass.nil? || !superclass.const_type?
131
+ return false if superclass.namespace&.short_name == SORBET_NAMESPACE
132
+
133
+ graphql_base?(superclass)
52
134
  end
53
135
 
54
- def child_nodes(node)
55
- if node.body.instance_of? RuboCop::AST::Node
56
- node.body.child_nodes
57
- else
58
- node.child_nodes
136
+ def graphql_base?(const_node)
137
+ name = const_node.short_name.to_s
138
+ return true if additional_type_bases.include?(name)
139
+ return graphql_namespace?(const_node) if name == "Base"
140
+ return true if SUFFIX_BASES.any? { |suffix| name.end_with?(suffix) }
141
+
142
+ EXACT_BASES.include?(name.delete_prefix("Base"))
143
+ end
144
+
145
+ def graphql_namespace?(const_node)
146
+ namespace = const_node.namespace
147
+ return false if namespace.nil? || !namespace.const_type?
148
+
149
+ GRAPHQL_NAMESPACES.include?(namespace.short_name.to_s)
150
+ end
151
+
152
+ # A method call with no explicit receiver (e.g. `description "..."`,
153
+ # `include Foo`) - the shape of a GraphQL type DSL declaration.
154
+ def bare_call?(node)
155
+ node.send_type? && node.receiver.nil?
156
+ end
157
+
158
+ # Single pass over the module body: an interface module both `include`s an
159
+ # `*Interface` base and (when compliant) declares a `description`.
160
+ def undescribed_interface_module?(node)
161
+ interface = false
162
+ described = false
163
+
164
+ body_nodes(node).each do |child|
165
+ described ||= description_method_call?(child)
166
+ interface ||= interface_include?(child)
167
+ end
168
+
169
+ interface && !described
170
+ end
171
+
172
+ def interface_include?(node)
173
+ return false unless bare_call?(node) && node.method?(:include)
174
+
175
+ arg = node.first_argument
176
+ return false unless arg&.const_type?
177
+
178
+ name = arg.short_name.to_s
179
+ name.end_with?("Interface") || additional_type_bases.include?(name)
180
+ end
181
+
182
+ # Single pass over the class body: a `description` satisfies the cop, and a
183
+ # `graphql_name "Query"` marks a root operation type declared under some
184
+ # other class name, which is exempt like a class named `Query` itself.
185
+ def described_or_root_named?(node)
186
+ body_nodes(node).any? do |child|
187
+ description_method_call?(child) || root_graphql_name?(child)
59
188
  end
60
189
  end
190
+
191
+ def root_graphql_name?(node)
192
+ return false unless ignore_root_types?
193
+ return false unless bare_call?(node) && node.method?(:graphql_name)
194
+
195
+ arg = node.first_argument
196
+ return false unless arg&.str_type?
197
+
198
+ ROOT_TYPE_NAMES.include?(arg.value)
199
+ end
200
+
201
+ def body_nodes(node)
202
+ body = node.body
203
+ return [] if body.nil?
204
+
205
+ body.begin_type? ? body.child_nodes : [body]
206
+ end
207
+
208
+ def ignore_root_types?
209
+ cop_config.fetch("IgnoreRootTypes", true)
210
+ end
211
+
212
+ def additional_type_bases
213
+ @additional_type_bases ||= Array(cop_config["AdditionalTypeBases"])
214
+ end
61
215
  end
62
216
  end
63
217
  end
@@ -50,7 +50,26 @@ module RuboCop
50
50
  PATTERN
51
51
 
52
52
  def on_class(node)
53
- field_declarations(node).each_cons(2) do |previous, current|
53
+ check_field_ordering(direct_field_declarations(node))
54
+ end
55
+
56
+ alias on_module on_class
57
+
58
+ def on_block(node)
59
+ return if node.method?(:field)
60
+ return unless node.each_ancestor(:class, :module).any?
61
+
62
+ fields = field_declarations(node).select { |f| f.each_ancestor(:block).first == node }
63
+ check_field_ordering(fields)
64
+ end
65
+
66
+ alias on_itblock on_block
67
+ alias on_numblock on_block
68
+
69
+ private
70
+
71
+ def check_field_ordering(fields)
72
+ fields.each_cons(2) do |previous, current|
54
73
  next unless consecutive_fields(previous, current)
55
74
  next if correct_order?(field_name(previous), field_name(current))
56
75
 
@@ -58,9 +77,19 @@ module RuboCop
58
77
  end
59
78
  end
60
79
 
61
- alias on_module on_class
80
+ def direct_field_declarations(node)
81
+ field_declarations(node).select { |field| direct_child_field?(field, node) }
82
+ end
62
83
 
63
- private
84
+ def direct_child_field?(field_node, class_node)
85
+ return false unless field_node.each_ancestor(:class, :module).first == class_node
86
+
87
+ field_node.each_ancestor(:block).none? do |block_ancestor|
88
+ next false if block_ancestor.each_ancestor(:class, :module).first != class_node
89
+
90
+ !block_ancestor.method?(:field)
91
+ end
92
+ end
64
93
 
65
94
  def consecutive_fields(previous, current)
66
95
  return true if cop_config["Groups"] == false
@@ -109,7 +109,7 @@ module RuboCop
109
109
  node.node_parts[0]
110
110
  end.to_set
111
111
  declared_args = declared_arg_nodes.map { |node| RuboCop::GraphQL::Argument.new(node) }
112
- declared_args.map(&method(:arg_name)).uniq.reject do |declared_arg_name|
112
+ declared_args.map(&:keyword).uniq.reject do |declared_arg_name|
113
113
  resolve_method_kwargs_names.include?(declared_arg_name)
114
114
  end
115
115
  end
@@ -147,26 +147,6 @@ module RuboCop
147
147
  node.source_range.end
148
148
  end
149
149
 
150
- def inferred_arg_name(name_as_string)
151
- case name_as_string
152
- when /_id$/
153
- name_as_string.sub(/_id$/, "").to_sym
154
- when /_ids$/
155
- name_as_string.sub(/_ids$/, "")
156
- .sub(/([^s])$/, "\\1s")
157
- .to_sym
158
- else
159
- name_as_string.to_sym
160
- end
161
- end
162
-
163
- def arg_name(declared_arg)
164
- return declared_arg.as if declared_arg.kwargs.as
165
- return inferred_arg_name(declared_arg.name.to_s) if declared_arg.kwargs.loads
166
-
167
- declared_arg.name
168
- end
169
-
170
150
  def scoped_node?(node)
171
151
  scope_changing_syntax?(node) || block_or_lambda?(node)
172
152
  end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module GraphQL
6
+ # This cop detects `resolver_method:` or `method:` options that have no
7
+ # effect, either because:
8
+ #
9
+ # - the same field also sets `resolver:` -- graphql-ruby always calls
10
+ # the resolver class's own resolver_method once `resolver:` is set,
11
+ # silently ignoring both `resolver_method:` and `method:` regardless
12
+ # of whether a method by either name exists; or
13
+ # - a method matching the field's own plain name is also defined on the
14
+ # type -- that method is checked (and wins) before `method:` is ever
15
+ # considered, since `method:` is only consulted as a fallback when no
16
+ # such method exists. This only applies to `method:`: `resolver_method:`
17
+ # redirects which name is checked instead of competing with it, so it
18
+ # can't be shadowed by a same-named `def` the way `method:` can.
19
+ #
20
+ # @example
21
+ # # good
22
+ #
23
+ # class Types::PostType < Types::BaseObject
24
+ # field :author, resolver: Resolvers::AuthorResolver
25
+ # end
26
+ #
27
+ # class Types::PostType < Types::BaseObject
28
+ # field :author, String, null: true, method: :ghostwriter
29
+ # end
30
+ #
31
+ # # bad
32
+ #
33
+ # class Types::PostType < Types::BaseObject
34
+ # field :author, resolver: Resolvers::AuthorResolver, resolver_method: :fetch_author
35
+ # end
36
+ #
37
+ # class Types::PostType < Types::BaseObject
38
+ # field :author, resolver: Resolvers::AuthorResolver, method: :ghostwriter
39
+ # end
40
+ #
41
+ # class Types::PostType < Types::BaseObject
42
+ # field :author, String, null: true, method: :ghostwriter
43
+ #
44
+ # def author
45
+ # object.author
46
+ # end
47
+ # end
48
+ #
49
+ class UselessMethodOption < Base
50
+ include RuboCop::GraphQL::NodePattern
51
+
52
+ RESOLVER_MSG = "Remove `%<option>s:`, it has no effect: `resolver: %<resolver>s` " \
53
+ "always takes precedence."
54
+ DEF_MSG = "Remove `method:`, it has no effect: `def %<field_name>s` on the type " \
55
+ "always takes precedence."
56
+ RESTRICT_ON_SEND = %i[field].freeze
57
+
58
+ # @!method resolver_method_pair(node)
59
+ def_node_search :resolver_method_pair, "(pair (sym :resolver_method) ...)"
60
+
61
+ # @!method method_pair(node)
62
+ def_node_search :method_pair, "(pair (sym :method) ...)"
63
+
64
+ def on_send(node)
65
+ return unless field_definition?(node)
66
+
67
+ field = RuboCop::GraphQL::Field.new(node)
68
+
69
+ if field.kwargs.resolver
70
+ register_resolver_offenses(node, field)
71
+ else
72
+ register_method_shadowed_by_def_offense(node, field)
73
+ end
74
+ end
75
+
76
+ private
77
+
78
+ def register_resolver_offenses(node, field)
79
+ resolver_source = field.kwargs.resolver.value.source
80
+
81
+ [resolver_method_pair(node).first, method_pair(node).first].compact.each do |pair|
82
+ message = format(RESOLVER_MSG, option: pair.key.value, resolver: resolver_source)
83
+ add_offense(pair, message: message)
84
+ end
85
+ end
86
+
87
+ def register_method_shadowed_by_def_offense(node, field)
88
+ pair = method_pair(node).first
89
+ return unless pair
90
+
91
+ shadowed_def = field.schema_member.find_method_definition(field.name)
92
+ return unless shadowed_def
93
+
94
+ message = format(DEF_MSG, field_name: field.name)
95
+ add_offense(pair, message: message)
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ # Cops for the `GraphQL` department. The department's cops are
6
+ # registered for lazy loading and their files are loaded on demand.
7
+ module GraphQL
8
+ extend LazyLoader
9
+
10
+ register_cop :ArgumentDescription, "#{__dir__}/graphql/argument_description"
11
+ register_cop :ArgumentName, "#{__dir__}/graphql/argument_name"
12
+ register_cop :ArgumentUniqueness, "#{__dir__}/graphql/argument_uniqueness"
13
+ register_cop :ContextWriteInType, "#{__dir__}/graphql/context_write_in_type"
14
+ register_cop :DefaultForOptionalArgument, "#{__dir__}/graphql/default_for_optional_argument"
15
+ register_cop :DisallowedTypes, "#{__dir__}/graphql/disallowed_types"
16
+ register_cop :ExtractInputType, "#{__dir__}/graphql/extract_input_type"
17
+ register_cop :ExtractType, "#{__dir__}/graphql/extract_type"
18
+ register_cop :FieldDefinitions, "#{__dir__}/graphql/field_definitions"
19
+ register_cop :FieldDescription, "#{__dir__}/graphql/field_description"
20
+ register_cop :FieldHashKey, "#{__dir__}/graphql/field_hash_key"
21
+ register_cop :FieldMethod, "#{__dir__}/graphql/field_method"
22
+ register_cop :FieldName, "#{__dir__}/graphql/field_name"
23
+ register_cop :FieldUniqueness, "#{__dir__}/graphql/field_uniqueness"
24
+ register_cop :GraphqlName, "#{__dir__}/graphql/graphql_name"
25
+ register_cop :LegacyDsl, "#{__dir__}/graphql/legacy_dsl"
26
+ register_cop :MaxComplexitySchema, "#{__dir__}/graphql/max_complexity_schema"
27
+ register_cop :MaxDepthSchema, "#{__dir__}/graphql/max_depth_schema"
28
+ register_cop :MethodShadowedByResolverMethod, "#{__dir__}/graphql/method_shadowed_by_resolver_method"
29
+ register_cop :MultipleFieldDefinitions, "#{__dir__}/graphql/multiple_field_definitions"
30
+ register_cop :NotAuthorizedNodeType, "#{__dir__}/graphql/not_authorized_node_type"
31
+ register_cop :NullabilityMismatch, "#{__dir__}/graphql/nullability_mismatch"
32
+ register_cop :ResolverMethodLength, "#{__dir__}/graphql/resolver_method_length"
33
+ register_cop :ObjectDescription, "#{__dir__}/graphql/object_description"
34
+ register_cop :OrderedArguments, "#{__dir__}/graphql/ordered_arguments"
35
+ register_cop :OrderedFields, "#{__dir__}/graphql/ordered_fields"
36
+ register_cop :PrepareMethod, "#{__dir__}/graphql/prepare_method"
37
+ register_cop :UnusedArgument, "#{__dir__}/graphql/unused_argument"
38
+ register_cop :UnnecessaryArgumentCamelize, "#{__dir__}/graphql/unnecessary_argument_camelize"
39
+ register_cop :UnnecessaryFieldAlias, "#{__dir__}/graphql/unnecessary_field_alias"
40
+ register_cop :UnnecessaryFieldCamelize, "#{__dir__}/graphql/unnecessary_field_camelize"
41
+ register_cop :UselessMethodOption, "#{__dir__}/graphql/useless_method_option"
42
+ end
43
+ end
44
+ end
@@ -1,29 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "graphql/argument_description"
4
- require_relative "graphql/argument_name"
5
- require_relative "graphql/argument_uniqueness"
6
- require_relative "graphql/context_write_in_type"
7
- require_relative "graphql/extract_input_type"
8
- require_relative "graphql/extract_type"
9
- require_relative "graphql/field_definitions"
10
- require_relative "graphql/field_description"
11
- require_relative "graphql/field_hash_key"
12
- require_relative "graphql/field_method"
13
- require_relative "graphql/field_name"
14
- require_relative "graphql/field_uniqueness"
15
- require_relative "graphql/graphql_name"
16
- require_relative "graphql/legacy_dsl"
17
- require_relative "graphql/max_complexity_schema"
18
- require_relative "graphql/max_depth_schema"
19
- require_relative "graphql/multiple_field_definitions"
20
- require_relative "graphql/not_authorized_node_type"
21
- require_relative "graphql/resolver_method_length"
22
- require_relative "graphql/object_description"
23
- require_relative "graphql/ordered_arguments"
24
- require_relative "graphql/ordered_fields"
25
- require_relative "graphql/prepare_method"
26
- require_relative "graphql/unused_argument"
27
- require_relative "graphql/unnecessary_argument_camelize"
28
- require_relative "graphql/unnecessary_field_alias"
29
- require_relative "graphql/unnecessary_field_camelize"
3
+ # @deprecated This file is deprecated. Cops are registered for lazy loading in
4
+ # `rubocop/cop/graphql`; this file is kept for compatibility with code
5
+ # that requires it directly.
6
+ require_relative "graphql"
@@ -36,6 +36,16 @@ module RuboCop
36
36
  (pair (sym :camelize) ...)
37
37
  PATTERN
38
38
 
39
+ # @!method required_kwarg?(node)
40
+ def_node_matcher :required_kwarg?, <<~PATTERN
41
+ (pair (sym :required) ...)
42
+ PATTERN
43
+
44
+ # @!method default_value_kwarg?(node)
45
+ def_node_matcher :default_value_kwarg?, <<~PATTERN
46
+ (pair (sym :default_value) ...)
47
+ PATTERN
48
+
39
49
  def initialize(argument_node)
40
50
  @nodes = argument_kwargs(argument_node) || []
41
51
  end
@@ -55,6 +65,14 @@ module RuboCop
55
65
  def as
56
66
  @nodes.find { |kwarg| as_kwarg?(kwarg) }
57
67
  end
68
+
69
+ def required
70
+ @nodes.find { |kwarg| required_kwarg?(kwarg) }
71
+ end
72
+
73
+ def default_value
74
+ @nodes.find { |kwarg| default_value_kwarg?(kwarg) }
75
+ end
58
76
  end
59
77
  end
60
78
  end
@@ -20,6 +20,11 @@ module RuboCop
20
20
  (pair (sym :as) (sym $_))
21
21
  PATTERN
22
22
 
23
+ # @!method argument_required(node)
24
+ def_node_matcher :argument_required, <<~PATTERN
25
+ (pair (sym :required) $_)
26
+ PATTERN
27
+
23
28
  attr_reader :node
24
29
 
25
30
  def initialize(node)
@@ -34,6 +39,29 @@ module RuboCop
34
39
  @as ||= argument_as(kwargs.as)
35
40
  end
36
41
 
42
+ # The keyword this argument is passed as to #resolve and friends. `as:` wins when both
43
+ # are given, matching graphql-ruby's `kwargs[:as] ||= inferred_arg_name`.
44
+ def keyword
45
+ return as if kwargs.as
46
+ return inferred_keyword if kwargs.loads
47
+
48
+ name
49
+ end
50
+
51
+ # `required: false` is the only value that lets graphql-ruby omit the keyword:
52
+ # `required: :nullable` still demands the argument be present, though it may be null.
53
+ def optional?
54
+ required_node = kwargs.required
55
+ return false unless required_node
56
+
57
+ argument_required(required_node)&.false_type? || false
58
+ end
59
+
60
+ # A configured default is always passed, even when the client omits the argument.
61
+ def default_value?
62
+ !kwargs.default_value.nil?
63
+ end
64
+
37
65
  def description
38
66
  @description ||= argument_description(@node) || kwargs.description || block.description
39
67
  end
@@ -45,6 +73,23 @@ module RuboCop
45
73
  def block
46
74
  @block ||= Argument::Block.new(@node.parent)
47
75
  end
76
+
77
+ private
78
+
79
+ # Mirrors graphql-ruby: a `loads:` argument named `foo_id` arrives as `foo:`, and one
80
+ # named `foo_ids` arrives as `foos:`.
81
+ def inferred_keyword
82
+ name_as_string = name.to_s
83
+
84
+ case name_as_string
85
+ when /_id$/
86
+ name_as_string.sub(/_id$/, "").to_sym
87
+ when /_ids$/
88
+ name_as_string.sub(/_ids$/, "").sub(/([^s])$/, "\\1s").to_sym
89
+ else
90
+ name
91
+ end
92
+ end
48
93
  end
49
94
  end
50
95
  end
@@ -21,6 +21,16 @@ module RuboCop
21
21
  (pair (sym :resolver) ...)
22
22
  PATTERN
23
23
 
24
+ # @!method mutation_kwarg?(node)
25
+ def_node_matcher :mutation_kwarg?, <<~PATTERN
26
+ (pair (sym :mutation) ...)
27
+ PATTERN
28
+
29
+ # @!method subscription_kwarg?(node)
30
+ def_node_matcher :subscription_kwarg?, <<~PATTERN
31
+ (pair (sym :subscription) ...)
32
+ PATTERN
33
+
24
34
  # @!method method_kwarg?(node)
25
35
  def_node_matcher :method_kwarg?, <<~PATTERN
26
36
  (pair (sym :method) ...)
@@ -59,6 +69,20 @@ module RuboCop
59
69
  @nodes.find { |kwarg| resolver_kwarg?(kwarg) }
60
70
  end
61
71
 
72
+ def mutation
73
+ @nodes.find { |kwarg| mutation_kwarg?(kwarg) }
74
+ end
75
+
76
+ def subscription
77
+ @nodes.find { |kwarg| subscription_kwarg?(kwarg) }
78
+ end
79
+
80
+ # graphql-ruby builds the field's resolver class out of any one of these
81
+ # options, and the field then inherits configuration from that class.
82
+ def resolver_class
83
+ resolver || mutation || subscription
84
+ end
85
+
62
86
  def method
63
87
  @nodes.find { |kwarg| method_kwarg?(kwarg) }
64
88
  end
@@ -1,5 +1,5 @@
1
1
  module RuboCop
2
2
  module GraphQL
3
- VERSION = "1.6.0".freeze
3
+ VERSION = "1.8.0".freeze
4
4
  end
5
5
  end
@@ -24,4 +24,4 @@ require_relative "rubocop/graphql/field/block"
24
24
  require_relative "rubocop/graphql/field/kwargs"
25
25
  require_relative "rubocop/graphql/schema_member"
26
26
 
27
- require_relative "rubocop/cop/graphql_cops"
27
+ require_relative "rubocop/cop/graphql"