Skip to content

Commit

Permalink
Create a cop to validate gem version annotations in RBI files
Browse files Browse the repository at this point in the history
While adding gem version annotations to RBI files would allow developers
to write more comprehensive RBIs for their gems, it has has the
potential to create RBI files that are cluttered, disorganized, or
incorrect.

This would be the first of a few cops meant to keep versioned RBIs clean
and accurate.

This cop checks that every version included in a "@Version" annotation
fits the format specified by the RBI library.
  • Loading branch information
egiurleo committed Feb 2, 2024
1 parent 5e5e119 commit 436eddc
Show file tree
Hide file tree
Showing 7 changed files with 201 additions and 3 deletions.
18 changes: 15 additions & 3 deletions config/default.yml
Original file line number Diff line number Diff line change
Expand Up @@ -188,12 +188,12 @@ Sorbet/BuggyObsoleteStrictMemoization:
Checks for the a mistaken variant of the "obsolete memoization pattern" that used to be required
for older Sorbet versions in `#typed: strict` files. The mistaken variant would overwrite the ivar with `nil`
on every call, causing the memoized value to be discarded and recomputed on every call.
This cop will correct it to read from the ivar instead of `nil`, which will memoize it correctly.
The result of this correction will be the "obsolete memoization pattern", which can further be corrected by
the `Sorbet/ObsoleteStrictMemoization` cop.
See `Sorbet/ObsoleteStrictMemoization` for more details.
Enabled: true
VersionAdded: '0.7.3'
Expand Down Expand Up @@ -293,3 +293,15 @@ Sorbet/ValidSigil:
- bin/**/*
- db/**/*.rb
- script/**/*

Sorbet/ValidGemVersionAnnotations:
Description: >-
Ensures all gem version annotations in RBI files are correctly formatted per
Ruby's gem version specification guidelines.
See the rubygems.org documentation for more information on how to format gem
versions: https://guides.rubygems.org/patterns/#pessimistic-version-constraint
Enabled: true
VersionAdded: 0.7.7
Include:
- "**/*.rbi"
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# frozen_string_literal: true

module RuboCop
module Cop
module Sorbet
module GemVersionAnnotationHelper
VERSION_PREFIX = "# @version "

def gem_version_annotations
processed_source.comments.select do |comment|
gem_version_annotation?(comment)
end
end

private

def gem_version_annotation?(comment)
comment.text.start_with?(VERSION_PREFIX)
end

def gem_versions(comment)
comment.text.delete_prefix(VERSION_PREFIX).split(", ")
end
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# frozen_string_literal: true

module RuboCop
module Cop
module Sorbet
# Checks that gem versions in RBI annotations are properly formatted per the Bundler gem specification.
#
# @example
# # bad
# # @version > not a version number
#
# # good
# # @version = 1
#
# # good
# # @version > 1.2.3
#
# # good
# # @version <= 4.3-preview
#
class ValidGemVersionAnnotations < Base
include GemVersionAnnotationHelper

MSG = "Invalid gem version(s) detected: %<versions>s"
VALID_OPERATORS = ["=", "!=", ">", ">=", "<", "<=", "~>"]

def on_new_investigation
gem_version_annotations.each do |comment|
invalid_versions = gem_versions(comment).select do |version|
!valid_version?(version)
end

unless invalid_versions.empty?
message = format(MSG, versions: invalid_versions.join(", "))
add_offense(comment, message: message)
end
end
end

private

def valid_version?(version_string)
parts = version_string.strip.split(" ")
operator, version = parts

return false if operator.nil? || parts.nil?

return false unless VALID_OPERATORS.include?(operator)

begin
Gem::Version.new(version)
rescue ArgumentError
return false
end

true
end
end
end
end
end
3 changes: 3 additions & 0 deletions lib/rubocop/cop/sorbet_cops.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@
require_relative "sorbet/rbi/forbid_rbi_outside_of_allowed_paths"
require_relative "sorbet/rbi/single_line_rbi_class_module_definitions"

require_relative "sorbet/rbi_versioning/gem_version_annotation_helper"
require_relative "sorbet/rbi_versioning/valid_gem_version_annotations"

require_relative "sorbet/signatures/allow_incompatible_override"
require_relative "sorbet/signatures/checked_true_in_signature"
require_relative "sorbet/signatures/keyword_argument_ordering"
Expand Down
1 change: 1 addition & 0 deletions manual/cops.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ In the following section you find all available cops:
* [Sorbet/StrongSigil](cops_sorbet.md#sorbetstrongsigil)
* [Sorbet/TrueSigil](cops_sorbet.md#sorbettruesigil)
* [Sorbet/TypeAliasName](cops_sorbet.md#sorbettypealiasname)
* [Sorbet/ValidGemVersionAnnotations](cops_sorbet.md#sorbetvalidgemversionannotations)
* [Sorbet/ValidSigil](cops_sorbet.md#sorbetvalidsigil)

<!-- END_COP_LIST -->
Expand Down
30 changes: 30 additions & 0 deletions manual/cops_sorbet.md
Original file line number Diff line number Diff line change
Expand Up @@ -855,6 +855,36 @@ Name | Default value | Configurable values
Include | `**/*.{rb,rbi,rake,ru}` | Array
Exclude | `bin/**/*`, `db/**/*.rb`, `script/**/*` | Array
## Sorbet/ValidGemVersionAnnotations
Enabled by default | Safe | Supports autocorrection | VersionAdded | VersionChanged
--- | --- | --- | --- | ---
Enabled | Yes | No | 0.7.7 | -
Checks that gem versions in RBI annotations are properly formatted per the Bundler gem specification.
### Examples
```ruby
# bad
# @version > not a version number
# good
# @version = 1
# good
# @version > 1.2.3
# good
# @version <= 4.3-preview
```
### Configurable attributes
Name | Default value | Configurable values
--- | --- | ---
Include | `**/*.rbi` | Array
## Sorbet/ValidSigil
Enabled by default | Safe | Supports autocorrection | VersionAdded | VersionChanged
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# frozen_string_literal: true

RSpec.describe(RuboCop::Cop::Sorbet::ValidGemVersionAnnotations, :config) do
it "does not register an offense when comment is not a version annotation" do
expect_no_offenses(<<~RUBY)
# a random comment
RUBY
end

it "does not register an offense when comment is a valid version annotation" do
expect_no_offenses(<<~RUBY)
# @version = 1.3.4-prerelease
RUBY
end

it "does not register an offense when comment uses AND version annotations" do
expect_no_offenses(<<~RUBY)
# @version > 1, < 3.5
RUBY
end

it "does not register an offense when comment uses OR version annotations" do
expect_no_offenses(<<~RUBY)
# @version > 1.3.6
# @version <= 4
RUBY
end

it "registers an offense when gem version is not formatted correctly" do
expect_offense(<<~RUBY)
# @version = blah
^^^^^^^^^^^^^^^^^ Invalid gem version(s) detected: = blah
RUBY
end

it "registers an offense when one gem version out of the list is not formatted correctly" do
expect_offense(<<~RUBY)
# @version < 3.2, > 4, ~> five
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Invalid gem version(s) detected: ~> five
RUBY
end

it "registers an offense when one gem version is not formatted correctly in an OR" do
expect_offense(<<~RUBY)
# @version < 3.2, > 4
# @version ~> five
^^^^^^^^^^^^^^^^^^ Invalid gem version(s) detected: ~> five
RUBY
end

it "registers an offense for multiple incorrectly formatted versions" do
expect_offense(<<~RUBY)
# @version < 3.2, ~> five, = blah
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Invalid gem version(s) detected: ~> five, = blah
RUBY
end

it "registers an offense if operator is invalid" do
expect_offense(<<~RUBY)
# @version << 3.2
^^^^^^^^^^^^^^^^^ Invalid gem version(s) detected: << 3.2
RUBY
end
end

0 comments on commit 436eddc

Please sign in to comment.