From a157ab370fd33d501500f3858d4f5aa601d133cd Mon Sep 17 00:00:00 2001 From: Stan Lo Date: Mon, 12 Oct 2026 20:24:05 +0100 Subject: [PATCH] Expose RBS syntax and strictness helpers for rbs_rewriter --- lib/spoom/sorbet/sigils.rb | 15 ++++++++ .../translate/rbs_comments_to_sorbet_sigs.rb | 37 ++++++++++++------- rbi/spoom.rbi | 8 +++- test/spoom/sorbet/sigils_test.rb | 23 ++++++++++++ .../rbs_comments_to_sorbet_sigs_test.rb | 8 ++++ 5 files changed, 75 insertions(+), 16 deletions(-) diff --git a/lib/spoom/sorbet/sigils.rb b/lib/spoom/sorbet/sigils.rb index 74c2f06c..65697b73 100644 --- a/lib/spoom/sorbet/sigils.rb +++ b/lib/spoom/sorbet/sigils.rb @@ -51,6 +51,21 @@ def contains_valid_sigil?(content) !!strictness && valid_strictness?(strictness) end + # returns true if the passed content contains a valid sigil at or above the passed strictness + # internal sigils do not satisfy a minimum strictness + #: (String content, String strictness) -> bool + def satisfies_minimum_strictness?(content, strictness) + return false unless contains_valid_sigil?(content) + + current_strictness = strictness_in_content(content) + strictness = strictness.strip + return false if current_strictness == STRICTNESS_INTERNAL || strictness == STRICTNESS_INTERNAL + + strictness_index = VALID_STRICTNESS.index(current_strictness) + minimum_index = VALID_STRICTNESS.index(strictness) + !!strictness_index && !!minimum_index && strictness_index >= minimum_index + end + # returns a string which is the passed content but with the sigil updated to a new strictness #: (String content, String new_strictness) -> String def update_sigil(content, new_strictness) diff --git a/lib/spoom/sorbet/translate/rbs_comments_to_sorbet_sigs.rb b/lib/spoom/sorbet/translate/rbs_comments_to_sorbet_sigs.rb index c0f99afb..83419109 100644 --- a/lib/spoom/sorbet/translate/rbs_comments_to_sorbet_sigs.rb +++ b/lib/spoom/sorbet/translate/rbs_comments_to_sorbet_sigs.rb @@ -5,20 +5,29 @@ module Spoom module Sorbet module Translate module RBSCommentsToSorbetSigs - RBS_ANNOTATION_MARKERS = [ - "# @abstract", - "# @interface", - "# @sealed", - "# @final", - "# @requires_ancestor:", - "# @override", - "# @overridable", - "# @without_runtime", - ].freeze #: Array[String] - RBS_REWRITE_PATTERN = Regexp.union(["#:", "#|", *RBS_ANNOTATION_MARKERS]).freeze #: Regexp - private_constant :RBS_ANNOTATION_MARKERS - class << self + RBS_REWRITE_PATTERN = Regexp.union([ + "#:", + "#|", + "# @abstract", + "# @interface", + "# @sealed", + "# @final", + "# @requires_ancestor:", + "# @override", + "# @overridable", + "# @without_runtime", + ]).freeze #: Regexp + private_constant :RBS_REWRITE_PATTERN + + # Detects RBS comment markers (`#:`, `#|`, and supported `# @...` annotations). + # Use this check to skip parsing files without those markers. + # It does not validate RBS syntax or check the `typed:` sigil. + #: (String ruby_contents) -> bool + def contains_rbs_syntax?(ruby_contents) + ruby_contents.match?(RBS_REWRITE_PATTERN) + end + #: ( #| String ruby_contents, #| file: String, @@ -34,7 +43,7 @@ def rewrite_if_needed( erase_generic_types: false, force: false ) - return ruby_contents unless ruby_contents.match?(RBS_REWRITE_PATTERN) && + return ruby_contents unless contains_rbs_syntax?(ruby_contents) && (force || Sigils.contains_valid_sigil?(ruby_contents)) options = Options.new( diff --git a/rbi/spoom.rbi b/rbi/spoom.rbi index f760b1a6..b6702ba9 100644 --- a/rbi/spoom.rbi +++ b/rbi/spoom.rbi @@ -3247,6 +3247,9 @@ module Spoom::Sorbet::Sigils sig { params(path: T.any(::Pathname, ::String)).returns(T.nilable(::String)) } def file_strictness(path); end + sig { params(content: ::String, strictness: ::String).returns(T::Boolean) } + def satisfies_minimum_strictness?(content, strictness); end + sig { params(strictness: ::String).returns(::String) } def sigil_string(strictness); end @@ -3353,6 +3356,9 @@ end module Spoom::Sorbet::Translate::RBSCommentsToSorbetSigs class << self + sig { params(ruby_contents: ::String).returns(T::Boolean) } + def contains_rbs_syntax?(ruby_contents); end + sig do params( ruby_contents: ::String, @@ -3611,8 +3617,6 @@ class Spoom::Sorbet::Translate::RBSCommentsToSorbetSigs::Options end Spoom::Sorbet::Translate::RBSCommentsToSorbetSigs::Options::ALLOWED_OVERLOAD_STRATEGIES = T.let(T.unsafe(nil), Array) -Spoom::Sorbet::Translate::RBSCommentsToSorbetSigs::RBS_ANNOTATION_MARKERS = T.let(T.unsafe(nil), Array) -Spoom::Sorbet::Translate::RBSCommentsToSorbetSigs::RBS_REWRITE_PATTERN = T.let(T.unsafe(nil), Regexp) class Spoom::Sorbet::Translate::SorbetAssertionsToRBSComments < ::Spoom::Sorbet::Translate::Translator sig do diff --git a/test/spoom/sorbet/sigils_test.rb b/test/spoom/sorbet/sigils_test.rb index 9f5b99db..74f16c5e 100644 --- a/test/spoom/sorbet/sigils_test.rb +++ b/test/spoom/sorbet/sigils_test.rb @@ -87,10 +87,33 @@ class A; end def test_contains_valid_sigil assert(Sigils.contains_valid_sigil?("# typed: true\nclass A; end\n")) + assert(Sigils.contains_valid_sigil?("# typed: __STDLIB_INTERNAL\n")) refute(Sigils.contains_valid_sigil?("class A; end\n")) refute(Sigils.contains_valid_sigil?("# typed: foo\nclass A; end\n")) end + def test_satisfies_minimum_strictness + ["true", "strict", "strong"].each do |strictness| + assert(Sigils.satisfies_minimum_strictness?("# typed: #{strictness}\n", "true")) + end + + ["ignore", "false", "foo", "__STDLIB_INTERNAL", "true#"].each do |strictness| + refute(Sigils.satisfies_minimum_strictness?("# typed: #{strictness}\n", "true")) + end + + refute(Sigils.satisfies_minimum_strictness?("class A; end\n", "true")) + refute(Sigils.satisfies_minimum_strictness?("# typed: false\n# typed: strict\n", "true")) + refute(Sigils.satisfies_minimum_strictness?("# typed: true\n", "strict")) + assert(Sigils.satisfies_minimum_strictness?("# typed: strict\n", "strict")) + assert(Sigils.satisfies_minimum_strictness?("# typed: strong\n", "strict")) + end + + def test_satisfies_minimum_strictness_with_invalid_minimum + ["", "foo", "__STDLIB_INTERNAL"].each do |minimum_strictness| + refute(Sigils.satisfies_minimum_strictness?("# typed: strong\n", minimum_strictness)) + end + end + def test_update_sigil_to_use_valid_strictness content = <<~STR # typed: ignore diff --git a/test/spoom/sorbet/translate/rbs_comments_to_sorbet_sigs_test.rb b/test/spoom/sorbet/translate/rbs_comments_to_sorbet_sigs_test.rb index 85db456a..2f2ed0f7 100644 --- a/test/spoom/sorbet/translate/rbs_comments_to_sorbet_sigs_test.rb +++ b/test/spoom/sorbet/translate/rbs_comments_to_sorbet_sigs_test.rb @@ -7,6 +7,14 @@ module Spoom module Sorbet module Translate class RBSCommentsToSorbetSigsTest < Minitest::Test + def test_contains_rbs_syntax + assert(RBSCommentsToSorbetSigs.contains_rbs_syntax?("#: -> void\ndef foo; end\n")) + assert(RBSCommentsToSorbetSigs.contains_rbs_syntax?("#| String\n")) + assert(RBSCommentsToSorbetSigs.contains_rbs_syntax?("# @abstract\nclass A; end\n")) + assert(RBSCommentsToSorbetSigs.contains_rbs_syntax?("# typed: false\n#: -> void\ndef foo; end\n")) + refute(RBSCommentsToSorbetSigs.contains_rbs_syntax?("# typed: true\ndef foo; end\n")) + end + def test_translate_to_rbi_empty contents = "" assert_rewrites_rbs(