rubocop-graphql 1.7.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.
- checksums.yaml +4 -4
- data/config/default.yml +27 -2
- data/lib/rubocop/cop/graphql/default_for_optional_argument.rb +187 -0
- data/lib/rubocop/cop/graphql/disallowed_types.rb +114 -0
- data/lib/rubocop/cop/graphql/field_description.rb +11 -0
- data/lib/rubocop/cop/graphql/nullability_mismatch.rb +137 -0
- data/lib/rubocop/cop/graphql/object_description.rb +171 -17
- data/lib/rubocop/cop/graphql/unused_argument.rb +1 -21
- data/lib/rubocop/cop/graphql.rb +44 -0
- data/lib/rubocop/cop/graphql_cops.rb +4 -29
- data/lib/rubocop/graphql/argument/kwargs.rb +18 -0
- data/lib/rubocop/graphql/argument.rb +45 -0
- data/lib/rubocop/graphql/field/kwargs.rb +24 -0
- data/lib/rubocop/graphql/version.rb +1 -1
- data/lib/rubocop-graphql.rb +1 -1
- metadata +8 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f7108c819a363b06725b9de204f6e1eb26a19c0e022d07a51f7cac3570ccc041
|
|
4
|
+
data.tar.gz: 5b5b231155cf6ac19e58df7ab0ded5060517f0a59ed5b3bcef77db04adf9355a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2bd922b4a7234a4ca1315dc42fd6b3adf7fff850bce9ec71abf41df175fd56889cd76be254dcdb4bbec09615dc85a8317a87884778fc62797ab991f63eee04bf
|
|
7
|
+
data.tar.gz: 282acda7b0c3f54e51a7e77245964e55d3d9d0847009490bfa096959cb49d5de72536e0e49b94f148dbfe1c72c40877494ce8edc9d5187c9b8eacdcf33be6129
|
data/config/default.yml
CHANGED
|
@@ -28,6 +28,19 @@ GraphQL/ContextWriteInType:
|
|
|
28
28
|
Include:
|
|
29
29
|
- '**/graphql/types/**/*'
|
|
30
30
|
|
|
31
|
+
GraphQL/DefaultForOptionalArgument:
|
|
32
|
+
Enabled: true
|
|
33
|
+
VersionAdded: '1.8.0'
|
|
34
|
+
Description: 'Optional arguments should have a default value in the resolver signature'
|
|
35
|
+
|
|
36
|
+
GraphQL/DisallowedTypes:
|
|
37
|
+
Enabled: false
|
|
38
|
+
VersionAdded: '1.8.0'
|
|
39
|
+
Description: 'Flags field and argument types the project has decided not to expose'
|
|
40
|
+
# Type name => the message explaining what to use instead. Nothing is disallowed
|
|
41
|
+
# until this is configured, so the cop is inert out of the box.
|
|
42
|
+
Types: {}
|
|
43
|
+
|
|
31
44
|
GraphQL/ExtractInputType:
|
|
32
45
|
Enabled: true
|
|
33
46
|
VersionAdded: '0.2.0'
|
|
@@ -119,7 +132,7 @@ GraphQL/MaxDepthSchema:
|
|
|
119
132
|
|
|
120
133
|
GraphQL/MethodShadowedByResolverMethod:
|
|
121
134
|
Enabled: true
|
|
122
|
-
VersionAdded: '
|
|
135
|
+
VersionAdded: '1.8.0'
|
|
123
136
|
Description: 'Checks for method definitions shadowed by the field''s effective resolver_method'
|
|
124
137
|
|
|
125
138
|
GraphQL/MultipleFieldDefinitions:
|
|
@@ -135,6 +148,11 @@ GraphQL/NotAuthorizedNodeType:
|
|
|
135
148
|
- '**/graphql/types/**/*'
|
|
136
149
|
SafeBaseClasses: []
|
|
137
150
|
|
|
151
|
+
GraphQL/NullabilityMismatch:
|
|
152
|
+
Enabled: true
|
|
153
|
+
VersionAdded: '1.8.0'
|
|
154
|
+
Description: 'Detects a non-null field whose Sorbet resolver signature returns a nilable type'
|
|
155
|
+
|
|
138
156
|
GraphQL/ResolverMethodLength:
|
|
139
157
|
Enabled: true
|
|
140
158
|
VersionAdded: '0.1.0'
|
|
@@ -148,6 +166,13 @@ GraphQL/ObjectDescription:
|
|
|
148
166
|
Enabled: true
|
|
149
167
|
VersionAdded: '0.3.0'
|
|
150
168
|
Description: 'Ensures all types have a description'
|
|
169
|
+
# Skip the root operation types (Query, Mutation, Subscription), which are
|
|
170
|
+
# usually left undescribed. Set to false to require a description on them too.
|
|
171
|
+
IgnoreRootTypes: true
|
|
172
|
+
# Superclass (or included module) names to treat as GraphQL bases, on top of the
|
|
173
|
+
# conventional ones. Use when your base types are named unconventionally, e.g.
|
|
174
|
+
# `class Types::UserType < ApplicationType`.
|
|
175
|
+
AdditionalTypeBases: []
|
|
151
176
|
Exclude:
|
|
152
177
|
- "spec/**/*"
|
|
153
178
|
- "test/**/*"
|
|
@@ -190,5 +215,5 @@ GraphQL/UnnecessaryFieldCamelize:
|
|
|
190
215
|
|
|
191
216
|
GraphQL/UselessMethodOption:
|
|
192
217
|
Enabled: true
|
|
193
|
-
VersionAdded: '
|
|
218
|
+
VersionAdded: '1.8.0'
|
|
194
219
|
Description: 'Checks for resolver_method:/method: made ineffective by resolver:, or method: shadowed by a same-named def'
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module GraphQL
|
|
6
|
+
# Optional arguments should have a default value in the resolver signature.
|
|
7
|
+
#
|
|
8
|
+
# When the client omits an argument declared `required: false`, graphql-ruby leaves it
|
|
9
|
+
# out of the keyword arguments entirely, so a required keyword raises
|
|
10
|
+
# `ArgumentError: missing keyword`. Giving the keyword a default value is what makes the
|
|
11
|
+
# argument actually optional at runtime.
|
|
12
|
+
#
|
|
13
|
+
# Arguments declared with a `default_value:` are always passed, so they are not reported.
|
|
14
|
+
# Neither is `required: :nullable`, which still demands the argument be present.
|
|
15
|
+
#
|
|
16
|
+
# Both class-level arguments (checked against `#resolve` and `#authorized?`) and
|
|
17
|
+
# arguments defined inside a field block (checked against that field's resolver method)
|
|
18
|
+
# are covered.
|
|
19
|
+
#
|
|
20
|
+
# @example
|
|
21
|
+
# # bad
|
|
22
|
+
#
|
|
23
|
+
# class SomeResolver < Resolvers::Base
|
|
24
|
+
# argument :name, String, required: false
|
|
25
|
+
#
|
|
26
|
+
# def resolve(name:); end
|
|
27
|
+
# end
|
|
28
|
+
#
|
|
29
|
+
# # good
|
|
30
|
+
#
|
|
31
|
+
# class SomeResolver < Resolvers::Base
|
|
32
|
+
# argument :name, String, required: false
|
|
33
|
+
#
|
|
34
|
+
# def resolve(name: nil); end
|
|
35
|
+
# end
|
|
36
|
+
#
|
|
37
|
+
# # good - a default value means the keyword is always passed
|
|
38
|
+
#
|
|
39
|
+
# class SomeResolver < Resolvers::Base
|
|
40
|
+
# argument :name, String, required: false, default_value: "anonymous"
|
|
41
|
+
#
|
|
42
|
+
# def resolve(name:); end
|
|
43
|
+
# end
|
|
44
|
+
#
|
|
45
|
+
# # bad
|
|
46
|
+
#
|
|
47
|
+
# class UserType < BaseObject
|
|
48
|
+
# field :posts, [PostType], null: false do
|
|
49
|
+
# argument :limit, Integer, required: false
|
|
50
|
+
# end
|
|
51
|
+
#
|
|
52
|
+
# def posts(limit:); end
|
|
53
|
+
# end
|
|
54
|
+
#
|
|
55
|
+
# # good
|
|
56
|
+
#
|
|
57
|
+
# class UserType < BaseObject
|
|
58
|
+
# field :posts, [PostType], null: false do
|
|
59
|
+
# argument :limit, Integer, required: false
|
|
60
|
+
# end
|
|
61
|
+
#
|
|
62
|
+
# def posts(limit: 10); end
|
|
63
|
+
# end
|
|
64
|
+
#
|
|
65
|
+
class DefaultForOptionalArgument < Base
|
|
66
|
+
MSG = "Optional argument `%<keyword>s` has no default value in `%<method>s`, so " \
|
|
67
|
+
"omitting it raises ArgumentError."
|
|
68
|
+
|
|
69
|
+
RESOLVER_METHODS = %i[resolve authorized?].freeze
|
|
70
|
+
|
|
71
|
+
def on_class(node)
|
|
72
|
+
body = node.body
|
|
73
|
+
return unless body
|
|
74
|
+
|
|
75
|
+
defs = definitions_in(node)
|
|
76
|
+
return if defs.empty?
|
|
77
|
+
|
|
78
|
+
check_class_arguments(node, defs)
|
|
79
|
+
check_field_arguments(node, defs)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
def check_class_arguments(class_node, defs)
|
|
85
|
+
keywords = optional_keywords(class_arguments(class_node))
|
|
86
|
+
return if keywords.empty?
|
|
87
|
+
|
|
88
|
+
RESOLVER_METHODS.each do |method_name|
|
|
89
|
+
check_signature(defs[method_name], keywords)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def check_field_arguments(class_node, defs)
|
|
94
|
+
field_blocks(class_node).each do |block_node|
|
|
95
|
+
field = RuboCop::GraphQL::Field.new(block_node.send_node)
|
|
96
|
+
next if field.kwargs.resolver
|
|
97
|
+
|
|
98
|
+
keywords = optional_keywords(block_arguments(block_node))
|
|
99
|
+
next if keywords.empty?
|
|
100
|
+
|
|
101
|
+
check_signature(defs[field.resolver_method_name.to_sym], keywords)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def check_signature(def_node, keywords)
|
|
106
|
+
return unless def_node
|
|
107
|
+
|
|
108
|
+
def_node.arguments.each do |arg_node|
|
|
109
|
+
next unless arg_node.kwarg_type?
|
|
110
|
+
next unless keywords.include?(arg_node.node_parts[0])
|
|
111
|
+
|
|
112
|
+
add_offense(
|
|
113
|
+
arg_node,
|
|
114
|
+
message: format(MSG, keyword: arg_node.node_parts[0], method: def_node.method_name)
|
|
115
|
+
)
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Keywords of arguments that graphql-ruby may leave out of the resolver call.
|
|
120
|
+
def optional_keywords(argument_nodes)
|
|
121
|
+
argument_nodes.filter_map do |argument_node|
|
|
122
|
+
argument = RuboCop::GraphQL::Argument.new(argument_node)
|
|
123
|
+
argument.keyword if argument.optional? && !argument.default_value?
|
|
124
|
+
end.to_set
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# `def`s owned by this class, keyed by name. A nested class gets its own `on_class`.
|
|
128
|
+
def definitions_in(class_node)
|
|
129
|
+
each_in_scope(class_node.body, :def).each_with_object({}) do |def_node, defs|
|
|
130
|
+
defs[def_node.method_name] ||= def_node
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Class-level `argument` calls: the walk stops at blocks, so field-block arguments
|
|
135
|
+
# (handled separately, against a different method) are not picked up here.
|
|
136
|
+
def class_arguments(class_node)
|
|
137
|
+
each_in_scope(class_node.body, :send, stop_at_block: true).select do |send_node|
|
|
138
|
+
argument_declaration?(send_node)
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def block_arguments(block_node)
|
|
143
|
+
body = block_node.body
|
|
144
|
+
return [] unless body
|
|
145
|
+
|
|
146
|
+
each_in_scope(body, :send).select { |send_node| argument_declaration?(send_node) }
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def field_blocks(class_node)
|
|
150
|
+
each_in_scope(class_node.body, :block).select do |block_node|
|
|
151
|
+
field_declaration?(block_node.send_node)
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Collects nodes of `type` without descending into a nested class or module body.
|
|
156
|
+
# With `stop_at_block`, a block contributes only the call that opens it, so a nested
|
|
157
|
+
# `field ... do ... end` keeps its own arguments out of the enclosing scope.
|
|
158
|
+
def each_in_scope(node, type, stop_at_block: false, found: [])
|
|
159
|
+
found << node if node.type == type
|
|
160
|
+
return found if node.type?(:class, :module, :sclass)
|
|
161
|
+
|
|
162
|
+
children =
|
|
163
|
+
if stop_at_block && node.type?(:any_block)
|
|
164
|
+
[node.send_node]
|
|
165
|
+
else
|
|
166
|
+
node.each_child_node
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
children.each do |child|
|
|
170
|
+
each_in_scope(child, type, stop_at_block: stop_at_block, found: found)
|
|
171
|
+
end
|
|
172
|
+
found
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# @!method argument_declaration?(node)
|
|
176
|
+
def_node_matcher :argument_declaration?, <<~PATTERN
|
|
177
|
+
(send nil? :argument (:sym _) ...)
|
|
178
|
+
PATTERN
|
|
179
|
+
|
|
180
|
+
# @!method field_declaration?(node)
|
|
181
|
+
def_node_matcher :field_declaration?, <<~PATTERN
|
|
182
|
+
(send nil? :field (:sym _) ...)
|
|
183
|
+
PATTERN
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module GraphQL
|
|
6
|
+
# Flags field and argument types the project has decided not to expose, with a message
|
|
7
|
+
# explaining what to use instead.
|
|
8
|
+
#
|
|
9
|
+
# Every schema accumulates types that are still resolvable but shouldn't be reached for
|
|
10
|
+
# in new code: a scalar kept alive only for backwards compatibility, a type that predates
|
|
11
|
+
# a better one, or a builtin whose semantics don't fit the domain -- `Float` for money,
|
|
12
|
+
# say, where the serialization loses precision. The convention is usually documented and
|
|
13
|
+
# then re-litigated in review; this makes it fail the build instead.
|
|
14
|
+
#
|
|
15
|
+
# Nothing is disallowed by default: the cop is inert until `Types` is configured.
|
|
16
|
+
#
|
|
17
|
+
# A configured name matches the written constant exactly, or as a trailing segment of it,
|
|
18
|
+
# so `Float` covers `Float`, `Types::Float` and `GraphQL::Types::Float`. Configure
|
|
19
|
+
# `GraphQL::Types::Float` instead to match only the fully qualified form.
|
|
20
|
+
#
|
|
21
|
+
# List types are unwrapped, so `[Float]` and `[Float, null: true]` are flagged too, and
|
|
22
|
+
# both the positional type and the `type:` keyword are checked.
|
|
23
|
+
#
|
|
24
|
+
# @example Types: {'Float' => 'Use Types::Decimal, which serializes as a string.'}
|
|
25
|
+
# # bad
|
|
26
|
+
# field :amount, Float, null: false
|
|
27
|
+
# argument :amount, Float, required: true
|
|
28
|
+
# field :amounts, [Float], null: false
|
|
29
|
+
# field :amount, type: Float, null: false
|
|
30
|
+
#
|
|
31
|
+
# # good
|
|
32
|
+
# field :amount, Types::Decimal, null: false
|
|
33
|
+
# argument :amount, Types::Decimal, required: true
|
|
34
|
+
#
|
|
35
|
+
# @example Types: {'Types::LegacyDate' => 'Use GraphQL::Types::ISO8601Date.'}
|
|
36
|
+
# # bad
|
|
37
|
+
# field :starts_on, Types::LegacyDate, null: false
|
|
38
|
+
#
|
|
39
|
+
# # good
|
|
40
|
+
# field :starts_on, GraphQL::Types::ISO8601Date, null: false
|
|
41
|
+
#
|
|
42
|
+
class DisallowedTypes < Base
|
|
43
|
+
MSG = "`%<type>s` is not allowed as a field or argument type."
|
|
44
|
+
MSG_WITH_REASON = "`%<type>s` is not allowed as a field or argument type. %<reason>s"
|
|
45
|
+
|
|
46
|
+
RESTRICT_ON_SEND = %i[field argument].freeze
|
|
47
|
+
|
|
48
|
+
def on_send(node)
|
|
49
|
+
return if disallowed_types.empty?
|
|
50
|
+
return unless type_declaration?(node)
|
|
51
|
+
|
|
52
|
+
each_type_const(node) do |const_node|
|
|
53
|
+
configured_name = disallowed_name_for(const_node)
|
|
54
|
+
next unless configured_name
|
|
55
|
+
|
|
56
|
+
add_offense(const_node, message: message_for(configured_name))
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def disallowed_types
|
|
63
|
+
@disallowed_types ||= (cop_config["Types"] || {}).transform_keys(&:to_s)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def message_for(configured_name)
|
|
67
|
+
reason = disallowed_types[configured_name].to_s.strip
|
|
68
|
+
|
|
69
|
+
if reason.empty?
|
|
70
|
+
format(MSG, type: configured_name)
|
|
71
|
+
else
|
|
72
|
+
format(MSG_WITH_REASON, type: configured_name, reason: reason)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def disallowed_name_for(const_node)
|
|
77
|
+
written_name = const_node.const_name
|
|
78
|
+
|
|
79
|
+
disallowed_types.keys.find do |configured_name|
|
|
80
|
+
written_name == configured_name || written_name.end_with?("::#{configured_name}")
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Yields the constants a `field`/`argument` call names as its type: the positional type
|
|
85
|
+
# and the `type:` keyword, each unwrapped through any list nesting.
|
|
86
|
+
def each_type_const(send_node, &block)
|
|
87
|
+
each_const_in(send_node.arguments[1], &block)
|
|
88
|
+
each_const_in(type_kwarg(send_node), &block)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def each_const_in(node, &block)
|
|
92
|
+
return unless node
|
|
93
|
+
|
|
94
|
+
case node.type
|
|
95
|
+
when :const then yield node
|
|
96
|
+
when :array then node.children.each { |child| each_const_in(child, &block) }
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# An explicit receiver means this is some other `field`/`argument` method, not the DSL.
|
|
101
|
+
#
|
|
102
|
+
# @!method type_declaration?(node)
|
|
103
|
+
def_node_matcher :type_declaration?, <<~PATTERN
|
|
104
|
+
(send nil? {:field :argument} ...)
|
|
105
|
+
PATTERN
|
|
106
|
+
|
|
107
|
+
# @!method type_kwarg(node)
|
|
108
|
+
def_node_matcher :type_kwarg, <<~PATTERN
|
|
109
|
+
(send nil? {:field :argument} ... (hash <(pair (sym :type) $_) ...>))
|
|
110
|
+
PATTERN
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
@@ -18,6 +18,16 @@ module RuboCop
|
|
|
18
18
|
# field :name, String, null: true
|
|
19
19
|
# end
|
|
20
20
|
#
|
|
21
|
+
# Fields built from a resolver, mutation or subscription class are not
|
|
22
|
+
# flagged: graphql-ruby takes their description from that class.
|
|
23
|
+
#
|
|
24
|
+
# @example
|
|
25
|
+
# # good
|
|
26
|
+
#
|
|
27
|
+
# class UserType < BaseType
|
|
28
|
+
# field :posts, resolver: PostsResolver
|
|
29
|
+
# end
|
|
30
|
+
#
|
|
21
31
|
class FieldDescription < Base
|
|
22
32
|
include RuboCop::GraphQL::NodePattern
|
|
23
33
|
|
|
@@ -28,6 +38,7 @@ module RuboCop
|
|
|
28
38
|
return unless field_definition?(node)
|
|
29
39
|
|
|
30
40
|
field = RuboCop::GraphQL::Field.new(node)
|
|
41
|
+
return if field.kwargs.resolver_class
|
|
31
42
|
|
|
32
43
|
add_offense(node) unless field.description
|
|
33
44
|
end
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module GraphQL
|
|
6
|
+
# A non-null field should not have a resolver whose Sorbet signature returns a nilable
|
|
7
|
+
# type. The two disagree: the schema promises a value, the signature admits `nil`, and
|
|
8
|
+
# graphql-ruby raises an invalid-null error for every object that resolves to `nil`.
|
|
9
|
+
#
|
|
10
|
+
# Sorbet cannot catch this, because the field declaration is not part of the signature.
|
|
11
|
+
# Only the crashing direction is reported: a nullable field with a non-nilable resolver
|
|
12
|
+
# is merely imprecise, not broken.
|
|
13
|
+
#
|
|
14
|
+
# Codebases without Sorbet signatures never trigger this cop.
|
|
15
|
+
#
|
|
16
|
+
# @example
|
|
17
|
+
# # bad
|
|
18
|
+
#
|
|
19
|
+
# class UserType < BaseObject
|
|
20
|
+
# field :name, String, null: false
|
|
21
|
+
#
|
|
22
|
+
# sig { override.returns(T.nilable(String)) }
|
|
23
|
+
# def name
|
|
24
|
+
# object.name
|
|
25
|
+
# end
|
|
26
|
+
# end
|
|
27
|
+
#
|
|
28
|
+
# # good - the schema admits what the resolver may return
|
|
29
|
+
#
|
|
30
|
+
# class UserType < BaseObject
|
|
31
|
+
# field :name, String, null: true
|
|
32
|
+
#
|
|
33
|
+
# sig { override.returns(T.nilable(String)) }
|
|
34
|
+
# def name
|
|
35
|
+
# object.name
|
|
36
|
+
# end
|
|
37
|
+
# end
|
|
38
|
+
#
|
|
39
|
+
# # good - the resolver guarantees what the schema promises
|
|
40
|
+
#
|
|
41
|
+
# class UserType < BaseObject
|
|
42
|
+
# field :name, String, null: false
|
|
43
|
+
#
|
|
44
|
+
# sig { override.returns(String) }
|
|
45
|
+
# def name
|
|
46
|
+
# object.name || "anonymous"
|
|
47
|
+
# end
|
|
48
|
+
# end
|
|
49
|
+
#
|
|
50
|
+
class NullabilityMismatch < Base
|
|
51
|
+
include RuboCop::GraphQL::Sorbet
|
|
52
|
+
|
|
53
|
+
MSG = "Field `%<field>s` is `null: false` but its resolver signature returns a " \
|
|
54
|
+
"nilable type, so a nil resolves to an invalid null error."
|
|
55
|
+
|
|
56
|
+
def on_class(node)
|
|
57
|
+
non_null_fields = collect_non_null_fields(node)
|
|
58
|
+
return if non_null_fields.empty?
|
|
59
|
+
|
|
60
|
+
node.each_descendant(:def) do |def_node|
|
|
61
|
+
next unless owned_by?(def_node, node)
|
|
62
|
+
|
|
63
|
+
field_node = non_null_fields[def_node.method_name]
|
|
64
|
+
next unless field_node && nilable_signature?(def_node)
|
|
65
|
+
|
|
66
|
+
field_name = RuboCop::GraphQL::Field.new(field_node).name
|
|
67
|
+
add_offense(field_node, message: format(MSG, field: field_name))
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
alias on_module on_class
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
# Non-null fields owned by this type, keyed by the method that resolves them. Fields
|
|
75
|
+
# handed off to a `resolver:` class are resolved elsewhere, so they are skipped.
|
|
76
|
+
def collect_non_null_fields(node)
|
|
77
|
+
fields = {}
|
|
78
|
+
|
|
79
|
+
node.each_descendant(:send) do |send_node|
|
|
80
|
+
next unless non_null_field?(send_node) && owned_by?(send_node, node)
|
|
81
|
+
|
|
82
|
+
field = RuboCop::GraphQL::Field.new(send_node)
|
|
83
|
+
next if field.kwargs.resolver
|
|
84
|
+
|
|
85
|
+
fields[field.resolver_method_name.to_sym] ||= send_node
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
fields
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def nilable_signature?(def_node)
|
|
92
|
+
signature = sorbet_signature_for(def_node)
|
|
93
|
+
return false unless signature
|
|
94
|
+
|
|
95
|
+
signature.each_descendant(:send).any? do |send_node|
|
|
96
|
+
send_node.method?(:returns) && nilable_type?(send_node.first_argument)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Only the outermost type counts. `T::Array[T.nilable(String)]` is a non-null list of
|
|
101
|
+
# nullable items, which pairs correctly with a `null: false` field.
|
|
102
|
+
def nilable_type?(node)
|
|
103
|
+
return false unless node
|
|
104
|
+
return true if t_nilable?(node)
|
|
105
|
+
|
|
106
|
+
Array(t_any_types(node)).any? { |type| nil_class?(type) }
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# True when `node`'s nearest enclosing class or module is `owner`, so that a nested
|
|
110
|
+
# type's fields and methods are not attributed to the one around it.
|
|
111
|
+
def owned_by?(node, owner)
|
|
112
|
+
node.each_ancestor(:class, :module).first == owner
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# @!method non_null_field?(node)
|
|
116
|
+
def_node_matcher :non_null_field?, <<~PATTERN
|
|
117
|
+
(send nil? :field (sym _) ... (hash <(pair (sym :null) (false)) ...>))
|
|
118
|
+
PATTERN
|
|
119
|
+
|
|
120
|
+
# @!method t_nilable?(node)
|
|
121
|
+
def_node_matcher :t_nilable?, <<~PATTERN
|
|
122
|
+
(send (const {nil? cbase} :T) :nilable ...)
|
|
123
|
+
PATTERN
|
|
124
|
+
|
|
125
|
+
# @!method t_any_types(node)
|
|
126
|
+
def_node_matcher :t_any_types, <<~PATTERN
|
|
127
|
+
(send (const {nil? cbase} :T) :any $...)
|
|
128
|
+
PATTERN
|
|
129
|
+
|
|
130
|
+
# @!method nil_class?(node)
|
|
131
|
+
def_node_matcher :nil_class?, <<~PATTERN
|
|
132
|
+
(const {nil? cbase} :NilClass)
|
|
133
|
+
PATTERN
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
@@ -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
|
-
#
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
|
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
|
|
104
|
+
return if abstract_base?(node)
|
|
105
|
+
return unless undescribed_interface_module?(node)
|
|
42
106
|
|
|
43
|
-
|
|
44
|
-
add_offense(node.identifier)
|
|
45
|
-
end
|
|
107
|
+
add_offense(node.identifier)
|
|
46
108
|
end
|
|
47
109
|
|
|
48
110
|
private
|
|
49
111
|
|
|
50
|
-
|
|
51
|
-
|
|
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
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
|
@@ -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(
|
|
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,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,31 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
require_relative "graphql
|
|
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/method_shadowed_by_resolver_method"
|
|
20
|
-
require_relative "graphql/multiple_field_definitions"
|
|
21
|
-
require_relative "graphql/not_authorized_node_type"
|
|
22
|
-
require_relative "graphql/resolver_method_length"
|
|
23
|
-
require_relative "graphql/object_description"
|
|
24
|
-
require_relative "graphql/ordered_arguments"
|
|
25
|
-
require_relative "graphql/ordered_fields"
|
|
26
|
-
require_relative "graphql/prepare_method"
|
|
27
|
-
require_relative "graphql/unused_argument"
|
|
28
|
-
require_relative "graphql/unnecessary_argument_camelize"
|
|
29
|
-
require_relative "graphql/unnecessary_field_alias"
|
|
30
|
-
require_relative "graphql/unnecessary_field_camelize"
|
|
31
|
-
require_relative "graphql/useless_method_option"
|
|
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
|
data/lib/rubocop-graphql.rb
CHANGED
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.8.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dmitry Tsepelev
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-08-
|
|
10
|
+
date: 2026-08-20 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: bundler
|
|
@@ -71,7 +71,7 @@ dependencies:
|
|
|
71
71
|
requirements:
|
|
72
72
|
- - ">="
|
|
73
73
|
- !ruby/object:Gem::Version
|
|
74
|
-
version: 1.
|
|
74
|
+
version: '1.89'
|
|
75
75
|
- - "<"
|
|
76
76
|
- !ruby/object:Gem::Version
|
|
77
77
|
version: '2'
|
|
@@ -81,7 +81,7 @@ dependencies:
|
|
|
81
81
|
requirements:
|
|
82
82
|
- - ">="
|
|
83
83
|
- !ruby/object:Gem::Version
|
|
84
|
-
version: 1.
|
|
84
|
+
version: '1.89'
|
|
85
85
|
- - "<"
|
|
86
86
|
- !ruby/object:Gem::Version
|
|
87
87
|
version: '2'
|
|
@@ -97,10 +97,13 @@ files:
|
|
|
97
97
|
- config/default.yml
|
|
98
98
|
- lib/refinements/underscore_string.rb
|
|
99
99
|
- lib/rubocop-graphql.rb
|
|
100
|
+
- lib/rubocop/cop/graphql.rb
|
|
100
101
|
- lib/rubocop/cop/graphql/argument_description.rb
|
|
101
102
|
- lib/rubocop/cop/graphql/argument_name.rb
|
|
102
103
|
- lib/rubocop/cop/graphql/argument_uniqueness.rb
|
|
103
104
|
- lib/rubocop/cop/graphql/context_write_in_type.rb
|
|
105
|
+
- lib/rubocop/cop/graphql/default_for_optional_argument.rb
|
|
106
|
+
- lib/rubocop/cop/graphql/disallowed_types.rb
|
|
104
107
|
- lib/rubocop/cop/graphql/extract_input_type.rb
|
|
105
108
|
- lib/rubocop/cop/graphql/extract_type.rb
|
|
106
109
|
- lib/rubocop/cop/graphql/field_definitions.rb
|
|
@@ -116,6 +119,7 @@ files:
|
|
|
116
119
|
- lib/rubocop/cop/graphql/method_shadowed_by_resolver_method.rb
|
|
117
120
|
- lib/rubocop/cop/graphql/multiple_field_definitions.rb
|
|
118
121
|
- lib/rubocop/cop/graphql/not_authorized_node_type.rb
|
|
122
|
+
- lib/rubocop/cop/graphql/nullability_mismatch.rb
|
|
119
123
|
- lib/rubocop/cop/graphql/object_description.rb
|
|
120
124
|
- lib/rubocop/cop/graphql/ordered_arguments.rb
|
|
121
125
|
- lib/rubocop/cop/graphql/ordered_fields.rb
|