├── .ci.gemfile
├── .github
└── workflows
│ └── ci.yml
├── .gitignore
├── CHANGELOG
├── MIT-LICENSE
├── README.rdoc
├── Rakefile
├── deprecate_public.gemspec
├── lib
└── deprecate_public.rb
└── test
└── deprecate_public_test.rb
/.ci.gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 | gem "rake"
4 | gem "minitest-global_expectations"
5 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches: [ master ]
6 | pull_request:
7 | branches: [ master ]
8 |
9 | permissions:
10 | contents: read
11 |
12 | jobs:
13 | tests:
14 | strategy:
15 | fail-fast: false
16 | matrix:
17 | os: [ubuntu-latest]
18 | ruby: [ "2.0.0", 2.1, 2.3, 2.4, 2.5, 2.6, 2.7, "3.0", 3.1, 3.2, 3.3, 3.4, jruby-9.3, jruby-9.4, jruby-10.0 ]
19 | include:
20 | - { os: ubuntu-22.04, ruby: "1.9.3" }
21 | - { os: ubuntu-22.04, ruby: jruby-9.1 }
22 | - { os: ubuntu-22.04, ruby: jruby-9.2 }
23 | runs-on: ${{ matrix.os }}
24 | name: ${{ matrix.ruby }}
25 | env:
26 | BUNDLE_GEMFILE: .ci.gemfile
27 | steps:
28 | - uses: actions/checkout@v4
29 | - uses: ruby/setup-ruby@v1
30 | with:
31 | ruby-version: ${{ matrix.ruby }}
32 | bundler-cache: true
33 | - run: bundle exec rake
34 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /rdoc
2 | /deprecate_public-*.gem
3 | /coverage
4 |
--------------------------------------------------------------------------------
/CHANGELOG:
--------------------------------------------------------------------------------
1 | === 1.1.0 (2019-04-24)
2 |
3 | * Add Module#deprecate_public_constant on Ruby 2.6+ (jeremyevans)
4 |
5 | * Work on Ruby 1.8+, fix tests on Ruby 2.0-2.4 (jeremyevans)
6 |
7 | === 1.0.0 (2017-12-13)
8 |
9 | * Initial public release
10 |
--------------------------------------------------------------------------------
/MIT-LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2017 Jeremy Evans
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to
5 | deal in the Software without restriction, including without limitation the
6 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
7 | sell copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in
11 | all copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
16 | THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
19 |
--------------------------------------------------------------------------------
/README.rdoc:
--------------------------------------------------------------------------------
1 | = ruby-deprecate_public
2 |
3 | ruby-deprecate_public exists so that library authors can take an
4 | existing public method in one of their classes, and make it a
5 | private method, but still allow it to be called as a public method,
6 | emitting a warning if called as a public method.
7 |
8 | On Ruby 2.6+, you can also take an existing public constant,
9 | make it a private constant, but still allow it to be accessed as a
10 | public constant, emitting a warning if accessed as a public constant.
11 |
12 | == Usage
13 |
14 | === Basic
15 |
16 | require 'deprecate_public'
17 | class Foo
18 | private
19 |
20 | def meth
21 | :return_value
22 | end
23 | end
24 |
25 | foo = Foo.new
26 |
27 | foo.send(:meth)
28 | # => :return_value
29 |
30 | foo.meth
31 | # raises NoMethodError: private method `meth' called for ...
32 |
33 | Foo.deprecate_public :meth
34 | foo.meth
35 | # warning emitting: "calling Foo#meth using deprecated public interface"
36 | # => :return_value
37 |
38 | ## Ruby 2.6+: Deprecating Constants
39 |
40 | class Foo
41 | BAR = 1
42 | private_constant :BAR
43 | end
44 |
45 | Foo::BAR
46 | # raises NameError: private constant Foo::BAR referenced
47 |
48 | Foo.deprecate_public_constant :Bar
49 | Foo::BAR
50 | # warning emitting: "accessing Foo::BAR using deprecated public interface"
51 | # => 1
52 |
53 | === Arguments
54 |
55 | # Deprecate multiple methods at once
56 | deprecate_public([:meth1, :meth2])
57 |
58 | # Override message
59 | deprecate_public(:meth1, "meth1 is private method, stop calling it!")
60 |
61 | # Ruby 2.6+: deprecate_public_constant uses same interface
62 |
63 | == Design
64 |
65 | +deprecate_public+ works by overriding +method_missing+, which is called
66 | if you call a private method with a specified receiver. That's one
67 | of the reasons calling a private method raises +NoMethodError+ instead
68 | of +MethodVisibilityError+ or something like that.
69 |
70 | On Ruby 2.6+, +deprecate_public_constant+ works by overriding
71 | +const_missing+, which is called if you access a constant using the
72 | public interface (Foo::BAR in the above example).
73 |
74 | == Caveats
75 |
76 | Before Ruby 3, while you can call +deprecate_public+ on a Module, you should
77 | only do so if the module has not been included in another Module or Class, as
78 | when called on a module it only affects future cases where the module was
79 | included in a class.
80 |
81 | While you can call +deprecate_public_constant+ on a Module, you should
82 | only do so if the module has not been included in another Module or Class, as
83 | when called on a module it only affects future cases where the module was
84 | included in a class, even in Ruby 3.
85 |
86 | == Installation
87 |
88 | ruby-deprecate_public is distributed as a gem, and can be installed with:
89 |
90 | gem install deprecate_public
91 |
92 | == Source
93 |
94 | ruby-deprecate_public is hosted on GitHub:
95 |
96 | https://github.com/jeremyevans/ruby-deprecate_public
97 |
98 | == Issues
99 |
100 | ruby-deprecate_public uses GitHub Issues for issue tracking:
101 |
102 | https://github.com/jeremyevans/ruby-deprecate_public/issues
103 |
104 | == Author
105 |
106 | Jeremy Evans
107 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require "rake/clean"
2 |
3 | CLEAN.include ["*.gem", "rdoc", "coverage"]
4 |
5 | desc "Generate rdoc"
6 | task :rdoc do
7 | rdoc_dir = "rdoc"
8 | rdoc_opts = ["--line-numbers", "--inline-source", '--title', 'deprecate_public: Warn when calling private methods via public interface']
9 |
10 | begin
11 | gem 'hanna'
12 | rdoc_opts.concat(['-f', 'hanna'])
13 | rescue Gem::LoadError
14 | end
15 |
16 | rdoc_opts.concat(['--main', 'README.rdoc', "-o", rdoc_dir] +
17 | %w"README.rdoc CHANGELOG MIT-LICENSE" +
18 | Dir["lib/**/*.rb"]
19 | )
20 |
21 | FileUtils.rm_rf(rdoc_dir)
22 |
23 | require "rdoc"
24 | RDoc::RDoc.new.document(rdoc_opts)
25 | end
26 |
27 | desc "Run specs"
28 | task :test do
29 | sh "#{FileUtils::RUBY} -w #{'-W:strict_unused_block' if RUBY_VERSION >= '3.4'} test/deprecate_public_test.rb"
30 | end
31 | task :default=>[:test]
32 |
33 | desc "Run specs with coverage"
34 | task :test_cov do
35 | ENV['COVERAGE'] = '1'
36 | sh "#{FileUtils::RUBY} test/deprecate_public_test.rb"
37 | end
38 |
39 | desc "Package deprecate_public"
40 | task :package=>[:clean] do
41 | sh "#{FileUtils::RUBY} -S gem build deprecate_public.gemspec"
42 | end
43 |
--------------------------------------------------------------------------------
/deprecate_public.gemspec:
--------------------------------------------------------------------------------
1 | Gem::Specification.new do |s|
2 | s.name = 'deprecate_public'
3 | s.version = '1.1.0'
4 | s.platform = Gem::Platform::RUBY
5 | s.extra_rdoc_files = ["README.rdoc", "CHANGELOG", "MIT-LICENSE"]
6 | s.rdoc_options += ["--quiet", '--inline-source', '--line-numbers', '--title', 'deprecate_public: Warn when calling private methods and accessing private constants via public interface', '--main', 'README.rdoc']
7 | s.summary = "Warn when calling private methods and accessing private constants via public interface"
8 | s.description = s.summary
9 | s.license = 'MIT'
10 | s.author = "Jeremy Evans"
11 | s.email = "code@jeremyevans.net"
12 | s.homepage = "https://github.com/jeremyevans/ruby-deprecate_public"
13 | s.files = %w(MIT-LICENSE CHANGELOG README.rdoc) + Dir["lib/deprecate_public.rb"]
14 | s.add_development_dependency "minitest-global_expectations"
15 | end
16 |
--------------------------------------------------------------------------------
/lib/deprecate_public.rb:
--------------------------------------------------------------------------------
1 | class Module
2 | module DeprecatePublic
3 | # Handle calls to methods where +deprecate_public+ has been called
4 | # for the method, printing the warning and then using +send+ to
5 | # invoke the method.
6 | def method_missing(meth, *args, &block)
7 | check_meth = "_deprecated_public_message_#{meth}"
8 | if respond_to?(meth, true) && respond_to?(check_meth, true) && (msg = send(check_meth))
9 | if RUBY_VERSION >= '2.5'
10 | Kernel.warn(msg, :uplevel => 1)
11 | # :nocov:
12 | elsif RUBY_VERSION >= '2.0'
13 | Kernel.warn("#{caller(1,1)[0].sub(/in `.*'\z/, '')} warning: #{msg}")
14 | else
15 | Kernel.warn("#{caller(1)[0].sub(/in `.*'\z/, '')} warning: #{msg}")
16 | # :nocov:
17 | end
18 |
19 | send(meth, *args, &block)
20 | else
21 | super
22 | end
23 | end
24 | end
25 |
26 | # Allow but deprecate public method calls to +meth+, if +meth+ is
27 | # protected or private. +meth+ can be an array of method name strings
28 | # or symbols to handle multiple methods at once. If +msg+ is specified,
29 | # it can be used to customize the warning printed.
30 | def deprecate_public(meth, msg=nil)
31 | include DeprecatePublic
32 |
33 | Array(meth).each do |m|
34 | message = (msg || "calling #{name}##{m} using deprecated public interface").dup.freeze
35 | message_meth = :"_deprecated_public_message_#{m}"
36 | define_method(message_meth){message}
37 | private message_meth
38 | end
39 |
40 | nil
41 | end
42 |
43 | # :nocov:
44 | if RUBY_VERSION >= '2.6'
45 | # :nocov:
46 | module DeprecatePublicConstant
47 | # Handle access to constants where +deprecate_public_constant+ has been called
48 | # for the constant, printing the warning and then using +const_get+ to
49 | # access the constant.
50 | def const_missing(const)
51 | check_meth = "_deprecated_public_constant_message_#{const}"
52 | if const_defined?(const, true) && respond_to?(check_meth, true) && (msg = send(check_meth))
53 | Kernel.warn(msg, :uplevel => 1)
54 | const_get(const, true)
55 | else
56 | super
57 | end
58 | end
59 | end
60 |
61 | # Allow but deprecate public constant access to +const+, if +const+ is
62 | # a private constant. +const+ can be an array of method name strings
63 | # or symbols to handle multiple methods at once. If +msg+ is specified,
64 | # it can be used to customize the warning printed.
65 | def deprecate_public_constant(const, msg=nil)
66 | extend DeprecatePublicConstant
67 |
68 | Array(const).each do |c|
69 | message = (msg || "accessing #{name}::#{c} using deprecated public interface").dup.freeze
70 | message_meth = :"_deprecated_public_constant_message_#{c}"
71 | define_singleton_method(message_meth){message}
72 | singleton_class.send(:private, message_meth)
73 | end
74 |
75 | nil
76 | end
77 | end
78 | end
79 |
--------------------------------------------------------------------------------
/test/deprecate_public_test.rb:
--------------------------------------------------------------------------------
1 | if ENV.delete('COVERAGE')
2 | require 'simplecov'
3 |
4 | SimpleCov.start do
5 | enable_coverage :branch
6 | add_filter "/test/"
7 | add_group('Missing'){|src| src.covered_percent < 100}
8 | add_group('Covered'){|src| src.covered_percent == 100}
9 | end
10 | end
11 |
12 | require File.join(File.dirname(File.dirname(File.expand_path(__FILE__))), 'lib', 'deprecate_public')
13 | require 'rubygems'
14 | ENV['MT_NO_PLUGINS'] = '1' # Work around stupid autoloading of plugins
15 | gem 'minitest'
16 | require 'minitest/global_expectations/autorun'
17 |
18 | warnings = []
19 | (class << Kernel; self; end).module_eval do
20 | alias warn warn
21 | define_method(:warn) do |*args|
22 | warnings << if args.last == {:uplevel => 1}
23 | args.first
24 | else
25 | args.first.sub(/\A.+warning: (.+)\z/, '\1')
26 | end
27 | end
28 | end
29 |
30 | class DPSTest
31 | if RUBY_VERSION >= '2.6'
32 | PUB = 1
33 |
34 | DEPPUB = 2
35 | deprecate_public_constant :DEPPUB
36 |
37 | PRIV = 3
38 | private_constant :PRIV
39 |
40 | DEPPRIV = 4
41 | private_constant :DEPPRIV
42 | deprecate_public_constant :DEPPRIV
43 |
44 | DEPPRIV2 = 5
45 | private_constant :DEPPRIV2
46 | deprecate_public_constant :DEPPRIV2, "custom warning for DEPPRIV2"
47 | end
48 |
49 | def pub; 1 end
50 | def dep_pub; 8 end
51 | deprecate_public(:dep_pub)
52 |
53 | def test_prot(other)
54 | other.prot
55 | end
56 | def test_dep_prot(other)
57 | other.dep_prot
58 | end
59 |
60 | protected
61 |
62 | def prot; 2 end
63 | def dep_prot; 9 end
64 | deprecate_public(:dep_prot)
65 |
66 | private
67 |
68 | def priv; 3 end
69 | def dep_priv; 4 end
70 | deprecate_public(:dep_priv)
71 |
72 | def dep_priv1; 5 end
73 | def dep_priv2; 6 end
74 | deprecate_public([:dep_priv1, :dep_priv2])
75 |
76 | def dep_priv_msg; 7 end
77 | deprecate_public(:dep_priv_msg, "custom warning for dep_priv_msg")
78 | end
79 |
80 | class DPSTest2
81 | def test_dep_prot(other)
82 | other.dep_prot
83 | end
84 | end
85 |
86 | describe "Module#deprecate_public" do
87 | before do
88 | @obj = DPSTest.new
89 | warnings.clear
90 | end
91 |
92 | after do
93 | warnings.must_equal []
94 | end
95 |
96 | it "should not warn for methods if deprecate_public is not used and access should be allowed" do
97 | @obj.pub.must_equal 1
98 | @obj.test_prot(DPSTest.new).must_equal 2
99 | @obj.instance_exec do
100 | prot.must_equal 2
101 | priv.must_equal 3
102 | end
103 | end
104 |
105 | it "should raise NoMethodError for private/protected method calls if deprecate_public is not used" do
106 | proc{@obj.prot}.must_raise NoMethodError
107 | proc{@obj.priv}.must_raise NoMethodError
108 | end
109 |
110 | it "should not warn if methods are public and deprecate_public is used" do
111 | @obj.dep_pub.must_equal 8
112 | end
113 |
114 | it "should not warn if methods are protected and deprecate_public is used and access should be allowed" do
115 | @obj.test_dep_prot(DPSTest.new).must_equal 9
116 | end
117 |
118 | it "should warn if methods are private and deprecate_public is used" do
119 | @obj.dep_priv.must_equal 4
120 | @obj.dep_priv1.must_equal 5
121 | @obj.dep_priv2.must_equal 6
122 | @obj.dep_priv_msg.must_equal 7
123 | warnings.must_equal [
124 | "calling DPSTest#dep_priv using deprecated public interface",
125 | "calling DPSTest#dep_priv1 using deprecated public interface",
126 | "calling DPSTest#dep_priv2 using deprecated public interface",
127 | "custom warning for dep_priv_msg"
128 | ]
129 | warnings.clear
130 | end
131 |
132 | it "should warn if methods are protected and deprecate_public is used" do
133 | DPSTest2.new.test_dep_prot(@obj).must_equal 9
134 | warnings.must_equal ["calling DPSTest#dep_prot using deprecated public interface"]
135 | warnings.clear
136 | end
137 |
138 | it "should raise NoMethodError if the method is not defined" do
139 | class << @obj
140 | undef :dep_priv
141 | end
142 | proc{@obj.dep_priv}.must_raise NoMethodError
143 | end
144 | end
145 |
146 | if RUBY_VERSION >= '2.6'
147 | describe "Module#deprecate_public_constant" do
148 | before do
149 | warnings.clear
150 | end
151 | after do
152 | warnings.must_equal []
153 | end
154 |
155 | it "should not warn for constant if deprecate_public_constant is not used and access should be allowed" do
156 | DPSTest::PUB.must_equal 1
157 | class DPSTest
158 | PRIV.must_equal 3
159 | end
160 | end
161 |
162 | it "should raise NameError for private constants if deprecate_public_constant is not used" do
163 | proc{DPSTest::PRIV}.must_raise NameError
164 | end
165 |
166 | it "should not warn if constants are public and deprecate_public_constant is used" do
167 | DPSTest::DEPPUB.must_equal 2
168 | end
169 |
170 | it "should warn if constants are private and deprecate_public_constant is used" do
171 | DPSTest::DEPPRIV.must_equal 4
172 | DPSTest::DEPPRIV2.must_equal 5
173 | warnings.must_equal [
174 | "accessing DPSTest::DEPPRIV using deprecated public interface",
175 | "custom warning for DEPPRIV2"
176 | ]
177 | warnings.clear
178 | end
179 |
180 | it "should raise NameError if the constant is not defined" do
181 | c = Class.new
182 | c.deprecate_public_constant :PUB
183 | proc{c::PUB}.must_raise NameError
184 | end
185 | end
186 | end
187 |
188 | if RUBY_VERSION >= '3.0'
189 | module DPSTestMod
190 | end
191 | class DPSTestClass
192 | include DPSTestMod
193 | end
194 |
195 | module DPSTestMod
196 | if RUBY_VERSION >= '2.6'
197 | PUB = 1
198 |
199 | DEPPUB = 2
200 | deprecate_public_constant :DEPPUB
201 |
202 | PRIV = 3
203 | private_constant :PRIV
204 |
205 | DEPPRIV = 4
206 | private_constant :DEPPRIV
207 | deprecate_public_constant :DEPPRIV
208 |
209 | DEPPRIV2 = 5
210 | private_constant :DEPPRIV2
211 | deprecate_public_constant :DEPPRIV2, "custom warning for DEPPRIV2"
212 | end
213 |
214 | def pub; 1 end
215 | def dep_pub; 8 end
216 | deprecate_public(:dep_pub)
217 |
218 | def test_prot(other)
219 | other.prot
220 | end
221 | def test_dep_prot(other)
222 | other.dep_prot
223 | end
224 |
225 | protected
226 |
227 | def prot; 2 end
228 | def dep_prot; 9 end
229 | deprecate_public(:dep_prot)
230 |
231 | private
232 |
233 | def priv; 3 end
234 | def dep_priv; 4 end
235 | deprecate_public(:dep_priv)
236 |
237 | def dep_priv1; 5 end
238 | def dep_priv2; 6 end
239 | deprecate_public([:dep_priv1, :dep_priv2])
240 |
241 | def dep_priv_msg; 7 end
242 | deprecate_public(:dep_priv_msg, "custom warning for dep_priv_msg")
243 | end
244 |
245 | class DPSTestClass
246 | include DPSTestMod
247 | end
248 |
249 | describe "Module#deprecate_public" do
250 | before do
251 | @obj = DPSTestClass.new
252 | warnings.clear
253 | end
254 |
255 | after do
256 | warnings.must_equal []
257 | end
258 |
259 | it "should not warn for methods if deprecate_public is not used and access should be allowed" do
260 | @obj.pub.must_equal 1
261 | @obj.test_prot(DPSTestClass.new).must_equal 2
262 | @obj.instance_exec do
263 | prot.must_equal 2
264 | priv.must_equal 3
265 | end
266 | end
267 |
268 | it "should raise NoMethodError for private/protected method calls if deprecate_public is not used" do
269 | proc{@obj.prot}.must_raise NoMethodError
270 | proc{@obj.priv}.must_raise NoMethodError
271 | end
272 |
273 | it "should not warn if methods are public and deprecate_public is used" do
274 | @obj.dep_pub.must_equal 8
275 | end
276 |
277 | it "should not warn if methods are protected and deprecate_public is used and access should be allowed" do
278 | @obj.test_dep_prot(DPSTestClass.new).must_equal 9
279 | end
280 |
281 | it "should warn if methods are private and deprecate_public is used" do
282 | @obj.dep_priv.must_equal 4
283 | @obj.dep_priv1.must_equal 5
284 | @obj.dep_priv2.must_equal 6
285 | @obj.dep_priv_msg.must_equal 7
286 | warnings.must_equal [
287 | "calling DPSTestMod#dep_priv using deprecated public interface",
288 | "calling DPSTestMod#dep_priv1 using deprecated public interface",
289 | "calling DPSTestMod#dep_priv2 using deprecated public interface",
290 | "custom warning for dep_priv_msg"
291 | ]
292 | warnings.clear
293 | end
294 |
295 | it "should warn if methods are protected and deprecate_public is used" do
296 | DPSTest2.new.test_dep_prot(@obj).must_equal 9
297 | warnings.must_equal ["calling DPSTestMod#dep_prot using deprecated public interface"]
298 | warnings.clear
299 | end
300 |
301 | it "should raise NoMethodError if the method is not defined" do
302 | class << @obj
303 | undef :dep_priv
304 | end
305 | proc{@obj.dep_priv}.must_raise NoMethodError
306 | end
307 | end
308 | end
309 |
--------------------------------------------------------------------------------