rubocop-graphql 0.1.2 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
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: