private_please 0.0.2 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. data/.ruby-version +1 -0
  2. data/CHANGELOG +6 -0
  3. data/README.md +51 -7
  4. data/TODO +11 -13
  5. data/lib/private_please/candidate.rb +41 -0
  6. data/lib/private_please/report/reporter.rb +52 -0
  7. data/lib/private_please/report/templates/simple.txt.erb +61 -0
  8. data/lib/private_please/ruby_backports.rb +22 -0
  9. data/lib/private_please/storage/calls_store.rb +41 -0
  10. data/lib/private_please/storage/candidates_store.rb +48 -0
  11. data/lib/private_please/storage/methods_names.rb +9 -0
  12. data/lib/private_please/storage/methods_names_bucket.rb +69 -0
  13. data/lib/private_please/tracking/extension.rb +19 -0
  14. data/lib/private_please/tracking/instrumentor.rb +71 -0
  15. data/lib/private_please/tracking/instruments_all_below.rb +41 -0
  16. data/lib/private_please/tracking/line_change_tracker.rb +36 -0
  17. data/lib/private_please/version.rb +1 -1
  18. data/lib/private_please.rb +33 -50
  19. data/private_please.gemspec +1 -0
  20. data/sample.rb +68 -0
  21. data/spec/01_marking_candidate_methods_to_observe_spec.rb +153 -0
  22. data/spec/02_logging_calls_on_candidate_methods_spec.rb +39 -0
  23. data/spec/04_instrumented_program_activity_observation_result_spec.rb +89 -0
  24. data/spec/fixtures/sample_class_for_report.rb +54 -0
  25. data/spec/fixtures/sample_class_with_all_calls_combinations.rb +69 -0
  26. data/spec/spec_helper.rb +52 -1
  27. data/spec/units/calls_store_spec.rb +15 -0
  28. data/spec/units/candidates_store_spec.rb +55 -0
  29. metadata +58 -28
  30. data/lib/private_please/candidates.rb +0 -37
  31. data/lib/private_please/configuration.rb +0 -21
  32. data/lib/private_please/line_change_tracker.rb +0 -19
  33. data/lib/private_please/recorder.rb +0 -34
  34. data/lib/private_please/report/template.txt.erb +0 -15
  35. data/lib/private_please/report.rb +0 -47
  36. data/spec/01_marking_methods_spec.rb +0 -30
  37. data/spec/02_calling_methods_spec.rb +0 -58
  38. data/spec/03_configuration_spec.rb +0 -52
  39. data/spec/04_at_exit_report_printing_spec.rb +0 -47
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 1.8.7
data/CHANGELOG CHANGED
@@ -1,3 +1,9 @@
1
+ v0.0.3
2
+ - private_please without parameters observes all the methods defined afterwards
3
+ - PP is active once you require the gem
4
+ - modules' instance methods are observable too
5
+ - big code cleanup (development)
6
+
1
7
  v0.0.2
2
8
  - only display the report if PrivatePlease.active?
3
9
 
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # PrivatePlease
2
2
 
3
- TODO: Write a gem description
3
+ limitation : Ruby 1.8.7
4
4
 
5
5
  ## Installation
6
6
 
@@ -8,17 +8,61 @@ Add this line to your application's Gemfile:
8
8
 
9
9
  gem 'private_please'
10
10
 
11
- And then execute:
11
+ ## Usage
12
12
 
13
- $ bundle
13
+ ```ruby
14
14
 
15
- Or install it yourself as:
15
+ require 'private_please' # step 1
16
16
 
17
- $ gem install private_please
17
+ class CouldBeMorePrivate
18
+ def to_s # not observed -> won't appear in the report.
19
+ # ...
20
+ end
18
21
 
19
- ## Usage
22
+ private_please # step 2 : start observing
23
+
24
+ def do_the_thing # is called by class' users => must stay public (case #1)
25
+ part_1
26
+ part_2
27
+ end
28
+ def part_1 ; end # only called by #do_the_thing => should be private (case #2)
29
+ def part_2 ; end # only called by #do_the_thing => should be private (case #2)
30
+
31
+ def part_3 ; end # is never used -> will be detected. (case #3)
32
+ end
33
+
34
+ c = CouldBeMorePrivate.new
35
+ c.do_the_thing # step 3 : execute the code, so PP can observe and deduce.
36
+ ```
37
+ A report is automatically printed in the console when the program exits.
38
+ For the code above, the report would be :
39
+
40
+ ====================================================================================
41
+ = PrivatePlease report : =
42
+ ====================================================================================
43
+
44
+ **********************************************************
45
+ CouldBeMorePrivate
46
+ **********************************************************
47
+
48
+ * Good candidates : can be made private :
49
+ ------------------------------------------
50
+
51
+ #part_1
52
+ #part_2
53
+
54
+ * Bad candidates : must stay public/protected
55
+ ------------------------------------------
56
+
57
+ #do_the_thing
58
+
59
+ * Methods that were never called
60
+ ------------------------------------------
61
+
62
+ #part_3
63
+
64
+ ====================================================================================
20
65
 
21
- TODO: Write usage instructions here
22
66
 
23
67
  ## Contributing
24
68
 
data/TODO CHANGED
@@ -1,22 +1,20 @@
1
1
 
2
- DSL
3
- - global method marking :
4
-
5
- ex:
2
+ install only in classes (? and modules)
3
+ def self.install
4
+ Object.send :include, PrivatePlease::Tracking::Extension
5
+ ^^^^^^
6
+ TOO WIDE
7
+ end
6
8
 
7
- private_please
8
- def foo ..
9
- def bar ..
10
- public
11
9
 
12
- is the same as :
13
- ...
14
- def foo ..
15
- def bar ..
16
- private_please :foo, :bar
10
+ DSL
11
+ - observe class methods too
17
12
 
18
13
  Report :
14
+ - option : show file and line number of good candidate definition
15
+ - option : show code snippet around the good candidate
19
16
  - display list of methods that were never called
17
+ - display candidates that are already private
20
18
 
21
19
  Doc
22
20
  - write README
@@ -0,0 +1,41 @@
1
+ # Holds the details of 1 method that was marked via `private_please`.
2
+
3
+ module PrivatePlease
4
+ class Candidate
5
+
6
+ def initialize(klass, method_name, is_instance_method)
7
+ @klass, @method_name, @is_instance_method = klass, method_name, is_instance_method
8
+ @klass_name = klass.to_s
9
+ end
10
+
11
+ def self.for_instance_method(klass, method_name)
12
+ new(klass, method_name, true)
13
+ end
14
+
15
+ def self.for_class_method(klass, method_name)
16
+ new(klass, method_name, false)
17
+ end
18
+
19
+ #----------------------------------------------------------------------------
20
+ # QUERIES:
21
+ #----------------------------------------------------------------------------
22
+
23
+ attr_reader :klass,
24
+ :klass_name,
25
+ :method_name,
26
+ :is_instance_method
27
+
28
+ alias_method :instance_method?, :is_instance_method
29
+
30
+ def already_instrumented?
31
+ candidates_store.stored?(self)
32
+ end
33
+
34
+ #----------------------------------------------------------------------------
35
+ private
36
+
37
+ def candidates_store
38
+ PrivatePlease.candidates_store
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,52 @@
1
+ require 'erb'
2
+ module PrivatePlease ; module Report
3
+ class Reporter
4
+
5
+ TEMPLATE_PATH = File.expand_path(File.dirname(__FILE__) + '/templates/simple.txt.erb')
6
+
7
+ attr_reader :candidates_store, :calls_store,
8
+ :good_candidates, :bad_candidates,
9
+ :good_candidates_c, :bad_candidates_c,
10
+ :never_called_candidates, :never_called_candidates_c,
11
+ :building_time
12
+
13
+
14
+ def initialize(candidates_store, calls_store)
15
+ @candidates_store = candidates_store
16
+ @calls_store = calls_store
17
+
18
+ prepare_report_data
19
+ end
20
+
21
+ def to_s
22
+ erb = ERB.new(File.read(TEMPLATE_PATH), 0, "%<>")
23
+ erb.result(binding)
24
+ end
25
+
26
+ private
27
+
28
+ def prepare_report_data
29
+ start_time = Time.now
30
+ @bad_candidates = calls_store.external_calls .clone
31
+ @bad_candidates_c = calls_store.class_external_calls.clone
32
+ # TODO : optimize
33
+ @good_candidates = calls_store.internal_calls .clone.remove(@bad_candidates)
34
+ @good_candidates_c= calls_store.class_internal_calls.clone.remove(@bad_candidates_c)
35
+
36
+ @never_called_candidates = candidates_store.instance_methods.clone.
37
+ remove(@good_candidates).
38
+ remove(@bad_candidates )
39
+
40
+ @never_called_candidates_c = candidates_store.class_methods.clone.
41
+ remove(@good_candidates_c).
42
+ remove(@bad_candidates_c )
43
+ @building_time = Time.now - start_time
44
+
45
+ @candidates_classes_names = (candidates_store.instance_methods.classes_names +
46
+ candidates_store.class_methods .classes_names ).uniq.sort
47
+ @good_candidates_classes_names = (@good_candidates_c.classes_names + @good_candidates.classes_names).uniq.sort
48
+ @bad_candidates_classes_names = (@bad_candidates_c .classes_names + @bad_candidates .classes_names).uniq.sort
49
+ @never_called_candidates_classes_names = (@never_called_candidates_c .classes_names + @never_called_candidates.classes_names).uniq.sort
50
+ end
51
+ end
52
+ end end
@@ -0,0 +1,61 @@
1
+ ====================================================================================
2
+ = PrivatePlease report : =
3
+ ====================================================================================
4
+ % @candidates_classes_names .sort.each do |class_name|
5
+ %
6
+ % good_lines =
7
+ % [good_candidates_c.get_methods_names(class_name).collect{|n|".#{n}"},
8
+ % good_candidates .get_methods_names(class_name).collect{|n|"##{n}"}
9
+ % ].reject(&:empty?)
10
+ % good_lines.insert(1, '') if good_lines.length == 2
11
+ % good_lines.flatten!
12
+ %
13
+ % bad_lines =
14
+ % [bad_candidates_c.get_methods_names(class_name).collect{|n|".#{n}"},
15
+ % bad_candidates .get_methods_names(class_name).collect{|n|"##{n}"}
16
+ % ].reject(&:empty?)
17
+ % bad_lines.insert(1, '') if bad_lines.length == 2
18
+ % bad_lines.flatten!
19
+
20
+ % never_called_lines =
21
+ % [never_called_candidates_c.get_methods_names(class_name).collect{|n|".#{n}"},
22
+ % never_called_candidates .get_methods_names(class_name).collect{|n|"##{n}"}
23
+ % ]
24
+ % never_called_lines.insert(1, '') if never_called_lines.length == 2
25
+
26
+ %
27
+ **********************************************************
28
+ <%= class_name
29
+ %>
30
+ **********************************************************
31
+ %
32
+ % unless good_lines.empty?
33
+
34
+ * Good candidates : can be made private :
35
+ ------------------------------------------
36
+
37
+ % good_lines.each do |line|
38
+ <%= line %>
39
+ % end
40
+ % end
41
+ % unless bad_lines.empty?
42
+
43
+ * Bad candidates : must stay public/protected
44
+ ------------------------------------------
45
+
46
+ % bad_lines.each do |line|
47
+ <%= line %>
48
+ % end
49
+ % end
50
+ %
51
+ % unless never_called_lines.empty?
52
+
53
+ * Methods that were never called
54
+ ------------------------------------------
55
+ % never_called_lines.each do |line|
56
+ <%= line %>
57
+ % end
58
+ % end
59
+
60
+ % end
61
+ ====================================================================================
@@ -0,0 +1,22 @@
1
+ # Unify the Ruby APIs across versions
2
+ #
3
+
4
+ # src : https://github.com/marcandre/backports/blob/master/lib/backports/1.9.1/kernel/define_singleton_method.rb
5
+ unless Kernel.method_defined? :define_singleton_method
6
+ module Kernel
7
+ def define_singleton_method(*args, &block)
8
+ class << self
9
+ self
10
+ end.send(:define_method, *args, &block)
11
+ end
12
+ end
13
+ end
14
+
15
+ # src: https://github.com/marcandre/backports/blob/master/lib/backports/1.9.2/kernel/singleton_class.rb
16
+ unless Kernel.method_defined? :singleton_class
17
+ module Kernel
18
+ def singleton_class
19
+ class << self; self; end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,41 @@
1
+ # As the main program runs, the various method calls are logged here
2
+ # (only for the methods marked with `private_please`)
3
+
4
+ module PrivatePlease
5
+ module Storage
6
+
7
+ class CallsStore
8
+
9
+ def initialize
10
+ @internal_calls = MethodsNamesBucket.new
11
+ @external_calls = MethodsNamesBucket.new
12
+ @class_internal_calls = MethodsNamesBucket.new
13
+ @class_external_calls = MethodsNamesBucket.new
14
+ end
15
+
16
+ #--------------------------------------------------------------------------
17
+ # QUERIES:
18
+ #--------------------------------------------------------------------------
19
+
20
+ attr_reader :internal_calls,
21
+ :external_calls,
22
+ :class_internal_calls,
23
+ :class_external_calls
24
+
25
+ #--------------------------------------------------------------------------
26
+ # COMMANDS:
27
+ #--------------------------------------------------------------------------
28
+
29
+ def store_outside_call(candidate)
30
+ bucket = candidate.instance_method? ? external_calls : class_external_calls
31
+ bucket.add_method_name(candidate.klass_name, candidate.method_name)
32
+ end
33
+
34
+ def store_inside_call(candidate)
35
+ bucket = candidate.instance_method? ? internal_calls : class_internal_calls
36
+ bucket.add_method_name(candidate.klass_name, candidate.method_name)
37
+ end
38
+ end
39
+
40
+ end
41
+ end
@@ -0,0 +1,48 @@
1
+ # Holds in 2 "buckets" the details (class name + methods names) of the
2
+ # methods that are candidate for privatization.
3
+ # Those methods were marked via `private_please` in the code.
4
+ # Instance methods and class methods are kept separate.
5
+
6
+ module PrivatePlease
7
+ module Storage
8
+
9
+ class CandidatesStore
10
+
11
+ def initialize
12
+ @instance_methods = MethodsNamesBucket.new
13
+ @class_methods = MethodsNamesBucket.new
14
+ end
15
+
16
+ #--------------------------------------------------------------------------
17
+ # QUERIES:
18
+ #--------------------------------------------------------------------------
19
+
20
+ attr_reader :instance_methods,
21
+ :class_methods
22
+
23
+ def empty?
24
+ instance_methods.empty? && class_methods.empty?
25
+ end
26
+
27
+ def stored?(candidate)
28
+ bucket_for(candidate).get_methods_names(candidate.klass_name).include?(candidate.method_name)
29
+ end
30
+
31
+ #--------------------------------------------------------------------------
32
+ # COMMANDS:
33
+ #--------------------------------------------------------------------------
34
+
35
+ def store(candidate)
36
+ bucket_for(candidate).add_method_name(candidate.klass_name, candidate.method_name)
37
+ end
38
+
39
+ #--------------------------------------------------------------------------
40
+ private
41
+
42
+ def bucket_for(candidate)
43
+ candidate.instance_method? ? @instance_methods : @class_methods
44
+ end
45
+
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,9 @@
1
+ module PrivatePlease
2
+ module Storage
3
+
4
+ class MethodsNames < Set
5
+ # based on Set => no risk of duplicates when using #add
6
+ end
7
+
8
+ end
9
+ end
@@ -0,0 +1,69 @@
1
+ # Associates and indexes classes (name) and some of their methods (names).
2
+ # Ex:
3
+ # +-------------+---------------------------------------+
4
+ # | class name => 1+ methods names |
5
+ # +-------------+---------------------------------------+
6
+ # | 'Foo' | MethodsNames.new('foo to_baz to_bar') |
7
+ # | 'Bar' | MethodsNames.new('qux') |
8
+ # +-------------+---------------------------------------+
9
+
10
+ module PrivatePlease
11
+ module Storage
12
+
13
+ class MethodsNamesBucket < Hash
14
+
15
+ def initialize
16
+ super{|hash, class_name|
17
+ #hash[class_name] = PrivatePlease::Storage::MethodsNames.new
18
+ hash.set_methods_names(class_name, MethodsNames.new)
19
+ }
20
+ end
21
+
22
+ def clone
23
+ (self.class).new.tap do |klone|
24
+ classes_names.each do |class_name|
25
+ klone.set_methods_names(class_name, (self).get_methods_names(class_name))
26
+ end
27
+ end
28
+ end
29
+
30
+ #--------------------------------------------------------------------------
31
+ # QUERIES:
32
+ #--------------------------------------------------------------------------
33
+
34
+ alias_method :classes_names, :keys
35
+ alias_method :get_methods_names, :[]
36
+ # undef :[], :keys # should be undef-ed, but that complexifies the test #TODO : undef :[], :keys
37
+ # ( => must define ==(other) and cast type of 'other')
38
+
39
+ #--------------------------------------------------------------------------
40
+ # COMMANDS:
41
+ #--------------------------------------------------------------------------
42
+
43
+ alias_method :set_methods_names, :[]=
44
+ undef :[]=
45
+
46
+ def add_method_name(class_name, method_name)
47
+ self.get_methods_names(class_name).add(method_name)
48
+ end
49
+
50
+ def remove(other)
51
+ other.classes_names.each do |cn|
52
+ next if (methods_to_remove = other.get_methods_names(cn)).empty?
53
+ next if (methods_before = self .get_methods_names(cn)).empty?
54
+ difference = methods_before - methods_to_remove
55
+ self.set_methods_names(cn, difference)
56
+ end
57
+ prune!
58
+ self
59
+ end
60
+
61
+ private
62
+
63
+ def prune!
64
+ self.reject!{|_, v|v.empty?}
65
+ end
66
+ end
67
+
68
+ end
69
+ end
@@ -0,0 +1,19 @@
1
+ module PrivatePlease ; module Tracking
2
+
3
+ module Extension
4
+
5
+ def private_please(*methods_to_observe)
6
+ parameterless_call = methods_to_observe.empty?
7
+ klass = self
8
+
9
+ if parameterless_call
10
+ klass.send :include, PrivatePlease::Tracking::InstrumentsAllBelow
11
+
12
+ else
13
+ Instrumentor.instrument_instance_methods_for_pp_observation(klass, methods_to_observe)
14
+ end
15
+ end
16
+
17
+ end
18
+
19
+ end end
@@ -0,0 +1,71 @@
1
+ module PrivatePlease ; module Tracking
2
+
3
+ module Instrumentor
4
+
5
+ def self.instrument_instance_methods_for_pp_observation(klass, methods_to_observe)
6
+ class_instance_methods = klass.instance_methods.collect(&:to_sym)
7
+ methods_to_observe = methods_to_observe.collect(&:to_sym)
8
+ # reject invalid methods names
9
+ methods_to_observe.reject! do |m|
10
+ already_defined_instance_method = class_instance_methods.include?(m)
11
+ invalid = !already_defined_instance_method
12
+ end
13
+
14
+ methods_to_observe.each do |method_name|
15
+ candidate = Candidate.for_instance_method(klass, method_name)
16
+ instrument_candidate_for_pp_observation(candidate) # end
17
+ end
18
+ end
19
+
20
+
21
+ def self.instrument_candidate_for_pp_observation(candidate)
22
+ return if candidate.already_instrumented?
23
+ PrivatePlease.remember_candidate(candidate)
24
+
25
+ klass, method_name = candidate.klass, candidate.method_name
26
+ candidate.instance_method? ?
27
+ instrument_instance_method_with_pp_observation(klass, method_name) :
28
+ instrument_class_method_with_pp_observation( klass, method_name)
29
+ end
30
+
31
+
32
+
33
+ def self.instrument_class_method_with_pp_observation(klass, method_name)
34
+ orig_method = klass.singleton_class.instance_method(method_name)
35
+ klass.class_eval <<RUBY
36
+ define_singleton_method(method_name) do |*args, &blk| # def self.observed_method_i(..)
37
+ set_trace_func(nil) #don't track activity while here #
38
+ #
39
+ zelf_class=self #
40
+ candidate = PrivatePlease::Candidate.for_class_method(zelf_class, method_name)
41
+ PrivatePlease.after_method_call(candidate, LineChangeTracker.outside_class_method_call_detected?(zelf_class))
42
+ #
43
+ set_trace_func(LineChangeTracker::MY_TRACE_FUN) #
44
+ # make the call : #
45
+ orig_method.bind(self).call(*args, &blk) # <call original method>
46
+ end # end
47
+ RUBY
48
+ end
49
+ private_class_method :instrument_class_method_with_pp_observation
50
+
51
+
52
+ def self.instrument_instance_method_with_pp_observation(klass, method_name)
53
+ orig_method = klass.instance_method(method_name)
54
+ klass.class_eval <<RUBY
55
+ define_method(method_name) do |*args, &blk| # def observed_method_i(..)
56
+ set_trace_func(nil) #don't track activity while here #
57
+ #
58
+ candidate = PrivatePlease::Candidate.for_instance_method(self.class, method_name)
59
+ PrivatePlease.after_method_call(candidate, LineChangeTracker.outside_instance_method_call_detected?(self)) #
60
+ #
61
+ set_trace_func(LineChangeTracker::MY_TRACE_FUN) #
62
+ # make the call : #
63
+ orig_method.bind(self).call(*args, &blk) # <call original method>
64
+ end # end
65
+ RUBY
66
+ end
67
+ private_class_method :instrument_instance_method_with_pp_observation
68
+
69
+ end
70
+
71
+ end end
@@ -0,0 +1,41 @@
1
+ # Usage :
2
+ # class MarkingTest::Automatic2
3
+ # def foo ; end <---- but not this one.
4
+ # include PrivatePlease::Tracking::InstrumentsAllBelow <-- add this line
5
+ #
6
+ # def baz ; end <---- to observe this method
7
+ # protected
8
+ # def self.qux ; end <---- and this one too
9
+ # end
10
+
11
+ module PrivatePlease ; module Tracking
12
+
13
+ module InstrumentsAllBelow
14
+ include PrivatePlease::Tracking::Extension
15
+
16
+ def self.included(base)
17
+
18
+ def base.singleton_method_added(method_name)
19
+ return if [:method_added, :singleton_method_added].include?(method_name)
20
+ return if [:included].include?(method_name) && !self.is_a?(Class)
21
+
22
+ is_private_class_method = singleton_class.private_method_defined?(method_name)
23
+ return if is_private_class_method
24
+
25
+ candidate = Candidate.for_class_method(klass = self, method_name)
26
+ Tracking::Instrumentor.instrument_candidate_for_pp_observation(candidate)
27
+ end
28
+
29
+
30
+ def base.method_added(method_name)
31
+ is_private_instance_method = self.private_method_defined?(method_name)
32
+ return if is_private_instance_method
33
+
34
+ candidate = Candidate.for_instance_method(klass = self, method_name)
35
+ Tracking::Instrumentor.instrument_candidate_for_pp_observation(candidate)
36
+ end
37
+ end
38
+
39
+ end
40
+
41
+ end end
@@ -0,0 +1,36 @@
1
+ module PrivatePlease ; module Tracking
2
+
3
+ class LineChangeTracker
4
+ class << self
5
+ attr_accessor :prev_prev_self, :prev_self, :curr_self
6
+ @@prev_self = @@curr_self = nil
7
+ end
8
+
9
+ MY_TRACE_FUN = lambda do |event, file, line, id, binding, klass|
10
+ return unless 'line'==event
11
+ LineChangeTracker.prev_prev_self = LineChangeTracker.prev_self
12
+ LineChangeTracker.prev_self = LineChangeTracker.curr_self
13
+ LineChangeTracker.curr_self = (eval 'self', binding)
14
+ #puts "my : #{event} in #{file}/#{line} id:#{id} klass:#{klass} - self = #{(eval'self', binding).inspect}"
15
+ end
16
+
17
+ def self.outside_instance_method_call_detected?(zelf)
18
+ caller_class != zelf.class
19
+ end
20
+
21
+ def self.outside_class_method_call_detected?(zelf_class)
22
+ caller_class != zelf_class
23
+ end
24
+
25
+ private
26
+
27
+ def self.caller_class
28
+ call_initiator = LineChangeTracker.prev_self
29
+ (caller_is_class_method = call_initiator.is_a?(Class)) ?
30
+ call_initiator :
31
+ call_initiator.class
32
+ end
33
+ end
34
+ end end
35
+
36
+ set_trace_func(PrivatePlease::Tracking::LineChangeTracker::MY_TRACE_FUN) #
@@ -1,3 +1,3 @@
1
1
  module PrivatePlease
2
- VERSION = "0.0.2"
2
+ VERSION = "0.0.3"
3
3
  end