rubocop-graphql 1.6.0 → 1.7.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1a19f13dab5d269042eab2a3dfb06746f70d78ebd512e4656cb384d1e2f0a77c
4
- data.tar.gz: e67b156307473082e114b860097ca80d1ba23c9987359ad6ee6c56d7641552d3
3
+ metadata.gz: 8ff189b7091fa3e71688dcb2a2ef36b15edbe018796943ef3b3dfee85b1c32f8
4
+ data.tar.gz: 2d2ffa4470e74e3ae9d90fac4fc85a173db7d95eb2d5a6bfd844759fad59c514
5
5
  SHA512:
6
- metadata.gz: 412c2b4547281975d17d77cbff5a40e737d49a597747bdd85ccba722dfac63c35f30e33027bb252c3ad6939d43348609ac291baeb37f7f4c2f187e4e427635f6
7
- data.tar.gz: '080b77b04d717024d92a79ec83635b1f43e56412240a6a4d6068bec5060a65c5365438158742d579c1fe9d4fd77636ddd19d02055b0003a9d6083c6da15d5d59'
6
+ metadata.gz: 1651ab77310a0146fe7847161a6a18afa0c5d83148eca7701def628f66d31b24cfa4760fe94843d9c10db45ca43aa7b7c0d0539901c71be89bf5b3299242caca
7
+ data.tar.gz: 8b2d079c283776eaac5fdd151fc589d9f29e7afe7826657f23f577a98882166f1a31c57727a1f8dca90a49c058b9def85d3026be5fa483b11227fc81006be50f
data/README.md CHANGED
@@ -2,7 +2,10 @@
2
2
 
3
3
  [Rubocop](https://github.com/rubocop-hq/rubocop) extension for enforcing [graphql-ruby](https://github.com/rmosolgo/graphql-ruby) best practices.
4
4
 
5
- You can support my open–source work [here](https://boosty.to/dmitry_tsepelev).
5
+ ## Professional Support
6
+
7
+ Need help with your GraphQL API architecture, schema design, or performance?
8
+ I'm available for consulting — [get in touch](https://dmitrytsepelev.dev/consulting).
6
9
 
7
10
  ## Installation
8
11
 
data/config/default.yml CHANGED
@@ -117,6 +117,11 @@ GraphQL/MaxDepthSchema:
117
117
  Include:
118
118
  - '**/graphql/**/*_schema.rb'
119
119
 
120
+ GraphQL/MethodShadowedByResolverMethod:
121
+ Enabled: true
122
+ VersionAdded: '<<next>>'
123
+ Description: 'Checks for method definitions shadowed by the field''s effective resolver_method'
124
+
120
125
  GraphQL/MultipleFieldDefinitions:
121
126
  Enabled: true
122
127
  VersionAdded: '0.15.0'
@@ -182,3 +187,8 @@ GraphQL/UnnecessaryFieldCamelize:
182
187
  Enabled: true
183
188
  VersionAdded: '0.18.0'
184
189
  Description: "Camelize isn't necessary if the field name doesn't contain underscores"
190
+
191
+ GraphQL/UselessMethodOption:
192
+ Enabled: true
193
+ VersionAdded: '<<next>>'
194
+ Description: 'Checks for resolver_method:/method: made ineffective by resolver:, or method: shadowed by a same-named def'
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module GraphQL
6
+ # This cop detects method definitions that are never called because
7
+ # the field's effective `resolver_method` points somewhere else.
8
+ #
9
+ # graphql-ruby only ever calls a single method on the type instance:
10
+ # the field's `resolver_method` (which is the resolver class's own
11
+ # method when `resolver:` is set, the explicit `resolver_method:`
12
+ # value when given, or the field name otherwise). Any other
13
+ # same-named method left on the type is unreachable:
14
+ #
15
+ # - When `resolver:` is set, the resolver class handles resolution
16
+ # entirely, so neither the field name, `resolver_method:`, nor
17
+ # `method:` (if also given) is ever dispatched to a method on the type.
18
+ # - When only `resolver_method:` is set, that name is the one
19
+ # actually called -- a leftover method matching the plain field
20
+ # name is never reached.
21
+ #
22
+ # @example
23
+ # # good
24
+ #
25
+ # class Types::PostType < Types::BaseObject
26
+ # field :author, resolver: Resolvers::AuthorResolver
27
+ # end
28
+ #
29
+ # class Types::PostType < Types::BaseObject
30
+ # field :author, String, null: true, resolver_method: :fetch_author
31
+ #
32
+ # def fetch_author
33
+ # object.author
34
+ # end
35
+ # end
36
+ #
37
+ # # bad
38
+ #
39
+ # class Types::PostType < Types::BaseObject
40
+ # field :author, resolver: Resolvers::AuthorResolver
41
+ #
42
+ # def author
43
+ # object.author
44
+ # end
45
+ # end
46
+ #
47
+ # class Types::PostType < Types::BaseObject
48
+ # field :author, String, null: true, resolver_method: :fetch_author
49
+ #
50
+ # def author
51
+ # object.author
52
+ # end
53
+ #
54
+ # def fetch_author
55
+ # object.author
56
+ # end
57
+ # end
58
+ #
59
+ class MethodShadowedByResolverMethod < Base
60
+ include RuboCop::GraphQL::NodePattern
61
+
62
+ RESOLVER_MSG = "Remove this method, it is never called: `resolver: %<resolver>s` " \
63
+ "resolves this field instead."
64
+ RESOLVER_METHOD_MSG = "Remove this method, it is never called: " \
65
+ "`resolver_method: :%<resolver_method>s` is called instead."
66
+ RESTRICT_ON_SEND = %i[field].freeze
67
+
68
+ def on_send(node)
69
+ return unless field_definition?(node)
70
+
71
+ field = RuboCop::GraphQL::Field.new(node)
72
+
73
+ if field.kwargs.resolver
74
+ register_resolver_offenses(field)
75
+ else
76
+ register_resolver_method_offense(field)
77
+ end
78
+ end
79
+
80
+ private
81
+
82
+ # Neither `resolver_method:` nor `method:` has any effect once `resolver:` is
83
+ # set (graphql-ruby always calls the resolver class's own resolver_method), so
84
+ # a method matching the field's own name, an explicit `resolver_method:`, or
85
+ # an explicit `method:` is equally unreachable.
86
+ def register_resolver_offenses(field)
87
+ resolver_source = field.kwargs.resolver.value.source
88
+ message = format(RESOLVER_MSG, resolver: resolver_source)
89
+
90
+ candidate_names = [
91
+ field.name, field.kwargs.resolver_method_name, method_kwarg_name(field)
92
+ ].compact.uniq
93
+ candidate_names.each do |method_name|
94
+ shadowed_method = field.schema_member.find_method_definition(method_name)
95
+ add_offense(shadowed_method.loc.name, message: message) if shadowed_method
96
+ end
97
+ end
98
+
99
+ # The `resolver_method:`-named method is the one actually called; only a
100
+ # leftover method matching the plain field name is dead.
101
+ def register_resolver_method_offense(field)
102
+ resolver_method_kwarg = field.kwargs.resolver_method_name
103
+ return unless resolver_method_kwarg && resolver_method_kwarg != field.name
104
+
105
+ shadowed_method = field.schema_member.find_method_definition(field.name)
106
+ return unless shadowed_method
107
+
108
+ message = format(RESOLVER_METHOD_MSG, resolver_method: resolver_method_kwarg)
109
+ add_offense(shadowed_method.loc.name, message: message)
110
+ end
111
+
112
+ def method_kwarg_name(field)
113
+ pair = field.kwargs.method
114
+ return nil unless pair
115
+
116
+ value_node = pair.value
117
+ value_node.value if value_node.sym_type?
118
+ end
119
+ end
120
+ end
121
+ end
122
+ 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
@@ -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
@@ -16,6 +16,7 @@ require_relative "graphql/graphql_name"
16
16
  require_relative "graphql/legacy_dsl"
17
17
  require_relative "graphql/max_complexity_schema"
18
18
  require_relative "graphql/max_depth_schema"
19
+ require_relative "graphql/method_shadowed_by_resolver_method"
19
20
  require_relative "graphql/multiple_field_definitions"
20
21
  require_relative "graphql/not_authorized_node_type"
21
22
  require_relative "graphql/resolver_method_length"
@@ -27,3 +28,4 @@ require_relative "graphql/unused_argument"
27
28
  require_relative "graphql/unnecessary_argument_camelize"
28
29
  require_relative "graphql/unnecessary_field_alias"
29
30
  require_relative "graphql/unnecessary_field_camelize"
31
+ require_relative "graphql/useless_method_option"
@@ -1,5 +1,5 @@
1
1
  module RuboCop
2
2
  module GraphQL
3
- VERSION = "1.6.0".freeze
3
+ VERSION = "1.7.0".freeze
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rubocop-graphql
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.6.0
4
+ version: 1.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dmitry Tsepelev
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2026-02-14 00:00:00.000000000 Z
10
+ date: 2026-08-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: bundler
@@ -113,6 +113,7 @@ files:
113
113
  - lib/rubocop/cop/graphql/legacy_dsl.rb
114
114
  - lib/rubocop/cop/graphql/max_complexity_schema.rb
115
115
  - lib/rubocop/cop/graphql/max_depth_schema.rb
116
+ - lib/rubocop/cop/graphql/method_shadowed_by_resolver_method.rb
116
117
  - lib/rubocop/cop/graphql/multiple_field_definitions.rb
117
118
  - lib/rubocop/cop/graphql/not_authorized_node_type.rb
118
119
  - lib/rubocop/cop/graphql/object_description.rb
@@ -124,6 +125,7 @@ files:
124
125
  - lib/rubocop/cop/graphql/unnecessary_field_alias.rb
125
126
  - lib/rubocop/cop/graphql/unnecessary_field_camelize.rb
126
127
  - lib/rubocop/cop/graphql/unused_argument.rb
128
+ - lib/rubocop/cop/graphql/useless_method_option.rb
127
129
  - lib/rubocop/cop/graphql_cops.rb
128
130
  - lib/rubocop/graphql.rb
129
131
  - lib/rubocop/graphql/argument.rb