Skip to content

Commit

Permalink
Support inline RBI versioning
Browse files Browse the repository at this point in the history
This commit adds the notion of versioning to the RBI gem.

1. Users can specify versions for parts of their RBI files using
`@version` annotations in comments above any class, module, or method
definition.

2. The `FilterVersions` rewriter will take an RBI file, as well as a
version number, and rewrite the RBI to only include the portions that
are relevant for a specific gem version.

3. RBI without any version annotations will continue to behave as expected.

Co-authored-by: Alexandre Terrasa <[email protected]>
Co-authored-by: Aiden Storey <[email protected]>
  • Loading branch information
3 people committed May 19, 2023
1 parent 7bd776c commit 03c4000
Show file tree
Hide file tree
Showing 3 changed files with 347 additions and 0 deletions.
1 change: 1 addition & 0 deletions lib/rbi.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ class Error < StandardError; end
require "rbi/rewriters/add_sig_templates"
require "rbi/rewriters/annotate"
require "rbi/rewriters/deannotate"
require "rbi/rewriters/filter_versions"
require "rbi/rewriters/merge_trees"
require "rbi/rewriters/nest_singleton_methods"
require "rbi/rewriters/nest_non_public_methods"
Expand Down
62 changes: 62 additions & 0 deletions lib/rbi/rewriters/filter_versions.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# typed: strict
# frozen_string_literal: true

module RBI
module Rewriters
class FilterVersions < Visitor
extend T::Sig

VERSION_PREFIX = "version "

class << self
extend T::Sig

sig { params(tree: Tree, version: Gem::Version).void }
def filter(tree, version)
v = new(version)
v.visit(tree)
end
end

sig { params(version: Gem::Version).void }
def initialize(version)
super()
@version = version
end

sig { override.params(node: T.nilable(Node)).void }
def visit(node)
return unless node

unless node.satisfies_version?(@version)
node.detach
return
end

visit_all(node.nodes.dup) if node.is_a?(Tree)
end
end
end

class Node
sig { params(version: Gem::Version).returns(T::Boolean) }
def satisfies_version?(version)
return true unless is_a?(NodeWithComments)

requirements = version_requirements
requirements.empty? || requirements.any? { |req| req.satisfied_by?(version) }
end
end

class NodeWithComments
sig { returns(T::Array[Gem::Requirement]) }
def version_requirements
annotations.select do |annotation|
annotation.start_with?(Rewriters::FilterVersions::VERSION_PREFIX)
end.map do |annotation|
versions = annotation.delete_prefix(Rewriters::FilterVersions::VERSION_PREFIX).split(/, */)
Gem::Requirement.new(versions)
end
end
end
end
284 changes: 284 additions & 0 deletions test/rbi/rewriters/filter_versions_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,284 @@
# typed: true
# frozen_string_literal: true

require "test_helper"

module RBI
class FilterVersions < Minitest::Test
def test_filter_versions_do_nothing
rbi = <<~RBI
class Foo; end
class Bar; end
class Baz; end
class Buzz; end
RBI

tree = Parser.parse_string(rbi)

Rewriters::FilterVersions.filter(tree, Gem::Version.new("0.4.0"))
assert_equal(<<~RBI, tree.string)
class Foo; end
class Bar; end
class Baz; end
class Buzz; end
RBI
end

def test_filter_versions_less_than
rbi = <<~RBI
# @version < 1.0.0
class Foo; end
# @version < 0.4.0-prerelease
class Bar; end
# @version < 0.4.0
class Baz; end
# @version < 0.3.0
class Buzz; end
RBI

tree = Parser.parse_string(rbi)

Rewriters::FilterVersions.filter(tree, Gem::Version.new("0.4.0"))
assert_equal(<<~RBI, tree.string)
# @version < 1.0.0
class Foo; end
RBI
end

def test_filter_versions_greater_than
rbi = <<~RBI
# @version > 1.0.0
class Foo; end
# @version > 0.4.0-prerelease
class Bar; end
# @version > 0.4.0
class Baz; end
# @version > 0.3.0
class Buzz; end
RBI

tree = Parser.parse_string(rbi)

Rewriters::FilterVersions.filter(tree, Gem::Version.new("0.4.0"))
assert_equal(<<~RBI, tree.string)
# @version > 0.4.0-prerelease
class Bar; end
# @version > 0.3.0
class Buzz; end
RBI
end

def test_filter_versions_equals
rbi = <<~RBI
# @version = 1.0.0
class Foo; end
# @version = 0.4.0-prerelease
class Bar; end
# @version = 0.4.0
class Baz; end
# @version = 0.3.0
class Buzz; end
RBI

tree = Parser.parse_string(rbi)

Rewriters::FilterVersions.filter(tree, Gem::Version.new("0.4.0"))
assert_equal(<<~RBI, tree.string)
# @version = 0.4.0
class Baz; end
RBI
end

def test_filter_versions_greater_than_or_equals
rbi = <<~RBI
# @version >= 1.0.0
class Foo; end
# @version >= 0.4.0-prerelease
class Bar; end
# @version >= 0.4.0
class Baz; end
# @version >= 0.3.0
class Buzz; end
RBI

tree = Parser.parse_string(rbi)

Rewriters::FilterVersions.filter(tree, Gem::Version.new("0.4.0"))
assert_equal(<<~RBI, tree.string)
# @version >= 0.4.0-prerelease
class Bar; end
# @version >= 0.4.0
class Baz; end
# @version >= 0.3.0
class Buzz; end
RBI
end

def test_filter_versions_less_than_or_equals
rbi = <<~RBI
# @version <= 1.0.0
class Foo; end
# @version <= 0.4.0-prerelease
class Bar; end
# @version <= 0.4.0
class Baz; end
# @version <= 0.3.0
class Buzz; end
RBI

tree = Parser.parse_string(rbi)

Rewriters::FilterVersions.filter(tree, Gem::Version.new("0.4.0"))
assert_equal(<<~RBI, tree.string)
# @version <= 1.0.0
class Foo; end
# @version <= 0.4.0
class Baz; end
RBI
end

def test_filter_versions_prerelease
rbi = <<~RBI
# @version <= 1.0.0
class Foo; end
# @version <= 0.4.0-prerelease
class Bar; end
# @version = 0.4.0
class Baz; end
# @version >= 0.4.0-prerelease
class Buzz; end
RBI

tree = Parser.parse_string(rbi)

Rewriters::FilterVersions.filter(tree, Gem::Version.new("0.4.0-prerelease"))
assert_equal(<<~RBI, tree.string)
# @version <= 1.0.0
class Foo; end
# @version <= 0.4.0-prerelease
class Bar; end
# @version >= 0.4.0-prerelease
class Buzz; end
RBI
end

def test_filter_versions_and
rbi = <<~RBI
# @version > 0.3.0, < 1.0.0
class Foo; end
# @version >= 1.1.0, < 2.0.0
class Bar; end
# @version > 0.3.2, <= 0.4.2
class Baz; end
RBI

tree = Parser.parse_string(rbi)

Rewriters::FilterVersions.filter(tree, Gem::Version.new("0.4.0"))
assert_equal(<<~RBI, tree.string)
# @version > 0.3.0, < 1.0.0
class Foo; end
# @version > 0.3.2, <= 0.4.2
class Baz; end
RBI
end

def test_filter_versions_or
rbi = <<~RBI
# @version < 0.3.0
# @version > 1.0.0
class Foo; end
# @version = 0.4.0
# @version > 0.5.0
class Bar; end
# @version < 0.3.2
# @version = 0.4.0-prerelease
# @version > 0.4.0
class Baz; end
RBI

tree = Parser.parse_string(rbi)

Rewriters::FilterVersions.filter(tree, Gem::Version.new("0.4.0"))
assert_equal(<<~RBI, tree.string)
# @version = 0.4.0
# @version > 0.5.0
class Bar; end
RBI
end

def test_filter_versions_andor
rbi = <<~RBI
# @version > 0.3.0, < 1.0.0
# @version > 1.5.0
class Foo; end
# @version >= 0.1.0, < 0.2.3
# @version = 0.4.0
class Bar; end
# @version > 0.2.5, < 0.2.7
# @version > 0.4.0, < 0.5.0
class Baz; end
RBI

tree = Parser.parse_string(rbi)

Rewriters::FilterVersions.filter(tree, Gem::Version.new("0.4.0"))
assert_equal(<<~RBI, tree.string)
# @version > 0.3.0, < 1.0.0
# @version > 1.5.0
class Foo; end
# @version >= 0.1.0, < 0.2.3
# @version = 0.4.0
class Bar; end
RBI
end

def test_filter_versions_parse_errors
rbi = <<~RBI
# @version >
class Foo; end
RBI

tree = Parser.parse_string(rbi)

assert_raises(Gem::Requirement::BadRequirementError) do
Rewriters::FilterVersions.filter(tree, Gem::Version.new("0.4.0"))
end
end
end
end

0 comments on commit 03c4000

Please sign in to comment.