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 +4 -4
- data/README.md +4 -1
- data/config/default.yml +10 -0
- data/lib/rubocop/cop/graphql/method_shadowed_by_resolver_method.rb +122 -0
- data/lib/rubocop/cop/graphql/ordered_fields.rb +32 -3
- data/lib/rubocop/cop/graphql/useless_method_option.rb +100 -0
- data/lib/rubocop/cop/graphql_cops.rb +2 -0
- data/lib/rubocop/graphql/version.rb +1 -1
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8ff189b7091fa3e71688dcb2a2ef36b15edbe018796943ef3b3dfee85b1c32f8
|
|
4
|
+
data.tar.gz: 2d2ffa4470e74e3ae9d90fac4fc85a173db7d95eb2d5a6bfd844759fad59c514
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
80
|
+
def direct_field_declarations(node)
|
|
81
|
+
field_declarations(node).select { |field| direct_child_field?(field, node) }
|
|
82
|
+
end
|
|
62
83
|
|
|
63
|
-
|
|
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"
|
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.
|
|
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
|
|
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
|