deprecatable 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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