├── .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 | --------------------------------------------------------------------------------