deprecatable 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.autotest +9 -0
- data/.gemtest +1 -0
- data/HISTORY.rdoc +4 -0
- data/Manifest.txt +26 -0
- data/README.rdoc +175 -0
- data/Rakefile +33 -0
- data/examples/alert_frequency.rb +101 -0
- data/examples/at_exit.rb +68 -0
- data/examples/caller_context_padding.rb +97 -0
- data/lib/deprecatable.rb +106 -0
- data/lib/deprecatable/alerter.rb +162 -0
- data/lib/deprecatable/call_site.rb +91 -0
- data/lib/deprecatable/call_site_context.rb +112 -0
- data/lib/deprecatable/deprecated_method.rb +210 -0
- data/lib/deprecatable/options.rb +138 -0
- data/lib/deprecatable/registry.rb +55 -0
- data/lib/deprecatable/util.rb +26 -0
- data/test/helpers.rb +18 -0
- data/test/test_deprecatable.rb +149 -0
- data/test/test_deprecatable_alerter.rb +41 -0
- data/test/test_deprecatable_call_site.rb +28 -0
- data/test/test_deprecatable_call_site_context.rb +57 -0
- data/test/test_deprecatable_deprecated_method.rb +61 -0
- data/test/test_deprecatable_options.rb +83 -0
- data/test/test_deprecatable_registry.rb +32 -0
- data/test/test_deprecatable_util.rb +13 -0
- metadata +146 -0
data/lib/deprecatable.rb
ADDED
@@ -0,0 +1,106 @@
|
|
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
|
@@ -0,0 +1,162 @@
|
|
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
|
@@ -0,0 +1,91 @@
|
|
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
|
@@ -0,0 +1,112 @@
|
|
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
|