diff --git a/CHANGELOG.md b/CHANGELOG.md index d4ef199..0ba30ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## master +- [PR#34](https://github.com/DmitryTsepelev/rubocop-graphql/pull/34) Add OrderedArguments cop ([@kiskoza][]) - [PR#33](https://github.com/DmitryTsepelev/rubocop-graphql/pull/33) Add autocorrect for OrderedFields cop ([@kiskoza][]) ## 0.6.2 (2021-03-03) diff --git a/lib/rubocop/cop/graphql/ordered_arguments.rb b/lib/rubocop/cop/graphql/ordered_arguments.rb new file mode 100644 index 0000000..a20bfb1 --- /dev/null +++ b/lib/rubocop/cop/graphql/ordered_arguments.rb @@ -0,0 +1,120 @@ +# frozen_string_literal: true + +module RuboCop + module Cop + module GraphQL + # Arguments should be alphabetically sorted within groups. + # + # @example + # # good + # + # class UpdateProfile < BaseMutation + # argument :email, String, required: false + # argument :name, String, required: false + # end + # + # # good + # + # class UpdateProfile < BaseMutation + # argument :uuid, ID, required: true + # + # argument :email, String, required: false + # argument :name, String, required: false + # end + # + # # good + # + # class UserType < BaseType + # field :posts, PostType do + # argument :created_after, ISO8601DateTime, required: false + # argument :created_before, ISO8601DateTime, required: false + # end + # end + # + # # bad + # + # class UpdateProfile < BaseMutation + # argument :uuid, ID, required: true + # argument :name, String, required: false + # argument :email, String, required: false + # end + # + # # bad + # + # class UserType < BaseType + # field :posts, PostType do + # argument :created_before, ISO8601DateTime, required: false + # argument :created_after, ISO8601DateTime, required: false + # end + # end + # + class OrderedArguments < Cop + MSG = "Arguments should be sorted in an alphabetical order within their section. " \ + "Field `%s` should appear before `%s`." + + def investigate(processed_source) + return if processed_source.blank? + + argument_declarations(processed_source.ast) + .each_cons(2) do |previous, current| + next unless consecutive_lines(previous, current) + next if argument_name(current) > argument_name(previous) + + register_offense(previous, current) + end + end + + def autocorrect(node) + declarations = argument_declarations(processed_source.ast) + node_index = declarations.map(&:location).find_index(node.location) + previous_declaration = declarations.to_a[node_index - 1] + + current_range = declaration(node) + previous_range = declaration(previous_declaration) + + lambda do |corrector| + swap_range(corrector, current_range, previous_range) + end + end + + private + + def declaration(node) + buffer = processed_source.buffer + begin_pos = node.source_range.begin_pos + end_line = buffer.line_for_position(node.loc.expression.end_pos) + end_pos = buffer.line_range(end_line).end_pos + Parser::Source::Range.new(buffer, begin_pos, end_pos) + end + + def swap_range(corrector, range1, range2) + src1 = range1.source + src2 = range2.source + corrector.replace(range1, src2) + corrector.replace(range2, src1) + end + + def register_offense(previous, current) + message = format( + self.class::MSG, + previous: argument_name(previous), + current: argument_name(current) + ) + add_offense(current, message: message) + end + + def argument_name(node) + node.first_argument.value.to_s + end + + def consecutive_lines(previous, current) + previous.source_range.last_line == current.source_range.first_line - 1 + end + + def_node_search :argument_declarations, <<~PATTERN + (send nil? :argument (:sym _) ...) + PATTERN + end + end + end +end diff --git a/lib/rubocop/cop/graphql_cops.rb b/lib/rubocop/cop/graphql_cops.rb index edc7ad2..649145d 100644 --- a/lib/rubocop/cop/graphql_cops.rb +++ b/lib/rubocop/cop/graphql_cops.rb @@ -13,4 +13,5 @@ require_relative "graphql/field_name" require_relative "graphql/resolver_method_length" require_relative "graphql/object_description" +require_relative "graphql/ordered_arguments" require_relative "graphql/ordered_fields" diff --git a/spec/rubocop/cop/graphql/ordered_arguments_spec.rb b/spec/rubocop/cop/graphql/ordered_arguments_spec.rb new file mode 100644 index 0000000..280a6c2 --- /dev/null +++ b/spec/rubocop/cop/graphql/ordered_arguments_spec.rb @@ -0,0 +1,104 @@ +# frozen_string_literal: true + +RSpec.describe RuboCop::Cop::GraphQL::OrderedArguments do + subject(:cop) { described_class.new(config) } + + let(:config) { RuboCop::Config.new } + + context "when arguments are alphabetically sorted" do + it "not registers an offense" do + expect_no_offenses(<<~RUBY) + class UpdateProfile < BaseMutation + argument :email, String, required: false + argument :name, String, required: false + end + RUBY + end + end + + context "when each individual groups are alphabetically sorted" do + it "not registers an offense" do + expect_no_offenses(<<~RUBY) + class UpdateProfile < BaseMutation + argument :uuid, ID, required: true + + argument :email, String, required: false + argument :name, String, required: false + end + RUBY + end + end + + context "when arguments are alphabetically sorted inside a field declaration" do + it "not registers an offense" do + expect_no_offenses(<<~RUBY) + class UserType < BaseType + field :posts, PostType do + argument :created_after, ISO8601DateTime, required: false + argument :created_before, ISO8601DateTime, required: false + end + end + RUBY + end + end + + context "when arguments are not alphabetically sorted" do + it "registers an offense" do + expect_offense(<<~RUBY) + class UpdateProfile < BaseMutation + argument :uuid, ID, required: true + argument :email, String, required: false + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Arguments should be sorted in an alphabetical order within their section. Field `email` should appear before `uuid`. + argument :name, String, required: false + end + RUBY + + expect_correction(<<~RUBY) + class UpdateProfile < BaseMutation + argument :email, String, required: false + argument :name, String, required: false + argument :uuid, ID, required: true + end + RUBY + end + end + + context "when arguments are not alphabetically sorted inside a field declaration" do + it "not registers an offense" do + expect_offense(<<~RUBY) + class UserType < BaseType + field :posts, PostType do + argument :created_before, ISO8601DateTime, required: false + argument :created_after, ISO8601DateTime, required: false + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Arguments should be sorted in an alphabetical order within their section. Field `created_after` should appear before `created_before`. + end + end + RUBY + end + end + + context "when an unordered argument declaration takes several lines" do + it "registers an offense" do + expect_offense(<<~RUBY) + class UpdateProfile < BaseMutation + argument :uuid, + ID, + required: true + argument :email, String, required: false + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Arguments should be sorted in an alphabetical order within their section. Field `email` should appear before `uuid`. + argument :name, String, required: false + end + RUBY + + expect_correction(<<~RUBY) + class UpdateProfile < BaseMutation + argument :email, String, required: false + argument :name, String, required: false + argument :uuid, + ID, + required: true + end + RUBY + end + end +end