rubocop-graphql 0.1.2 → 0.4.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: 30e4dfa482d073cf52c315042d736a54ce725491b3dcf130c9fecd3987bed8d2
4
- data.tar.gz: 4e2467eab2c0ae0be138f6acf3b21912ad888c7b3cbcad98c2c7b0da3d9579af
3
+ metadata.gz: 5be7099abfc014351e097467d9dfe7c687d4a0323241f7ce592e0abe25f6b0cb
4
+ data.tar.gz: da4e69f7810f416873d3f3f0742b70758a3c66c343bbd0d2572c223605156c77
5
5
  SHA512:
6
- metadata.gz: 6099e724a272914bac6212d07ad92256316db2874b26edcd2daf9e8155668f5ddb6b394221b740e8ed51273b49eb101cd83cba55c59352a6c2cd178e972afb9b
7
- data.tar.gz: 80f3d7cfa9e10f209e3f8d2cbf373ac370dc738da62e05b704f37cb8aaccd41216aed6294f063092d26c606645877b3bc0c2aba77bfabf6b5590b79d31e6928f
6
+ metadata.gz: ea0737a86c403dbd57b5777c221ce80ec86e97c6fce499d00f573ef7adeb15f37e2db7b2c8f6275ff7aa58e1a17b12472a748cdf06f26c0411cb9943075386fd
7
+ data.tar.gz: c0b0235d1035b9b77ffaaac3ee419b4e240c9eb7363033f37f9396a028930e33f6d11d3145f95c708e68a0b2f7566de0e1cf96264619eb1da0c3626e99304a0b
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Rubocop::GraphQL
1
+ # RuboCop::GraphQL
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
 
@@ -24,7 +24,7 @@ gem 'rubocop-graphql', require: false
24
24
 
25
25
  ## Usage
26
26
 
27
- You need to tell RuboCop to load the Rails extension. There are three ways to do this:
27
+ You need to tell RuboCop to load the GraphQL extension. There are three ways to do this:
28
28
 
29
29
  ### RuboCop configuration file
30
30
 
@@ -42,7 +42,7 @@ require:
42
42
  - rubocop-graphql
43
43
  ```
44
44
 
45
- Now you can run `rubocop` and it will automatically load the RuboCop Rails cops together with the standard cops.
45
+ Now you can run `rubocop` and it will automatically load the RuboCop GraphQL cops together with the standard cops.
46
46
 
47
47
  ### Command line
48
48
 
@@ -62,7 +62,7 @@ end
62
62
 
63
63
  All cops are located under [`lib/rubocop/cop/graphql`](lib/rubocop/cop/graphql), and contain examples and documentation.
64
64
 
65
- In your `.rubocop.yml`, you may treat the Rails cops just like any other cop. For example:
65
+ In your `.rubocop.yml`, you may treat the GraphQL cops just like any other cop. For example:
66
66
 
67
67
  ```yaml
68
68
  GraphQL/ResolverMethodLength:
@@ -3,10 +3,20 @@ AllCops:
3
3
  Patterns:
4
4
  - "(?:^|/)graphql/"
5
5
 
6
+ GraphQL/ArgumentDescription:
7
+ Enabled: true
8
+ VersionAdded: '0.80'
9
+ Description: 'Ensures all arguments have a description'
10
+
11
+ GraphQL/ArgumentName:
12
+ Enabled: true
13
+ VersionAdded: '0.80'
14
+ Description: 'This cop checks whether argument names are snake_case'
15
+
6
16
  GraphQL/ResolverMethodLength:
7
17
  Enabled: true
8
18
  VersionAdded: '0.80'
9
- Description: 'Avoid resolver methods longer than 10 lines of code.'
19
+ Description: 'Checks resolver methods are not too long'
10
20
  Max: 10
11
21
  CountComments: false
12
22
  ExcludedMethods: []
@@ -23,4 +33,45 @@ GraphQL/FieldDefinitions:
23
33
  GraphQL/FieldDescription:
24
34
  Enabled: true
25
35
  VersionAdded: '0.80'
26
- Description: 'Missing field description'
36
+ Description: 'Ensures all fields have a description'
37
+
38
+ GraphQL/FieldHashKey:
39
+ Enabled: true
40
+ VersionAdded: '0.80'
41
+ Description: 'Checks :hash_key option is used for appropriate fields'
42
+
43
+ GraphQL/FieldMethod:
44
+ Enabled: true
45
+ VersionAdded: '0.80'
46
+ Description: 'Checks :method option is used for appropriate fields'
47
+
48
+ GraphQL/FieldName:
49
+ Enabled: true
50
+ VersionAdded: '0.80'
51
+ Description: 'This cop checks whether field names are snake_case'
52
+
53
+ GraphQL/ExtractInputType:
54
+ Enabled: true
55
+ VersionAdded: '0.80'
56
+ Description: 'Suggests using input type instead of many arguments'
57
+ MaxArguments: 2
58
+
59
+ GraphQL/ExtractType:
60
+ Enabled: true
61
+ VersionAdded: '0.80'
62
+ Description: 'Suggests extracting fields with common prefixes to the separate type'
63
+ MaxFields: 2
64
+ Prefixes:
65
+ - is
66
+ - with
67
+ - avg
68
+ - min
69
+ - max
70
+
71
+ GraphQL/ObjectDescription:
72
+ Enabled: true
73
+ VersionAdded: '0.80'
74
+ Description: 'Ensures all types have a description'
75
+ Exclude:
76
+ - '**/*_schema.rb'
77
+ - '**/base_*.rb'
@@ -2,11 +2,20 @@
2
2
 
3
3
  require "rubocop"
4
4
 
5
+ require_relative "rubocop/graphql/ext/snake_case"
6
+
5
7
  require_relative "rubocop/graphql"
6
8
  require_relative "rubocop/graphql/version"
7
9
  require_relative "rubocop/graphql/inject"
8
10
  require_relative "rubocop/graphql/node_pattern"
11
+
12
+ require_relative "rubocop/graphql/argument"
13
+ require_relative "rubocop/graphql/argument/block"
14
+ require_relative "rubocop/graphql/argument/kwargs"
9
15
  require_relative "rubocop/graphql/field"
16
+ require_relative "rubocop/graphql/field/block"
17
+ require_relative "rubocop/graphql/field/kwargs"
18
+ require_relative "rubocop/graphql/schema_member"
10
19
 
11
20
  RuboCop::GraphQL::Inject.defaults!
12
21
 
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module GraphQL
6
+ # This cop checks if each field has a description.
7
+ #
8
+ # @example
9
+ # # good
10
+ #
11
+ # class BanUser < BaseMutation
12
+ # argument :uuid, ID, required: true, description: "UUID of the user to ban"
13
+ # end
14
+ #
15
+ # # bad
16
+ #
17
+ # class BanUser < BaseMutation
18
+ # argument :uuid, ID, required: true
19
+ # end
20
+ #
21
+ class ArgumentDescription < Cop
22
+ include RuboCop::GraphQL::NodePattern
23
+
24
+ MSG = "Missing argument description"
25
+
26
+ def on_send(node)
27
+ return unless argument?(node)
28
+
29
+ argument = RuboCop::GraphQL::Argument.new(node)
30
+
31
+ add_offense(node) unless argument.description
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module GraphQL
6
+ # This cop checks whether field names are snake_case.
7
+ #
8
+ # @example
9
+ # # good
10
+ #
11
+ # class BanUser < BaseMutation
12
+ # argument :user_id, ID, required: true
13
+ # end
14
+ #
15
+ # # bad
16
+ #
17
+ # class BanUser < BaseMutation
18
+ # argument :userId, ID, required: true
19
+ # end
20
+ #
21
+ class ArgumentName < Cop
22
+ include RuboCop::GraphQL::NodePattern
23
+
24
+ using RuboCop::GraphQL::Ext::SnakeCase
25
+
26
+ MSG = "Use snake_case for argument names"
27
+
28
+ def on_send(node)
29
+ return unless argument?(node)
30
+
31
+ argument = RuboCop::GraphQL::Argument.new(node)
32
+ return if argument.name.snake_case?
33
+
34
+ add_offense(node)
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module GraphQL
6
+ class ExtractInputType < Cop
7
+ # This cop checks fields on common prefix groups
8
+ #
9
+ # # @example
10
+ # # good
11
+ #
12
+ # class UpdateUser < BaseMutation
13
+ # argument :uuid, ID, required: true
14
+ # argument :user_attributes, UserAttributesInputType
15
+ # end
16
+ #
17
+ # # bad
18
+ #
19
+ # class UpdateUser < BaseMutation
20
+ # argument :uuid, ID, required: true
21
+ # argument :first_name, String, required: true
22
+ # argument :last_name, String, required: true
23
+ # end
24
+ #
25
+ include RuboCop::GraphQL::NodePattern
26
+
27
+ MSG = "Consider moving arguments to a new input type"
28
+
29
+ def on_class(node)
30
+ schema_member = RuboCop::GraphQL::SchemaMember.new(node)
31
+
32
+ if (body = schema_member.body)
33
+ arguments = body.select { |node| argument?(node) }
34
+
35
+ add_offense(arguments.last) if arguments.count > cop_config["MaxArguments"]
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module GraphQL
6
+ class ExtractType < Cop
7
+ # This cop checks fields on common prefix groups
8
+ #
9
+ # # @example
10
+ # # good
11
+ #
12
+ # class Types::UserType < Types::BaseObject
13
+ # field :registered_at, String, null: false
14
+ # field :contact, Types::ContactType, null: false
15
+ #
16
+ # def contact
17
+ # self
18
+ # end
19
+ # end
20
+ #
21
+ # class Types::ContactType < Types::BaseObject
22
+ # field :phone, String, null: false
23
+ # field :first_name, String, null: false
24
+ # field :last_name, String, null: false
25
+ # end
26
+ #
27
+ # # bad
28
+ #
29
+ # class Types::UserType < Types::BaseObject
30
+ # field :registered_at, String, null: false
31
+ # field :contact_phone, String, null: false
32
+ # field :contact_first_name, String, null: false
33
+ # field :contact_last_name, String, null: false
34
+ # end
35
+ #
36
+ include RuboCop::GraphQL::NodePattern
37
+
38
+ def on_class(node)
39
+ schema_member = RuboCop::GraphQL::SchemaMember.new(node)
40
+
41
+ if (body = schema_member.body)
42
+ check_fields_prefixes(body)
43
+ end
44
+ end
45
+
46
+ private
47
+
48
+ MSG = "Consider moving %<field_names>s to a new type and adding the `%<prefix>s` field instead"
49
+
50
+ def check_fields_prefixes(body)
51
+ sorted_prefixes = fractured(body).sort_by { |k, _| k.size }.reverse
52
+ already_offenced_fields = []
53
+
54
+ sorted_prefixes.each do |prefix, fields|
55
+ fields -= already_offenced_fields
56
+
57
+ next if fields.count < cop_config["MaxFields"]
58
+
59
+ add_offense(
60
+ fields.last.node,
61
+ message: message(prefix, fields.map(&:name).join(", "))
62
+ )
63
+
64
+ already_offenced_fields += fields
65
+ end
66
+ end
67
+
68
+ def fractured(body)
69
+ body.each_with_object({}) do |node, acc|
70
+ next unless field?(node)
71
+
72
+ field = RuboCop::GraphQL::Field.new(node)
73
+ next unless field.underscore_name.include?("_")
74
+
75
+ *prefixes, _ = field.underscore_name.split("_")
76
+
77
+ prefixes.each_with_object([]) do |prefix, prev_prefix|
78
+ prefix = (prev_prefix + [prefix]).join("_")
79
+ break if ignored_prefix?(prefix)
80
+
81
+ (acc[prefix] ||= []) << field
82
+
83
+ prev_prefix << prefix
84
+ end
85
+ end
86
+ end
87
+
88
+ def message(prefix, field_names)
89
+ format(MSG, field_names: field_names, prefix: prefix)
90
+ end
91
+
92
+ def ignored_prefix?(word)
93
+ cop_config["Prefixes"].include?(word)
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
@@ -47,6 +47,7 @@ module RuboCop
47
47
  class FieldDefinitions < Cop
48
48
  include ConfigurableEnforcedStyle
49
49
  include RuboCop::GraphQL::NodePattern
50
+ include RuboCop::Cop::RangeHelp
50
51
 
51
52
  def_node_matcher :field_kwargs, <<~PATTERN
52
53
  (send nil? :field
@@ -57,10 +58,6 @@ module RuboCop
57
58
  )
58
59
  PATTERN
59
60
 
60
- def_node_matcher :class_body, <<~PATTERN
61
- (class ... (begin $...))
62
- PATTERN
63
-
64
61
  def on_send(node)
65
62
  return if !field_definition?(node) || style != :define_resolver_after_definition
66
63
 
@@ -71,8 +68,22 @@ module RuboCop
71
68
  def on_class(node)
72
69
  return if style != :group_definitions
73
70
 
74
- body = class_body(node)
75
- check_grouped_field_declarations(body) if body
71
+ schema_member = RuboCop::GraphQL::SchemaMember.new(node)
72
+
73
+ if (body = schema_member.body)
74
+ check_grouped_field_declarations(body)
75
+ end
76
+ end
77
+
78
+ def autocorrect(node)
79
+ lambda do |corrector|
80
+ case style
81
+ when :define_resolver_after_definition
82
+ place_resolver_after_definitions(corrector, node)
83
+ when :group_definitions
84
+ group_field_declarations(corrector, node)
85
+ end
86
+ end
76
87
  end
77
88
 
78
89
  private
@@ -80,7 +91,7 @@ module RuboCop
80
91
  GROUP_DEFS_MSG = "Group all field definitions together."
81
92
 
82
93
  def check_grouped_field_declarations(body)
83
- fields = body.select { |node| field_definition?(node) || field_definition_with_body?(node) }
94
+ fields = body.select { |node| field?(node) }
84
95
 
85
96
  first_field = fields.first
86
97
 
@@ -91,13 +102,26 @@ module RuboCop
91
102
  end
92
103
  end
93
104
 
105
+ def group_field_declarations(corrector, node)
106
+ field = RuboCop::GraphQL::Field.new(node)
107
+
108
+ first_field = field.schema_member.body.find { |node|
109
+ field_definition?(node) || field_definition_with_body?(node)
110
+ }
111
+
112
+ source_to_insert = "\n" + indent(node) + node.source
113
+ corrector.insert_after(first_field.loc.expression, source_to_insert)
114
+
115
+ range = range_with_surrounding_space(range: node.loc.expression, side: :left)
116
+ corrector.remove(range)
117
+ end
118
+
94
119
  RESOLVER_AFTER_FIELD_MSG = "Define resolver method after field definition."
95
120
 
96
121
  def check_resolver_is_defined_after_definition(field)
97
- return if field.resolver || field.method || field.hash_key
122
+ return if field.kwargs.resolver || field.kwargs.method || field.kwargs.hash_key
98
123
 
99
- root = field.ancestors.find { |parent| root_node?(parent) }
100
- method_definition = find_method_definition(root, field.resolver_method_name)
124
+ method_definition = field.schema_member.find_method_definition(field.resolver_method_name)
101
125
  return unless method_definition
102
126
 
103
127
  field_sibling_index = if field_definition_with_body?(field.parent)
@@ -111,16 +135,23 @@ module RuboCop
111
135
  add_offense(field.node, message: RESOLVER_AFTER_FIELD_MSG)
112
136
  end
113
137
 
114
- def find_method_definition(root, method_name)
115
- class_body(root).find { |node| node.def_type? && node.method_name == method_name }
116
- end
138
+ def place_resolver_after_definitions(corrector, node)
139
+ field = RuboCop::GraphQL::Field.new(node)
140
+
141
+ method_definition = field.schema_member.find_method_definition(field.resolver_method_name)
142
+
143
+ field_definition = field_definition_with_body?(node.parent) ? node.parent : node
144
+
145
+ source_to_insert = indent(method_definition) + field_definition.source + "\n\n"
146
+ method_range = range_by_whole_lines(method_definition.loc.expression)
147
+ corrector.insert_before(method_range, source_to_insert)
117
148
 
118
- def root_node?(node)
119
- node.parent.nil? || root_with_siblings?(node.parent)
149
+ range_to_remove = range_with_surrounding_space(range: field_definition.loc.expression, side: :left)
150
+ corrector.remove(range_to_remove)
120
151
  end
121
152
 
122
- def root_with_siblings?(node)
123
- node.begin_type? && node.parent.nil?
153
+ def indent(node)
154
+ " " * node.location.column
124
155
  end
125
156
  end
126
157
  end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module GraphQL
6
+ # This cop prevents defining unnecessary resolver methods in cases
7
+ # when :hash_key option can be used
8
+ #
9
+ # @example
10
+ # # good
11
+ #
12
+ # class Types::UserType < Types::BaseObject
13
+ # field :phone, String, null: true, hash_key: :home_phone
14
+ # end
15
+ #
16
+ # # bad
17
+ #
18
+ # class Types::UserType < Types::BaseObject
19
+ # field :phone, String, null: true
20
+ #
21
+ # def phone
22
+ # object[:home_phone]
23
+ # end
24
+ # end
25
+ #
26
+ class FieldHashKey < Cop
27
+ include RuboCop::GraphQL::NodePattern
28
+ include RuboCop::Cop::RangeHelp
29
+
30
+ def_node_matcher :hash_key_to_use, <<~PATTERN
31
+ (def
32
+ _
33
+ (args)
34
+ (send
35
+ (send nil? :object) :[]
36
+ (_type $_)
37
+ )
38
+ )
39
+ PATTERN
40
+
41
+ MSG = "Use hash_key: %<hash_key>p"
42
+
43
+ def on_send(node)
44
+ return unless field_definition?(node)
45
+
46
+ field = RuboCop::GraphQL::Field.new(node)
47
+ method_definition = resolver_method_definition_for(field)
48
+
49
+ if (suggested_hash_key_name = hash_key_to_use(method_definition))
50
+ add_offense(node, message: message(suggested_hash_key_name))
51
+ end
52
+ end
53
+
54
+ def autocorrect(node)
55
+ lambda do |corrector|
56
+ field = RuboCop::GraphQL::Field.new(node)
57
+ method_definition = resolver_method_definition_for(field)
58
+ suggested_hash_key_name = hash_key_to_use(method_definition)
59
+
60
+ corrector.insert_after(node.loc.expression, ", hash_key: #{suggested_hash_key_name.inspect}")
61
+
62
+ range = range_with_surrounding_space(range: method_definition.loc.expression, side: :left)
63
+ corrector.remove(range)
64
+ end
65
+ end
66
+
67
+ private
68
+
69
+ def message(hash_key)
70
+ format(MSG, hash_key: hash_key)
71
+ end
72
+
73
+ def resolver_method_definition_for(field)
74
+ method_name = field.resolver_method_name
75
+ field.schema_member.find_method_definition(method_name)
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module GraphQL
6
+ # This cop prevents defining unnecessary resolver methods in cases
7
+ # when :method option can be used
8
+ #
9
+ # @example
10
+ # # good
11
+ #
12
+ # class Types::UserType < Types::BaseObject
13
+ # field :phone, String, null: true, method: :home_phone
14
+ # end
15
+ #
16
+ # # bad
17
+ #
18
+ # class Types::UserType < Types::BaseObject
19
+ # field :phone, String, null: true
20
+ #
21
+ # def phone
22
+ # object.home_phone
23
+ # end
24
+ # end
25
+ #
26
+ class FieldMethod < Cop
27
+ include RuboCop::GraphQL::NodePattern
28
+ include RuboCop::Cop::RangeHelp
29
+
30
+ def_node_matcher :method_to_use, <<~PATTERN
31
+ (def
32
+ _
33
+ (args)
34
+ (send
35
+ (send nil? :object) $_
36
+ )
37
+ )
38
+ PATTERN
39
+
40
+ MSG = "Use method: :%<method_name>s"
41
+
42
+ def on_send(node)
43
+ return unless field_definition?(node)
44
+
45
+ field = RuboCop::GraphQL::Field.new(node)
46
+ method_definition = suggest_method_name_for(field)
47
+
48
+ if (suggested_method_name = method_to_use(method_definition))
49
+ add_offense(node, message: message(suggested_method_name))
50
+ end
51
+ end
52
+
53
+ def autocorrect(node)
54
+ lambda do |corrector|
55
+ field = RuboCop::GraphQL::Field.new(node)
56
+ method_definition = suggest_method_name_for(field)
57
+ suggested_method_name = method_to_use(method_definition)
58
+
59
+ corrector.insert_after(node.loc.expression, ", method: :#{suggested_method_name}")
60
+
61
+ range = range_with_surrounding_space(range: method_definition.loc.expression, side: :left)
62
+ corrector.remove(range)
63
+ end
64
+ end
65
+
66
+ private
67
+
68
+ def message(method_name)
69
+ format(MSG, method_name: method_name)
70
+ end
71
+
72
+ def suggest_method_name_for(field)
73
+ method_name = field.resolver_method_name
74
+ field.schema_member.find_method_definition(method_name)
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module GraphQL
6
+ # This cop checks whether field names are snake_case.
7
+ #
8
+ # @example
9
+ # # good
10
+ #
11
+ # class UserType < BaseType
12
+ # field :first_name, String, null: true
13
+ # end
14
+ #
15
+ # # bad
16
+ #
17
+ # class UserType < BaseType
18
+ # field :firstName, String, null: true
19
+ # end
20
+ #
21
+ class FieldName < Cop
22
+ include RuboCop::GraphQL::NodePattern
23
+
24
+ using RuboCop::GraphQL::Ext::SnakeCase
25
+
26
+ MSG = "Use snake_case for field names"
27
+
28
+ def on_send(node)
29
+ return unless field_definition?(node)
30
+
31
+ field = RuboCop::GraphQL::Field.new(node)
32
+ return if field.name.snake_case?
33
+
34
+ add_offense(node)
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module GraphQL
6
+ # This cop checks if a type (object, input, interface, scalar, union, mutation, subscription, and resolver) has a description.
7
+ #
8
+ # @example
9
+ # # good
10
+ #
11
+ # class Types::UserType < Types::BaseObject
12
+ # description "Represents application user"
13
+ # # ...
14
+ # end
15
+ #
16
+ # # bad
17
+ #
18
+ # class Types::UserType < Types::BaseObject
19
+ # # ...
20
+ # end
21
+ #
22
+ class ObjectDescription < Cop
23
+ include RuboCop::GraphQL::NodePattern
24
+
25
+ MSG = "Missing type description"
26
+
27
+ def_node_matcher :has_i18n_description?, <<~PATTERN
28
+ (send nil? :description (send (const nil? :I18n) :t ...))
29
+ PATTERN
30
+
31
+ def_node_matcher :has_string_description?, <<~PATTERN
32
+ (send nil? :description (:str $_))
33
+ PATTERN
34
+
35
+ def_node_matcher :interface?, <<~PATTERN
36
+ (send nil? :include (const ...))
37
+ PATTERN
38
+
39
+ def on_class(node)
40
+ return if child_nodes(node).find { |child_node| has_description?(child_node) }
41
+
42
+ add_offense(node.identifier)
43
+ end
44
+
45
+ def on_module(node)
46
+ return if child_nodes(node).none? { |child_node| interface?(child_node) }
47
+
48
+ add_offense(node.identifier) if child_nodes(node).none? { |child_node| has_description?(child_node) }
49
+ end
50
+
51
+ private
52
+
53
+ def has_description?(node)
54
+ has_i18n_description?(node) || has_string_description?(node)
55
+ end
56
+
57
+ def child_nodes(node)
58
+ if node.body.instance_of? RuboCop::AST::Node
59
+ node.body.child_nodes
60
+ else
61
+ node.child_nodes
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -2,6 +2,14 @@
2
2
 
3
3
  require_relative "graphql/cop"
4
4
 
5
+ require_relative "graphql/argument_description"
6
+ require_relative "graphql/argument_name"
7
+ require_relative "graphql/extract_input_type"
8
+ require_relative "graphql/extract_type"
5
9
  require_relative "graphql/field_definitions"
6
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"
7
14
  require_relative "graphql/resolver_method_length"
15
+ require_relative "graphql/object_description"
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module GraphQL
5
+ class Argument
6
+ extend RuboCop::NodePattern::Macros
7
+
8
+ def_node_matcher :argument_description, <<~PATTERN
9
+ (send nil? :argument _ _ (:str $_) ...)
10
+ PATTERN
11
+
12
+ def_node_matcher :argument_name, <<~PATTERN
13
+ (send nil? :argument (:sym $_) ...)
14
+ PATTERN
15
+
16
+ attr_reader :node
17
+
18
+ def initialize(node)
19
+ @node = node
20
+ end
21
+
22
+ def name
23
+ @name ||= argument_name(@node)
24
+ end
25
+
26
+ def description
27
+ @description ||= argument_description(@node) || kwargs.description || block.description
28
+ end
29
+
30
+ def kwargs
31
+ @kwargs ||= Argument::Kwargs.new(@node)
32
+ end
33
+
34
+ def block
35
+ @block ||= Argument::Block.new(@node.parent)
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module GraphQL
5
+ class Argument
6
+ class Block
7
+ extend RuboCop::NodePattern::Macros
8
+
9
+ def_node_matcher :argument_block, <<~PATTERN
10
+ (block
11
+ (send nil? :argument ...)
12
+ (args)
13
+ $...
14
+ )
15
+ PATTERN
16
+
17
+ def_node_matcher :description_kwarg?, <<~PATTERN
18
+ (send nil? :description (str ...))
19
+ PATTERN
20
+
21
+ def initialize(argument_node)
22
+ @nodes = argument_block(argument_node) || []
23
+ end
24
+
25
+ def description
26
+ @nodes.find { |kwarg| description_kwarg?(kwarg) }
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module GraphQL
5
+ class Argument
6
+ class Kwargs
7
+ extend RuboCop::NodePattern::Macros
8
+
9
+ def_node_matcher :argument_kwargs, <<~PATTERN
10
+ (send nil? :argument
11
+ ...
12
+ (hash
13
+ $...
14
+ )
15
+ )
16
+ PATTERN
17
+
18
+ def_node_matcher :description_kwarg?, <<~PATTERN
19
+ (pair (sym :description) ...)
20
+ PATTERN
21
+
22
+ def initialize(argument_node)
23
+ @nodes = argument_kwargs(argument_node)
24
+ end
25
+
26
+ def description
27
+ @nodes.find { |kwarg| description_kwarg?(kwarg) }
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module GraphQL
5
+ module Ext
6
+ module SnakeCase
7
+ SNAKE_CASE = /^[\da-z_]+[!?=]?$/
8
+
9
+ refine Symbol do
10
+ def snake_case?
11
+ match?(SNAKE_CASE)
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -6,39 +6,19 @@ module RuboCop
6
6
  extend Forwardable
7
7
  extend RuboCop::NodePattern::Macros
8
8
 
9
- def_delegators :@node, :sibling_index, :parent, :ancestors
10
-
11
- def_node_matcher :field_kwargs, <<~PATTERN
12
- (send nil? :field
13
- ...
14
- (hash
15
- $...
16
- )
17
- )
18
- PATTERN
9
+ def_delegators :@node, :sibling_index, :parent
19
10
 
20
11
  def_node_matcher :field_name, <<~PATTERN
21
- (send nil? :field (:sym $...) ...)
12
+ (send nil? :field (:sym $_) ...)
22
13
  PATTERN
23
14
 
24
- def_node_matcher :field_description, <<~PATTERN
25
- (send nil? :field _ _ (:str $...) ...)
15
+ def_node_matcher :field_with_body_name, <<~PATTERN
16
+ (block
17
+ (send nil? :field (:sym $_) ...)...)
26
18
  PATTERN
27
19
 
28
- def_node_matcher :resolver_kwarg?, <<~PATTERN
29
- (pair (sym :resolver) ...)
30
- PATTERN
31
-
32
- def_node_matcher :method_kwarg?, <<~PATTERN
33
- (pair (sym :method) ...)
34
- PATTERN
35
-
36
- def_node_matcher :hash_key_kwarg?, <<~PATTERN
37
- (pair (sym :hash_key) ...)
38
- PATTERN
39
-
40
- def_node_matcher :resolver_method_option, <<~PATTERN
41
- (pair (sym :resolver_method) (sym $...))
20
+ def_node_matcher :field_description, <<~PATTERN
21
+ (send nil? :field _ _ (:str $_) ...)
42
22
  PATTERN
43
23
 
44
24
  attr_reader :node
@@ -48,31 +28,52 @@ module RuboCop
48
28
  end
49
29
 
50
30
  def name
51
- @name ||= field_name(@node).first
31
+ @name ||= field_name(@node) || field_with_body_name(@node)
32
+ end
33
+
34
+ def underscore_name
35
+ @underscore_name ||= begin
36
+ word = name.to_s.dup
37
+ word.gsub!(/([A-Z\d]+)([A-Z][a-z])/, '\1_\2')
38
+ word.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
39
+ word.tr!("-", "_")
40
+ word.downcase!
41
+ word
42
+ end
52
43
  end
53
44
 
54
45
  def description
55
- @name ||= field_description(@node)
46
+ @description ||= field_description(@node) || kwargs.description || block.description
47
+ end
48
+
49
+ def resolver_method_name
50
+ kwargs.resolver_method_name || name
56
51
  end
57
52
 
58
53
  def kwargs
59
- @kwargs ||= field_kwargs(@node) || []
54
+ @kwargs ||= Field::Kwargs.new(@node)
60
55
  end
61
56
 
62
- def resolver
63
- kwargs.find { |kwarg| resolver_kwarg?(kwarg) }
57
+ def block
58
+ @block ||= Field::Block.new(@node.parent)
64
59
  end
65
60
 
66
- def method
67
- kwargs.find { |kwarg| method_kwarg?(kwarg) }
61
+ def schema_member
62
+ @schema_member ||= SchemaMember.new(root_node)
68
63
  end
69
64
 
70
- def hash_key
71
- kwargs.find { |kwarg| hash_key_kwarg?(kwarg) }
65
+ private
66
+
67
+ def root_node
68
+ @node.ancestors.find { |parent| root_node?(parent) }
72
69
  end
73
70
 
74
- def resolver_method_name
75
- @resolver_method_name ||= kwargs.flat_map { |kwarg| resolver_method_option(kwarg) }.compact.first || name
71
+ def root_node?(node)
72
+ node.parent.nil? || node.parent.module_type? || root_with_siblings?(node.parent)
73
+ end
74
+
75
+ def root_with_siblings?(node)
76
+ node.begin_type? && node.parent.nil?
76
77
  end
77
78
  end
78
79
  end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module GraphQL
5
+ class Field
6
+ class Block
7
+ extend RuboCop::NodePattern::Macros
8
+
9
+ def_node_matcher :field_block, <<~PATTERN
10
+ (block
11
+ (send nil? :field ...)
12
+ (args)
13
+ $...
14
+ )
15
+ PATTERN
16
+
17
+ def_node_matcher :description_kwarg?, <<~PATTERN
18
+ (send nil? :description (str ...))
19
+ PATTERN
20
+
21
+ def initialize(field_node)
22
+ @nodes = field_block(field_node) || []
23
+ end
24
+
25
+ def description
26
+ @nodes.find { |kwarg| description_kwarg?(kwarg) }
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module GraphQL
5
+ class Field
6
+ class Kwargs
7
+ extend RuboCop::NodePattern::Macros
8
+
9
+ def_node_matcher :field_kwargs, <<~PATTERN
10
+ (send nil? :field
11
+ ...
12
+ (hash
13
+ $...
14
+ )
15
+ )
16
+ PATTERN
17
+
18
+ def_node_matcher :resolver_kwarg?, <<~PATTERN
19
+ (pair (sym :resolver) ...)
20
+ PATTERN
21
+
22
+ def_node_matcher :method_kwarg?, <<~PATTERN
23
+ (pair (sym :method) ...)
24
+ PATTERN
25
+
26
+ def_node_matcher :hash_key_kwarg?, <<~PATTERN
27
+ (pair (sym :hash_key) ...)
28
+ PATTERN
29
+
30
+ def_node_matcher :description_kwarg?, <<~PATTERN
31
+ (pair (sym :description) ...)
32
+ PATTERN
33
+
34
+ def_node_matcher :resolver_method_option, <<~PATTERN
35
+ (pair (sym :resolver_method) (sym $...))
36
+ PATTERN
37
+
38
+ def initialize(field_node)
39
+ @nodes = field_kwargs(field_node) || []
40
+ end
41
+
42
+ def resolver
43
+ @nodes.find { |kwarg| resolver_kwarg?(kwarg) }
44
+ end
45
+
46
+ def method
47
+ @nodes.find { |kwarg| method_kwarg?(kwarg) }
48
+ end
49
+
50
+ def hash_key
51
+ @nodes.find { |kwarg| hash_key_kwarg?(kwarg) }
52
+ end
53
+
54
+ def description
55
+ @nodes.find { |kwarg| description_kwarg?(kwarg) }
56
+ end
57
+
58
+ def resolver_method_name
59
+ @resolver_method_name ||= @nodes.flat_map { |kwarg| resolver_method_option(kwarg) }.compact.first
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -6,15 +6,23 @@ module RuboCop
6
6
  extend RuboCop::NodePattern::Macros
7
7
 
8
8
  def_node_matcher :field_definition?, <<~PATTERN
9
- (send nil? :field ...)
9
+ (send nil? :field (:sym _) ...)
10
10
  PATTERN
11
11
 
12
12
  def_node_matcher :field_definition_with_body?, <<~PATTERN
13
13
  (block
14
- (send nil? :field ...)
14
+ (send nil? :field (:sym _) ...)
15
15
  ...
16
16
  )
17
17
  PATTERN
18
+
19
+ def_node_matcher :argument?, <<~PATTERN
20
+ (send nil? :argument (:sym _) ...)
21
+ PATTERN
22
+
23
+ def field?(node)
24
+ field_definition?(node) || field_definition_with_body?(node)
25
+ end
18
26
  end
19
27
  end
20
28
  end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module GraphQL
5
+ class SchemaMember
6
+ extend RuboCop::NodePattern::Macros
7
+
8
+ def_node_matcher :class_contents, <<~PATTERN
9
+ (class _ _ $_)
10
+ PATTERN
11
+
12
+ attr_reader :node
13
+
14
+ def initialize(node)
15
+ @node = node
16
+ end
17
+
18
+ def find_method_definition(method_name)
19
+ body.find { |node| node.def_type? && node.method_name == method_name }
20
+ end
21
+
22
+ def body
23
+ contents = class_contents(@node)
24
+
25
+ if contents.nil?
26
+ []
27
+ elsif contents.begin_type?
28
+ contents.child_nodes
29
+ else
30
+ [contents]
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -1,5 +1,5 @@
1
1
  module RuboCop
2
2
  module GraphQL
3
- VERSION = "0.1.2"
3
+ VERSION = "0.4.0"
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rubocop-graphql
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dmitry Tsepelev
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-05-25 00:00:00.000000000 Z
11
+ date: 2020-07-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -77,15 +77,30 @@ files:
77
77
  - README.md
78
78
  - config/default.yml
79
79
  - lib/rubocop-graphql.rb
80
+ - lib/rubocop/cop/graphql/argument_description.rb
81
+ - lib/rubocop/cop/graphql/argument_name.rb
80
82
  - lib/rubocop/cop/graphql/cop.rb
83
+ - lib/rubocop/cop/graphql/extract_input_type.rb
84
+ - lib/rubocop/cop/graphql/extract_type.rb
81
85
  - lib/rubocop/cop/graphql/field_definitions.rb
82
86
  - lib/rubocop/cop/graphql/field_description.rb
87
+ - lib/rubocop/cop/graphql/field_hash_key.rb
88
+ - lib/rubocop/cop/graphql/field_method.rb
89
+ - lib/rubocop/cop/graphql/field_name.rb
90
+ - lib/rubocop/cop/graphql/object_description.rb
83
91
  - lib/rubocop/cop/graphql/resolver_method_length.rb
84
92
  - lib/rubocop/cop/graphql_cops.rb
85
93
  - lib/rubocop/graphql.rb
94
+ - lib/rubocop/graphql/argument.rb
95
+ - lib/rubocop/graphql/argument/block.rb
96
+ - lib/rubocop/graphql/argument/kwargs.rb
97
+ - lib/rubocop/graphql/ext/snake_case.rb
86
98
  - lib/rubocop/graphql/field.rb
99
+ - lib/rubocop/graphql/field/block.rb
100
+ - lib/rubocop/graphql/field/kwargs.rb
87
101
  - lib/rubocop/graphql/inject.rb
88
102
  - lib/rubocop/graphql/node_pattern.rb
103
+ - lib/rubocop/graphql/schema_member.rb
89
104
  - lib/rubocop/graphql/version.rb
90
105
  homepage: https://github.com/DmitryTsepelev/rubocop-graphql
91
106
  licenses: