Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create a cop to validate gem version annotations in RBI files #199

Merged
merged 1 commit into from
Jul 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions config/rbi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -269,3 +269,13 @@ Style/MixinUsage:
Style/ModuleFunction:
Enabled: true
EnforcedStyle: forbidden

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: false
VersionAdded: 0.8.5
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(/, ?/).map(&:strip)
end
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# 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 < RuboCop::Cop::Base
include GemVersionAnnotationHelper

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

def on_new_investigation
gem_version_annotations.each do |comment|
gem_versions = gem_versions(comment)

if gem_versions.empty?
message = format(MSG, versions: "empty version")
add_offense(comment, message: message)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm assuming this cop will be enabled in rbi-central. Have we considered having the default case not error to make the onboarding easier (both here and in rbi logic)? Or do we want to manually add versions to existing annotations?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh this is only if someone writes # @version but then doesn't actually provide any version numbers! Any code without any comment or a different comment will be left alone :)

break
end

invalid_versions = gem_versions.reject do |version|
valid_version?(version)
end

unless invalid_versions.empty?
message = format(MSG, versions: invalid_versions.map(&:strip).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 unless operator && parts
return false unless VALID_OPERATORS.include?(operator)

Gem::Version.correct?(version)
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/void_checked_tests"
Expand Down
1 change: 1 addition & 0 deletions manual/cops.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,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)
* [Sorbet/VoidCheckedTests](cops_sorbet.md#sorbetvoidcheckedtests)

Expand Down
24 changes: 24 additions & 0 deletions manual/cops_sorbet.md
Original file line number Diff line number Diff line change
Expand Up @@ -892,6 +892,30 @@ 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 | - | -

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
```

## 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,78 @@
# 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 for an empty version annotation" do
expect_offense(<<~RUBY)
# @version
^^^^^^^^^^ Invalid gem version(s) detected: empty version
RUBY
end

it "registers an offense for an annotation with no operator" do
expect_offense(<<~RUBY)
# @version blah
^^^^^^^^^^^^^^^ Invalid gem version(s) detected: blah
RUBY
end

it "registers an offense when gem version is not formatted correctly" do
egiurleo marked this conversation as resolved.
Show resolved Hide resolved
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
Loading