Skip to content

Commit 41e8118

Browse files
agrbergclaude
andcommitted
Stop polluting Cacheable namespace with interceptor constants
Previously, each class that included Cacheable created a constant on the Cacheable module (e.g. Cacheable::MyClassCacher) via const_set. This polluted the namespace and leaked memory for anonymous classes. Re-including Cacheable used remove_const which left ghost modules in the MRO. Now interceptor modules are stored as instance variables on the including class (@_cacheable_interceptor). The module gets a descriptive to_s/inspect for debugging without creating constants. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent f7f6cba commit 41e8118

3 files changed

Lines changed: 10 additions & 13 deletions

File tree

lib/cacheable.rb

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,10 @@ def self.included(base)
3636
base.extend(Cacheable::MethodGenerator)
3737

3838
interceptor_name = base.send(:method_interceptor_module_name)
39-
remove_const(interceptor_name) if const_defined?(interceptor_name)
40-
41-
base.prepend const_set(interceptor_name, Module.new)
39+
interceptor = Module.new
40+
interceptor.define_singleton_method(:to_s) { interceptor_name }
41+
interceptor.define_singleton_method(:inspect) { interceptor_name }
42+
base.instance_variable_set(:@_cacheable_interceptor, interceptor)
43+
base.prepend interceptor
4244
end
4345
end

lib/cacheable/method_generator.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ def create_cacheable_methods(original_method_name, opts = {})
2222

2323
unless_proc = opts[:unless].is_a?(Symbol) ? opts[:unless].to_proc : opts[:unless]
2424

25-
const_get(method_interceptor_module_name).class_eval do
25+
@_cacheable_interceptor.class_eval do
2626
define_method(method_names[:key_format_method_name]) do |*args, **kwargs|
2727
key_format_proc.call(self, original_method_name, args, **kwargs)
2828
end

spec/cacheable/cacheable_spec.rb

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -89,22 +89,17 @@
8989
end
9090

9191
it 'uses the class name to define an interceptor module' do
92-
# This is done specifically this way to be compatible w/ RSpec best practices
93-
# Once Cacheable is included in a class, it uses the name of the class to define the
94-
# interceptor module. However, it is considered bad practice to create constants in RSpec
95-
# so they're typically made with `stub_const`. We need to include Cacheable after the
96-
# anonymous class has been created and assigned to the stubbed constant for this order to work.
9792
stub_const('RealClassName', Class.new)
98-
class_name = RealClassName.include(described_class)
93+
RealClassName.include(described_class)
9994

100-
expect(class_name.ancestors.map(&:to_s)).to include("Cacheable::#{class_name}Cacher")
95+
expect(RealClassName.ancestors.map(&:to_s)).to include('RealClassNameCacher')
10196
end
10297

10398
it 'uses the class address to define an interceptor module for anonymous classes' do
10499
custom_class = Class.new { include Cacheable }
105100
class_name = custom_class.to_s.tr('#:<>', '')
106101

107-
expect(custom_class.ancestors.map(&:to_s)).to include("Cacheable::#{class_name}Cacher")
102+
expect(custom_class.ancestors.map(&:to_s)).to include("#{class_name}Cacher")
108103
end
109104

110105
context 'when the method name has special characters' do
@@ -126,7 +121,7 @@
126121
stub_const('Outer::Inner', Class.new)
127122
Outer::Inner.include(described_class)
128123

129-
expect(Outer::Inner.ancestors.map(&:to_s)).to include('Cacheable::OuterInnerCacher')
124+
expect(Outer::Inner.ancestors.map(&:to_s)).to include('OuterInnerCacher')
130125
end
131126
end
132127
end

0 commit comments

Comments
 (0)