From abee0343686b476139c476952b1c68f1fba6b3d0 Mon Sep 17 00:00:00 2001
From: lulalala <mark@goodlife.tw>
Date: Sat, 31 Mar 2018 22:42:40 +0800
Subject: Raise deprecation for calling `[:f] = 'b'` or `[:f] << 'b'`

Revert some tests to ensure back compatibility
---
 .../test/template/active_model_helper_test.rb      | 10 ++--
 .../test/template/form_options_helper_test.rb      | 14 ++++-
 activemodel/lib/active_model/errors.rb             | 50 ++++++++++++++++--
 activemodel/test/cases/errors_test.rb              | 60 +++++++++++++++++++---
 .../cases/validations/validations_context_test.rb  |  4 +-
 .../test/cases/validations/with_validation_test.rb |  8 +--
 activemodel/test/cases/validations_test.rb         |  2 +-
 activemodel/test/models/person_with_validator.rb   |  2 +-
 activemodel/test/models/reply.rb                   | 10 ++--
 activemodel/test/validators/email_validator.rb     |  5 +-
 .../has_many_through_associations_test.rb          | 16 +++---
 activerecord/test/models/reply.rb                  | 12 ++---
 guides/source/active_record_validations.md         | 12 ++---
 13 files changed, 155 insertions(+), 50 deletions(-)

diff --git a/actionview/test/template/active_model_helper_test.rb b/actionview/test/template/active_model_helper_test.rb
index 36afed6dd6..c6a3c9064f 100644
--- a/actionview/test/template/active_model_helper_test.rb
+++ b/actionview/test/template/active_model_helper_test.rb
@@ -20,11 +20,11 @@ class ActiveModelHelperTest < ActionView::TestCase
     super
 
     @post = Post.new
-    @post.errors[:author_name] << "can't be empty"
-    @post.errors[:body] << "foo"
-    @post.errors[:category] << "must exist"
-    @post.errors[:published] << "must be accepted"
-    @post.errors[:updated_at] << "bar"
+    assert_deprecated { @post.errors[:author_name] << "can't be empty" }
+    assert_deprecated { @post.errors[:body] << "foo" }
+    assert_deprecated { @post.errors[:category] << "must exist" }
+    assert_deprecated { @post.errors[:published] << "must be accepted" }
+    assert_deprecated { @post.errors[:updated_at] << "bar" }
 
     @post.author_name = ""
     @post.body        = "Back to the hill and over it again!"
diff --git a/actionview/test/template/form_options_helper_test.rb b/actionview/test/template/form_options_helper_test.rb
index 4ccd3ae336..c9c36917d6 100644
--- a/actionview/test/template/form_options_helper_test.rb
+++ b/actionview/test/template/form_options_helper_test.rb
@@ -29,10 +29,20 @@ class FormOptionsHelperTest < ActionView::TestCase
                   end
     Continent   = Struct.new("Continent", :continent_name, :countries)
     Country     = Struct.new("Country", :country_id, :country_name)
-    Firm        = Struct.new("Firm", :time_zone)
     Album       = Struct.new("Album", :id, :title, :genre)
   end
 
+  class Firm
+    include ActiveModel::Validations
+    extend ActiveModel::Naming
+
+    attr_accessor :time_zone
+
+    def initialize(time_zone = nil)
+      @time_zone = time_zone
+    end
+  end
+
   module FakeZones
     FakeZone = Struct.new(:name) do
       def to_s; name; end
@@ -1294,7 +1304,7 @@ class FormOptionsHelperTest < ActionView::TestCase
   def test_time_zone_select_with_priority_zones_and_errors
     @firm = Firm.new("D")
     @firm.extend ActiveModel::Validations
-    @firm.errors[:time_zone] << "invalid"
+    assert_deprecated { @firm.errors[:time_zone] << "invalid" }
     zones = [ ActiveSupport::TimeZone.new("A"), ActiveSupport::TimeZone.new("D") ]
     html = time_zone_select("firm", "time_zone", zones)
     assert_dom_equal "<div class=\"field_with_errors\">" \
diff --git a/activemodel/lib/active_model/errors.rb b/activemodel/lib/active_model/errors.rb
index 7c6346f577..5440988d27 100644
--- a/activemodel/lib/active_model/errors.rb
+++ b/activemodel/lib/active_model/errors.rb
@@ -302,11 +302,11 @@ module ActiveModel
     #   person.errors.to_hash(true) # => {:name=>["name cannot be nil"]}
     def to_hash(full_messages = false)
       hash = {}
-      message_method = full_messages ? :full_message : :message
+      message_method = full_messages ? :full_messages_for : :messages_for
       group_by_attribute.each do |attribute, errors|
-        hash[attribute] = errors.map(&message_method)
+        hash[attribute] = public_send(message_method, attribute)
       end
-      hash
+      DeprecationHandlingMessageHash.new(hash, self)
     end
     alias :messages :to_hash
 
@@ -458,7 +458,7 @@ module ActiveModel
     end
 
     def messages_for(attribute)
-      where(attribute).map(&:message).freeze
+      DeprecationHandlingMessageArray.new(where(attribute).map(&:message).freeze, self, attribute)
     end
 
     # Returns a full message for a given attribute.
@@ -619,6 +619,48 @@ module ActiveModel
       end
   end
 
+  class DeprecationHandlingMessageHash < SimpleDelegator
+    def initialize(content, errors)
+      @errors = errors
+      super(content)
+    end
+
+    def []=(attribute, value)
+      ActiveSupport::Deprecation.warn("Calling `[]=` to an ActiveModel::Errors is deprecated. Please call `ActiveModel::Errors#add` instead.")
+
+      Array(value).each do |message|
+        @errors.add(attribute, message)
+      end
+      __setobj__ @errors.messages
+      value
+    end
+
+    def [](attribute)
+      messages = super(attribute)
+
+      return messages if messages
+
+      __getobj__[attribute]= DeprecationHandlingMessageArray.new([], @errors, attribute)
+    end
+  end
+
+  class DeprecationHandlingMessageArray < SimpleDelegator
+    def initialize(content, errors, attribute)
+      @errors = errors
+      @attribute = attribute
+      super(content)
+    end
+
+    def <<(message)
+      ActiveSupport::Deprecation.warn("Calling `<<` to an ActiveModel::Errors message array in order to add an error is deprecated. Please call `ActiveModel::Errors#add` instead.")
+
+      @errors.add(@attribute, message)
+      __setobj__ @errors[@attribute]
+      self
+    end
+  end
+
+
   # Raised when a validation cannot be corrected by end users and are considered
   # exceptional.
   #
diff --git a/activemodel/test/cases/errors_test.rb b/activemodel/test/cases/errors_test.rb
index 5dbbf910de..513bd163fc 100644
--- a/activemodel/test/cases/errors_test.rb
+++ b/activemodel/test/cases/errors_test.rb
@@ -39,7 +39,7 @@ class ErrorsTest < ActiveModel::TestCase
 
   def test_include?
     errors = ActiveModel::Errors.new(Person.new)
-    errors[:foo] << "omg"
+    assert_deprecated { errors[:foo] << "omg" }
     assert_includes errors, :foo, "errors should include :foo"
     assert_includes errors, "foo", "errors should include 'foo' as :foo"
   end
@@ -93,8 +93,8 @@ class ErrorsTest < ActiveModel::TestCase
 
   test "values returns an array of messages" do
     errors = ActiveModel::Errors.new(Person.new)
-    errors.messages[:foo] = "omg"
-    errors.messages[:baz] = "zomg"
+    assert_deprecated { errors.messages[:foo] = "omg" }
+    assert_deprecated { errors.messages[:baz] = "zomg" }
 
     assert_deprecated do
       assert_equal ["omg", "zomg"], errors.values
@@ -113,11 +113,11 @@ class ErrorsTest < ActiveModel::TestCase
 
   test "keys returns the error keys" do
     errors = ActiveModel::Errors.new(Person.new)
-    errors.add(:name)
-    errors.add(:age)
+    assert_deprecated { errors.messages[:foo] << "omg" }
+    assert_deprecated { errors.messages[:baz] << "zomg" }
 
     assert_deprecated do
-      assert_equal [:name, :age], errors.keys
+      assert_equal [:foo, :baz], errors.keys
     end
   end
 
@@ -153,6 +153,32 @@ class ErrorsTest < ActiveModel::TestCase
     assert_equal ["cannot be nil"], person.errors[:name]
   end
 
+  test "add an error message on a specific attribute (deprecated)" do
+    person = Person.new
+    person.errors.add(:name, "cannot be blank")
+    assert_equal ["cannot be blank"], person.errors[:name]
+  end
+
+  test "add an error message on a specific attribute with a defined type (deprecated)" do
+    person = Person.new
+    person.errors.add(:name, :blank, message: "cannot be blank")
+    assert_equal ["cannot be blank"], person.errors[:name]
+  end
+
+  test "add an error with a symbol (deprecated)" do
+    person = Person.new
+    person.errors.add(:name, :blank)
+    message = person.errors.generate_message(:name, :blank)
+    assert_equal [message], person.errors[:name]
+  end
+
+  test "add an error with a proc (deprecated)" do
+    person = Person.new
+    message = Proc.new { "cannot be blank" }
+    person.errors.add(:name, message)
+    assert_equal ["cannot be blank"], person.errors[:name]
+  end
+
   test "add creates an error object and returns it" do
     person = Person.new
     error = person.errors.add(:name, :blank)
@@ -532,6 +558,16 @@ class ErrorsTest < ActiveModel::TestCase
     assert_empty person.errors.details
   end
 
+  test "copy errors (deprecated)" do
+    errors = ActiveModel::Errors.new(Person.new)
+    errors.add(:name, :invalid)
+    person = Person.new
+    person.errors.copy!(errors)
+
+    assert_equal [:name], person.errors.messages.keys
+    assert_equal [:name], person.errors.details.keys
+  end
+
   test "copy errors" do
     errors = ActiveModel::Errors.new(Person.new)
     errors.add(:name, :invalid)
@@ -544,6 +580,18 @@ class ErrorsTest < ActiveModel::TestCase
     end
   end
 
+  test "merge errors (deprecated)" do
+    errors = ActiveModel::Errors.new(Person.new)
+    errors.add(:name, :invalid)
+
+    person = Person.new
+    person.errors.add(:name, :blank)
+    person.errors.merge!(errors)
+
+    assert_equal({ name: ["can't be blank", "is invalid"] }, person.errors.messages)
+    assert_equal({ name: [{ error: :blank }, { error: :invalid }] }, person.errors.details)
+  end
+
   test "merge errors" do
     errors = ActiveModel::Errors.new(Person.new)
     errors.add(:name, :invalid)
diff --git a/activemodel/test/cases/validations/validations_context_test.rb b/activemodel/test/cases/validations/validations_context_test.rb
index 024eb1882f..3d2dea9828 100644
--- a/activemodel/test/cases/validations/validations_context_test.rb
+++ b/activemodel/test/cases/validations/validations_context_test.rb
@@ -14,13 +14,13 @@ class ValidationsContextTest < ActiveModel::TestCase
 
   class ValidatorThatAddsErrors < ActiveModel::Validator
     def validate(record)
-      record.errors[:base] << ERROR_MESSAGE
+      record.errors.add(:base, ERROR_MESSAGE)
     end
   end
 
   class AnotherValidatorThatAddsErrors < ActiveModel::Validator
     def validate(record)
-      record.errors[:base] << ANOTHER_ERROR_MESSAGE
+      record.errors.add(:base, ANOTHER_ERROR_MESSAGE)
     end
   end
 
diff --git a/activemodel/test/cases/validations/with_validation_test.rb b/activemodel/test/cases/validations/with_validation_test.rb
index 8239792c79..e6ae6603f2 100644
--- a/activemodel/test/cases/validations/with_validation_test.rb
+++ b/activemodel/test/cases/validations/with_validation_test.rb
@@ -14,13 +14,13 @@ class ValidatesWithTest < ActiveModel::TestCase
 
   class ValidatorThatAddsErrors < ActiveModel::Validator
     def validate(record)
-      record.errors[:base] << ERROR_MESSAGE
+      record.errors.add(:base, message: ERROR_MESSAGE)
     end
   end
 
   class OtherValidatorThatAddsErrors < ActiveModel::Validator
     def validate(record)
-      record.errors[:base] << OTHER_ERROR_MESSAGE
+      record.errors.add(:base, message: OTHER_ERROR_MESSAGE)
     end
   end
 
@@ -32,14 +32,14 @@ class ValidatesWithTest < ActiveModel::TestCase
   class ValidatorThatValidatesOptions < ActiveModel::Validator
     def validate(record)
       if options[:field] == :first_name
-        record.errors[:base] << ERROR_MESSAGE
+        record.errors.add(:base, message: ERROR_MESSAGE)
       end
     end
   end
 
   class ValidatorPerEachAttribute < ActiveModel::EachValidator
     def validate_each(record, attribute, value)
-      record.errors[attribute] << "Value is #{value}"
+      record.errors.add(attribute, message: "Value is #{value}")
     end
   end
 
diff --git a/activemodel/test/cases/validations_test.rb b/activemodel/test/cases/validations_test.rb
index 7776233db5..82bb66b550 100644
--- a/activemodel/test/cases/validations_test.rb
+++ b/activemodel/test/cases/validations_test.rb
@@ -74,7 +74,7 @@ class ValidationsTest < ActiveModel::TestCase
 
   def test_errors_on_nested_attributes_expands_name
     t = Topic.new
-    t.errors["replies.name"] << "can't be blank"
+    assert_deprecated { t.errors["replies.name"] << "can't be blank" }
     assert_equal ["Replies name can't be blank"], t.errors.full_messages
   end
 
diff --git a/activemodel/test/models/person_with_validator.rb b/activemodel/test/models/person_with_validator.rb
index 44e78cbc29..fbb28d2a0f 100644
--- a/activemodel/test/models/person_with_validator.rb
+++ b/activemodel/test/models/person_with_validator.rb
@@ -5,7 +5,7 @@ class PersonWithValidator
 
   class PresenceValidator < ActiveModel::EachValidator
     def validate_each(record, attribute, value)
-      record.errors[attribute] << "Local validator#{options[:custom]}" if value.blank?
+      record.errors.add(attribute, message: "Local validator#{options[:custom]}") if value.blank?
     end
   end
 
diff --git a/activemodel/test/models/reply.rb b/activemodel/test/models/reply.rb
index 6bb18f95fe..f340b6fb14 100644
--- a/activemodel/test/models/reply.rb
+++ b/activemodel/test/models/reply.rb
@@ -11,24 +11,24 @@ class Reply < Topic
   validate :check_wrong_update,     on: :update
 
   def check_empty_title
-    errors[:title] << "is Empty" unless title && title.size > 0
+    errors.add(:title, "is Empty") unless title && title.size > 0
   end
 
   def errors_on_empty_content
-    errors[:content] << "is Empty" unless content && content.size > 0
+    errors.add(:content, "is Empty") unless content && content.size > 0
   end
 
   def check_content_mismatch
     if title && content && content == "Mismatch"
-      errors[:title] << "is Content Mismatch"
+      errors.add(:title, "is Content Mismatch")
     end
   end
 
   def title_is_wrong_create
-    errors[:title] << "is Wrong Create" if title && title == "Wrong Create"
+    errors.add(:title, "is Wrong Create") if title && title == "Wrong Create"
   end
 
   def check_wrong_update
-    errors[:title] << "is Wrong Update" if title && title == "Wrong Update"
+    errors.add(:title, "is Wrong Update") if title && title == "Wrong Update"
   end
 end
diff --git a/activemodel/test/validators/email_validator.rb b/activemodel/test/validators/email_validator.rb
index 0c634d8659..774a10b2ba 100644
--- a/activemodel/test/validators/email_validator.rb
+++ b/activemodel/test/validators/email_validator.rb
@@ -2,7 +2,8 @@
 
 class EmailValidator < ActiveModel::EachValidator
   def validate_each(record, attribute, value)
-    record.errors[attribute] << (options[:message] || "is not an email") unless
-      /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i.match?(value)
+    unless /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i.match?(value)
+      record.errors.add(attribute, message: options[:message] || "is not an email")
+    end
   end
 end
diff --git a/activerecord/test/cases/associations/has_many_through_associations_test.rb b/activerecord/test/cases/associations/has_many_through_associations_test.rb
index fa540c9dab..620c89ddca 100644
--- a/activerecord/test/cases/associations/has_many_through_associations_test.rb
+++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb
@@ -746,10 +746,14 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
 
       firm = companies(:first_firm)
       lifo = Developer.new(name: "lifo")
-      assert_raises(ActiveRecord::RecordInvalid) { firm.developers << lifo }
+      assert_raises(ActiveRecord::RecordInvalid) do
+        assert_deprecated { firm.developers << lifo }
+      end
 
       lifo = Developer.create!(name: "lifo")
-      assert_raises(ActiveRecord::RecordInvalid) { firm.developers << lifo }
+      assert_raises(ActiveRecord::RecordInvalid) do
+        assert_deprecated { firm.developers << lifo }
+      end
     end
   end
 
@@ -1160,7 +1164,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
   def test_create_should_not_raise_exception_when_join_record_has_errors
     repair_validations(Categorization) do
       Categorization.validate { |r| r.errors[:base] << "Invalid Categorization" }
-      Category.create(name: "Fishing", authors: [Author.first])
+      assert_deprecated { Category.create(name: "Fishing", authors: [Author.first]) }
     end
   end
 
@@ -1173,7 +1177,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
     repair_validations(Categorization) do
       Categorization.validate { |r| r.errors[:base] << "Invalid Categorization" }
       assert_raises(ActiveRecord::RecordInvalid) do
-        Category.create!(name: "Fishing", authors: [Author.first])
+        assert_deprecated { Category.create!(name: "Fishing", authors: [Author.first]) }
       end
     end
   end
@@ -1183,7 +1187,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
       Categorization.validate { |r| r.errors[:base] << "Invalid Categorization" }
       c = Category.new(name: "Fishing", authors: [Author.first])
       assert_raises(ActiveRecord::RecordInvalid) do
-        c.save!
+        assert_deprecated { c.save! }
       end
     end
   end
@@ -1192,7 +1196,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
     repair_validations(Categorization) do
       Categorization.validate { |r| r.errors[:base] << "Invalid Categorization" }
       c = Category.new(name: "Fishing", authors: [Author.first])
-      assert_not c.save
+      assert_deprecated { assert_not c.save }
     end
   end
 
diff --git a/activerecord/test/models/reply.rb b/activerecord/test/models/reply.rb
index b35623a344..f6ab9c8a8f 100644
--- a/activerecord/test/models/reply.rb
+++ b/activerecord/test/models/reply.rb
@@ -34,29 +34,29 @@ class WrongReply < Reply
   validate :check_author_name_is_secret, on: :special_case
 
   def check_empty_title
-    errors[:title] << "Empty" unless attribute_present?("title")
+    errors.add(:title, "Empty") unless attribute_present?("title")
   end
 
   def errors_on_empty_content
-    errors[:content] << "Empty" unless attribute_present?("content")
+    errors.add(:content, "Empty") unless attribute_present?("content")
   end
 
   def check_content_mismatch
     if attribute_present?("title") && attribute_present?("content") && content == "Mismatch"
-      errors[:title] << "is Content Mismatch"
+      errors.add(:title, "is Content Mismatch")
     end
   end
 
   def title_is_wrong_create
-    errors[:title] << "is Wrong Create" if attribute_present?("title") && title == "Wrong Create"
+    errors.add(:title, "is Wrong Create") if attribute_present?("title") && title == "Wrong Create"
   end
 
   def check_wrong_update
-    errors[:title] << "is Wrong Update" if attribute_present?("title") && title == "Wrong Update"
+    errors.add(:title, "is Wrong Update") if attribute_present?("title") && title == "Wrong Update"
   end
 
   def check_author_name_is_secret
-    errors[:author_name] << "Invalid" unless author_name == "secret"
+    errors.add(:author_name, "Invalid") unless author_name == "secret"
   end
 end
 
diff --git a/guides/source/active_record_validations.md b/guides/source/active_record_validations.md
index e68f34dd5d..f904d4de65 100644
--- a/guides/source/active_record_validations.md
+++ b/guides/source/active_record_validations.md
@@ -664,7 +664,7 @@ This helper passes the record to a separate class for validation.
 class GoodnessValidator < ActiveModel::Validator
   def validate(record)
     if record.first_name == "Evil"
-      record.errors[:base] << "This person is evil"
+      record.errors.add :base, "This person is evil"
     end
   end
 end
@@ -692,7 +692,7 @@ validator class as `options`:
 class GoodnessValidator < ActiveModel::Validator
   def validate(record)
     if options[:fields].any?{|field| record.send(field) == "Evil" }
-      record.errors[:base] << "This person is evil"
+      record.errors.add :base, "This person is evil"
     end
   end
 end
@@ -723,7 +723,7 @@ class GoodnessValidator
 
   def validate
     if some_complex_condition_involving_ivars_and_private_methods?
-      @person.errors[:base] << "This person is evil"
+      @person.errors.add :base, "This person is evil"
     end
   end
 
@@ -1004,7 +1004,7 @@ and performs the validation on it. The custom validator is called using the
 class MyValidator < ActiveModel::Validator
   def validate(record)
     unless record.name.starts_with? 'X'
-      record.errors[:name] << 'Need a name starting with X please!'
+      record.errors.add :name, "Need a name starting with X please!"
     end
   end
 end
@@ -1026,7 +1026,7 @@ instance.
 class EmailValidator < ActiveModel::EachValidator
   def validate_each(record, attribute, value)
     unless value =~ /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i
-      record.errors[attribute] << (options[:message] || "is not an email")
+      record.errors.add attribute, (options[:message] || "is not an email")
     end
   end
 end
@@ -1203,7 +1203,7 @@ You can add error messages that are related to the object's state as a whole, in
 ```ruby
 class Person < ApplicationRecord
   def a_method_used_for_validation_purposes
-    errors[:base] << "This person is invalid because ..."
+    errors.add :base, "This person is invalid because ..."
   end
 end
 ```
-- 
cgit v1.2.3