let_it_go 0.0.1 → 0.0.2

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