rubocop-graphql 0.19.0 → 1.5.2
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 +86 -41
- data/lib/rubocop/cop/graphql/argument_description.rb +1 -0
- data/lib/rubocop/cop/graphql/argument_name.rb +1 -0
- data/lib/rubocop/cop/graphql/field_definitions.rb +14 -3
- data/lib/rubocop/cop/graphql/field_description.rb +1 -0
- data/lib/rubocop/cop/graphql/field_hash_key.rb +3 -2
- data/lib/rubocop/cop/graphql/field_method.rb +8 -2
- data/lib/rubocop/cop/graphql/field_name.rb +1 -0
- data/lib/rubocop/cop/graphql/field_uniqueness.rb +8 -1
- data/lib/rubocop/cop/graphql/graphql_name.rb +70 -0
- data/lib/rubocop/cop/graphql/legacy_dsl.rb +1 -0
- data/lib/rubocop/cop/graphql/max_complexity_schema.rb +31 -0
- data/lib/rubocop/cop/graphql/max_depth_schema.rb +31 -0
- data/lib/rubocop/cop/graphql/multiple_field_definitions.rb +2 -0
- data/lib/rubocop/cop/graphql/not_authorized_node_type.rb +134 -0
- data/lib/rubocop/cop/graphql/ordered_arguments.rb +15 -21
- data/lib/rubocop/cop/graphql/ordered_fields.rb +20 -15
- data/lib/rubocop/cop/graphql/prepare_method.rb +95 -0
- data/lib/rubocop/cop/graphql/resolver_method_length.rb +4 -6
- data/lib/rubocop/cop/graphql/unnecessary_argument_camelize.rb +11 -4
- data/lib/rubocop/cop/graphql/unnecessary_field_alias.rb +33 -9
- data/lib/rubocop/cop/graphql/unnecessary_field_camelize.rb +1 -0
- data/lib/rubocop/cop/graphql/unused_argument.rb +2 -2
- data/lib/rubocop/cop/graphql_cops.rb +5 -0
- data/lib/rubocop/graphql/argument/block.rb +1 -1
- data/lib/rubocop/graphql/compare_order.rb +51 -0
- data/lib/rubocop/graphql/ext/snake_case.rb +1 -1
- data/lib/rubocop/graphql/field.rb +5 -2
- data/lib/rubocop/graphql/heredoc.rb +1 -1
- data/lib/rubocop/graphql/schema_member.rb +1 -1
- data/lib/rubocop/graphql/sorbet.rb +2 -10
- data/lib/rubocop/graphql/version.rb +1 -1
- data/lib/rubocop-graphql.rb +1 -0
- metadata +11 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 54137ed6106e3d8c0d2c675ea02e41989aef1d4480edf875071c9c7918a96fba
|
4
|
+
data.tar.gz: a1a0b7d307854a39d4b4761d77cc829569dd9e97453a329d027886331407aae0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9e1fc8711b226bd656ca33d8409949a40667e9eabf31ca55a3536ae5f4f54f3af4e7254cbaa589e406172c724afdeb6772ffe5dd689909ebeb09608c7c94b848
|
7
|
+
data.tar.gz: 392b99c409b0e2b8247fff232d35fab529c1abf5eb3f7b648f40eb9020b07a5c56f074d88538626e564872a4a13f1cb59ee20a5ddc0c982099a2aa3edc596289
|
data/config/default.yml
CHANGED
@@ -8,123 +8,168 @@ GraphQL:
|
|
8
8
|
|
9
9
|
GraphQL/ArgumentDescription:
|
10
10
|
Enabled: true
|
11
|
-
VersionAdded: '0.
|
11
|
+
VersionAdded: '0.2.0'
|
12
12
|
Description: 'Ensures all arguments have a description'
|
13
13
|
|
14
14
|
GraphQL/ArgumentName:
|
15
15
|
Enabled: true
|
16
|
-
VersionAdded: '0.
|
16
|
+
VersionAdded: '0.2.0'
|
17
17
|
Description: 'This cop checks whether argument names are snake_case'
|
18
18
|
|
19
19
|
GraphQL/ArgumentUniqueness:
|
20
20
|
Enabled: true
|
21
|
-
VersionAdded: '0.
|
21
|
+
VersionAdded: '0.11.0'
|
22
22
|
Description: 'This cop enforces arguments to be defined once per block'
|
23
23
|
|
24
|
-
GraphQL/
|
24
|
+
GraphQL/ExtractInputType:
|
25
25
|
Enabled: true
|
26
|
-
VersionAdded: '0.
|
27
|
-
Description: '
|
28
|
-
|
29
|
-
|
30
|
-
|
26
|
+
VersionAdded: '0.2.0'
|
27
|
+
Description: 'Suggests using input type instead of many arguments'
|
28
|
+
MaxArguments: 2
|
29
|
+
Include:
|
30
|
+
- '**/graphql/mutations/**/*.rb'
|
31
|
+
|
32
|
+
GraphQL/ExtractType:
|
33
|
+
Enabled: true
|
34
|
+
VersionAdded: '0.2.0'
|
35
|
+
Description: 'Suggests extracting fields with common prefixes to the separate type'
|
36
|
+
MaxFields: 2
|
37
|
+
Prefixes:
|
38
|
+
- is
|
39
|
+
- has
|
40
|
+
- with
|
41
|
+
- avg
|
42
|
+
- min
|
43
|
+
- max
|
31
44
|
|
32
45
|
GraphQL/FieldDefinitions:
|
33
46
|
Enabled: true
|
34
|
-
VersionAdded: '0.
|
47
|
+
VersionAdded: '0.1.0'
|
35
48
|
Description: 'Checks consistency of field definitions'
|
36
49
|
EnforcedStyle: group_definitions
|
37
50
|
SupportedStyles:
|
38
51
|
- group_definitions
|
39
52
|
- define_resolver_after_definition
|
40
53
|
|
41
|
-
GraphQL/MultipleFieldDefinitions:
|
42
|
-
Enabled: true
|
43
|
-
Description: 'Ensures that fields with multiple definitions are grouped together'
|
44
|
-
|
45
54
|
GraphQL/FieldDescription:
|
46
55
|
Enabled: true
|
47
|
-
VersionAdded: '0.
|
56
|
+
VersionAdded: '0.1.0'
|
48
57
|
Description: 'Ensures all fields have a description'
|
49
58
|
|
50
59
|
GraphQL/FieldHashKey:
|
51
60
|
Enabled: true
|
52
|
-
VersionAdded: '0.
|
61
|
+
VersionAdded: '0.4.0'
|
53
62
|
Description: 'Checks :hash_key option is used for appropriate fields'
|
54
63
|
|
55
64
|
GraphQL/FieldMethod:
|
56
65
|
Enabled: true
|
57
|
-
VersionAdded: '0.
|
66
|
+
VersionAdded: '0.2.0'
|
58
67
|
Description: 'Checks :method option is used for appropriate fields'
|
59
68
|
|
60
69
|
GraphQL/FieldName:
|
61
70
|
Enabled: true
|
62
|
-
VersionAdded: '0.
|
71
|
+
VersionAdded: '0.2.0'
|
63
72
|
Description: 'This cop checks whether field names are snake_case'
|
64
73
|
SafeAutoCorrect: false
|
65
74
|
|
66
75
|
GraphQL/FieldUniqueness:
|
67
76
|
Enabled: true
|
68
|
-
VersionAdded: '0.
|
77
|
+
VersionAdded: '0.11.0'
|
69
78
|
Description: 'This cop enforces fields to be defined once'
|
70
79
|
|
71
|
-
GraphQL/
|
80
|
+
GraphQL/GraphqlName:
|
72
81
|
Enabled: true
|
73
|
-
VersionAdded: '0.
|
74
|
-
Description: '
|
75
|
-
MaxArguments: 2
|
82
|
+
VersionAdded: '1.0.0'
|
83
|
+
Description: 'This cop check proper configuration of graphql_name'
|
76
84
|
Include:
|
77
|
-
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
MaxFields: 2
|
84
|
-
Prefixes:
|
85
|
-
- is
|
86
|
-
- has
|
87
|
-
- with
|
88
|
-
- avg
|
89
|
-
- min
|
90
|
-
- max
|
85
|
+
- "**/graphql/types/**/*"
|
86
|
+
- "**/graphql/mutations/**/*"
|
87
|
+
EnforcedStyle: only_override
|
88
|
+
SupportedStyles:
|
89
|
+
- required
|
90
|
+
- only_override
|
91
91
|
|
92
92
|
GraphQL/LegacyDsl:
|
93
93
|
Enabled: true
|
94
|
-
VersionAdded: '0.
|
94
|
+
VersionAdded: '0.9.0'
|
95
95
|
Description: 'Checks that types are defined with class-based API'
|
96
96
|
|
97
|
+
GraphQL/MaxComplexitySchema:
|
98
|
+
Enabled: true
|
99
|
+
VersionAdded: '1.0.0'
|
100
|
+
Description: 'Enforces max_complexity configuration in schema'
|
101
|
+
Include:
|
102
|
+
- '**/graphql/**/*_schema.rb'
|
103
|
+
|
104
|
+
GraphQL/MaxDepthSchema:
|
105
|
+
Enabled: true
|
106
|
+
VersionAdded: '1.0.0'
|
107
|
+
Description: 'Enforces max_depth configuration in schema'
|
108
|
+
Include:
|
109
|
+
- '**/graphql/**/*_schema.rb'
|
110
|
+
|
111
|
+
GraphQL/MultipleFieldDefinitions:
|
112
|
+
Enabled: true
|
113
|
+
VersionAdded: '0.15.0'
|
114
|
+
Description: 'Ensures that fields with multiple definitions are grouped together'
|
115
|
+
|
116
|
+
GraphQL/NotAuthorizedNodeType:
|
117
|
+
Enabled: true
|
118
|
+
VersionAdded: '1.0.0'
|
119
|
+
Description: 'Detects types that implement Node interface and not have `.authorized?` check'
|
120
|
+
Include:
|
121
|
+
- '**/graphql/types/**/*'
|
122
|
+
SafeBaseClasses: []
|
123
|
+
|
124
|
+
GraphQL/ResolverMethodLength:
|
125
|
+
Enabled: true
|
126
|
+
VersionAdded: '0.1.0'
|
127
|
+
Description: 'Checks resolver methods are not too long'
|
128
|
+
Max: 10
|
129
|
+
CountComments: false
|
130
|
+
ExcludedMethods: []
|
131
|
+
CountAsOne: []
|
132
|
+
|
97
133
|
GraphQL/ObjectDescription:
|
98
134
|
Enabled: true
|
99
|
-
VersionAdded: '0.
|
135
|
+
VersionAdded: '0.3.0'
|
100
136
|
Description: 'Ensures all types have a description'
|
101
137
|
Exclude:
|
138
|
+
- "spec/**/*"
|
139
|
+
- "test/**/*"
|
102
140
|
- '**/*_schema.rb'
|
103
141
|
- '**/base_*.rb'
|
104
142
|
- '**/graphql/query_context.rb'
|
105
143
|
|
106
144
|
GraphQL/OrderedArguments:
|
107
145
|
Enabled: true
|
108
|
-
VersionAdded: '0.
|
146
|
+
VersionAdded: '0.7.0'
|
109
147
|
Description: 'Arguments should be alphabetically sorted within groups'
|
148
|
+
Order: null
|
110
149
|
|
111
150
|
GraphQL/OrderedFields:
|
112
151
|
Enabled: true
|
113
|
-
VersionAdded: '0.
|
152
|
+
VersionAdded: '0.5.0'
|
114
153
|
Description: 'Fields should be alphabetically sorted within groups'
|
154
|
+
Groups: true
|
155
|
+
Order: null
|
115
156
|
|
116
157
|
GraphQL/UnusedArgument:
|
117
158
|
Enabled: true
|
159
|
+
VersionAdded: '0.12.0'
|
118
160
|
Description: 'Arguments should either be listed explicitly or **rest should be in the resolve signature'
|
119
161
|
|
120
162
|
GraphQL/UnnecessaryFieldAlias:
|
121
163
|
Enabled: true
|
164
|
+
VersionAdded: '0.18.0'
|
122
165
|
Description: 'Field aliases should be different than their field names'
|
123
166
|
|
124
167
|
GraphQL/UnnecessaryArgumentCamelize:
|
125
168
|
Enabled: true
|
169
|
+
VersionAdded: '0.18.0'
|
126
170
|
Description: "Camelize isn't necessary if the argument name doesn't contain underscores"
|
127
171
|
|
128
172
|
GraphQL/UnnecessaryFieldCamelize:
|
129
173
|
Enabled: true
|
174
|
+
VersionAdded: '0.18.0'
|
130
175
|
Description: "Camelize isn't necessary if the field name doesn't contain underscores"
|
@@ -52,6 +52,8 @@ module RuboCop
|
|
52
52
|
include RuboCop::GraphQL::Sorbet
|
53
53
|
include RuboCop::GraphQL::Heredoc
|
54
54
|
|
55
|
+
RESTRICT_ON_SEND = %i[field].freeze
|
56
|
+
|
55
57
|
# @!method field_kwargs(node)
|
56
58
|
def_node_matcher :field_kwargs, <<~PATTERN
|
57
59
|
(send nil? :field
|
@@ -80,6 +82,16 @@ module RuboCop
|
|
80
82
|
end
|
81
83
|
end
|
82
84
|
|
85
|
+
def on_module(node)
|
86
|
+
return if style != :group_definitions
|
87
|
+
|
88
|
+
schema_member = RuboCop::GraphQL::SchemaMember.new(node)
|
89
|
+
|
90
|
+
if (body = schema_member.body)
|
91
|
+
check_grouped_field_declarations(body)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
83
95
|
private
|
84
96
|
|
85
97
|
GROUP_DEFS_MSG = "Group all field definitions together."
|
@@ -100,7 +112,6 @@ module RuboCop
|
|
100
112
|
|
101
113
|
def group_field_declarations(corrector, node)
|
102
114
|
field = RuboCop::GraphQL::Field.new(node)
|
103
|
-
|
104
115
|
first_field = field.schema_member.body.find do |node|
|
105
116
|
field_definition?(node) || field_definition_with_body?(node)
|
106
117
|
end
|
@@ -211,7 +222,7 @@ module RuboCop
|
|
211
222
|
|
212
223
|
def remove_old_resolver(corrector, resolver_definition)
|
213
224
|
range_to_remove = range_with_surrounding_space(
|
214
|
-
range: resolver_definition.
|
225
|
+
range: resolver_definition.source_range, side: :left
|
215
226
|
)
|
216
227
|
corrector.remove(range_to_remove)
|
217
228
|
|
@@ -220,7 +231,7 @@ module RuboCop
|
|
220
231
|
return unless resolver_signature
|
221
232
|
|
222
233
|
range_to_remove = range_with_surrounding_space(
|
223
|
-
range: resolver_signature.
|
234
|
+
range: resolver_signature.source_range, side: :left
|
224
235
|
)
|
225
236
|
corrector.remove(range_to_remove)
|
226
237
|
end
|
@@ -41,6 +41,7 @@ module RuboCop
|
|
41
41
|
PATTERN
|
42
42
|
|
43
43
|
MSG = "Use hash_key: %<hash_key>p"
|
44
|
+
RESTRICT_ON_SEND = %i[field].freeze
|
44
45
|
|
45
46
|
def on_send(node)
|
46
47
|
return unless field_definition?(node)
|
@@ -74,11 +75,11 @@ module RuboCop
|
|
74
75
|
suggested_hash_key_name = hash_key_to_use(method_definition)
|
75
76
|
|
76
77
|
corrector.insert_after(
|
77
|
-
node
|
78
|
+
node, ", hash_key: #{suggested_hash_key_name.inspect}"
|
78
79
|
)
|
79
80
|
|
80
81
|
range = range_with_surrounding_space(
|
81
|
-
range: method_definition.
|
82
|
+
range: method_definition.source_range, side: :left
|
82
83
|
)
|
83
84
|
|
84
85
|
corrector.remove(range)
|
@@ -40,6 +40,7 @@ module RuboCop
|
|
40
40
|
PATTERN
|
41
41
|
|
42
42
|
MSG = "Use method: :%<method_name>s"
|
43
|
+
RESTRICT_ON_SEND = %i[field].freeze
|
43
44
|
|
44
45
|
def on_send(node)
|
45
46
|
return unless field_definition?(node)
|
@@ -51,6 +52,7 @@ module RuboCop
|
|
51
52
|
|
52
53
|
return if suggested_method_name.nil?
|
53
54
|
return if RuboCop::GraphQL::Field::CONFLICT_FIELD_NAMES.include?(suggested_method_name)
|
55
|
+
return if method_kwarg_set?(field)
|
54
56
|
|
55
57
|
add_offense(node, message: message(suggested_method_name)) do |corrector|
|
56
58
|
autocorrect(corrector, node)
|
@@ -68,10 +70,10 @@ module RuboCop
|
|
68
70
|
method_definition = suggest_method_name_for(field)
|
69
71
|
suggested_method_name = method_to_use(method_definition)
|
70
72
|
|
71
|
-
corrector.insert_after(node
|
73
|
+
corrector.insert_after(node, ", method: :#{suggested_method_name}")
|
72
74
|
|
73
75
|
range = range_with_surrounding_space(
|
74
|
-
range: method_definition.
|
76
|
+
range: method_definition.source_range, side: :left
|
75
77
|
)
|
76
78
|
corrector.remove(range)
|
77
79
|
end
|
@@ -80,6 +82,10 @@ module RuboCop
|
|
80
82
|
method_name = field.resolver_method_name
|
81
83
|
field.schema_member.find_method_definition(method_name)
|
82
84
|
end
|
85
|
+
|
86
|
+
def method_kwarg_set?(field)
|
87
|
+
field.kwargs.method != nil
|
88
|
+
end
|
83
89
|
end
|
84
90
|
end
|
85
91
|
end
|
@@ -63,9 +63,16 @@ module RuboCop
|
|
63
63
|
end
|
64
64
|
|
65
65
|
def field_name(node)
|
66
|
-
node
|
66
|
+
field = RuboCop::GraphQL::Field.new(node)
|
67
|
+
|
68
|
+
"#{field.name}#{':non-camelized' if false_value?(field.kwargs.camelize)}"
|
67
69
|
end
|
68
70
|
|
71
|
+
# @!method false_value?(node)
|
72
|
+
def_node_matcher :false_value?, <<~PATTERN
|
73
|
+
(pair ... false)
|
74
|
+
PATTERN
|
75
|
+
|
69
76
|
# @!method field_declarations(node)
|
70
77
|
def_node_search :field_declarations, <<~PATTERN
|
71
78
|
{
|
@@ -0,0 +1,70 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module GraphQL
|
6
|
+
# Checks consistency of graphql_name usage
|
7
|
+
#
|
8
|
+
# EnforcedStyle supports two modes:
|
9
|
+
#
|
10
|
+
# `only_override` : types and mutations should have graphql_name configured only if it's
|
11
|
+
# different from the default name
|
12
|
+
#
|
13
|
+
# `required` : all types and mutations should have graphql_name configured
|
14
|
+
#
|
15
|
+
# @example EnforcedStyle: only_override (default)
|
16
|
+
# # good
|
17
|
+
#
|
18
|
+
# class UserType < BaseType
|
19
|
+
# graphql_name 'Viewer'
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
# # bad
|
23
|
+
#
|
24
|
+
# class UserType < BaseType
|
25
|
+
# graphql_name 'User'
|
26
|
+
# end
|
27
|
+
#
|
28
|
+
# @example EnforcedStyle: required
|
29
|
+
# # good
|
30
|
+
#
|
31
|
+
# class UserType < BaseType
|
32
|
+
# graphql_name 'User'
|
33
|
+
# end
|
34
|
+
#
|
35
|
+
# # bad
|
36
|
+
#
|
37
|
+
# class UserType < BaseType
|
38
|
+
# end
|
39
|
+
#
|
40
|
+
class GraphqlName < Base
|
41
|
+
include ConfigurableEnforcedStyle
|
42
|
+
|
43
|
+
# @!method graphql_name(node)
|
44
|
+
def_node_matcher :graphql_name, <<~PATTERN
|
45
|
+
`(send nil? :graphql_name (str $_))
|
46
|
+
PATTERN
|
47
|
+
|
48
|
+
# @!method class_name(node)
|
49
|
+
def_node_matcher :class_name, <<~PATTERN
|
50
|
+
(class (const _ $_) ...)
|
51
|
+
PATTERN
|
52
|
+
|
53
|
+
MISSING_NAME = "graphql_name should be configured."
|
54
|
+
UNNEEDED_OVERRIDE = "graphql_name should be specified only for overrides."
|
55
|
+
|
56
|
+
def on_class(node)
|
57
|
+
specified_name = graphql_name(node)
|
58
|
+
|
59
|
+
case style
|
60
|
+
when :required
|
61
|
+
add_offense(node, message: MISSING_NAME) if specified_name.nil?
|
62
|
+
when :only_override
|
63
|
+
default_graphql_name = class_name(node).to_s.sub(/Type\Z/, "")
|
64
|
+
add_offense(node, message: UNNEEDED_OVERRIDE) if specified_name == default_graphql_name
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module GraphQL
|
6
|
+
# Detects missing max_complexity configuration in schema files
|
7
|
+
#
|
8
|
+
# @example
|
9
|
+
# # good
|
10
|
+
#
|
11
|
+
# class AppSchema < BaseSchema
|
12
|
+
# max_complexity 42
|
13
|
+
# end
|
14
|
+
#
|
15
|
+
class MaxComplexitySchema < Base
|
16
|
+
# @!method max_complexity(node)
|
17
|
+
def_node_matcher :max_complexity, <<~PATTERN
|
18
|
+
`(send nil? :max_complexity ...)
|
19
|
+
PATTERN
|
20
|
+
|
21
|
+
MSG = "max_complexity should be configured for schema."
|
22
|
+
|
23
|
+
def on_class(node)
|
24
|
+
return if ::RuboCop::GraphQL::Class.new(node).nested? || max_complexity(node)
|
25
|
+
|
26
|
+
add_offense(node)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module GraphQL
|
6
|
+
# Detects missing max_depth configuration in schema files
|
7
|
+
#
|
8
|
+
# @example
|
9
|
+
# # good
|
10
|
+
#
|
11
|
+
# class AppSchema < BaseSchema
|
12
|
+
# max_depth 42
|
13
|
+
# end
|
14
|
+
#
|
15
|
+
class MaxDepthSchema < Base
|
16
|
+
# @!method max_depth(node)
|
17
|
+
def_node_matcher :max_depth, <<~PATTERN
|
18
|
+
`(send nil? :max_depth ...)
|
19
|
+
PATTERN
|
20
|
+
|
21
|
+
MSG = "max_depth should be configured for schema."
|
22
|
+
|
23
|
+
def on_class(node)
|
24
|
+
return if ::RuboCop::GraphQL::Class.new(node).nested? || max_depth(node)
|
25
|
+
|
26
|
+
add_offense(node)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,134 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module GraphQL
|
6
|
+
# Detects types that implement Node interface and not have `.authorized?` check.
|
7
|
+
# Such types can be fetched by ID and therefore should have type level check to
|
8
|
+
# avoid accidental information exposure.
|
9
|
+
#
|
10
|
+
# If `.authorized?` is defined in a parent class, you can add parent to the "SafeBaseClasses"
|
11
|
+
# to avoid offenses in children.
|
12
|
+
#
|
13
|
+
# This cop also checks the `can_can_action` or `pundit_role` methods that
|
14
|
+
# can be used as part of the Ruby GraphQL Pro.
|
15
|
+
#
|
16
|
+
# @example
|
17
|
+
# # good
|
18
|
+
#
|
19
|
+
# class UserType < BaseType
|
20
|
+
# implements GraphQL::Types::Relay::Node
|
21
|
+
#
|
22
|
+
# field :uuid, ID, null: false
|
23
|
+
#
|
24
|
+
# def self.authorized?(object, context)
|
25
|
+
# super && object.owner == context[:viewer]
|
26
|
+
# end
|
27
|
+
# end
|
28
|
+
#
|
29
|
+
# # good
|
30
|
+
#
|
31
|
+
# class UserType < BaseType
|
32
|
+
# implements GraphQL::Types::Relay::Node
|
33
|
+
#
|
34
|
+
# field :uuid, ID, null: false
|
35
|
+
#
|
36
|
+
# class << self
|
37
|
+
# def authorized?(object, context)
|
38
|
+
# super && object.owner == context[:viewer]
|
39
|
+
# end
|
40
|
+
# end
|
41
|
+
# end
|
42
|
+
#
|
43
|
+
# # good
|
44
|
+
#
|
45
|
+
# class UserType < BaseType
|
46
|
+
# implements GraphQL::Types::Relay::Node
|
47
|
+
#
|
48
|
+
# pundit_role :staff
|
49
|
+
#
|
50
|
+
# field :uuid, ID, null: false
|
51
|
+
# end
|
52
|
+
#
|
53
|
+
# # good
|
54
|
+
#
|
55
|
+
# class UserType < BaseType
|
56
|
+
# implements GraphQL::Types::Relay::Node
|
57
|
+
#
|
58
|
+
# can_can_action :staff
|
59
|
+
#
|
60
|
+
# field :uuid, ID, null: false
|
61
|
+
# end
|
62
|
+
#
|
63
|
+
# # bad
|
64
|
+
#
|
65
|
+
# class UserType < BaseType
|
66
|
+
# implements GraphQL::Types::Relay::Node
|
67
|
+
#
|
68
|
+
# field :uuid, ID, null: false
|
69
|
+
# end
|
70
|
+
#
|
71
|
+
class NotAuthorizedNodeType < Base
|
72
|
+
MSG = ".authorized? should be defined for types implementing Node interface."
|
73
|
+
|
74
|
+
# @!method implements_node_type?(node)
|
75
|
+
def_node_matcher :implements_node_type?, <<~PATTERN
|
76
|
+
`(send nil? :implements
|
77
|
+
(const
|
78
|
+
(const
|
79
|
+
(const
|
80
|
+
(const nil? :GraphQL) :Types) :Relay) :Node))
|
81
|
+
PATTERN
|
82
|
+
|
83
|
+
# @!method has_can_can_action?(node)
|
84
|
+
def_node_matcher :has_can_can_action?, <<~PATTERN
|
85
|
+
`(send nil? :can_can_action {nil_type? sym_type?})
|
86
|
+
PATTERN
|
87
|
+
|
88
|
+
# @!method has_pundit_role?(node)
|
89
|
+
def_node_matcher :has_pundit_role?, <<~PATTERN
|
90
|
+
`(send nil? :pundit_role {nil_type? sym_type?})
|
91
|
+
PATTERN
|
92
|
+
|
93
|
+
# @!method has_authorized_method?(node)
|
94
|
+
def_node_matcher :has_authorized_method?, <<~PATTERN
|
95
|
+
{`(:defs (:self) :authorized? ...) | `(:sclass (:self) `(:def :authorized? ...))}
|
96
|
+
PATTERN
|
97
|
+
|
98
|
+
def on_module(node)
|
99
|
+
@parent_modules ||= []
|
100
|
+
@parent_modules << node.child_nodes[0].const_name
|
101
|
+
end
|
102
|
+
|
103
|
+
def on_class(node)
|
104
|
+
@parent_modules ||= []
|
105
|
+
return if possible_parent_classes(node).any? { |klass| ignored_class?(klass) }
|
106
|
+
|
107
|
+
@parent_modules << node.child_nodes[0].const_name
|
108
|
+
|
109
|
+
add_offense(node) if implements_node_type?(node) && !implements_authorization?(node)
|
110
|
+
end
|
111
|
+
|
112
|
+
private
|
113
|
+
|
114
|
+
def implements_authorization?(node)
|
115
|
+
has_authorized_method?(node) || has_can_can_action?(node) || has_pundit_role?(node)
|
116
|
+
end
|
117
|
+
|
118
|
+
def possible_parent_classes(node)
|
119
|
+
klass = node.child_nodes[1].const_name
|
120
|
+
|
121
|
+
return [] if klass.nil?
|
122
|
+
return [klass] if node.child_nodes[1].absolute?
|
123
|
+
|
124
|
+
parent_module = "#{@parent_modules.join('::')}::"
|
125
|
+
[klass, parent_module + klass]
|
126
|
+
end
|
127
|
+
|
128
|
+
def ignored_class?(klass)
|
129
|
+
cop_config["SafeBaseClasses"].include?(klass)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|