let_it_go 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 0e07c3bf2d1c860ea3942429f6b70a9e0fff08e5
4
- data.tar.gz: 331e78ae77329a8b96c32a38038f40e972a8b96c
3
+ metadata.gz: 42a5bb8aaa66d8aff49ee60864cb68e81acd8789
4
+ data.tar.gz: 762339fb4d4aa69b3b39d862cd1cb932244d021e
5
5
  SHA512:
6
- metadata.gz: 4971320b95a2fd1fe09c37c1aa095d4090b9db878fb013ccf895bac610d6b76568a1c06dd4ab5ecc2c39b445fbd7df2a8262fd153bbf701f460ef46685ac4cb5
7
- data.tar.gz: 79cb35ec9297cb9692a1cb33f5bb7f26351fefa8fb6f7e1aaad227fe1ed6b7ecd4dcb4740ed2433f08bc954f9759b2bb66c868df0943f2e7f676fb02f9b13b49
6
+ metadata.gz: c1145f8bd66dd5a0a8897d80467c0947f0cd7f2ef2f886f97d0660be6b65a6e72d4a02355070fa7fdb2b07844f8cd459fddd29265e01cdb1aea37f726e076dc9
7
+ data.tar.gz: 4640afbfe44d014fccefbabd2821bb7126ba9800c456b65f3ee95a47b0db4c18ac34684a0750c706b4e78726fb38af5a7ab3e7e5e9552bd79657a0a1454965d2
data/README.md CHANGED
@@ -124,6 +124,23 @@ This extremely convoluted library works by watching all method calls using [Trac
124
124
 
125
125
  If you can think of a better way, please open up an issue and send me a proof of concept. I know what you're thinking and no, [programatically aliasing methods won't work for 100% of the time](http://stackoverflow.com/questions/30512945/programmatically-alias-method-that-uses-global-variable).
126
126
 
127
+ Note: This method fails for any Ruby code that can't be parsed in 1 line. For example:
128
+
129
+ ```
130
+ query = <<-SQL % known_coder_types.join(", ")
131
+ ```
132
+
133
+ and
134
+
135
+ ```
136
+ (attr[0] == :html && attr[1] == :attr && options[:hyphen_attrs].include?(attr[2]) &&
137
+ ```
138
+
139
+ Are not valid, complete Ruby instructions. That being said this lib is still relevant. To see what you're not able to parse, run with `ENV['LET_IT_GO_RECORD_FAILED_CODE']`
140
+
141
+
142
+
143
+
127
144
  ## Development
128
145
 
129
146
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `bin/console` for an interactive prompt that will allow you to experiment.
@@ -141,8 +158,5 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
141
158
 
142
159
  ## TODO
143
160
 
144
- - Count number of string literals * method calls instead of just method calls for methods that can take multiple string literals.
145
- - Display counts grouped by file then by line/method
146
- - Implicit methods i.e. 1 + 1 and [1] << 2
147
- - Global operators != && ==
148
- - Subclass support. Hook into a class created TracePoint to see if a class is a subclass and add it to the list
161
+ - Global operators != && == (maybe it's good enough to only track calls to string)
162
+ - Watch receivers such as "foo".eq(variable)
@@ -0,0 +1,31 @@
1
+ module LetItGo
2
+ # Given a single line from `caller` retrieves line_number, file_name
3
+ # and can read the contents of the file
4
+ class CallerLine
5
+ attr_accessor :line_number, :file_name
6
+ def initialize(string)
7
+ file_line = string.split(":in `".freeze).first
8
+ file_line_array = file_line.split(":".freeze)
9
+
10
+ @line_number = file_line_array.pop
11
+ @file_name = file_line_array.join(":".freeze) # name may have `:` in it
12
+ end
13
+
14
+ def contents
15
+ @contents ||= read || ""
16
+ end
17
+
18
+ private
19
+ def read
20
+ contents = ""
21
+ File.open(file_name).each_with_index do |line, index|
22
+ next unless index == Integer(line_number).pred
23
+ contents = line
24
+ break
25
+ end
26
+ contents
27
+ rescue Errno::ENOENT
28
+ nil
29
+ end
30
+ end
31
+ end
@@ -2,6 +2,7 @@
2
2
  LetItGo.watch_frozen(String, :+, positions: [0])
3
3
  LetItGo.watch_frozen(String, :<<, positions: [0])
4
4
  LetItGo.watch_frozen(String, :'<=>', positions: [0])
5
+ LetItGo.watch_frozen(String, :'==', positions: [0])
5
6
 
6
7
  # Positions: 0
7
8
 
@@ -0,0 +1,85 @@
1
+ require 'let_it_go/wtf_parser'
2
+
3
+ module LetItGo
4
+ # Wraps logic that require knowledge of the method call
5
+ # can parse original method call's source and determine if a string literal
6
+ # was passed into the method.
7
+ class MethodCall
8
+ attr_accessor :klass, :method_name, :positions, :line_number, :file_name, :call_count
9
+
10
+ def initialize(klass: , method_name: , kaller:, positions: )
11
+ @klass = klass
12
+ @method_name = method_name.to_s
13
+ # Subclasses report method definition as caller.first via TracePoint
14
+ @key = "Method: #{klass}##{method_name} [#{kaller.first(2).inspect}]"
15
+ @caller_lines = kaller.first(2).map {|kaller_line| CallerLine.new(kaller_line) }
16
+ @positions = positions
17
+ @call_count = 0
18
+ end
19
+
20
+ def count
21
+ call_count * string_allocation_count
22
+ end
23
+
24
+ def zero?
25
+ count.zero?
26
+ end
27
+
28
+ # Loop through each line in the caller and see if the method we're watching is being called
29
+ # This is needed due to the way TracePoint deals with inheritance
30
+ def method_array
31
+ @parser = nil
32
+ @caller_lines.each do |kaller|
33
+ code = Ripper.sexp(kaller.contents)
34
+ code ||= Ripper.sexp(kaller.contents.sub(/^\W*(if|unless)/, ''.freeze)) # if and unless "block" statements aren't valid one line ruby code
35
+ code ||= Ripper.sexp(kaller.contents.sub(/do \|.*\|$/, ''.freeze)) # remove trailing do |thing| to make valid code
36
+ code ||= Ripper.sexp(kaller.contents.sub(/(and|or)\W*$/, ''.freeze))# trailing and || or
37
+ code ||= Ripper.sexp(kaller.contents.sub(/:\W*$/, ''.freeze)) # multi line ternary statements
38
+ code ||= Ripper.sexp(kaller.contents.sub(/(^\W*)|({ \|?.*\|?)}/, ''.freeze)) # multi line blocks using {}
39
+
40
+ puts "LetItGoFailed parse (#{kaller.file_name}:#{kaller.line_number}: \n \033[0;31m"+ kaller.contents.strip + "\e[0m".freeze if ENV['LET_IT_GO_RECORD_FAILED_CODE'] && code.nil? && kaller.contents.match(/"|'/)
41
+
42
+ parser = ::LetItGo::WTFParser.new(code, contents: kaller.contents)
43
+
44
+ if parser.each_method.any? { |m| m.method_name == method_name }
45
+ @line_number = kaller.line_number
46
+ @file_name = kaller.file_name
47
+
48
+ @parser = parser
49
+ parser.each_method.each(&:arg_types)
50
+ break
51
+ else
52
+ next
53
+ end
54
+ end
55
+ @parser || []
56
+ end
57
+
58
+ def line_to_s
59
+ @line_to_s ||= contents_from_file_line(file_name, line_number)
60
+ end
61
+
62
+ def optimizable?
63
+ @optimizable ||= called_with_string_literal?
64
+ end
65
+
66
+ def string_allocation_count
67
+ @string_allocation_count
68
+ end
69
+
70
+ # Parses original method call location
71
+ # Determines if a string literal was used or not
72
+ def called_with_string_literal?
73
+ @string_allocation_count = 0
74
+ method_array.each do |m|
75
+ positions.each {|position| @string_allocation_count += 1 if m.arg_types[position] == :string_literal }
76
+ end
77
+ !@string_allocation_count.zero?
78
+ end
79
+
80
+ # Needs to be very low cost, cannot incur disk read
81
+ def key
82
+ @key
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,45 @@
1
+ module LetItGo
2
+
3
+ # Turns hash of keys into a semi-inteligable sorted result
4
+ class Report
5
+ def initialize(hash_of_reports)
6
+ @hash = hash_of_reports.reject {|k, obj| obj.zero? }
7
+ end
8
+
9
+ def count
10
+ @hash.inject(0) {|count, (k, obj)| count + obj.count; }
11
+ end
12
+
13
+ def report
14
+ @report = "## Un-Fozen Hotspots (#{count} total)\n\n"
15
+
16
+ file_names = @hash.values.map(&:file_name).uniq
17
+ file_name_hash = Hash.new { [] }
18
+ file_names.each do |name|
19
+ file_name_hash[name] = @hash.select {|_, obj| obj.file_name == name}.values.sort {|obj1, obj2| obj1.count <=> obj2.count }.reverse
20
+ end
21
+
22
+ file_name_hash = file_name_hash.sort {|(_, objects1), (_, objects2) |
23
+ count1 = objects1.inject(0) {|count, obj| count + obj.count }
24
+ count2 = objects2.inject(0) {|count, obj| count + obj.count }
25
+ count1 <=> count2
26
+ }.reverse
27
+
28
+ file_name_hash.each do |file_name, objects|
29
+ count = objects.inject(0) {|count, obj| count + obj.count }
30
+ @report << " #{count}) #{file_name}\n"
31
+ objects.each do |obj|
32
+ @report << " - #{obj.count}) #{obj.klass}##{obj.method_name} on line #{ obj.line_number }\n"
33
+ end
34
+ end
35
+
36
+ @report << " (none)" if @hash.empty?
37
+ @report << "\n"
38
+ @report
39
+ end
40
+
41
+ def print
42
+ puts report
43
+ end
44
+ end
45
+ end
@@ -1,3 +1,3 @@
1
1
  module LetItGo
2
- VERSION = "0.0.1"
2
+ VERSION = "0.0.2"
3
3
  end
@@ -73,16 +73,29 @@ module LetItGo
73
73
  # false]]]],
74
74
  # false]]]
75
75
  def args_add_block
76
- args_paren.last || @raw.find {|x| x.first == :args_add_block }
76
+ args_paren.last || @raw.find {|x| x.first == :args_add_block } || []
77
77
  end
78
78
 
79
79
  def args
80
- args_add_block.first(2).last || []
80
+ args = (args_add_block.first(2).last || [])
81
+ case args.first
82
+ when :args_add_star
83
+ args.shift
84
+ args
85
+ else
86
+ args
87
+ end
81
88
  end
82
89
 
90
+ # [:fcall, [:@ident, "foo", [1, 6]]],
91
+ # [:arg_paren,
92
+ # [:args_add_block, [:args_add_star, [], [:array, nil]], false]]
93
+
94
+ # [:args_add_star, [], [:array, nil]], false]
95
+
83
96
  # Returns argument types as an array of symbols [:regexp_literal, :string_literal]
84
97
  def arg_types
85
- args.map(&:first).map {|x| x.is_a?(Array) ? x.first : x }
98
+ args.map(&:first).map {|x| x.is_a?(Array) ? x.first : x }.compact
86
99
  end
87
100
  end
88
101
 
@@ -114,7 +127,7 @@ module LetItGo
114
127
  end
115
128
 
116
129
  def args_add_block
117
- @raw.find {|x| x.is_a?(Array) ? x.first == :args_add_block : false }
130
+ @raw.find {|x| x.is_a?(Array) ? x.first == :args_add_block : false } || []
118
131
  end
119
132
 
120
133
  def args
@@ -123,7 +136,7 @@ module LetItGo
123
136
 
124
137
  # Returns argument types as an array of symbols [:regexp_literal, :string_literal]
125
138
  def arg_types
126
- args.map(&:first).map {|x| x.is_a?(Array) ? x.first : x }
139
+ args.map(&:first).map {|x| x.is_a?(Array) ? x.first : x }.compact
127
140
  end
128
141
  end
129
142
 
@@ -151,12 +164,15 @@ module LetItGo
151
164
  end
152
165
 
153
166
  def arg_types
154
- args.map(&:first).map {|x| x.is_a?(Array) ? x.first : x }
167
+ args.map(&:first).map {|x| x.is_a?(Array) ? x.first : x }.compact
155
168
  end
156
169
  end
157
170
 
158
- def initialize(ripped_code)
159
- @raw = ripped_code
171
+ attr_accessor :contents
172
+
173
+ def initialize(ripped_code, contents: "")
174
+ @contents = contents
175
+ @raw = ripped_code || []
160
176
  end
161
177
 
162
178
  # Parses raw input recursively looking for :method_add_arg blocks
@@ -179,22 +195,36 @@ module LetItGo
179
195
  end
180
196
  end
181
197
 
182
- def method_add
198
+ def all_methods
183
199
  @method_add_array ||= begin
184
200
  method_add_array = []
185
201
  find_method_add_from_raw(@raw.dup, method_add_array)
186
202
  method_add_array
187
203
  end
188
204
  end
189
-
190
- def each_method
191
- if block_given?
192
- method_add.each do |obj|
193
- yield obj
205
+ alias :method_add :all_methods
206
+
207
+ def each
208
+ begin
209
+ if block_given?
210
+ all_methods.each do |obj|
211
+ begin
212
+ yield obj
213
+ rescue => e
214
+ end
215
+ end
216
+ else
217
+ enum_for(:each)
194
218
  end
195
- else
196
- enum_for(:each_method)
219
+ rescue => e
220
+ msg = "Could not parse seemingly valid Ruby code:\n\n"
221
+ msg << " #{ parser.contents.inspect }\n\n"
222
+ msg << e.message
223
+ raise e, msg
197
224
  end
198
225
  end
226
+ alias :each_method :each
227
+
228
+ include Enumerable
199
229
  end
200
230
  end
data/lib/let_it_go.rb CHANGED
@@ -7,12 +7,19 @@ require "let_it_go/version"
7
7
  module LetItGo
8
8
  end
9
9
 
10
- require 'let_it_go/wtf_parser'
11
-
12
10
  module LetItGo
13
11
  @mutex = Mutex.new
14
12
  @watching = {}
15
13
 
14
+ def self.watching_klasses
15
+ @watching.keys
16
+ end
17
+
18
+ def self.method_hash_for_klass(klass)
19
+ @watching[klass]
20
+ end
21
+
22
+
16
23
  def self.watching_positions(klass, method)
17
24
  @watching[klass] && @watching[klass][method]
18
25
  end
@@ -39,68 +46,17 @@ module LetItGo
39
46
  end
40
47
 
41
48
  class << self
42
- alias :cant_hold_it_back_anymore :record
43
- alias :do_you_want_to_build_a_snowman :record
44
- alias :turn_away_and_slam_the_door :record
49
+ alias :cant_hold_it_back_anymore :record
50
+ alias :do_you_want_to_build_a_snowman :record
51
+ alias :turn_away_and_slam_the_door :record
45
52
  alias :the_cold_never_bothered_me_anyway :record
46
- alias :let_it_go :record
53
+ alias :let_it_go :record
47
54
  end
48
55
 
49
56
  def self.recording?
50
57
  Thread.current[:let_it_go_recording] == :on
51
58
  end
52
59
 
53
- # Wraps logic that require knowledge of the method call
54
- # can parse original method call's source and determine if a string literal
55
- # was passed into the method.
56
- class MethodCall
57
- attr_accessor :line_number, :file_name, :klass, :method_name, :kaller, :positions
58
-
59
- def initialize(klass: , method_name: , kaller:, positions: )
60
- @klass = klass
61
- @method_name = method_name
62
- @kaller = kaller
63
- @positions = positions
64
-
65
- file_line = kaller.split(":in `".freeze).first # can't use gsub, because global variables get messed up
66
- file_line_array = file_line.split(":".freeze)
67
-
68
- @line_number = file_line_array.pop
69
- @file_name = file_line_array.join(":".freeze)
70
- end
71
-
72
- def line_to_s
73
- @line_to_s ||= begin
74
- contents = ""
75
- File.open(file_name).each_with_index do |line, index|
76
- next unless index == Integer(line_number).pred
77
- contents = line
78
- break
79
- end
80
- contents
81
- rescue Errno::ENOENT
82
- nil
83
- end
84
- end
85
-
86
- # Parses original method call location
87
- # Determines if a string literal was used or not
88
- def called_with_string_literal?(parser_klass = ::LetItGo::WTFParser)
89
- return true if line_to_s.nil?
90
-
91
- if parsed_code = Ripper.sexp(line_to_s)
92
- parser_klass.new(parsed_code).each_method.any? do |m|
93
- m.method_name == method_name.to_s && positions.any? {|position| m.arg_types[position] == :string_literal }
94
- end
95
- end
96
- end
97
-
98
- def key
99
- "Method: #{klass}##{method_name} [#{kaller}]"
100
- end
101
- end
102
-
103
-
104
60
 
105
61
  # Call to begin watching method for frozen violations
106
62
  def self.watch_frozen(klass, method_name, positions:)
@@ -108,7 +64,6 @@ module LetItGo
108
64
  @watching[klass][method_name] = positions
109
65
  end
110
66
 
111
-
112
67
  # If we are tracking it
113
68
  # If it has positive counter
114
69
  # Increment Counter
@@ -120,34 +75,13 @@ module LetItGo
120
75
  # If it does not
121
76
  # Set counter to
122
77
  def self.watched_method_was_called(meth)
123
- if LetItGo.record_exists?(meth.key)
124
- if Thread.current[:let_it_go_records][meth.key] > 0
125
- LetItGo.increment(meth.key)
126
- end
127
- else
128
- if meth.called_with_string_literal?
129
- LetItGo.store(meth.key, 1)
130
- else
131
- LetItGo.store(meth.key, 0)
132
- end
78
+ unless method = Thread.current[:let_it_go_records][meth.key]
79
+ Thread.current[:let_it_go_records][meth.key] = method = meth
133
80
  end
81
+ method.call_count += 1 if method.optimizable?
134
82
  end
135
83
 
136
84
 
137
- trace = TracePoint.trace(:call, :c_call) do |tp|
138
- tp.disable
139
- if LetItGo.recording?
140
- if positions = watching_positions(tp.defined_class, tp.method_id)
141
- meth = MethodCall.new(klass: tp.defined_class, method_name: tp.method_id, kaller: caller.first, positions: positions)
142
- LetItGo.watched_method_was_called(meth)
143
- end
144
- end
145
- tp.enable
146
- end
147
-
148
- trace.enable
149
-
150
-
151
85
  # Prevent looking
152
86
  def self.record_exists?(key)
153
87
  Thread.current[:let_it_go_records][key]
@@ -164,37 +98,29 @@ module LetItGo
164
98
  def self.increment(key)
165
99
  store(key, 1)
166
100
  end
101
+ end
167
102
 
168
- # Turns hash of keys into a semi-inteligable sorted result
169
- class Report
170
- def initialize(hash_of_reports)
171
- @hash = hash_of_reports.reject {|k, v| v.zero? }.sort {|(k1, v1), (k2, v2)| v1 <=> v2 }.reverse
172
- end
103
+ require 'let_it_go/middleware/olaf'
104
+ require 'let_it_go/caller_line'
105
+ require 'let_it_go/method_call'
106
+ require 'let_it_go/report'
173
107
 
174
- def count
175
- @hash.inject(0) {|count, (k, v)| count + v }
176
- end
177
108
 
178
- def report
179
- @report = "## Un-Fozen Hotspots (#{count} total)\n"
180
- @hash.each do |name_location, count|
181
- @report << " #{count}: #{name_location}\n"
182
- end
183
- @report << " (none)" if @hash.empty?
184
- @report << "\n"
185
- @report
186
- end
109
+ Dir[File.expand_path("../let_it_go/core_ext/*.rb", __FILE__)].each do |file|
110
+ require file
111
+ end
187
112
 
188
- def print
189
- puts report
190
- end
113
+ RubyVM::InstructionSequence.compile_option = { specialized_instruction: false }
191
114
 
115
+ TracePoint.trace(:call, :c_call) do |tp|
116
+ tp.disable
117
+ if LetItGo.recording?
118
+ if positions = LetItGo.watching_positions(tp.defined_class, tp.method_id)
119
+ meth = LetItGo::MethodCall.new(klass: tp.defined_class, method_name: tp.method_id, kaller: caller, positions: positions)
120
+ LetItGo.watched_method_was_called(meth)
121
+ end
192
122
  end
123
+ tp.enable
193
124
  end
194
125
 
195
- require 'let_it_go/middleware/olaf'
196
-
197
- Dir[File.expand_path("../let_it_go/core_ext/*.rb", __FILE__)].each do |file|
198
- require file
199
- end
200
126
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: let_it_go
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - schneems
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2015-07-19 00:00:00.000000000 Z
11
+ date: 2015-07-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -71,10 +71,13 @@ files:
71
71
  - bin/setup
72
72
  - let_it_go.gemspec
73
73
  - lib/let_it_go.rb
74
+ - lib/let_it_go/caller_line.rb
74
75
  - lib/let_it_go/core_ext/array.rb
75
76
  - lib/let_it_go/core_ext/pathname.rb
76
77
  - lib/let_it_go/core_ext/string.rb
78
+ - lib/let_it_go/method_call.rb
77
79
  - lib/let_it_go/middleware/olaf.rb
80
+ - lib/let_it_go/report.rb
78
81
  - lib/let_it_go/version.rb
79
82
  - lib/let_it_go/wtf_parser.rb
80
83
  - lib/untitled.rb