diff options
author | Godfrey Chan <godfreykfc@gmail.com> | 2014-01-24 18:28:05 -0800 |
---|---|---|
committer | Godfrey Chan <godfreykfc@gmail.com> | 2014-01-29 10:54:51 -0800 |
commit | 7e8e91c439c1a877f867cd7ba634f7297ccef04b (patch) | |
tree | 4d800773aa5f59b9293c52b3b46c44d27b3a4d5e | |
parent | 9ed66648b59b160b43c83c349263e8cb97eaa088 (diff) | |
download | rails-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*
-rw-r--r-- | activerecord/CHANGELOG.md | 10 | ||||
-rw-r--r-- | activerecord/lib/active_record/attribute_methods.rb | 26 | ||||
-rw-r--r-- | activerecord/lib/active_record/scoping/named.rb | 6 | ||||
-rw-r--r-- | activerecord/test/cases/scoping/named_scoping_test.rb | 57 |
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. |