RuboCop rule development guidelines

Our codebase style is defined and enforced by RuboCop.

You can check for any offenses locally with bundle exec rubocop --parallel. On the CI, this is automatically checked by the static-analysis jobs.

In addition, you can integrate RuboCop into supported IDEs using the Solargraph gem.

For RuboCop rules that we have not taken a decision on, follow the Ruby style guide to write idiomatic Ruby.

Reviewers/maintainers should be tolerant and not too pedantic about style.

Some RuboCop rules are disabled, and for those, reviewers/maintainers must not ask authors to use one style or the other, as both are accepted. This isn’t an ideal situation because this leaves space for bike-shedding. Ideally we should enable all RuboCop rules to avoid style-related discussions, nitpicking, or back-and-forth in reviews. The GitLab Ruby style guide includes a non-exhaustive list of styles that commonly come up in reviews and are not enforced.

Additionally, we have dedicated test-specific style guides and best practices.

Disabling rules inline

By default, RuboCop rules should not be disabled inline, because it negates agreed-upon code standards that the rule is attempting to apply to the codebase.

If you must use inline disable provide the reason as a code comment in the same line where the rule is disabled.

More context can go into code comments above this inline disable comment. To reduce verbose code comments link a resource (issue, epic, …) to provide detailed context.

For temporary inline disables use rubocop:todo and link the follow-up issue or epic.

For example:

# bad
module Types
  module Domain
    # rubocop:disable Graphql/AuthorizeTypes
    class SomeType < BaseObject
      if condition # rubocop:disable Style/GuardClause
        # more logic...
      end

      object.public_send(action) # rubocop:disable GitlabSecurity/PublicSend
    end
    # rubocop:enable Graphql/AuthorizeTypes
  end
end

# good
module Types
  module Domain
    # rubocop:disable Graphql/AuthorizeTypes -- already authroized in parent entity
    class SomeType < BaseObject
      if condition # rubocop:todo Style/GuardClause -- Cleanup via https://gitlab.com/gitlab-org/gitlab/-/issues/1234567890
        # more logic...
      end

      # At this point `action` is safe to be used in `public_send`.
      # See https://gitlab.com/gitlab-org/gitlab/-/issues/123457890.
      object.public_send(action) # rubocop:disable GitlabSecurity/PublicSend -- User input verified
    end
    # rubocop:enable Graphql/AuthorizeTypes
  end
end

Creating new RuboCop cops

Typically it is better for the linting rules to be enforced programmatically as it reduces the aforementioned bike-shedding.

To that end, we encourage creation of new RuboCop rules in the codebase.

Before adding a new cop to enforce a given style, make sure to discuss it with your team.

We maintain cops across several Ruby code bases, and not all of them are specific to the GitLab application. When creating a new cop that could be applied to multiple applications, we encourage you to add it to our gitlab-styles gem. If the cop targets rules that only apply to the main GitLab application, it should be added to GitLab instead.

Cop grace period

A cop is in a grace period if it is enabled and has Details: grace period defined in its TODO YAML configuration.

On the default branch, offenses from cops in the grace period do not fail the RuboCop CI job. Instead, the job notifies the #f_rubocop Slack channel. However, on other branches, the RuboCop job fails.

A grace period can safely be lifted as soon as there are no warnings for 1 week in the #f_rubocop channel on Slack.

Enabling a new cop

  1. Enable the new cop in .rubocop.yml (if not already done via gitlab-styles).
  2. Generate TODOs for the new cop.
  3. Set the new cop to grace period.
  4. Create an issue to fix TODOs and encourage community contributions (via ~"quick win" and/or ~"Seeking community contributions"). See some examples.
  5. Create an issue to remove grace period after 1 week of silence in the #f_rubocop Slack channel. See an example.

Silenced offenses

When offenses are silenced for cops in the grace period, the #f_rubocop Slack channel receives a notification message every 2 hours.

To fix this issue:

  1. Find cops with silenced offenses in the linked CI job.
  2. Generate TODOs for these cops.

RuboCop node pattern

When creating node patterns to match Ruby’s AST, you can use scripts/rubocop-parse. This displays the AST of a Ruby expression to help you create the matcher. See also !97024.

Resolving RuboCop exceptions

When the number of RuboCop exceptions exceeds the default exclude-limit of 15, we may want to resolve exceptions over multiple commits. To minimize confusion, we should track our progress through the exception list.

The preferred way to generate the initial list or a list for specific RuboCop rules is to run the Rake task rubocop:todo:generate:

# Initial list
bundle exec rake rubocop:todo:generate

# List for specific RuboCop rules
bundle exec rake 'rubocop:todo:generate[Gitlab/NamespacedClass,Lint/Syntax]'

This Rake task creates or updates the exception list in .rubocop_todo/. For example, the configuration for the RuboCop rule Gitlab/NamespacedClass is located in .rubocop_todo/gitlab/namespaced_class.yml.

Make sure to commit any changes in .rubocop_todo/ after running the Rake task.

Reveal existing RuboCop exceptions

To reveal existing RuboCop exceptions in the code that have been excluded via .rubocop_todo.yml and .rubocop_todo/**/*.yml, set the environment variable REVEAL_RUBOCOP_TODO to 1.

This allows you to reveal existing RuboCop exceptions during your daily work cycle and fix them along the way.

note
Define permanent Excludes in .rubocop.yml instead of .rubocop_todo/**/*.yml.