8000 Make String methods return String instances when called on a subclass instance by jeremyevans · Pull Request #3701 · ruby/ruby · GitHub
[go: up one dir, main page]

Skip to content

Conversation

@jeremyevans
Copy link
Contributor

This modifies the following String methods to return String instances
instead of subclass instances:

  • String#*
  • String#capitalize
  • String#center
  • String#chomp
  • String#chop
  • String#delete
  • String#delete_prefix
  • String#delete_suffix
  • String#downcase
  • String#dump
  • String#each/#each_line
  • String#gsub
  • String#ljust
  • String#lstrip
  • String#partition
  • String#reverse
  • String#rjust
  • String#rpartition
  • String#rstrip
  • String#scrub
  • String#slice!
  • String#slice/#[]
  • String#split
  • String#squeeze
  • String#strip
  • String#sub
  • String#succ/#next
  • String#swapcase
  • String#tr
  • String#tr_s
  • String#upcase

This also fixes a bug in String#swapcase where it would return the
receiver instead of a copy of the receiver if the receiver was the
empty string.

Some string methods were left to return subclass instances:

  • String#+@
  • String#-@

Both of these methods will return the receiver (subclass instance)
in some cases, so it is best to keep the returned class consistent.

Fixes [#10845]

@jeremyevans jeremyevans force-pushed the string-methods-return-string-10845 branch from 1f9340c to a94ff5a Compare October 25, 2020 22:58
@marcandre
Copy link
Member

Big PR 💪
Do we know if this breaks in any way Rails (in particular SafeBuffer)?

@jeremyevans
Copy link
Contributor Author

I haven't tested with Rails, but I think it would need some adjusting for this. Rails appears to mark many of these methods as unsafe (they are in UNSAFE_STRING_METHODS), but some methods it appears to assume it will return the same class, such as String#*. Rails tries to work around the existing Ruby behavior by manually copying an instance variable from the receiver to the new object in some cases.

@amatsuda
Copy link
Member

We don't have to worry about Rails that much in this case.

I suppose you're talking about this thing in Rails. https://github.com/rails/rails/blob/cf4fa5ca/activesupport/lib/active_support/core_ext/string/output_safety.rb#L133-L134
This SafeBuffer class already overrides all considerably "dangerous" String methods with its own implementations that return a String instance, e.g.

ActiveSupport::SafeBuffer.new('hello').reverse.class
#=> String

and so this change won't bring that much breakage to SafeBuffer.

In fact I could make all active_support and action_view tests pass on this branch with the following small patch for the SafeBuffer class.

diff --git a/activesupport/lib/active_support/core_ext/string/output_safety.rb b/activesupport/lib/active_support/core_ext/string/output_safety.rb
index 635f9cf457..c04b8baa30 100644
--- a/activesupport/lib/active_support/core_ext/string/output_safety.rb
+++ b/activesupport/lib/active_support/core_ext/string/output_safety.rb
@@ -152,12 +152,12 @@ def initialize
 
     def [](*args)
       if html_safe?
-        new_safe_buffer = super
+        new_string = super
 
-        if new_safe_buffer
-          new_safe_buffer.instance_variable_set :@html_safe, true
-        end
+        return unless new_string
 
+        new_safe_buffer = self.class.new(new_string)
+        new_safe_buffer.instance_variable_set :@html_safe, true
         new_safe_buffer
       else
         to_str[*args]
@@ -213,7 +213,7 @@ def +(other)
     end
 
     def *(*)
-      new_safe_buffer = super
+      new_safe_buffer = self.class.new(super)
       new_safe_buffer.instance_variable_set(:@html_safe, @html_safe)
       new_safe_buffer
     end

… instance

This modifies the following String methods to return String instances
instead of subclass instances:

* String#*
* String#capitalize
* String#center
* String#chomp
* String#chop
* String#delete
* String#delete_prefix
* String#delete_suffix
* String#downcase
* String#dump
* String#each/#each_line
* String#gsub
* String#ljust
* String#lstrip
* String#partition
* String#reverse
* String#rjust
* String#rpartition
* String#rstrip
* String#scrub
* String#slice!
* String#slice/#[]
* String#split
* String#squeeze
* String#strip
* String#sub
* String#succ/#next
* String#swapcase
* String#tr
* String#tr_s
* String#upcase

This also fixes a bug in String#swapcase where it would return the
receiver instead of a copy of the receiver if the receiver was the
empty string.

Some string methods were left to return subclass instances:

* String#+@
* String#-@

Both of these methods will return the receiver (subclass instance)
in some cases, so it is best to keep the returned class consistent.

Fixes [ruby#10845]
@jeremyevans jeremyevans force-pushed the string-methods-return-string-10845 branch from a94ff5a to 2cf092f Compare November 20, 2020 20:19
@jeremyevans jeremyevans merged commit 58325da into ruby:master Nov 21, 2020
amatsuda added a commit to amatsuda/rails that referenced this pull request Nov 21, 2020
… in Ruby 3

Ruby 3 introduces an incompatibility that String methods return String instances when called on a subclass instance.
https://bugs.ruby-lang.org/issues/10845
ruby/ruby#3701
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

0