├── .autotest ├── .gemtest ├── .gitignore ├── HISTORY.rdoc ├── Manifest.txt ├── README.rdoc ├── Rakefile ├── examples ├── alert_frequency.rb ├── at_exit.rb └── caller_context_padding.rb ├── lib ├── deprecatable.rb └── deprecatable │ ├── alerter.rb │ ├── call_site.rb │ ├── call_site_context.rb │ ├── deprecated_method.rb │ ├── options.rb │ ├── registry.rb │ └── util.rb └── test ├── helpers.rb ├── test_deprecatable.rb ├── test_deprecatable_alerter.rb ├── test_deprecatable_call_site.rb ├── test_deprecatable_call_site_context.rb ├── test_deprecatable_deprecated_method.rb ├── test_deprecatable_options.rb ├── test_deprecatable_registry.rb └── test_deprecatable_util.rb /.autotest: -------------------------------------------------------------------------------- 1 | # vim: syntax=ruby 2 | 3 | require 'autotest/restart' 4 | 5 | Autotest.add_hook :initialize do |at| 6 | at.add_exception 'coverage.info' 7 | at.add_exception 'coverage' 8 | at.add_exception '.git' 9 | end 10 | -------------------------------------------------------------------------------- /.gemtest: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *.swo 3 | *.swp 4 | doc/ 5 | coverage/ 6 | pkg/ 7 | coverage.info 8 | -------------------------------------------------------------------------------- /HISTORY.rdoc: -------------------------------------------------------------------------------- 1 | == 1.0.0 - 2011-08-09 2 | 3 | * Birthday! 4 | 5 | -------------------------------------------------------------------------------- /Manifest.txt: -------------------------------------------------------------------------------- 1 | .autotest 2 | .gemtest 3 | HISTORY.rdoc 4 | Manifest.txt 5 | README.rdoc 6 | Rakefile 7 | examples/alert_frequency.rb 8 | examples/at_exit.rb 9 | examples/caller_context_padding.rb 10 | lib/deprecatable.rb 11 | lib/deprecatable/alerter.rb 12 | lib/deprecatable/call_site.rb 13 | lib/deprecatable/call_site_context.rb 14 | lib/deprecatable/deprecated_method.rb 15 | lib/deprecatable/options.rb 16 | lib/deprecatable/registry.rb 17 | lib/deprecatable/util.rb 18 | test/helpers.rb 19 | test/test_deprecatable.rb 20 | test/test_deprecatable_alerter.rb 21 | test/test_deprecatable_call_site.rb 22 | test/test_deprecatable_call_site_context.rb 23 | test/test_deprecatable_deprecated_method.rb 24 | test/test_deprecatable_options.rb 25 | test/test_deprecatable_registry.rb 26 | test/test_deprecatable_util.rb 27 | -------------------------------------------------------------------------------- /README.rdoc: -------------------------------------------------------------------------------- 1 | = Deprecatable 2 | 3 | * http://github.com/copiousfreetime/deprecatable 4 | * http://www.copiousfreetime.org/projects/deprecatable 5 | 6 | == DESCRIPTION 7 | 8 | Deprecatable is a library to help you, as a developer, deprecate your API and be 9 | proactive about helping people who use your library find where they need to 10 | update. 11 | 12 | When using Deprecatable, you mark methods as 'deperecated' and then the users of 13 | your API will receive a helpful alert showing the exact line of code where they 14 | called the deprecated API, and what they need to do to fix it (although you need 15 | to supply this piece of information). 16 | 17 | Users will receive, by default, a single alert for each unique location a 18 | deprecated API method is invoked. They will also receive a final report 19 | detailing all the locations where deprecated APIs were invoked. 20 | 21 | The "noisiness" of the alerting and the final report is all configurable, via 22 | both code, and environment variables. See Deprecatable::Options. 23 | 24 | == SYNOPSIS 25 | 26 | class SomeClass 27 | extend Deprecatable 28 | 29 | def deprecate_me 30 | ... 31 | end 32 | deprecate :deprecate_me 33 | end 34 | 35 | == REQUIREMENTS 36 | 37 | No runtime requirements, other than Ruby itself. 38 | 39 | == INSTALL 40 | 41 | $ [sudo] gem install deprecatable 42 | 43 | == EXAMPLES 44 | 45 | All of the examles here are included in the examples/ directory. 46 | 47 | === Alerts 48 | 49 | Default alerting. Uses Ruby's 'warn' capability, which sends the output to 50 | standard error. 51 | 52 | $ ruby -Ilib examples/alert_frequency.rb once > /dev/null 53 | DEPRECATION WARNING: `A::B#deprecate_me` 54 | DEPRECATION WARNING: ------------------- 55 | DEPRECATION WARNING: 56 | DEPRECATION WARNING: * Originally defined at /Users/jeremy/Projects/deprecatable/examples/alert_frequency.rb:24 57 | DEPRECATION WARNING: * This method is to be completely removed 58 | DEPRECATION WARNING: * Will be removed in version 4.2 59 | DEPRECATION WARNING: 60 | DEPRECATION WARNING: Called from /Users/jeremy/Projects/deprecatable/examples/alert_frequency.rb:89 61 | DEPRECATION WARNING: 62 | DEPRECATION WARNING: 87: # Context before 1.1 63 | DEPRECATION WARNING: 88: # Context before 1.2 64 | DEPRECATION WARNING: ---> 89: b.deprecate_me 65 | DEPRECATION WARNING: 90: # Context after 1.1 66 | DEPRECATION WARNING: 91: # Context after 1.2 67 | DEPRECATION WARNING: 68 | DEPRECATION WARNING: To turn this report off do one of the following: 69 | DEPRECATION WARNING: * in your ruby code set `Deprecatable.options.alert_frequency = :never` 70 | DEPRECATION WARNING: * set the environment variable `DEPRECATABLE_ALERT_FREQUENCY="never"` 71 | DEPRECATION WARNING: 72 | 73 | Lets try that again and shut off the output 74 | 75 | $ DEPRECATABLE_ALERT_FREQUENCY=never ruby -Ilib examples/alert_frequency.rb once > /dev/null 76 | $ 77 | 78 | === A final report when the process exits. 79 | 80 | As a bonus, the 'at_exit' report is valid Markdown. 81 | 82 | $ ruby -Ilib examples/at_exit.rb > /dev/null 83 | Deprecatable 'at_exit' Report 84 | ============================= 85 | 86 | To turn this report off do one of the following: 87 | 88 | * in your ruby code set `Deprecatable.options.has_at_exit_report = false` 89 | * set the environment variable `DEPRECATABLE_HAS_AT_EXIT_REPORT="false"` 90 | 91 | `A::B#deprecate_me_2` 92 | --------------------- 93 | 94 | * Originally defined at /Users/jeremy/Projects/deprecatable/examples/at_exit.rb:24 95 | * This method is to be completely removed 96 | * Will be removed after 2020-02-20 97 | 98 | Called 2 time(s) from /Users/jeremy/Projects/deprecatable/examples/at_exit.rb:64 99 | 100 | 62: # Context before 4.1 101 | 63: # Context before 4.2 102 | ---> 64: b.deprecate_me_2 103 | 65: # Context after 4.1 104 | 66: # Context after 4.2 105 | 106 | Called 4 time(s) from /Users/jeremy/Projects/deprecatable/examples/at_exit.rb:48 107 | 108 | 46: # Context before 2.1 109 | 47: # Context before 2.2 110 | ---> 48: b.deprecate_me_2 111 | 49: # Context after 2.1 112 | 50: # Context after 2.2 113 | 114 | `A::B#deprecate_me_1` 115 | --------------------- 116 | 117 | * Originally defined at /Users/jeremy/Projects/deprecatable/examples/at_exit.rb:19 118 | * This method is to be completely removed 119 | * Will be removed in version 4.2 120 | 121 | Called 4 time(s) from /Users/jeremy/Projects/deprecatable/examples/at_exit.rb:42 122 | 123 | 40: # Context before 1.1 124 | 41: # Context before 1.2 125 | ---> 42: b.deprecate_me_1 126 | 43: # Context after 1.1 127 | 44: # Context after 1.2 128 | 129 | Called 2 time(s) from /Users/jeremy/Projects/deprecatable/examples/at_exit.rb:58 130 | 131 | 56: # Context before 3.1 132 | 57: # Context before 3.2 133 | ---> 58: b.deprecate_me_1 134 | 59: # Context after 3.1 135 | 60: # Context after 3.2 136 | 137 | And again, shutting off the output: 138 | 139 | $ DEPRECATABLE_HAS_AT_EXIT_REPORT="false" ruby -Ilib examples/at_exit.rb > /dev/null 140 | $ 141 | 142 | == DEVELOPERS 143 | 144 | If you would like to contribute to the project, you will need to check out the 145 | source from http://github.com/copiousfreetime/deprecatable and then if you are 146 | using a system installed ruby: 147 | 148 | $ rake newb 149 | 150 | If you are using a non-root installed ruby, for instance RVM, then you 151 | probably want to run: 152 | 153 | $ NOSUDO=true rake newb 154 | 155 | This task will install any missing dependencies, run the tests/specs, 156 | and generate the RDoc. 157 | 158 | 159 | == LICENSE 160 | 161 | (The ISC LICENSE) - http://www.opensource.org/licenses/ISC 162 | 163 | Copyright (c) 2011 Jeremy Hinegardner 164 | 165 | Permission to use, copy, modify, and/or distribute this software for any purpose 166 | with or without fee is hereby granted, provided that the above copyright notice 167 | and this permission notice appear in all copies. 168 | 169 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 170 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 171 | FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 172 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS 173 | OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER 174 | TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF 175 | THIS SOFTWARE. 176 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # vim: syntax=ruby 2 | 3 | begin 4 | require 'rubygems' 5 | require 'hoe' 6 | rescue LoadError 7 | abort <<-_ 8 | Developing deprecatable requires the use of rubygems and hoe. 9 | 10 | gem install hoe 11 | _ 12 | end 13 | 14 | Hoe.plugin :doofus, :git, :gemspec2, :minitest 15 | 16 | Hoe.spec 'deprecatable' do 17 | developer 'Jeremy Hinegardner', 'jeremy@copiousfreetime.org' 18 | 19 | # Use rdoc for history and readme 20 | self.history_file = 'HISTORY.rdoc' 21 | self.readme_file = 'README.rdoc' 22 | 23 | self.extra_rdoc_files = [ self.readme_file, self.history_file ] 24 | 25 | # Publish rdoc to a designated remote location 26 | with_config do |config, _| 27 | self.rdoc_locations << File.join(config['rdoc_remote_root'], self.remote_rdoc_dir) 28 | end 29 | 30 | self.extra_dev_deps << [ 'rcov', '~> 0.9.10'] 31 | 32 | end 33 | 34 | -------------------------------------------------------------------------------- /examples/alert_frequency.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # 3 | # An example showing how the alert_frequency option works 4 | # 5 | # You probably need to run from the parent directory as: 6 | # 7 | # ruby -Ilib examples/alert_frequency.rb 8 | # 9 | require 'deprecatable' 10 | 11 | #---------------------------------------------------------------------- 12 | # We create an example class with a deprecated method 13 | #---------------------------------------------------------------------- 14 | module A 15 | class B 16 | extend Deprecatable 17 | def initialize 18 | @call_count = 0 19 | end 20 | def deprecate_me 21 | @call_count += 1 22 | puts "deprecate_me call #{@call_count}" 23 | end 24 | deprecate :deprecate_me, :message => "This method is to be completely removed", :removal_version => "4.2" 25 | end 26 | end 27 | 28 | 29 | #---------------------------------------------------------------------- 30 | # usage, you can ignore this for now, this will get printed out if you 31 | # do not put any commandline arguments down 32 | #---------------------------------------------------------------------- 33 | def usage 34 | puts <<__ 35 | This is an example of showing how to affect the alert frequency 36 | You can change the alert frequency by: 37 | 38 | 1) Setting `Deprecatable.options.alert_freqeuncy` in ruby code. 39 | 2) Setting the DEPRECATABLE_ALERT_FREQUENCY envionment variable. 40 | 41 | They may be set to one of the following values: 'never', 'once', 'always' 42 | When you use both (1) and (2) simultaneously, you will see that 43 | setting the environment variable always overrides the code. 44 | 45 | Here are some example ways to run this program 46 | 47 | __ 48 | 49 | 50 | [ nil, "DEPRECATABLE_ALERT_FREQUENCY=" ].each do |env| 51 | %w[ never once always ].each do |env_setting| 52 | next if env.nil? and env_setting != 'never' 53 | %w[ never once always ].each do |cmd_line| 54 | next if env and (env_setting == cmd_line) 55 | parts = [ " " ] 56 | parts << "#{env}#{env_setting}" if env 57 | parts << "ruby -Ilib #{__FILE__} #{cmd_line}" 58 | puts parts.join(' ') 59 | end 60 | end 61 | end 62 | 63 | puts 64 | exit 1 65 | end 66 | 67 | if $0 == __FILE__ 68 | # Turning off the at exit report, for more information on them, 69 | # see the examples/at_exit.rb 70 | Deprecatable.options.has_at_exit_report = false 71 | 72 | # capture the parameters, we'll run if there is a commandline parameter 73 | # of if the environment variable is et 74 | alert_frequency = ARGV.shift 75 | usage unless alert_frequency || ENV['DEPRECATABLE_ALERT_FREQUENCY'] 76 | 77 | Deprecatable.options.alert_frequency = alert_frequency if alert_frequency 78 | 79 | puts 80 | puts "Running with ENV['DEPRECATABLE_ALERT_FREQUENCY'] => #{ENV['DEPRECATABLE_ALERT_FREQUENCY']}" 81 | puts "Running with Deprecatable.options.alert_frequency => #{Deprecatable.options.alert_frequency}" 82 | puts "-" * 72 83 | puts 84 | 85 | b = A::B.new 86 | 4.times do 87 | # Context before 1.1 88 | # Context before 1.2 89 | b.deprecate_me 90 | # Context after 1.1 91 | # Context after 1.2 92 | end 93 | 94 | 2.times do 95 | # Context before 2.1 96 | # Context before 2.2 97 | b.deprecate_me 98 | # Context after 2.1 99 | # Context after 2.2 100 | end 101 | end 102 | -------------------------------------------------------------------------------- /examples/at_exit.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # 3 | # An example showing how the at_exit handler work 4 | # 5 | # You probably need to run from the parent directory as: 6 | # 7 | # ruby -Ilib examples/at_exit.rb 8 | # 9 | require 'deprecatable' 10 | #---------------------------------------------------------------------- 11 | # We create an example class with some deprecated methods 12 | #---------------------------------------------------------------------- 13 | module A 14 | class B 15 | extend Deprecatable 16 | def deprecate_me_1 17 | puts "I've been deprecated! (1)" 18 | end 19 | deprecate :deprecate_me_1, :message => "This method is to be completely removed", :removal_version => "4.2" 20 | 21 | def deprecate_me_2 22 | puts "I've been deprecated! (2)" 23 | end 24 | deprecate :deprecate_me_2, :message => "This method is to be completely removed", :removal_date => "2020-02-20" 25 | end 26 | end 27 | 28 | if $0 == __FILE__ 29 | 30 | # turn off the individual alerting. To see more about the behavior, look at 31 | # examples/alert_frequency.rb 32 | Deprecatable.options.alert_frequency = :never 33 | 34 | # Make sure we have the at exit return turned on, it should be by default 35 | Deprecatable.options.has_at_exit_report = true 36 | 37 | b = A::B.new 38 | 39 | 4.times do 40 | # Context before 1.1 41 | # Context before 1.2 42 | b.deprecate_me_1 43 | # Context after 1.1 44 | # Context after 1.2 45 | 46 | # Context before 2.1 47 | # Context before 2.2 48 | b.deprecate_me_2 49 | # Context after 2.1 50 | # Context after 2.2 51 | end 52 | 53 | # do a bunch of things 54 | 55 | 2.times do 56 | # Context before 3.1 57 | # Context before 3.2 58 | b.deprecate_me_1 59 | # Context after 3.1 60 | # Context after 3.2 61 | 62 | # Context before 4.1 63 | # Context before 4.2 64 | b.deprecate_me_2 65 | # Context after 4.1 66 | # Context after 4.2 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /examples/caller_context_padding.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # 3 | # An example showing how the caller_context_padding option works 4 | # 5 | # You probably need to run from the parent directory as: 6 | # 7 | # ruby -Ilib examples/caller_context_padding.rb 8 | # 9 | require 'deprecatable' 10 | 11 | #---------------------------------------------------------------------- 12 | # We create an example class with a deprecated method 13 | #---------------------------------------------------------------------- 14 | module A 15 | class B 16 | extend Deprecatable 17 | def initialize 18 | @call_count = 0 19 | end 20 | def deprecate_me 21 | @call_count += 1 22 | puts "deprecate_me call #{@call_count}" 23 | end 24 | deprecate :deprecate_me, :message => "This method is to be completely removed", :removal_version => "4.2" 25 | end 26 | end 27 | 28 | #---------------------------------------------------------------------- 29 | # usage, you can ignore this for now, this will get printed out if you 30 | # do not put any commandline arguments down 31 | #---------------------------------------------------------------------- 32 | def usage 33 | puts <<__ 34 | This is an example of showing how to affect the caller context padding 35 | You can change the caller_context_padding by 36 | 37 | 1) Setting `Deprecatable.options.caller_context_padding` in ruby code. 38 | 2) Setting the DEPRECATABLE_CALLER_CONTEXT_PADDING envionment variable. 39 | 40 | They may be set to an integer value that is >= 1 41 | When you use both (1) and (2) simultaneously, you will see that 42 | setting the environment variable always overrides the code. 43 | 44 | Here are some example ways to run this program" 45 | 46 | __ 47 | 48 | [ nil, "DEPRECATABLE_CALLER_CONTEXT_PADDING=" ].each do |env| 49 | (1..3).each do |env_setting| 50 | next if env.nil? and env_setting > 1 51 | (1..3).each do |cmd_line| 52 | next if env and (env_setting == cmd_line) 53 | parts = [ " " ] 54 | parts << "#{env}#{env_setting}" if env 55 | parts << "ruby -Ilib #{__FILE__} #{cmd_line}" 56 | puts parts.join(' ') 57 | end 58 | end 59 | end 60 | puts 61 | exit 1 62 | end 63 | 64 | if $0 == __FILE__ 65 | # Turning off the at exit report, for more information on them, 66 | # see the examples/at_exit.rb 67 | Deprecatable.options.has_at_exit_report = false 68 | 69 | # capture the parameters, we'll run if there is a commandline parameter 70 | # of if the environment variable is et 71 | caller_context_padding = ARGV.shift 72 | usage unless caller_context_padding || ENV['DEPRECATABLE_CALLER_CONTEXT_PADDING'] 73 | Deprecatable.options.caller_context_padding = Float( caller_context_padding ).to_i if caller_context_padding 74 | 75 | puts 76 | puts "Running with ENV['DEPRECATABLE_CALLER_CONTEXT_PADDING'] => #{ENV['DEPRECATABLE_CALLER_CONTEXT_PADDING']}" 77 | puts "Running with Deprecatable.options.caller_context_padding => #{Deprecatable.options.caller_context_padding}" 78 | puts "-" * 72 79 | puts 80 | 81 | b = A::B.new 82 | 4.times do 83 | # Context before 1.1 84 | # Context before 1.2 85 | b.deprecate_me 86 | # Context after 1.1 87 | # Context after 1.2 88 | end 89 | 90 | 2.times do 91 | # Context before 2.1 92 | # Context before 2.2 93 | b.deprecate_me 94 | # Context after 2.1 95 | # Context after 2.2 96 | end 97 | end 98 | -------------------------------------------------------------------------------- /lib/deprecatable.rb: -------------------------------------------------------------------------------- 1 | require 'deprecatable/options' 2 | require 'deprecatable/registry' 3 | require 'deprecatable/alerter' 4 | 5 | # Allow methods to be deprecated and record and alert when those 6 | # deprecated methods are called. 7 | # 8 | # There are configurable options for the extended class: 9 | # 10 | # For example: 11 | # 12 | # class Foo 13 | # extend Deprecatable 14 | # 15 | # def bar 16 | # ... 17 | # end 18 | # 19 | # deprecate :bar, :message => "Foo#bar has been deprecated, use Foo#foo instead" 20 | # 21 | # end 22 | # 23 | module Deprecatable 24 | 25 | VERSION = '1.0.0' 26 | 27 | # Public: Deprecate a method in the included class. 28 | # 29 | # method_name - The method in this class to deprecate. 30 | # options - a hash of the current understood options (default: {}) 31 | # :message - A String to output along with the rest of 32 | # the notifcations about the deprecated 33 | # method. 34 | # :removal_date - The date on which the deprecated method 35 | # will be removed. 36 | # :removal_version - The version on which the deprecated 37 | # method will be removed. 38 | # 39 | # returns the instance of DeprecatedMethod created to track this deprecation. 40 | def deprecate( method_name, options = {} ) 41 | file, line = Util.location_of_caller 42 | dm = DeprecatedMethod.new( self, method_name, file, line, options ) 43 | 44 | Deprecatable.registry.register( dm ) 45 | 46 | return dm 47 | end 48 | 49 | # The global Deprecatable::Registry instance. It is set here so it is 50 | # allocated at parse time. 51 | @registry = Deprecatable::Registry.new 52 | 53 | # Public: Get the global Deprecatable::Registry instance 54 | # 55 | # Returns the global Deprecatable::Registry instance. 56 | def self.registry 57 | @registry 58 | end 59 | 60 | # The global options for Deprecatable. It is set here so it is allocated at 61 | # parse time. 62 | @options = Deprecatable::Options.new 63 | 64 | # Public: Access the global Options 65 | # 66 | # Returns the global Deprecatable::Options instance. 67 | def self.options 68 | @options 69 | end 70 | 71 | # The global Alerter for Deprecatable. It is set here so it is allocated at 72 | # parse time. 73 | @alerter = Deprecatable::Alerter.new 74 | 75 | # Public: Access the global Alerter 76 | # 77 | # Returns the global Alerter instance 78 | def self.alerter 79 | @alerter 80 | end 81 | 82 | # Public: Set the global Alerter 83 | # 84 | # alerter - Generally an instance of Alerter, but may be anything that 85 | # responds_to? both :alert and :report. See the Alerter 86 | # documetation for more information 87 | # 88 | # Returns nothing. 89 | def self.alerter=( a ) 90 | @alerter = a 91 | end 92 | end 93 | 94 | require 'deprecatable/util' 95 | require 'deprecatable/call_site_context' 96 | require 'deprecatable/call_site' 97 | require 'deprecatable/deprecated_method' 98 | 99 | # The at_exit handler is set at all times, and it will always fire, unless the 100 | # process is killed with prejudice and/or the ruby process exists using 'exit!' 101 | # instead of the normal 'exit' 102 | at_exit do 103 | if ::Deprecatable.options.has_at_exit_report? then 104 | ::Deprecatable.alerter.final_report 105 | end 106 | end 107 | -------------------------------------------------------------------------------- /lib/deprecatable/alerter.rb: -------------------------------------------------------------------------------- 1 | require 'stringio' 2 | module Deprecatable 3 | # An Alerter formats and emits alerts, and formats and emits reports. 4 | # 5 | # If you wish to impelement your own Alerter class, then it must implement 6 | # the following methods: 7 | # 8 | # * alert( DeprecatedMethod, CallSite ) 9 | # * final_reort() 10 | # 11 | # These are the two methods that are invoked by the Deprecatable system at 12 | # various points. 13 | # 14 | class Alerter 15 | # Public: Alert that the deprecated method was invoked at a specific call 16 | # site. 17 | # 18 | # deprecated_method - an instance of DeprecatedMethod 19 | # call_site - an instance of CallSite showing this particular 20 | # invocation 21 | # 22 | # Returns nothing. 23 | def alert( deprecated_method, call_site ) 24 | lines = deprecated_method_report( deprecated_method, call_site ) 25 | lines << "To turn this report off do one of the following:" 26 | lines << "* in your ruby code set `Deprecatable.options.alert_frequency = :never`" 27 | lines << "* set the environment variable `DEPRECATABLE_ALERT_FREQUENCY=\"never\"`" 28 | lines << "" 29 | lines.each { |l| warn_with_prefix l } 30 | end 31 | 32 | # Public: Render the final deprecation report showing when and where all 33 | # deprecated methods in the Registry were calld. 34 | # 35 | # registry - An instance of Deprecatable::Registry 36 | # (default: Deprecatable.registry) 37 | # 38 | # Returns nothing. 39 | def final_report( registry = Deprecatable.registry ) 40 | lines = [ "Deprecatable 'at_exit' Report", 41 | "=============================" ] 42 | lines << "" 43 | lines << "To turn this report off do one of the following:" 44 | lines << "" 45 | lines << "* in your ruby code set `Deprecatable.options.has_at_exit_report = false`" 46 | lines << "* set the environment variable `DEPRECATABLE_HAS_AT_EXIT_REPORT=\"false\"`" 47 | lines << "" 48 | 49 | registry.items.each do |dm| 50 | lines += deprecated_method_report( dm ) 51 | end 52 | lines.each { |l| warn_without_prefix l } 53 | end 54 | 55 | ################################################################### 56 | private 57 | ################################################################### 58 | 59 | # Format a report of the data in a DeprecatedMethod 60 | # 61 | # dm - A DeprecatedMethod instance 62 | # call_site - A CallSite instance (default :nil) 63 | # 64 | # Returns an Array of Strings which are the lines of the report. 65 | def deprecated_method_report( dm, call_site = nil ) 66 | m = "`#{dm.klass}##{dm.method}`" 67 | lines = [ m ] 68 | lines << "-" * m.length 69 | lines << "" 70 | lines << "* Originally defined at #{dm.file}:#{dm.line_number}" 71 | 72 | if msg = dm.message then 73 | lines << "* #{msg}" 74 | end 75 | if rd = dm.removal_date then 76 | lines << "* Will be removed after #{rd}" 77 | end 78 | 79 | if rv = dm.removal_version then 80 | lines << "* Will be removed in version #{rv}" 81 | end 82 | lines << "" 83 | 84 | if call_site then 85 | lines += call_site_report( call_site ) 86 | else 87 | dm.call_sites.each do |cs| 88 | lines += call_site_report( cs, true ) 89 | end 90 | end 91 | return lines 92 | end 93 | 94 | # Format a report about a CallSite 95 | # 96 | # cs - A CallSite instance 97 | # include_count - Should the report include the invocation count from the 98 | # CallSite instance. (default: false) 99 | # 100 | # Returns an Array of Strings which are the lines of the report. 101 | def call_site_report( cs, include_count = false ) 102 | header = [ "Called" ] 103 | header << "#{cs.invocation_count} time(s)" if include_count 104 | header << "from #{cs.file}:#{cs.line_number}" 105 | 106 | lines = [ header.join(' ') ] 107 | lines << "" 108 | cs.formatted_context_lines.each do |l| 109 | lines << " #{l.rstrip}" 110 | end 111 | lines << "" 112 | return lines 113 | end 114 | 115 | # Emit a warning message without a prefix to the message. 116 | # 117 | # Returns nothing. 118 | def warn_without_prefix( msg = "" ) 119 | warn msg 120 | end 121 | 122 | # Emit a warning message WITH a prefix to the message. 123 | # 124 | # Returns nothing. 125 | def warn_with_prefix( msg = "" ) 126 | warn "DEPRECATION WARNING: #{msg}" 127 | end 128 | 129 | # Emit a warning message. 130 | # 131 | # Returns nothing. 132 | def warn( msg ) 133 | Kernel.warn( msg ) 134 | end 135 | end 136 | 137 | # StringIOAlerter is used to capture all alerts in an instance of StringIO 138 | # instead of emitting them as Ruby warnings. This is mainly used in testing, 139 | # and may have uses in other situations too. 140 | class StringIOAlerter < Alerter 141 | # Initialize the StringIOAlerter 142 | # 143 | # Returns nothing. 144 | def initialize 145 | @stringio = StringIO.new 146 | end 147 | 148 | # Capture the warning into the StringIO instance 149 | # 150 | # Returns nothing. 151 | def warn( msg ) 152 | @stringio.puts msg 153 | end 154 | 155 | # Access the contens of the internal StringIO instance. 156 | # 157 | # Returns a String containing all the warnings so far. 158 | def to_s 159 | @stringio.string 160 | end 161 | end 162 | end 163 | -------------------------------------------------------------------------------- /lib/deprecatable/call_site.rb: -------------------------------------------------------------------------------- 1 | require 'deprecatable/call_site_context' 2 | module Deprecatable 3 | # CallSite represents a location in the source code where a DeprecatedMethod 4 | # was invoked. It contains the location of the call site, the number of times 5 | # that it was invoked, and an extraction of the source code around the 6 | # invocation site 7 | class CallSite 8 | # Generate the hash key for a call site with the given file and line 9 | # number. 10 | # 11 | # file - A String that is the filesystem path to a file. 12 | # line_number - An Integer that is the line number in the given file. 13 | # 14 | # Returns a String that is generally used as a unique key. 15 | def self.gen_key( file, line_number ) 16 | "#{file}:#{line_number}" 17 | end 18 | 19 | # Public: Get the fully expand path of the file of the CallSite 20 | # 21 | # Returns the String filesystem path of the file. 22 | attr_reader :file 23 | 24 | # Public: Get the line number of the CallSite in the file. 25 | # Line numbers start at 1. 26 | # 27 | # Returns the line number of a line in the file. 28 | attr_reader :line_number 29 | 30 | # Public: Gets the number of lines before and after the line_nubmer 31 | # to also capture when gettin the context. 32 | # 33 | # This number is the number both before AND after 'line_number' to 34 | # capture. If this number is 2, then the total number of lines captured 35 | # should be 5. 2 before, the line in question, and 2 after. 36 | # 37 | # Returns the number of lines 38 | attr_reader :context_padding 39 | 40 | # Public: The number of times this CallSite has been invoked. 41 | # 42 | # Returns the Integer number of times this call site has been invoked. 43 | attr_reader :invocation_count 44 | 45 | # Create a new instance of CallSite 46 | # 47 | # file - A String pathname of the file where the CallSite 48 | # happend 49 | # line_number - The Integer line number in the file. 50 | # context_padding - The Integer number of lines both before and after 51 | # the 'line_nubmer' to capture. 52 | def initialize( file, line_number, context_padding ) 53 | @file = File.expand_path( file ) 54 | @line_number = line_number 55 | @context_padding = context_padding 56 | @invocation_count = 0 57 | end 58 | 59 | # The unique identifier of this CallSite. 60 | # 61 | # Returns the String key of this CallSite. 62 | def key 63 | CallSite.gen_key( file, line_number ) 64 | end 65 | 66 | # Increment the invocation count by the amount given 67 | # 68 | # count - The amount to increment the invocation count by 69 | # This should rarely, if ever be set. 70 | # (default: 1) 71 | # 72 | # Returns the Integer invocation count. 73 | def increment_invocation_count( count = 1 ) 74 | @invocation_count += count 75 | end 76 | 77 | # Retrieve the lazily loaded CallSiteContext. 78 | # 79 | # Returns an instances of CallSiteContext 80 | def context 81 | @context ||= CallSiteContext.new( @file, @line_number, @context_padding ) 82 | end 83 | 84 | # Access the lines of the context in a nicely formatted way. 85 | # 86 | # Returns an Array of Strings containing the formatted context. 87 | def formatted_context_lines 88 | context.formatted_context_lines 89 | end 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /lib/deprecatable/call_site_context.rb: -------------------------------------------------------------------------------- 1 | module Deprecatable 2 | # CallSiteContext captures the actual file context of the call site. 3 | # It goes to the source file and extracts the lines around the line in 4 | # question with the given padding and keeps it available for emitting 5 | class CallSiteContext 6 | # Public: The raw lines from the source file containing the context. 7 | # 8 | # Returns an Array of Strings of the lines from the file. 9 | attr_reader :context_lines 10 | 11 | # Public: The raw line numbers from the source file. The lines of 12 | # a source file start with 1. This is a parallel array to 'context_lines' 13 | # 14 | # Returns an Array of Integers of the line numbers from the flie. 15 | attr_reader :context_line_numbers 16 | 17 | # The marker used to prefix the formatted context line of the exact line of 18 | # the context where the CallSite took place 19 | # 20 | # Returns a String. 21 | def self.pointer 22 | "--->" 23 | end 24 | 25 | # The prefix to put in front of the CallSite context padding lines. 26 | # 27 | # Returns a String of blanks the same length as 'pointer' 28 | def self.not_pointer 29 | " " * pointer.length 30 | end 31 | 32 | # Create a new CallSiteContext. Upon instantiation, this will go to the 33 | # source file in question, and extract the CallSite line and a certain 34 | # number of 'padding' lines around it. 35 | # 36 | # file - The String pathname of the file from which to extract lines. 37 | # line_number - The 1 indexed line number within the file to be the center 38 | # of the extracted context. 39 | # padding - The Number of lines before and after 'line_number' to 40 | # extract along with the text at 'line_number' 41 | # 42 | # Returns nothing. 43 | def initialize( file, line_number, padding ) 44 | @file = file 45 | @line_number = line_number 46 | @padding = padding 47 | 48 | @context_line_numbers = [] 49 | @context_lines = [] 50 | @context_index = @padding + 1 51 | @formatted_context_lines = [] 52 | 53 | extract_context() 54 | end 55 | 56 | # Nicely format the context lines extracted from the file. 57 | # 58 | # Returns an Array of Strings containing the formatted lines. 59 | def formatted_context_lines 60 | if @formatted_context_lines.empty? then 61 | number_width = ("%d" % @context_line_numbers.last).length 62 | @context_lines.each_with_index do |line, idx| 63 | prefix = (idx == @context_index) ? CallSiteContext.pointer : CallSiteContext.not_pointer 64 | number = ("%d" % @context_line_numbers[idx]).rjust( number_width ) 65 | @formatted_context_lines << "#{prefix} #{number}: #{line}" 66 | end 67 | end 68 | return @formatted_context_lines 69 | end 70 | 71 | ########################################################################### 72 | private 73 | ########################################################################### 74 | 75 | # Extract the context from the source file. This goes to the file in 76 | # question, and extracts the line_number and the padding lines both 77 | # before and after the line_number. If the padding would cause the context 78 | # to go before the first line of the file, or after the last line of the 79 | # file, the padding is truncated accordingly. 80 | # 81 | # The result of this operation is the setting of many instance 82 | # variables 83 | # 84 | # @context_lines - An Array of String containging the line_number 85 | # line from the file and the 'padding' lines 86 | # before and after it. 87 | # @context_index - The index into @context_lines of the 88 | # line_number line from the file 89 | # @context_line_numbers - An Array of Integers that paralles 90 | # @context_lines contianing the 1 indexed line 91 | # numbers from the file corresponding to the lines 92 | # in @context_lines. 93 | # 94 | # Returns nothing. 95 | def extract_context 96 | if File.readable?( @file ) then 97 | file_lines = IO.readlines( @file ) 98 | @line_index = @line_number - 1 99 | 100 | start_line = @line_index - @padding 101 | start_line = 0 if start_line < 0 102 | 103 | stop_line = @line_index + @padding 104 | stop_line = (file_lines.size - 1) if stop_line >= file_lines.size 105 | 106 | @context_index = @line_index - start_line 107 | @context_line_numbers = (start_line+1..stop_line+1).to_a 108 | @context_lines = file_lines[start_line, @context_line_numbers.size] 109 | end 110 | end 111 | end 112 | end 113 | -------------------------------------------------------------------------------- /lib/deprecatable/deprecated_method.rb: -------------------------------------------------------------------------------- 1 | require 'deprecatable/util' 2 | require 'deprecatable/call_site' 3 | module Deprecatable 4 | # DeprecatedMethod holds all the information about a method that was marked 5 | # as 'deprecated' through the Deprecatable Module. The Class, method name, 6 | # and the file and line number of the deprecated method are stored in 7 | # DeprecatedMethod. 8 | # 9 | # It also is the location in which the calls to the deprected method are 10 | # stored. Each call to the deprecated method ends up with a call to 11 | # 'log_invocation'. The 'log_invocation' method records the CallSite of 12 | # where the deprecated method was called, and the number of times that the 13 | # deprecated method was called from that CallSite. 14 | # 15 | # In general, the first time a deprecated method is called from a particular 16 | # CallSite, the Alerter is invoked to report the invocation. All subsequent 17 | # calls to the deprecated method do not alert, although the invocation count 18 | # is increased. This behavior may be altered through the Deprecatable::Options 19 | # instance at Deprecatable.options. 20 | # 21 | class DeprecatedMethod 22 | include Util 23 | 24 | # Public: The Ruby class that has the method being deprecated. 25 | # 26 | # Returns the Class whos method is being deprecated. 27 | attr_reader :klass 28 | 29 | # Public: The method in the klass being deprecated. 30 | # 31 | # Returns the Symbol of the method being deprecated. 32 | attr_reader :method 33 | 34 | # Public: The filesystem path of the file where the deprecation took place. 35 | # 36 | # Returns the String path to the file. 37 | attr_reader :file 38 | 39 | # Public: The line number in the file where hte deprecation took place. This 40 | # is a 1 indexed value. 41 | # 42 | # Returns the Integer line number. 43 | attr_reader :line_number 44 | 45 | # Public: The additional message to output with the alerts and reports. 46 | # 47 | # Returns the String message. 48 | attr_reader :message 49 | 50 | # Public: The date on which the deprecate method will be removed. 51 | # 52 | # Returns the removal date. 53 | attr_reader :removal_date 54 | 55 | # Public: The version of the software which will no longer have the 56 | # deprecated method. 57 | # 58 | # Returns the version number. 59 | attr_reader :removal_version 60 | 61 | # The aliased name of the method being deprecated. This is what is called by 62 | # the wrapper to invoke the original deprecated method. 63 | # 64 | # Returns the String method name. 65 | attr_reader :deprecated_method_name 66 | 67 | # Create a new DeprecatedMethod. 68 | # 69 | # klass - The Class containing the deprecated method. 70 | # method - The Symbol method name of the deprecated method. 71 | # file - The String filesystem path where the deprecation took place. 72 | # line_number - The Integer line in the file where hte deprecation took 73 | # place. 74 | # options - The Hash optional parameters (default: {}) 75 | # :message - A String to output along with the rest of 76 | # the notifcations about the deprecated 77 | # method. 78 | # :removal_date - The date on which the deprecated method 79 | # will be removed. 80 | # :removal_version - The version on which the deprecated 81 | # method will be removed. 82 | def initialize( klass, method, file, line_number, options = {} ) 83 | @klass = klass 84 | @method = method 85 | @file = File.expand_path( file ) 86 | @line_number = Float(line_number).to_i 87 | @deprecated_method_name = "_deprecated_#{method}" 88 | @invocations = 0 89 | @call_sites = Hash.new 90 | @message = options[:message] 91 | @removal_date = options[:removal_date] 92 | @removal_version = options[:removal_version] 93 | @to_s = nil 94 | insert_shim( self ) 95 | end 96 | 97 | # Format the DeprecatedMethod as a String. 98 | # 99 | # Returns the DeprecatedMethod as a String. 100 | def to_s 101 | unless @to_s then 102 | target = @klass.kind_of?( Class ) ? "#{@klass.name}#" : "#{@klass.name}." 103 | @to_s = "#{target}#{@method} defined at #{@file}:#{@line_number}" 104 | end 105 | return @to_s 106 | end 107 | 108 | # Log the invocation of the DeprecatedMethod at the given CallSite. Alert 109 | # the media. 110 | # 111 | # file - The String path to the file in which the DeprecatedMethod 112 | # was invoked. 113 | # line_number - The Integer line_number in the file on which the 114 | # DeprecatedMethod was invoked. 115 | # 116 | # Returns nothing. 117 | def log_invocation( file, line_number ) 118 | call_site = call_site_for( file, line_number ) 119 | call_site.increment_invocation_count 120 | alert( call_site ) 121 | end 122 | 123 | # Tell the Deprecatable.alerter to alert if the number of invocations at the 124 | # CallSite is less than or equal to the alert frequency. 125 | # 126 | # call_site - The CallSite instance representing where this DeprecatedMethod 127 | # was invoked. 128 | # 129 | # Returns nothing. 130 | def alert( call_site ) 131 | if call_site.invocation_count <= ::Deprecatable.options.alert_frequency then 132 | ::Deprecatable.alerter.alert( self, call_site ) 133 | end 134 | end 135 | 136 | # Gets the lines of all the CallSites where this DeprecatedMethod was 137 | # invoked. 138 | # 139 | # Returns an Array of CallSite instances. 140 | def call_sites 141 | @call_sites.values 142 | end 143 | 144 | # Gets the sum total of all the invocations of this DeprecatedMethod. 145 | # 146 | # Returns the Integer count of invocations. 147 | def invocation_count 148 | sum = 0 149 | @call_sites.values.each { |cs| sum += cs.invocation_count } 150 | return sum 151 | end 152 | 153 | # Gets the unique count of CallSites representing the unique number of 154 | # locations where this DeprecatedMethod was invoked. 155 | # 156 | # Returnts the Integer unique count of CallSites. 157 | def call_site_count 158 | @call_sites.size 159 | end 160 | 161 | ################################################################### 162 | private 163 | ################################################################### 164 | 165 | # Find the CallSite representing the given file and line_number. It creates 166 | # a new CallSite instance if necessary. 167 | # 168 | # file - The String path to the file in which the DeprecatedMethod 169 | # was invoked. 170 | # line_number - The Integer line_number in the file on which the 171 | # DeprecatedMethod was invoked. 172 | # 173 | # Returns the CallSite representing the give file and line number. 174 | def call_site_for( file, line_number ) 175 | cs = @call_sites[CallSite.gen_key( file, line_number)] 176 | if cs.nil? then 177 | cs = CallSite.new( file, line_number, ::Deprecatable.options.caller_context_padding ) 178 | @call_sites[cs.key] = cs 179 | end 180 | return cs 181 | end 182 | 183 | # Create the wrapper method that replaces the deprecated method. This is 184 | # where the magic happens. 185 | # 186 | # This does the following: 187 | # 188 | # 1) aliases the deprecated method to a new method name 189 | # 2) creates a new method with the original name that 190 | # 1) logs the invocation of the deprecated method 191 | # 2) calls the original deprecated method. 192 | # 193 | # Returns nothing. 194 | def insert_shim( dm ) 195 | if not klass.method_defined?( dm.deprecated_method_name ) then 196 | 197 | klass.module_eval do 198 | 199 | alias_method dm.deprecated_method_name, dm.method 200 | 201 | define_method( dm.method ) do |*args, &block| 202 | dm.log_invocation( *Util.location_of_caller ) 203 | send( dm.deprecated_method_name, *args, &block ) 204 | end 205 | 206 | end 207 | end 208 | end 209 | end 210 | end 211 | -------------------------------------------------------------------------------- /lib/deprecatable/options.rb: -------------------------------------------------------------------------------- 1 | module Deprecatable 2 | # A Container for the options of Deprecatable. 3 | # 4 | # The available options are: 5 | # 6 | # caller_context_padding - The number of lines before and after the call 7 | # site of the deprecated method to record 8 | # alert_frequency - The maximum number of times to alert for a given 9 | # call to the deprecated method 10 | # at_exit_report - Whether or not a deprecation report is issued 11 | # when the program exits 12 | # 13 | # These options may also be overridden with environment varaibles 14 | # 15 | # DEPRECATABLE_CALLER_CONTEXT_PADDING 16 | # DEPRECATABLE_ALERT_FREQUENCY 17 | # DEPRECATABLE_AT_EXIT_REPORT 18 | # 19 | class Options 20 | # Create a new instance of Options. All of the default values for the 21 | # options are set. 22 | # 23 | # caller_context_padding - 2 24 | # has_at_exit_report - true 25 | # alert_frequency - 1 26 | def initialize 27 | reset 28 | end 29 | 30 | # Reset the options to their default values. 31 | # 32 | # Returns nothing. 33 | def reset 34 | @caller_context_padding = 2 35 | @has_at_exit_report = true 36 | @alert_frequency = 1 37 | end 38 | 39 | # Public: Set the number of lines of context surrounding the call site of 40 | # the deprecated method to display in the alerts and reports. (default: 2) 41 | # 42 | # count - The number of lines before and after the callsite to report. 43 | # This must be a positive number. 44 | # 45 | # Returns the count. 46 | def caller_context_padding=( count ) 47 | raise ArgumentError, "caller_content_padding must have a count > 0" unless count > 0 48 | @caller_context_padding = count 49 | end 50 | 51 | # Public: Get the number of lines of context padding. 52 | # 53 | # This may be overridden with the environment variable 54 | # DEPRECATABLE_CALLER_CONTEXT_PADDING. 55 | # 56 | # Returns the Integer number of context padding lines. 57 | def caller_context_padding 58 | p = ENV['DEPRECATABLE_CALLER_CONTEXT_PADDING'] 59 | if p then 60 | p = Float(p).to_i 61 | raise ArgumentError, "DEPRECATABLE_CALLER_CONTEXT_APDDING must have a value > 0, it is currently #{p}" unless p > 0 62 | return p 63 | end 64 | return @caller_context_padding 65 | end 66 | 67 | # Public: Set the maximum number of times an alert for a unqiue CallSite 68 | # of a DeprecatedMethod will be emitted. (default: :once) 69 | # 70 | # That is, when a deprecated method is called from a particular CallSite, 71 | # normally an 'alert' is sent. This setting controls the maximum number of 72 | # times that the 'alert' for a particular CallSite is emitted. 73 | # 74 | # freq - The alert frequency. This may be set to any number, or to one of 75 | # the special token values: 76 | # 77 | # :never - Never send any alerts 78 | # :once - Send an alert for a given CallSite only once. 79 | # :always - Send an alert for every invocation of the 80 | # DeprecatedMethod. 81 | # 82 | # Returns the alert_frequency. 83 | def alert_frequency=( freq ) 84 | @alert_frequency = frequency_of( freq ) 85 | end 86 | 87 | # Public: Get the current value of the alert_frequency. 88 | # 89 | # This may be overridden with the environment variable 90 | # DEPRECATABLE_ALERT_FREQUENCY. 91 | # 92 | # Returns the Integer value representing the alert_frequency. 93 | def alert_frequency 94 | p = ENV['DEPRECATABLE_ALERT_FREQUENCY'] 95 | return frequency_of(p) if p 96 | return @alert_frequency 97 | end 98 | 99 | # Public: Set whether or not the final at_exit_report should be emitted 100 | # 101 | # bool - true or false, shall the exit report be emitted. 102 | # 103 | # Returns the value set. 104 | attr_writer :has_at_exit_report 105 | 106 | # Public: Say whether or not the final at exit report shall be emitted. 107 | # 108 | # This may be overridden by the environment variable 109 | # DEPRECATABLE_HAS_AT_EXIT_REPORT. Setting the environment variable to 110 | # 'true' will override the existing setting. 111 | # 112 | # Returns the boolean of whether or not the exti report should be done. 113 | def has_at_exit_report? 114 | return true if ENV['DEPRECATABLE_HAS_AT_EXIT_REPORT'] == "true" 115 | return @has_at_exit_report 116 | end 117 | 118 | ################################################################## 119 | private 120 | ################################################################## 121 | 122 | # Convert the given frequency Symbol/String into its Numeric representation. 123 | # 124 | # Return the Numeric value of the input frequency. 125 | def frequency_of( frequency ) 126 | case frequency.to_s 127 | when 'always' 128 | (1.0/0.0) 129 | when 'once' 130 | 1 131 | when 'never' 132 | 0 133 | else 134 | Float(frequency).to_i 135 | end 136 | end 137 | end 138 | end 139 | -------------------------------------------------------------------------------- /lib/deprecatable/registry.rb: -------------------------------------------------------------------------------- 1 | module Deprecatable 2 | # The Registry is a container of unique DeprecatedMethod instances. 3 | # Normally there is only one in existence and it is accessed via 4 | # 'Deprecatable.registry' 5 | class Registry 6 | # Initialize the Registry, which amounts to creating a new Hash. 7 | # 8 | # Returns nothing. 9 | def initialize 10 | @registry = Hash.new 11 | end 12 | 13 | # Public: Return the number of instances of DeprecatedMethod there are in 14 | # the Registry. 15 | # 16 | # Returns an Integer of the size of the Regsitry. 17 | def size() 18 | @registry.size 19 | end 20 | 21 | # Public: Iterate over all items in the Registry 22 | # 23 | # Yields each DeprecatedMethod in the Registry 24 | # Returns nothing. 25 | def each 26 | items.each do |i| 27 | yield i 28 | end 29 | end 30 | 31 | # Public: Remove all items from the Registry. 32 | # 33 | # Returns nothing. 34 | def clear 35 | @registry.clear 36 | end 37 | 38 | # Public: Register a method to be deprecated. 39 | # 40 | # method - An instance of DeprecatedMethod 41 | # 42 | # Returns the instance that was passed in. 43 | def register( dm ) 44 | @registry[dm] = true 45 | return dm 46 | end 47 | 48 | # Public: Return all the DeprecatedMethod instances in the registry. 49 | # 50 | # Returns an Array of DeprecatedMethod instances. 51 | def items 52 | @registry.keys 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /lib/deprecatable/util.rb: -------------------------------------------------------------------------------- 1 | module Deprecatable 2 | # Common utility functions used by all the Deprecatable modules and classes 3 | module Util 4 | # Find the caller of the method that called the method where 5 | # location_of_call was invoked. 6 | # 7 | # Example: 8 | # 9 | # def foo 10 | # bar() # <--- this file and line number is returned 11 | # end 12 | # 13 | # def bar 14 | # Deprecatable::Util.location_of_caller 15 | # end 16 | # 17 | # Return the [ file, line number] tuple from which bar() was invoked. 18 | def self.location_of_caller 19 | call_line = caller[1] 20 | file, line, _ = call_line.split(':') 21 | file = File.expand_path( file ) 22 | line = Float(line).to_i 23 | return file, line 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /test/helpers.rb: -------------------------------------------------------------------------------- 1 | gem "minitest" # required for ruby 1.9 and 'rake rcov' 2 | require 'minitest/autorun' 3 | require 'deprecatable' 4 | 5 | ::Deprecatable.options.has_at_exit_report = false 6 | 7 | module MiniTest 8 | class Unit 9 | class TestCase 10 | def assert_array_equal( expected, actual, msg = nil ) 11 | assert_equal( expected.size, actual.size, msg ) 12 | expected.each_with_index do |i, idx| 13 | assert_equal( i, actual[idx], msg ) 14 | end 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /test/test_deprecatable.rb: -------------------------------------------------------------------------------- 1 | require "helpers" 2 | 3 | class TestDeprecatable < MiniTest::Unit::TestCase 4 | 5 | def setup 6 | Deprecatable.registry.clear 7 | Deprecatable.options.reset 8 | 9 | @deprecated_class = Class.new do 10 | extend Deprecatable 11 | def self.name 12 | "TestDeprecatable::DeprecateMe" 13 | end 14 | def deprecate_me; end 15 | deprecate :deprecate_me 16 | end 17 | 18 | @deprecatable_class = Class.new do 19 | extend Deprecatable 20 | def self.name 21 | "TestDeprecatable::DeprecatableClass" 22 | end 23 | def deprecate_me; end 24 | end 25 | end 26 | 27 | def test_deprecated_method_is_regsitered 28 | assert_equal( 1, Deprecatable.registry.size ) 29 | end 30 | 31 | def test_call_site_is_recorded 32 | i = @deprecated_class.new 33 | capture_io do 34 | i.deprecate_me 35 | end 36 | assert_equal( 1, Deprecatable.registry.items.first.invocation_count ) 37 | end 38 | 39 | def test_different_call_sites_are_recorded_independently 40 | i = @deprecated_class.new 41 | capture_io do 42 | 42.times { i.deprecate_me } 43 | 24.times { i.deprecate_me } 44 | end 45 | dm = Deprecatable.registry.items.first 46 | assert_equal( 66, dm.invocation_count ) 47 | assert_equal( 2, dm.call_site_count ) 48 | end 49 | 50 | def test_alerts_are_issued_only_once_for_a_callsite 51 | i = @deprecated_class.new 52 | callable = lambda { |c| c.deprecate_me } 53 | stdout, stderr = capture_io do 54 | callable.call( i ) 55 | end 56 | line = __LINE__ - 4 57 | 58 | assert_match( /#{File.expand_path( __FILE__ )}:#{line}/m, stderr ) 59 | assert_match( /---> #{line}:\s+callable = lambda \{ |c| c\.deprecate_me \}/, stderr ) 60 | 61 | assert_silent do 62 | callable.call( i ) 63 | end 64 | end 65 | 66 | def test_alerts_are_issued_never_for_a_callsite 67 | ::Deprecatable.options.alert_frequency = :never 68 | i = @deprecated_class.new 69 | assert_silent do 70 | i.deprecate_me 71 | end 72 | end 73 | 74 | def test_alerts_are_issued_for_every_call_to_a_callsite 75 | ::Deprecatable.options.alert_frequency = :always 76 | i = @deprecated_class.new 77 | stdout, stderr = capture_io do 78 | 42.times { i.deprecate_me } 79 | end 80 | line = __LINE__ - 2 81 | lines = stderr.split(/\n/) 82 | assert_equal( 42, lines.grep( /#{File.expand_path( __FILE__)}:#{line}/ ).size ) 83 | assert_equal( 42, lines.grep( /--->/ ).size ) 84 | end 85 | 86 | def test_raise_an_exception_if_deprecating_a_method_that_does_not_exist 87 | assert_raises( NameError ) do 88 | @deprecatable_class.deprecate :wibble 89 | end 90 | end 91 | 92 | def assert_alert_match( regex, klass, &block ) 93 | stdout, stderr = capture_io do 94 | i = klass.new 95 | i.deprecate_me 96 | end 97 | assert_match( regex, stderr ) 98 | return stderr 99 | end 100 | 101 | def test_adds_an_additional_message_when_given 102 | @deprecatable_class.deprecate :deprecate_me, :message => "You should switch to using Something#bar" 103 | assert_alert_match( /\* You should switch to using Something#bar/m, @deprecatable_class ) 104 | end 105 | 106 | def test_adds_a_removal_date_when_given 107 | @deprecatable_class.deprecate :deprecate_me, :removal_date => "2011-09-02" 108 | assert_alert_match( /Will be removed after 2011-09-02/m, @deprecatable_class ) 109 | end 110 | 111 | def test_adds_a_removal_version_when_given 112 | @deprecatable_class.deprecate :deprecate_me, :removal_version => "4.2" 113 | assert_alert_match( /Will be removed in version 4.2/m, @deprecatable_class ) 114 | end 115 | 116 | def test_deprecating_an_included_method 117 | mod = Module.new do 118 | extend Deprecatable 119 | def deprecate_me; end 120 | deprecate :deprecate_me, :message => "KABOOM!" 121 | end 122 | klass = Class.new do 123 | include mod 124 | end 125 | 126 | assert_alert_match( /KABOOM!/, klass ) 127 | end 128 | 129 | def test_deprecating_a_class_method 130 | klass = Class.new do 131 | class << self 132 | extend Deprecatable 133 | def deprecate_me; end 134 | deprecate :deprecate_me, :message => "Class Method KABOOM!" 135 | end 136 | end 137 | 138 | stdout, stderr = capture_io do 139 | klass.deprecate_me 140 | end 141 | assert_match( /Class Method KABOOM/, stderr ) 142 | end 143 | 144 | def test_has_an_at_exit_handler 145 | io = IO.popen( "ruby -Ilib examples/at_exit.rb 2>&1" ) 146 | lines = io.readlines.join('') 147 | assert_match( /Deprecatable 'at_exit' Report/m, lines ) 148 | end 149 | end 150 | -------------------------------------------------------------------------------- /test/test_deprecatable_alerter.rb: -------------------------------------------------------------------------------- 1 | require 'helpers' 2 | 3 | class TestDeprecatableAlerter < MiniTest::Unit::TestCase 4 | def setup 5 | Deprecatable.registry.clear 6 | Deprecatable.options.reset 7 | 8 | @old_alerter = Deprecatable.alerter 9 | @alerter = ::Deprecatable::StringIOAlerter.new 10 | Deprecatable.alerter = @alerter 11 | 12 | @deprecated_class = Class.new do 13 | extend Deprecatable 14 | def self.name 15 | "TestDeprecatable::DeprecateMe" 16 | end 17 | def deprecate_me; end 18 | deprecate :deprecate_me 19 | end 20 | end 21 | 22 | def teardown 23 | Deprecatable.alerter = @old_alerter 24 | end 25 | 26 | def test_alert 27 | i = @deprecated_class.new 28 | i.deprecate_me 29 | 30 | assert_match( /---> #{__LINE__ - 2}:/, Deprecatable.alerter.to_s ) 31 | end 32 | 33 | def test_final_report 34 | i = @deprecated_class.new 35 | 12.times { i.deprecate_me } 36 | 10.times { i.deprecate_me } 37 | Deprecatable.alerter.final_report 38 | assert_match( /Called 12 time/, Deprecatable.alerter.to_s ) 39 | assert_match( /Called 10 time/, Deprecatable.alerter.to_s ) 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /test/test_deprecatable_call_site.rb: -------------------------------------------------------------------------------- 1 | require 'helpers' 2 | # This file requires exact position of lines. If you should happent to chagen 3 | # tne numer of lines in this file that appear before the first test, some of the 4 | # tests will fail 5 | class TestDeprecatableCallSite < MiniTest::Unit::TestCase 6 | def setup 7 | @file = __FILE__ 8 | @before = 'This is the line before' 9 | @line = __LINE__ 10 | @after = 'This is the line after' 11 | @call_site = ::Deprecatable::CallSite.new( @file, @line, 1 ) 12 | end 13 | 14 | def test_initializes_with_a_filename_and_line_number 15 | assert_equal( File.expand_path( @file ), @call_site.file ) 16 | assert_equal( @line, @call_site.line_number ) 17 | assert_equal( 1, @call_site.context_padding ) 18 | end 19 | 20 | def test_captures_the_call_site_context 21 | context = [ 22 | " 8: @before = 'This is the line before'\n", 23 | "---> 9: @line = __LINE__\n", 24 | " 10: @after = 'This is the line after'\n", 25 | ] 26 | assert_array_equal( context, @call_site.formatted_context_lines ) 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /test/test_deprecatable_call_site_context.rb: -------------------------------------------------------------------------------- 1 | # Context line 1 2 | # Context line 2 3 | # Context line 3 4 | # Context line 4 5 | 6 | 7 | require 'helpers' 8 | 9 | # Context line 9 10 | # Context line 10 11 | # Context line 11 12 | # Context line 12 13 | # Context line 13 14 | 15 | class TestDeprecatableCallSiteContext < MiniTest::Unit::TestCase 16 | def setup 17 | @ctx = Deprecatable::CallSiteContext.new( File.expand_path(__FILE__), 11, 2 ) 18 | end 19 | 20 | def context_lines_for( range, arrow_line ) 21 | width = ("%d" % range.to_a.last).length 22 | range.map do |x| 23 | prefix = ( x == arrow_line ) ? "--->" : " " 24 | "#{prefix} #{("%d" % x).rjust( width )}: # Context line #{x}\n" 25 | end 26 | end 27 | 28 | def test_gets_the_context_from_the_middle_of_a_file 29 | context_lines = (9..13).map { |x| "# Context line #{x}\n" } 30 | assert_array_equal( context_lines, @ctx.context_lines ) 31 | end 32 | 33 | def test_properly_formats_the_context_from_the_middle_of_the_file 34 | formatted_lines = [ ] 35 | formatted_lines += context_lines_for( (9..13), 11 ) 36 | assert_array_equal( formatted_lines, @ctx.formatted_context_lines ) 37 | end 38 | 39 | def test_properly_formats_the_context_at_the_beginning_of_the_file 40 | ctx = Deprecatable::CallSiteContext.new( File.expand_path(__FILE__), 2, 2 ) 41 | formatted_lines = [ ] 42 | formatted_lines += context_lines_for( (1..4), 2 ) 43 | assert_array_equal( formatted_lines, ctx.formatted_context_lines ) 44 | end 45 | 46 | def test_properly_formats_the_context_at_the_end_of_the_file 47 | ctx = Deprecatable::CallSiteContext.new( File.expand_path(__FILE__), 56, 2 ) 48 | formatted_lines = [ ] 49 | formatted_lines += context_lines_for( (54..57), 56) 50 | assert_array_equal( formatted_lines, ctx.formatted_context_lines ) 51 | end 52 | end 53 | 54 | # Context line 54 55 | # Context line 55 56 | # Context line 56 57 | # Context line 57 58 | -------------------------------------------------------------------------------- /test/test_deprecatable_deprecated_method.rb: -------------------------------------------------------------------------------- 1 | require 'helpers' 2 | 3 | class TestDeprecatableDeprecatedMethod < MiniTest::Unit::TestCase 4 | def setup 5 | @klass = Class.new do 6 | def self.name 7 | "TestDeprecatableDeprecatedMethod::DeprecatableClass" 8 | end 9 | def m1 10 | "m1" 11 | end 12 | end 13 | 14 | @module = Module.new do 15 | def self.name 16 | "TestDeprecatableDeprecatedMethod::DeprecatableModule" 17 | end 18 | def m2; "m2"; end 19 | end 20 | 21 | @dep_class = Deprecatable::DeprecatedMethod.new( @klass, "m1", __FILE__, 7 ) 22 | end 23 | 24 | def test_records_meta_information_about_the_deprecation 25 | assert_equal( @klass , @dep_class.klass ) 26 | assert_equal( "m1" , @dep_class.method ) 27 | assert_equal( File.expand_path( __FILE__ ) , @dep_class.file ) 28 | assert_equal( 7 , @dep_class.line_number ) 29 | assert_equal( "_deprecated_m1" , @dep_class.deprecated_method_name ) 30 | end 31 | 32 | def test_records_an_invocation_of_an_instance_method 33 | dc = @klass.new 34 | m = nil 35 | capture_io do 36 | m = dc.m1 37 | end 38 | assert_equal( "m1", m ) 39 | assert_equal( 1, @dep_class.invocation_count ) 40 | end 41 | 42 | def test_records_uniq_call_sites 43 | dc = @klass.new 44 | capture_io do 45 | 3.times { dc.m1 } 46 | 3.times { dc.m1 } 47 | end 48 | assert_equal( 6, @dep_class.invocation_count ) 49 | assert_equal( 2, @dep_class.call_site_count ) 50 | assert_equal( 2, @dep_class.call_sites.size ) 51 | end 52 | 53 | def test_has_a_string_representation_of_a_deprecated_instance_method 54 | assert_equal( "TestDeprecatableDeprecatedMethod::DeprecatableClass#m1 defined at #{File.expand_path(__FILE__)}:7", @dep_class.to_s ) 55 | end 56 | 57 | def test_has_a_string_representation_of_a_deprecated_module_method 58 | dm = Deprecatable::DeprecatedMethod.new( @module, "m2", __FILE__, 11 ) 59 | assert_equal( "TestDeprecatableDeprecatedMethod::DeprecatableModule.m2 defined at #{File.expand_path(__FILE__)}:11", dm.to_s ) 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /test/test_deprecatable_options.rb: -------------------------------------------------------------------------------- 1 | require "helpers" 2 | 3 | class TestDeprecatableOptions < MiniTest::Unit::TestCase 4 | 5 | def setup 6 | @options = Deprecatable::Options.new 7 | end 8 | 9 | def teardown 10 | ENV.keys.each do |k| 11 | next unless k =~/^DEPRECATABLE/ 12 | ENV.delete( k ) 13 | end 14 | end 15 | 16 | def test_defaults_exist 17 | assert_equal( 2 , @options.caller_context_padding ) 18 | assert_equal( true , @options.has_at_exit_report? ) 19 | assert_equal( 1 , @options.alert_frequency ) 20 | end 21 | 22 | def test_caller_context_padding_raises_error_if_set_to_negative_number 23 | assert_raises( ArgumentError, "caller_context_mapping must be > 0" ) do 24 | @options.caller_context_padding = -1 25 | end 26 | end 27 | 28 | def test_caller_context_padding_raises_error_if_environemtn_set_to_negative_number 29 | ENV['DEPRECATABLE_CALLER_CONTEXT_PADDING'] = "-1" 30 | assert_raises( ArgumentError, "caller_context_mapping must be > 0" ) do 31 | @options.caller_context_padding 32 | end 33 | end 34 | 35 | def test_caller_context_padding_may_be_set 36 | @options.caller_context_padding = 4 37 | assert_equal( 4, @options.caller_context_padding ) 38 | end 39 | 40 | def test_caller_context_padding_may_be_overridden_by_environment_variable 41 | assert_equal( 2, @options.caller_context_padding ) 42 | @options.caller_context_padding = 4 43 | assert_equal( 4, @options.caller_context_padding ) 44 | ENV['DEPRECATABLE_CALLER_CONTEXT_PADDING'] = "10" 45 | assert_equal( 10, @options.caller_context_padding ) 46 | end 47 | 48 | def test_has_at_exit_report_may_be_turned_off 49 | @options.has_at_exit_report = false 50 | refute @options.has_at_exit_report?, "has_at_exit_report? must be false" 51 | end 52 | 53 | def test_has_at_exit_report_may_be_overridden_by_environment_variable 54 | @options.has_at_exit_report = false 55 | refute @options.has_at_exit_report? 56 | ENV['DEPRECATABLE_HAS_AT_EXIT_REPORT'] = "true" 57 | assert @options.has_at_exit_report?, "has_at_exit_report? must be true" 58 | end 59 | 60 | def test_alert_frequency_may_be_set 61 | @options.alert_frequency = :once 62 | assert_equal( 1, @options.alert_frequency ) 63 | 64 | @options.alert_frequency = :never 65 | assert_equal( 0, @options.alert_frequency ) 66 | 67 | @options.alert_frequency = :always 68 | assert_equal( "Infinity", @options.alert_frequency.to_s ) 69 | assert @options.alert_frequency.infinite? 70 | end 71 | 72 | def test_alert_frequency_may_be_overridden_by_environment_variable 73 | ENV['DEPRECATABLE_ALERT_FREQUENCY'] = "42" 74 | assert_equal( 42, @options.alert_frequency) 75 | ENV['DEPRECATABLE_ALERT_FREQUENCY'] = "once" 76 | assert_equal( 1, @options.alert_frequency) 77 | ENV['DEPRECATABLE_ALERT_FREQUENCY'] = "never" 78 | assert_equal( 0, @options.alert_frequency) 79 | ENV['DEPRECATABLE_ALERT_FREQUENCY'] = "always" 80 | assert @options.alert_frequency.infinite? 81 | end 82 | 83 | end 84 | -------------------------------------------------------------------------------- /test/test_deprecatable_registry.rb: -------------------------------------------------------------------------------- 1 | require 'helpers' 2 | 3 | class TestDeprecatableRegistry < MiniTest::Unit::TestCase 4 | class DeprecatedExample 5 | def boom; end 6 | end 7 | 8 | def setup 9 | @registry = Deprecatable::Registry.new 10 | @dm = Deprecatable::DeprecatedMethod.new( TestDeprecatableRegistry::DeprecatedExample, "boom", __FILE__, 6 ) 11 | end 12 | 13 | def test_registers_a_deprecated_method 14 | assert_equal( 0, @registry.size ) 15 | @registry.register( @dm ) 16 | assert_equal( 1, @registry.size ) 17 | end 18 | 19 | def test_that_a_given_method_may_only_be_registered_once 20 | assert_equal( 0, @registry.size ) 21 | @registry.register( @dm ) 22 | assert_equal( 1, @registry.size ) 23 | @registry.register( @dm ) 24 | assert_equal( 1, @registry.size ) 25 | end 26 | 27 | def test_returns_all_the_items_in_the_registry 28 | @registry.register( @dm ) 29 | assert_equal( 1, @registry.items.size ) 30 | end 31 | 32 | end 33 | -------------------------------------------------------------------------------- /test/test_deprecatable_util.rb: -------------------------------------------------------------------------------- 1 | require "helpers" 2 | 3 | class TestDeprecatableUtil < MiniTest::Unit::TestCase 4 | def i_was_called 5 | Deprecatable::Util.location_of_caller 6 | end 7 | 8 | def test_location_of_caller 9 | file, line = i_was_called 10 | assert_equal( File.expand_path(__FILE__), file ) 11 | assert_equal( 9, line ) 12 | end 13 | end 14 | --------------------------------------------------------------------------------