rubocop-graphql 0.3.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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 328663a392bcbb8e7a53bfaf2ba8e7323e9382893c8f290fa6b40e965155ebe1
4
+ data.tar.gz: cb76dad975e6dab191f1fac2ae6c45b677fb06bddbb26f36494a18108e1acf56
5
+ SHA512:
6
+ metadata.gz: a87c4e9ee55fb176eaf98ba520a7942c7a4ee4af3531b7cc90a0039f739a5e5400ce4e21b0d694e59b00ca0bb2771585f07a50699c55496a8794eec1a38900f3
7
+ data.tar.gz: cedeb68f255f806a1e12de6776722f3aadce5f82fe7c0e4ace7979593d7f84abc8f27fee1cff5ced953076f88247e4aadf0599f30b02b5a35436d946f976a353
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2020 DmitryTsepelev
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,78 @@
1
+ # RuboCop::GraphQL
2
+
3
+ [Rubocop](https://github.com/rubocop-hq/rubocop) extension for enforcing [graphql-ruby](https://github.com/rmosolgo/graphql-ruby) best practices.
4
+
5
+ <p align="center">
6
+ <a href="https://evilmartians.com/?utm_source=graphql-rubocop">
7
+ <img src="https://evilmartians.com/badges/sponsored-by-evil-martians.svg" alt="Sponsored by Evil Martians" width="236" height="54">
8
+ </a>
9
+ </p>
10
+
11
+ ## Installation
12
+
13
+ Install the gem:
14
+
15
+ ```bash
16
+ gem install rubocop-graphql
17
+ ```
18
+
19
+ If you use bundler put this in your Gemfile:
20
+
21
+ ```ruby
22
+ gem 'rubocop-graphql', require: false
23
+ ```
24
+
25
+ ## Usage
26
+
27
+ You need to tell RuboCop to load the GraphQL extension. There are three ways to do this:
28
+
29
+ ### RuboCop configuration file
30
+
31
+ Put this into your `.rubocop.yml`.
32
+
33
+ ```yaml
34
+ require: rubocop-graphql
35
+ ```
36
+
37
+ Alternatively, use the following array notation when specifying multiple extensions.
38
+
39
+ ```yaml
40
+ require:
41
+ - rubocop-other-extension
42
+ - rubocop-graphql
43
+ ```
44
+
45
+ Now you can run `rubocop` and it will automatically load the RuboCop GraphQL cops together with the standard cops.
46
+
47
+ ### Command line
48
+
49
+ ```sh
50
+ rubocop --require rubocop-graphql
51
+ ```
52
+
53
+ ### Rake task
54
+
55
+ ```ruby
56
+ RuboCop::RakeTask.new do |task|
57
+ task.requires << 'rubocop-graphql'
58
+ end
59
+ ```
60
+
61
+ ## The Cops
62
+
63
+ All cops are located under [`lib/rubocop/cop/graphql`](lib/rubocop/cop/graphql), and contain examples and documentation.
64
+
65
+ In your `.rubocop.yml`, you may treat the GraphQL cops just like any other cop. For example:
66
+
67
+ ```yaml
68
+ GraphQL/ResolverMethodLength:
69
+ Max: 3
70
+ ```
71
+
72
+ ## Contributing
73
+
74
+ Bug reports and pull requests are welcome on GitHub at https://github.com/DmitryTsepelev/rubocop-graphql.
75
+
76
+ ## License
77
+
78
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,72 @@
1
+ AllCops:
2
+ GraphQL:
3
+ Patterns:
4
+ - "(?:^|/)graphql/"
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
+
16
+ GraphQL/ResolverMethodLength:
17
+ Enabled: true
18
+ VersionAdded: '0.80'
19
+ Description: 'Checks resolver methods are not too long'
20
+ Max: 10
21
+ CountComments: false
22
+ ExcludedMethods: []
23
+
24
+ GraphQL/FieldDefinitions:
25
+ Enabled: true
26
+ VersionAdded: '0.80'
27
+ Description: 'Checks consistency of field definitions'
28
+ EnforcedStyle: group_definitions
29
+ SupportedStyles:
30
+ - group_definitions
31
+ - define_resolver_after_definition
32
+
33
+ GraphQL/FieldDescription:
34
+ Enabled: true
35
+ VersionAdded: '0.80'
36
+ Description: 'Ensures all fields have a description'
37
+
38
+ GraphQL/FieldMethod:
39
+ Enabled: true
40
+ VersionAdded: '0.80'
41
+ Description: 'Checks :method option is used for appropriate fields'
42
+
43
+ GraphQL/FieldName:
44
+ Enabled: true
45
+ VersionAdded: '0.80'
46
+ Description: 'This cop checks whether field names are snake_case'
47
+
48
+ GraphQL/ExtractInputType:
49
+ Enabled: true
50
+ VersionAdded: '0.80'
51
+ Description: 'Suggests using input type instead of many arguments'
52
+ MaxArguments: 2
53
+
54
+ GraphQL/ExtractType:
55
+ Enabled: true
56
+ VersionAdded: '0.80'
57
+ Description: 'Suggests extracting fields with common prefixes to the separate type'
58
+ MaxFields: 2
59
+ Prefixes:
60
+ - is
61
+ - with
62
+ - avg
63
+ - min
64
+ - max
65
+
66
+ GraphQL/ObjectDescription:
67
+ Enabled: true
68
+ VersionAdded: '0.80'
69
+ Description: 'Ensures all types have a description'
70
+ Exclude:
71
+ - '**/*_schema.rb'
72
+ - '**/base_*.rb'
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rubocop"
4
+
5
+ require_relative "rubocop/graphql/ext/snake_case"
6
+
7
+ require_relative "rubocop/graphql"
8
+ require_relative "rubocop/graphql/version"
9
+ require_relative "rubocop/graphql/inject"
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"
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"
19
+
20
+ RuboCop::GraphQL::Inject.defaults!
21
+
22
+ require_relative "rubocop/cop/graphql_cops"
@@ -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,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ # Inspied by https://github.com/rubocop-hq/rubocop-rspec/blob/3a2088f79737e2e8e0e21482783f7d61411bf021/lib/rubocop/cop/rspec/cop.rb
5
+ module Cop
6
+ WorkaroundGraphqlCop = Cop.dup
7
+
8
+ # Clone of the the normal RuboCop::Cop::Cop class so we can rewrite
9
+ # the inherited method without breaking functionality
10
+ class WorkaroundGraphqlCop
11
+ # Remove the Cop.inherited method to be a noop. Our RSpec::Cop
12
+ # class will invoke the inherited hook instead
13
+ class << self
14
+ undef inherited
15
+ def inherited(*)
16
+ end
17
+ end
18
+
19
+ # Special case `Module#<` so that the rspec support rubocop exports
20
+ # is compatible with our subclass
21
+ def self.<(other)
22
+ other.equal?(RuboCop::Cop::Cop) || super
23
+ end
24
+ end
25
+ private_constant(:WorkaroundGraphqlCop)
26
+
27
+ module GraphQL
28
+ # @abstract parent class to graphql-ruby cops
29
+ #
30
+ # The criteria for whether rubocop-rspec analyzes a certain ruby file
31
+ # is configured via `AllCops/GraphQL`. For example, if you want to
32
+ # customize your project to scan all files within a `graph/` directory
33
+ # then you could add this to your configuration:
34
+ #
35
+ # @example configuring analyzed paths
36
+ #
37
+ # AllCops:
38
+ # GraphQL:
39
+ # Patterns:
40
+ # - '(?:^|/)graph/'
41
+ class Cop < WorkaroundGraphqlCop
42
+ DEFAULT_CONFIGURATION =
43
+ RuboCop::GraphQL::CONFIG.fetch("AllCops").fetch("GraphQL")
44
+
45
+ DEFAULT_PATTERN_RE = Regexp.union(
46
+ DEFAULT_CONFIGURATION.fetch("Patterns")
47
+ .map(&Regexp.public_method(:new))
48
+ )
49
+
50
+ # Invoke the original inherited hook so our cops are recognized
51
+ def self.inherited(subclass)
52
+ RuboCop::Cop::Cop.inherited(subclass)
53
+ end
54
+
55
+ def relevant_file?(file)
56
+ relevant_rubocop_graphql_file?(file) && super
57
+ end
58
+
59
+ private
60
+
61
+ def relevant_rubocop_graphql_file?(file)
62
+ graphql_pattern =~ file
63
+ end
64
+
65
+ def graphql_pattern
66
+ if rspec_graphql_config?
67
+ Regexp.union(rspec_graphql_config.map(&Regexp.public_method(:new)))
68
+ else
69
+ DEFAULT_PATTERN_RE
70
+ end
71
+ end
72
+
73
+ def all_cops_config
74
+ config
75
+ .for_all_cops
76
+ end
77
+
78
+ def rspec_graphql_config?
79
+ return unless all_cops_config.key?("GraphQL")
80
+
81
+ all_cops_config.fetch("GraphQL").key?("Patterns")
82
+ end
83
+
84
+ def rspec_graphql_config
85
+ all_cops_config
86
+ .fetch("GraphQL", DEFAULT_CONFIGURATION)
87
+ .fetch("Patterns")
88
+ end
89
+ end
90
+ end
91
+ end
92
+ 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