From cfc8067c0865fe3eea46d6b29d12ddde15bad52b Mon Sep 17 00:00:00 2001 From: Luke Duncalfe <lduncalfe@eml.cc> Date: Mon, 6 May 2024 13:15:58 +1200 Subject: [PATCH] Add cop to ban using GraphQL ID type for IIDs This enforces some of the guidelines added around use of `ID` type added in https://gitlab.com/gitlab-org/gitlab/-/merge_requests/152002. --- .../mutations/design_management/base.rb | 2 +- .../resolvers/ci/project_pipeline_resolver.rb | 2 +- app/graphql/resolvers/deployment_resolver.rb | 2 +- app/graphql/types/milestone_type.rb | 2 +- ee/app/graphql/mutations/epics/base.rb | 2 +- ee/app/graphql/resolvers/epics_resolver.rb | 2 +- .../graphql/resolvers/iterations_resolver.rb | 3 +- .../requirements_resolver.rb | 2 +- rubocop/cop/graphql/id_type.rb | 27 +++++++--- spec/rubocop/cop/graphql/id_type_spec.rb | 54 ++++++++++++++++++- 10 files changed, 82 insertions(+), 16 deletions(-) diff --git a/app/graphql/mutations/design_management/base.rb b/app/graphql/mutations/design_management/base.rb index 56889166f4425..8cda2e6db3c00 100644 --- a/app/graphql/mutations/design_management/base.rb +++ b/app/graphql/mutations/design_management/base.rb @@ -9,7 +9,7 @@ class Base < ::Mutations::BaseMutation required: true, description: "Project where the issue is to upload designs for." - argument :iid, GraphQL::Types::ID, + argument :iid, GraphQL::Types::ID, # rubocop:disable Graphql/IDType -- Legacy argument using ID type kept for backwards compatibility required: true, description: "IID of the issue to modify designs for." diff --git a/app/graphql/resolvers/ci/project_pipeline_resolver.rb b/app/graphql/resolvers/ci/project_pipeline_resolver.rb index e60118a053e9b..6b50de3259e87 100644 --- a/app/graphql/resolvers/ci/project_pipeline_resolver.rb +++ b/app/graphql/resolvers/ci/project_pipeline_resolver.rb @@ -14,7 +14,7 @@ class ProjectPipelineResolver < BaseResolver description: 'Global ID of the Pipeline. For example, "gid://gitlab/Ci::Pipeline/314".', prepare: ->(pipeline_id, _ctx) { pipeline_id.model_id } - argument :iid, GraphQL::Types::ID, + argument :iid, GraphQL::Types::ID, # rubocop:disable Graphql/IDType -- Legacy argument using ID type kept for backwards compatibility required: false, description: 'IID of the Pipeline. For example, "1".' diff --git a/app/graphql/resolvers/deployment_resolver.rb b/app/graphql/resolvers/deployment_resolver.rb index d0101c26c0da5..9798bc43c6832 100644 --- a/app/graphql/resolvers/deployment_resolver.rb +++ b/app/graphql/resolvers/deployment_resolver.rb @@ -2,7 +2,7 @@ module Resolvers class DeploymentResolver < BaseResolver - argument :iid, + argument :iid, # rubocop:disable Graphql/IDType -- Legacy argument using ID type kept for backwards compatibility GraphQL::Types::ID, required: true, description: 'Project-level internal ID of the Deployment.' diff --git a/app/graphql/types/milestone_type.rb b/app/graphql/types/milestone_type.rb index e8b17a7470a48..36c7ee286e6a9 100644 --- a/app/graphql/types/milestone_type.rb +++ b/app/graphql/types/milestone_type.rb @@ -14,7 +14,7 @@ class MilestoneType < BaseObject field :id, GraphQL::Types::ID, null: false, description: 'ID of the milestone.' - field :iid, GraphQL::Types::ID, null: false, + field :iid, GraphQL::Types::ID, null: false, # rubocop:disable Graphql/IDType -- Legacy argument using ID type kept for backwards compatibility description: "Internal ID of the milestone." field :title, GraphQL::Types::String, null: false, diff --git a/ee/app/graphql/mutations/epics/base.rb b/ee/app/graphql/mutations/epics/base.rb index 467e457c89d74..0a3dce4deb870 100644 --- a/ee/app/graphql/mutations/epics/base.rb +++ b/ee/app/graphql/mutations/epics/base.rb @@ -5,7 +5,7 @@ module Epics class Base < ::Mutations::BaseMutation include Mutations::ResolvesIssuable - argument :iid, GraphQL::Types::ID, + argument :iid, GraphQL::Types::ID, # rubocop:disable Graphql/IDType -- Legacy argument using ID type kept for backwards compatibility required: true, description: "IID of the epic to mutate." diff --git a/ee/app/graphql/resolvers/epics_resolver.rb b/ee/app/graphql/resolvers/epics_resolver.rb index 8e871286fa3f0..ab48d012ecd4e 100644 --- a/ee/app/graphql/resolvers/epics_resolver.rb +++ b/ee/app/graphql/resolvers/epics_resolver.rb @@ -6,7 +6,7 @@ class EpicsResolver < BaseResolver include SearchArguments include LooksAhead - argument :iid, GraphQL::Types::ID, + argument :iid, GraphQL::Types::ID, # rubocop:disable Graphql/IDType -- Legacy argument using ID type kept for backwards compatibility required: false, description: 'IID of the epic, e.g., "1".' diff --git a/ee/app/graphql/resolvers/iterations_resolver.rb b/ee/app/graphql/resolvers/iterations_resolver.rb index b52f72f858d29..c4e7a20105aa9 100644 --- a/ee/app/graphql/resolvers/iterations_resolver.rb +++ b/ee/app/graphql/resolvers/iterations_resolver.rb @@ -29,9 +29,10 @@ class IterationsResolver < BaseResolver description: 'Global ID of the Iteration to look up.' # rubocop:enable Graphql/IDType - argument :iid, GraphQL::Types::ID, + argument :iid, GraphQL::Types::ID, # rubocop:disable Graphql/IDType -- Legacy argument using ID type kept for backwards compatibility required: false, description: 'Internal ID of the Iteration to look up.' + argument :include_ancestors, GraphQL::Types::Boolean, required: false, description: 'Whether to include ancestor iterations. Defaults to true.' diff --git a/ee/app/graphql/resolvers/requirements_management/requirements_resolver.rb b/ee/app/graphql/resolvers/requirements_management/requirements_resolver.rb index 288951ae14d5d..80fc959315f96 100644 --- a/ee/app/graphql/resolvers/requirements_management/requirements_resolver.rb +++ b/ee/app/graphql/resolvers/requirements_management/requirements_resolver.rb @@ -8,7 +8,7 @@ class RequirementsResolver < BaseResolver type ::Types::RequirementsManagement::RequirementType.connection_type, null: true - argument :iid, GraphQL::Types::ID, + argument :iid, GraphQL::Types::ID, # rubocop:disable Graphql/IDType -- Legacy argument using ID type kept for backwards compatibility required: false, deprecated: { reason: 'Use work_item_iid instead', milestone: '15.8' }, description: 'IID of the requirement, for example, "1".' diff --git a/rubocop/cop/graphql/id_type.rb b/rubocop/cop/graphql/id_type.rb index 430f69ff67c08..2a1cbdca6c8cf 100644 --- a/rubocop/cop/graphql/id_type.rb +++ b/rubocop/cop/graphql/id_type.rb @@ -4,27 +4,40 @@ module RuboCop module Cop module Graphql class IDType < RuboCop::Cop::Base - MSG = 'Do not use GraphQL::Types::ID, use a specific GlobalIDType instead' + MSG_USE_STRING_FOR_IID = 'Do not use GraphQL::Types::ID for IIDs, use GraphQL::Types::String instead' + MSG_USE_GLOBAL_ID = 'Do not use GraphQL::Types::ID, use a specific GlobalIDType instead' ALLOWLISTED_ARGUMENTS = %i[ - iid full_path project_path group_path target_project_path target_group_path target_path namespace_path + full_path project_path group_path target_project_path target_group_path target_path namespace_path context_namespace_path ].freeze - def_node_search :graphql_id_type?, <<~PATTERN + def_node_matcher :iid_with_id?, <<~PATTERN + (send nil? {:field :argument} + (sym #iid?) + (const (const (const nil? :GraphQL) :Types) :ID) + (...)?) + PATTERN + + def_node_search :graphql_id_allowed?, <<~PATTERN (send nil? :argument (_ #does_not_match?) (const (const (const nil? :GraphQL) :Types) :ID) ...) PATTERN def on_send(node) - return unless graphql_id_type?(node) + return add_offense(node, message: MSG_USE_STRING_FOR_IID) if iid_with_id?(node) + return unless graphql_id_allowed?(node) - add_offense(node) + add_offense(node, message: MSG_USE_GLOBAL_ID) end private - def does_not_match?(arg) - !ALLOWLISTED_ARGUMENTS.include?(arg) # rubocop:disable Rails/NegateInclude + def does_not_match?(arg_name) + ALLOWLISTED_ARGUMENTS.exclude?(arg_name) + end + + def iid?(arg_name) + arg_name == :iid || arg_name.end_with?('_iid') end end end diff --git a/spec/rubocop/cop/graphql/id_type_spec.rb b/spec/rubocop/cop/graphql/id_type_spec.rb index 6eb4890c06447..4f35c0594bb30 100644 --- a/spec/rubocop/cop/graphql/id_type_spec.rb +++ b/spec/rubocop/cop/graphql/id_type_spec.rb @@ -4,7 +4,7 @@ require_relative '../../../../rubocop/cop/graphql/id_type' -RSpec.describe RuboCop::Cop::Graphql::IDType do +RSpec.describe RuboCop::Cop::Graphql::IDType, feature_category: :api do it 'adds an offense when GraphQL::Types::ID is used as a param to #argument' do expect_offense(<<~TYPE) argument :some_arg, GraphQL::Types::ID, some: other, params: do_not_matter @@ -27,4 +27,56 @@ argument :some_arg, ::Types::GlobalIDType[::Awardable], some: other, params: do_not_matter TYPE end + + it 'adds an offense when GraphQL::Types::ID is used with an IID field' do + expect_offense(<<~TYPE) + field :iid, GraphQL::Types::ID, some: other, params: do_not_matter + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Do not use GraphQL::Types::ID for IIDs, use GraphQL::Types::String instead + TYPE + end + + it 'adds an offense when GraphQL::Types::ID is used with an IID-like field' do + expect_offense(<<~TYPE) + field :issue_iid, GraphQL::Types::ID, some: other, params: do_not_matter + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Do not use GraphQL::Types::ID for IIDs, use GraphQL::Types::String instead + TYPE + end + + it 'adds an offense when GraphQL::Types::ID is used with an IID argument' do + expect_offense(<<~TYPE) + argument :iid, GraphQL::Types::ID, some: other, params: do_not_matter + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Do not use GraphQL::Types::ID for IIDs, use GraphQL::Types::String instead + TYPE + end + + it 'adds an offense when GraphQL::Types::ID is used with an IID-like argument' do + expect_offense(<<~TYPE) + argument :merge_request_iid, GraphQL::Types::ID, some: other, params: do_not_matter + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Do not use GraphQL::Types::ID for IIDs, use GraphQL::Types::String instead + TYPE + end + + it 'does not add an offense when GraphQL::Types::String is used with an IID field' do + expect_no_offenses(<<~TYPE) + field :iid, GraphQL::Types::String, some: other, params: do_not_matter + TYPE + end + + it 'does not add an offense when GraphQL::Types::String is used with an IID-like field' do + expect_no_offenses(<<~TYPE) + field :issue_iid, GraphQL::Types::String, some: other, params: do_not_matter + TYPE + end + + it 'does not add an offense when GraphQL::Types::String is used with an IID argument' do + expect_no_offenses(<<~TYPE) + argument :iid, GraphQL::Types::String, some: other, params: do_not_matter + TYPE + end + + it 'does not add an offense when GraphQL::Types::String is used with an IID-like argument' do + expect_no_offenses(<<~TYPE) + argument :merge_request_iid, GraphQL::Types::String, some: other, params: do_not_matter + TYPE + end end -- GitLab