aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord
diff options
context:
space:
mode:
authorGodfrey Chan <godfreykfc@gmail.com>2014-01-24 18:28:05 -0800
committerGodfrey Chan <godfreykfc@gmail.com>2014-01-29 10:54:51 -0800
commit7e8e91c439c1a877f867cd7ba634f7297ccef04b (patch)
tree4d800773aa5f59b9293c52b3b46c44d27b3a4d5e /activerecord
parent9ed66648b59b160b43c83c349263e8cb97eaa088 (diff)
downloadrails-7e8e91c439c1a877f867cd7ba634f7297ccef04b.tar.gz
rails-7e8e91c439c1a877f867cd7ba634f7297ccef04b.tar.bz2
rails-7e8e91c439c1a877f867cd7ba634f7297ccef04b.zip
`scope` now raises on "dangerous" name conflicts
Similar to dangerous attribute methods, a scope name conflict is dangerous if it conflicts with an existing class method defined within `ActiveRecord::Base` but not its ancestors. See also #13389. *Godfrey Chan*, *Philippe Creux*
Diffstat (limited to 'activerecord')
-rw-r--r--activerecord/CHANGELOG.md10
-rw-r--r--activerecord/lib/active_record/attribute_methods.rb26
-rw-r--r--activerecord/lib/active_record/scoping/named.rb6
-rw-r--r--activerecord/test/cases/scoping/named_scoping_test.rb57
4 files changed, 95 insertions, 4 deletions
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md
index ddf5592a78..aaa0fb07ab 100644
--- a/activerecord/CHANGELOG.md
+++ b/activerecord/CHANGELOG.md
@@ -1,3 +1,13 @@
+* `scope` now raises on "dangerous" name conflicts
+
+ Similar to dangerous attribute methods, a scope name conflict is
+ dangerous if it conflicts with an existing class method defined within
+ `ActiveRecord::Base` but not its ancestors.
+
+ See also #13389.
+
+ *Godfrey Chan*, *Philippe Creux*
+
* Correctly send an user provided statement to a `lock!()` call.
person.lock! 'FOR SHARE NOWAIT'
diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb
index 73761520f7..ccbff8d1ff 100644
--- a/activerecord/lib/active_record/attribute_methods.rb
+++ b/activerecord/lib/active_record/attribute_methods.rb
@@ -110,16 +110,34 @@ module ActiveRecord
end
end
- # A method name is 'dangerous' if it is already defined by Active Record, but
+ # A method name is 'dangerous' if it is already (re)defined by Active Record, but
# not by any ancestors. (So 'puts' is not dangerous but 'save' is.)
def dangerous_attribute_method?(name) # :nodoc:
method_defined_within?(name, Base)
end
- def method_defined_within?(name, klass, sup = klass.superclass) # :nodoc:
+ def method_defined_within?(name, klass, superklass = klass.superclass) # :nodoc:
if klass.method_defined?(name) || klass.private_method_defined?(name)
- if sup.method_defined?(name) || sup.private_method_defined?(name)
- klass.instance_method(name).owner != sup.instance_method(name).owner
+ if superklass.method_defined?(name) || superklass.private_method_defined?(name)
+ klass.instance_method(name).owner != superklass.instance_method(name).owner
+ else
+ true
+ end
+ else
+ false
+ end
+ end
+
+ # A class method is 'dangerous' if it is already (re)defined by Active Record, but
+ # not by any ancestors. (So 'puts' is not dangerous but 'new' is.)
+ def dangerous_class_method?(method_name)
+ class_method_defined_within?(method_name, Base)
+ end
+
+ def class_method_defined_within?(name, klass, superklass = klass.superclass) # :nodoc
+ if klass.respond_to?(name, true)
+ if superklass.respond_to?(name, true)
+ klass.method(name).owner != superklass.method(name).owner
else
true
end
diff --git a/activerecord/lib/active_record/scoping/named.rb b/activerecord/lib/active_record/scoping/named.rb
index 2a5718f388..49cadb66d0 100644
--- a/activerecord/lib/active_record/scoping/named.rb
+++ b/activerecord/lib/active_record/scoping/named.rb
@@ -139,6 +139,12 @@ module ActiveRecord
# Article.published.featured.latest_article
# Article.featured.titles
def scope(name, body, &block)
+ if dangerous_class_method?(name)
+ raise ArgumentError, "You tried to define a scope named \"#{name}\" " \
+ "on the model \"#{self.name}\", but Active Record already defined " \
+ "a class method with the same name."
+ end
+
extension = Module.new(&block) if block
singleton_class.send(:define_method, name) do |*args|
diff --git a/activerecord/test/cases/scoping/named_scoping_test.rb b/activerecord/test/cases/scoping/named_scoping_test.rb
index 086977d9a2..9dc26cfd4d 100644
--- a/activerecord/test/cases/scoping/named_scoping_test.rb
+++ b/activerecord/test/cases/scoping/named_scoping_test.rb
@@ -266,6 +266,63 @@ class NamedScopingTest < ActiveRecord::TestCase
assert_equal 'lifo', topic.author_name
end
+ def test_reserved_scope_names
+ klass = Class.new(ActiveRecord::Base) do
+ self.table_name = "topics"
+
+ scope :approved, -> { where(approved: true) }
+
+ class << self
+ public
+ def pub; end
+
+ private
+ def pri; end
+
+ protected
+ def pro; end
+ end
+ end
+
+ subklass = Class.new(klass)
+
+ conflicts = [
+ :create, # public class method on AR::Base
+ :relation, # private class method on AR::Base
+ :new, # redefined class method on AR::Base
+ :all, # a default scope
+ ]
+
+ non_conflicts = [
+ :find_by_title, # dynamic finder method
+ :approved, # existing scope
+ :pub, # existing public class method
+ :pri, # existing private class method
+ :pro, # existing protected class method
+ :open, # a ::Kernel method
+ ]
+
+ conflicts.each do |name|
+ assert_raises(ArgumentError, "scope `#{name}` should not be allowed") do
+ klass.class_eval { scope name, ->{ where(approved: true) } }
+ end
+
+ assert_raises(ArgumentError, "scope `#{name}` should not be allowed") do
+ subklass.class_eval { scope name, ->{ where(approved: true) } }
+ end
+ end
+
+ non_conflicts.each do |name|
+ assert_nothing_raised do
+ klass.class_eval { scope name, ->{ where(approved: true) } }
+ end
+
+ assert_nothing_raised do
+ subklass.class_eval { scope name, ->{ where(approved: true) } }
+ end
+ end
+ end
+
# Method delegation for scope names which look like /\A[a-zA-Z_]\w*[!?]?\z/
# has been done by evaluating a string with a plain def statement. For scope
# names which contain spaces this approach doesn't work.