rubocop-graphql 0.3.0

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