let_it_go 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 0e07c3bf2d1c860ea3942429f6b70a9e0fff08e5
4
+ data.tar.gz: 331e78ae77329a8b96c32a38038f40e972a8b96c
5
+ SHA512:
6
+ metadata.gz: 4971320b95a2fd1fe09c37c1aa095d4090b9db878fb013ccf895bac610d6b76568a1c06dd4ab5ecc2c39b445fbd7df2a8262fd153bbf701f460ef46685ac4cb5
7
+ data.tar.gz: 79cb35ec9297cb9692a1cb33f5bb7f26351fefa8fb6f7e1aaad227fe1ed6b7ecd4dcb4740ed2433f08bc954f9759b2bb66c868df0943f2e7f676fb02f9b13b49
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
@@ -0,0 +1,3 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.2.2
@@ -0,0 +1,13 @@
1
+ # Contributor Code of Conduct
2
+
3
+ As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities.
4
+
5
+ We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, age, or religion.
6
+
7
+ Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct.
8
+
9
+ Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team.
10
+
11
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers.
12
+
13
+ This Code of Conduct is adapted from the [Contributor Covenant](http:contributor-covenant.org), version 1.0.0, available at [http://contributor-covenant.org/version/1/0/0/](http://contributor-covenant.org/version/1/0/0/)
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in let_it_go.gemspec
4
+ gemspec
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015 schneems
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,148 @@
1
+ # Let It Go
2
+
3
+ Frozen string literals can save time and memory when used correctly. This library looks for common places that can easily accept frozen string literals and lets you know if a non-frozen string is being used instead so you can speed up your programs.
4
+
5
+ For more info on the relationship betwen speed, objects, and memory in Ruby Check out [How Ruby Uses Memory](http://www.schneems.com/2015/05/11/how-ruby-uses-memory.html).
6
+
7
+ Note: This library only works with Ruby files on disk, and not with interactive sessions like `irb`. Why? Because intercepting arguments to c defined methods isn't possible [Attempt 1](http://stackoverflow.com/questions/30512945/programmatically-alias-method-that-uses-global-variable) [Attempt 2](http://stackoverflow.com/questions/30584454/get-method-arguments-using-rubys-tracepoint).
8
+
9
+ ## What is a Frozen String Literal?
10
+
11
+ This is a frozen string literal:
12
+
13
+ ```ruby
14
+ "foo".freeze
15
+ ```
16
+
17
+ The `freeze` method is a way to tell the Ruby interpreter that we will not modify that string in the future. When we do this, the Ruby interpreter only ever has to create one object that can be re-used instead of having to create a new string each time. This is how we save CPU cycles. Passing frozen strings to methods like `String#gsub` that do not modify their arguments is best practice when possible:
18
+
19
+ ```ruby
20
+ matchdata.captures.map do |e|
21
+ e.gsub(/_|,/, '-'.freeze)
22
+ end
23
+ ```
24
+
25
+ ## Installation
26
+
27
+ Requires Ruby 2.0+
28
+
29
+ Add this line to your application's Gemfile:
30
+
31
+ ```ruby
32
+ gem 'let_it_go', group: :development
33
+ ```
34
+
35
+ And then execute:
36
+
37
+ $ bundle
38
+
39
+ It's really important you don't run this in production, it would really slow stuff down.
40
+
41
+
42
+ ## Middleware Use
43
+
44
+ You can profile method calls during a request by using a middleware.
45
+
46
+
47
+ ```ruby
48
+ # config/initializers/let_it_go.rb
49
+
50
+ if defined?(LetItGo::Middleware::Olaf)
51
+ Rails.application.config.middleware.insert(0, LetItGo::Middleware::Olaf)
52
+ end
53
+ ```
54
+
55
+ Now every time a page is rendered, you'll get a list of un-frozen methods in your standard out.
56
+
57
+ ## Direct Use
58
+
59
+ Anywhere you want to check for non-frozen string use call:
60
+
61
+ ```ruby
62
+ LetItGo.record do
63
+ "foo".gsub(/f/, "")
64
+ end.print
65
+
66
+ ## Un-Fozen Hotspots
67
+ # 1: Method: String#gsub [(irb):2:in `block in irb_binding']
68
+ ```
69
+
70
+ Each time the same method is called it is counted
71
+
72
+ ```ruby
73
+ LetItGo.record do
74
+ 99.times { "foo".gsub(/f/, "") }
75
+ end.print
76
+
77
+ ## Un-Fozen Hotspots
78
+ # 99: Method: String#gsub [(irb):6:in `block (2 levels) in irb_binding']
79
+ ```
80
+
81
+ When you're running this against a file, `LetItGo` will try to parse the calling line to determine if a string literal was used.
82
+
83
+ ```
84
+ $ cat << EOF > foo.rb
85
+ require 'let_it_go'
86
+
87
+ LetItGo.record do
88
+ "foo".gsub(/f/, "")
89
+ end.print
90
+ EOF
91
+ $ ruby foo.rb
92
+ ## Un-Fozen Hotspots
93
+ 1: Method: String#gsub [foo.rb:4:in `block in <main>']
94
+ ```
95
+
96
+ If you try again with a string variable or a modified string (anything not a string literal) it will be ignored
97
+
98
+ ```
99
+ $ cat << EOF > foo.rb
100
+ require 'let_it_go'
101
+
102
+ LetItGo.record do
103
+ "foo".gsub(/f/, "".downcase) # freezing downcase would not help with memory or speed here
104
+ end.print
105
+ EOF
106
+ $ ruby foo.rb
107
+ ## Un-Fozen Hotspots
108
+ (none)
109
+ ```
110
+
111
+ ## Watching Frozen (methods)
112
+
113
+ For a list of all methods that are watched check in lib/let_it_go/core_ext. You can manually add your own by using `LetItGo.watch_frozen`. For example `[].join("")` is a potential hotspot. To watch this method we would call
114
+
115
+ ```ruby
116
+ LetItGo.watch_frozen(Array, :join, positions: [0])
117
+ ```
118
+
119
+ The positions named argument is an array containing the indexes of the method arguments you want to watch. In this case `join` only takes one method argument, so we are only watching the first one (index of 0). If there are other common method invocations that can ALWAYS take in a frozen string (i.e. they NEVER modify the string argument) then please submit a PR to this library by adding it to `lib/let_it_go/core_ext`. Please add a test to the corresponding spec file.
120
+
121
+ ## How
122
+
123
+ This extremely convoluted library works by watching all method calls using [TracePoint](http://ruby-doc.org/core-2.2.2/TracePoint.html) to see when a method we are watching is called. Since we [cannot use TracePoint to get all method arguments](http://stackoverflow.com/questions/30584454/get-method-arguments-using-rubys-tracepoint) we instead resort to parsing Ruby code on disk to see if a string literal is used. The parsing functionality is achieved by reading in the line of the caller and parsing it with [Ripper](http://ruby-doc.org/stdlib-2.2.2/libdoc/ripper/rdoc/Ripper.html) which is then translated by lib/let_it_go/wtf_parser.rb. It probably has bugs, and it won't work with weirly formatted or multi line code.
124
+
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
+
127
+ ## Development
128
+
129
+ 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.
130
+
131
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release` to create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
132
+
133
+ ## Contributing
134
+
135
+ 1. Fork it ( https://github.com/[my-github-username]/let_it_go/fork )
136
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
137
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
138
+ 4. Push to the branch (`git push origin my-new-feature`)
139
+ 5. Create a new Pull Request
140
+
141
+
142
+ ## TODO
143
+
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
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "let_it_go"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
@@ -0,0 +1,7 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+
5
+ bundle install
6
+
7
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,26 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'let_it_go/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "let_it_go"
8
+ spec.version = LetItGo::VERSION
9
+ spec.authors = ["schneems"]
10
+ spec.email = ["richard.schneeman@gmail.com"]
11
+
12
+ spec.summary = %q{ Finds un-frozen string literals in your program }
13
+ spec.description = %q{ Finds un-frozen string literals in your program }
14
+ spec.homepage = "https://github.com/schneems/let_it_go"
15
+ spec.license = "MIT"
16
+
17
+
18
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
19
+ spec.bindir = "exe"
20
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
21
+ spec.require_paths = ["lib"]
22
+
23
+ spec.add_development_dependency "bundler", "~> 1.9"
24
+ spec.add_development_dependency "rspec"
25
+ spec.add_development_dependency "rake", "~> 10.0"
26
+ end
@@ -0,0 +1,200 @@
1
+ require 'ripper'
2
+ require 'pp'
3
+ require 'thread'
4
+
5
+ require "let_it_go/version"
6
+
7
+ module LetItGo
8
+ end
9
+
10
+ require 'let_it_go/wtf_parser'
11
+
12
+ module LetItGo
13
+ @mutex = Mutex.new
14
+ @watching = {}
15
+
16
+ def self.watching_positions(klass, method)
17
+ @watching[klass] && @watching[klass][method]
18
+ end
19
+
20
+ # Main method, wrap code you want to check for frozen violations in
21
+ # a `let_it_go` block.
22
+ #
23
+ # By default it will try to parse source of the method call to determine
24
+ # if a string literal or variable was used. We only care about string literals.
25
+ def self.record
26
+ @mutex.synchronize do
27
+ Thread.current[:let_it_go_recording] = :on
28
+ Thread.current[:let_it_go_records] = {} # nil => never checked, 0 => checked, no string literals, positive => checked, positive literals detected
29
+ end
30
+ yield
31
+ records = Thread.current[:let_it_go_records]
32
+ report = Report.new(records)
33
+ return report
34
+ ensure
35
+ @mutex.synchronize do
36
+ Thread.current[:let_it_go_recording] = nil
37
+ Thread.current[:let_it_go_records] = nil
38
+ end
39
+ end
40
+
41
+ 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
45
+ alias :the_cold_never_bothered_me_anyway :record
46
+ alias :let_it_go :record
47
+ end
48
+
49
+ def self.recording?
50
+ Thread.current[:let_it_go_recording] == :on
51
+ end
52
+
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
+
105
+ # Call to begin watching method for frozen violations
106
+ def self.watch_frozen(klass, method_name, positions:)
107
+ @watching[klass] ||= {}
108
+ @watching[klass][method_name] = positions
109
+ end
110
+
111
+
112
+ # If we are tracking it
113
+ # If it has positive counter
114
+ # Increment Counter
115
+ # If not
116
+ # do nothing
117
+ # else we are not tracking it
118
+ # If it has a frozen string literal
119
+ # Set counter to 1
120
+ # If it does not
121
+ # Set counter to
122
+ 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
133
+ end
134
+ end
135
+
136
+
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
+ # Prevent looking
152
+ def self.record_exists?(key)
153
+ Thread.current[:let_it_go_records][key]
154
+ end
155
+
156
+ # Records when a method has been called without passing in a frozen object
157
+ def self.store(key, increment = 0)
158
+ @mutex.synchronize do
159
+ Thread.current[:let_it_go_records][key] ||= 0
160
+ Thread.current[:let_it_go_records][key] += increment
161
+ end
162
+ end
163
+
164
+ def self.increment(key)
165
+ store(key, 1)
166
+ end
167
+
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
173
+
174
+ def count
175
+ @hash.inject(0) {|count, (k, v)| count + v }
176
+ end
177
+
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
187
+
188
+ def print
189
+ puts report
190
+ end
191
+
192
+ end
193
+ end
194
+
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
+
@@ -0,0 +1,13 @@
1
+ # positions: 0
2
+ LetItGo.watch_frozen(Array, :join, positions: [0])
3
+ LetItGo.watch_frozen(Array, :include?, positions: [0])
4
+ LetItGo.watch_frozen(Array, :assoc, positions: [0])
5
+ LetItGo.watch_frozen(Array, :count, positions: [0])
6
+ LetItGo.watch_frozen(Array, :delete, positions: [0])
7
+ LetItGo.watch_frozen(Array, :fill, positions: [0])
8
+ LetItGo.watch_frozen(Array, :index, positions: [0])
9
+ LetItGo.watch_frozen(Array, :find_index, positions: [0])
10
+ LetItGo.watch_frozen(Array, :rindex, positions: [0])
11
+
12
+ # positions: 1
13
+ LetItGo.watch_frozen(Array, :fetch, positions: [1])
@@ -0,0 +1 @@
1
+ LetItGo.watch_frozen(Pathname, :new, positions: [0])
@@ -0,0 +1,49 @@
1
+ # Needs parser fixing to work
2
+ LetItGo.watch_frozen(String, :+, positions: [0])
3
+ LetItGo.watch_frozen(String, :<<, positions: [0])
4
+ LetItGo.watch_frozen(String, :'<=>', positions: [0])
5
+
6
+ # Positions: 0
7
+
8
+ LetItGo.watch_frozen(String, :split, positions: [0])
9
+ LetItGo.watch_frozen(String, :concat, positions: [0])
10
+ LetItGo.watch_frozen(String, :casecmp, positions: [0])
11
+ LetItGo.watch_frozen(String, :chomp, positions: [0])
12
+ LetItGo.watch_frozen(String, :count, positions: [0])
13
+ LetItGo.watch_frozen(String, :crypt, positions: [0])
14
+ LetItGo.watch_frozen(String, :delete, positions: [0])
15
+ LetItGo.watch_frozen(String, :delete!, positions: [0])
16
+ LetItGo.watch_frozen(String, :each_line, positions: [0])
17
+ LetItGo.watch_frozen(String, :lines, positions: [0])
18
+ LetItGo.watch_frozen(String, :include?, positions: [0])
19
+ LetItGo.watch_frozen(String, :index, positions: [0])
20
+ LetItGo.watch_frozen(String, :rindex, positions: [0])
21
+ LetItGo.watch_frozen(String, :replace, positions: [0])
22
+ LetItGo.watch_frozen(String, :match, positions: [0])
23
+ LetItGo.watch_frozen(String, :partition, positions: [0])
24
+ LetItGo.watch_frozen(String, :rpartition, positions: [0])
25
+ LetItGo.watch_frozen(String, :prepend, positions: [0])
26
+ LetItGo.watch_frozen(String, :scan, positions: [0])
27
+ LetItGo.watch_frozen(String, :slice, positions: [0])
28
+ LetItGo.watch_frozen(String, :slice!, positions: [0])
29
+ LetItGo.watch_frozen(String, :squeeze, positions: [0])
30
+ LetItGo.watch_frozen(String, :start_with?, positions: [0])
31
+ LetItGo.watch_frozen(String, :unpack, positions: [0])
32
+ LetItGo.watch_frozen(String, :upto, positions: [0])
33
+
34
+
35
+ # Positions; 0, 1
36
+
37
+ LetItGo.watch_frozen(String, :gsub, positions: [0, 1])
38
+ LetItGo.watch_frozen(String, :gsub!, positions: [0, 1])
39
+ LetItGo.watch_frozen(String, :sub, positions: [0, 1])
40
+ LetItGo.watch_frozen(String, :sub!, positions: [0, 1])
41
+ LetItGo.watch_frozen(String, :tr, positions: [0, 1])
42
+ LetItGo.watch_frozen(String, :tr!, positions: [0, 1])
43
+
44
+
45
+ # Positions: 1
46
+
47
+ LetItGo.watch_frozen(String, :insert, positions: [1])
48
+ LetItGo.watch_frozen(String, :ljust, positions: [1])
49
+ LetItGo.watch_frozen(String, :rjust, positions: [1])
@@ -0,0 +1,31 @@
1
+ module LetItGo
2
+ module Middleware
3
+ class Olaf
4
+ def initialize(app)
5
+ @app = app
6
+ end
7
+
8
+ def asset_request?(path)
9
+ path.match(/^\/assets\/|favicon.ico$/i)
10
+ end
11
+
12
+ def not_asset_request?(path)
13
+ !asset_request?(path)
14
+ end
15
+
16
+ def call(env)
17
+ result = nil
18
+ report = LetItGo.record do
19
+ result = @app.call(env)
20
+ end
21
+ puts ""
22
+ report.print if not_asset_request?(env["REQUEST_PATH".freeze])
23
+ result
24
+ end
25
+ end
26
+
27
+ # Pick your favorite character!
28
+ Elsa = Olaf
29
+ Anna = Olaf
30
+ end
31
+ end
@@ -0,0 +1,3 @@
1
+ module LetItGo
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,200 @@
1
+
2
+ module LetItGo
3
+ # Used for parsing the output of Ripper from a single line of Ruby.
4
+ #
5
+ # Pulls out method calls, and arguments to those method calls.
6
+ # We only care about when a string literal isn't frozen
7
+ class WTFParser
8
+
9
+ # Holds the "MethodAdd" components of parsed Ripper output
10
+ # Capable of pulling out `method_name`, and argument types such as
11
+ # :string_literal
12
+ class MethodAdd
13
+ # @raw =
14
+ # [
15
+ # [:string_literal, [:string_content, [:@tstring_content, "foo", [1, 7]]]],
16
+ # :".",
17
+ # [:@ident, "gsub", [1, 12]]],
18
+ # [:arg_paren,
19
+ # [:args_add_block,
20
+ # [[:regexp_literal, [], [:@regexp_end, "/", [1, 18]]],
21
+ # [:string_literal,
22
+ # [:string_content, [:@tstring_content, "blerg", [1, 22]]]]],
23
+ # false]]
24
+ def initialize(ripped_code)
25
+ ripped_code = ripped_code.dup
26
+ raise "Wrong node" unless ripped_code.shift == :method_add_arg
27
+ @raw = ripped_code
28
+ end
29
+
30
+ # [
31
+ # :call,
32
+ # [:string_literal, [:string_content, [:@tstring_content, "foo", [1, 7]]]],
33
+ # :".",
34
+ # [:@ident, "gsub", [1, 12]]
35
+ # ]
36
+ def call
37
+ @raw.find {|x| x.first == :call || x.first == :fcall}
38
+ end
39
+
40
+ # For gsub we want to pull from [:@ident, "gsub", [1, 12]] from `call`
41
+ def method_name
42
+ call.find {|x| x.is_a?(Array) && x.first == :@ident }[1]
43
+ end
44
+
45
+ # [:arg_paren,
46
+ # [
47
+ # :args_add_block,
48
+ # # ..
49
+ # ]
50
+ def args_paren
51
+ @raw.find {|x| x.first == :arg_paren } || []
52
+ end
53
+
54
+ # [:args_add_block,
55
+ # [[:regexp_literal, [], [:@regexp_end, "/", [1, 18]]],
56
+ # [:string_literal, [:string_content, [:@tstring_content, "blerg", [1, 22]]]]],
57
+ # false]
58
+ #
59
+ # or
60
+ #
61
+ # [:args_add_block,
62
+ # [[:regexp_literal, [], [:@regexp_end, "/", [1, 18]]],
63
+ # [[:call,
64
+ # [:string_literal,
65
+ # [:string_content, [:@tstring_content, "bar", [1, 22]]]],
66
+ # :".",
67
+ # [:@ident, "gsub!", [1, 27]]],
68
+ # [:arg_paren,
69
+ # [:args_add_block,
70
+ # [[:regexp_literal, [], [:@regexp_end, "/", [1, 34]]],
71
+ # [:string_literal,
72
+ # [:string_content, [:@tstring_content, "zoo", [1, 38]]]]],
73
+ # false]]]],
74
+ # false]]]
75
+ def args_add_block
76
+ args_paren.last || @raw.find {|x| x.first == :args_add_block }
77
+ end
78
+
79
+ def args
80
+ args_add_block.first(2).last || []
81
+ end
82
+
83
+ # Returns argument types as an array of symbols [:regexp_literal, :string_literal]
84
+ def arg_types
85
+ args.map(&:first).map {|x| x.is_a?(Array) ? x.first : x }
86
+ end
87
+ end
88
+
89
+ # I think "command calls" are method invocations without parens
90
+ # Like `puts "hello world"`. For some unholy reason, their structure
91
+ # is different than regular method calls?
92
+ class CommandCall < MethodAdd
93
+ # @raw =
94
+ # [
95
+ # [:string_literal, [:string_content, [:@tstring_content, "foo", [1, 7]]]],
96
+ # :".",
97
+ # [:@ident, "gsub", [1, 12]],
98
+ # [:args_add_block,
99
+ # [[:regexp_literal, [], [:@regexp_end, "/", [1, 18]]],
100
+ # [:call,
101
+ # [:string_literal,
102
+ # [:string_content, [:@tstring_content, "blerg", [1, 22]]]],
103
+ # :".",
104
+ # [:@ident, "downcase", [1, 29]]]],
105
+ # false]]
106
+ def initialize(ripped_code)
107
+ ripped_code = ripped_code.dup
108
+ raise "Wrong node" unless ripped_code.shift == :command_call
109
+ @raw = ripped_code
110
+ end
111
+
112
+ def method_name
113
+ @raw.find {|x| x.is_a?(Array) ? x.first == :@ident : false }[1]
114
+ end
115
+
116
+ def args_add_block
117
+ @raw.find {|x| x.is_a?(Array) ? x.first == :args_add_block : false }
118
+ end
119
+
120
+ def args
121
+ args_add_block.first(2).last || []
122
+ end
123
+
124
+ # Returns argument types as an array of symbols [:regexp_literal, :string_literal]
125
+ def arg_types
126
+ args.map(&:first).map {|x| x.is_a?(Array) ? x.first : x }
127
+ end
128
+ end
129
+
130
+ # These are calls to operators that take 1 argument such as `1 + 1` or `[] << 1`
131
+ class BinaryCall
132
+ # @raw =
133
+ # [
134
+ # [:string_literal, [:string_content, [:@tstring_content, "hello", [1, 7]]]],
135
+ # :+,
136
+ # [:string_literal,
137
+ # [:string_content, [:@tstring_content, "there", [1, 17]]]]]
138
+ def initialize(ripped_code)
139
+ ripped_code = ripped_code.dup
140
+ raise "Wrong node" unless ripped_code.shift == :binary
141
+ @raw = ripped_code
142
+ end
143
+
144
+ # For `1 + 1` we want to pull "+"
145
+ def method_name
146
+ @raw[1].to_s
147
+ end
148
+
149
+ def args
150
+ [@raw.last]
151
+ end
152
+
153
+ def arg_types
154
+ args.map(&:first).map {|x| x.is_a?(Array) ? x.first : x }
155
+ end
156
+ end
157
+
158
+ def initialize(ripped_code)
159
+ @raw = ripped_code
160
+ end
161
+
162
+ # Parses raw input recursively looking for :method_add_arg blocks
163
+ def find_method_add_from_raw(ripped_code, array = [])
164
+ return false unless ripped_code.is_a?(Array)
165
+
166
+ case ripped_code.first
167
+ when :method_add_arg
168
+ array << MethodAdd.new(ripped_code)
169
+ ripped_code.shift
170
+ when :command_call
171
+ array << CommandCall.new(ripped_code)
172
+ ripped_code.shift
173
+ when :binary
174
+ array << BinaryCall.new(ripped_code)
175
+ ripped_code.shift
176
+ end
177
+ ripped_code.each do |code|
178
+ find_method_add_from_raw(code, array) unless ripped_code.empty?
179
+ end
180
+ end
181
+
182
+ def method_add
183
+ @method_add_array ||= begin
184
+ method_add_array = []
185
+ find_method_add_from_raw(@raw.dup, method_add_array)
186
+ method_add_array
187
+ end
188
+ end
189
+
190
+ def each_method
191
+ if block_given?
192
+ method_add.each do |obj|
193
+ yield obj
194
+ end
195
+ else
196
+ enum_for(:each_method)
197
+ end
198
+ end
199
+ end
200
+ end
@@ -0,0 +1,78 @@
1
+ require 'ripper'
2
+ require 'pp'
3
+
4
+ # code = <<-CODE
5
+ # "foo".gsub(//, "blerg".downcase)
6
+ # CODE
7
+
8
+ # ripped_code = Ripper.sexp(code)
9
+
10
+ code = <<-CODE
11
+ "foo".gsub //, "blerg".downcase
12
+ CODE
13
+
14
+ ripped_code = Ripper.sexp(code)
15
+ # pp ripped_code
16
+
17
+ puts "============="
18
+
19
+
20
+ # code = <<-CODE
21
+ # ["hello"] << "there"
22
+ # CODE
23
+
24
+ # ripped_code = Ripper.sexp(code)
25
+ # pp ripped_code
26
+
27
+
28
+ code = <<-CODE
29
+ ps.find_all { |l| followpos(l).include?(DUMMY) }
30
+ CODE
31
+ ripped_code = Ripper.sexp(code)
32
+ pp ripped_code
33
+
34
+ def find_method_adds(ripped_code, array = [])
35
+ return false unless ripped_code.is_a?(Array)
36
+ case ripped_code.first
37
+ when :method_add_arg, :command_call, :binary
38
+ array << ripped_code
39
+ ripped_code.shift
40
+ ripped_code.each do |code|
41
+ find_method_adds(code, array)
42
+ end
43
+ else
44
+ ripped_code.each do |code|
45
+ find_method_adds(code, array) unless ripped_code.empty?
46
+ end
47
+ end
48
+ end
49
+
50
+ array = []
51
+ find_method_adds(ripped_code, array)
52
+
53
+ array.uniq!
54
+
55
+ puts array.count
56
+ pp array
57
+
58
+
59
+
60
+ def foo
61
+ end
62
+
63
+ # trace = TracePoint.trace(:call, :c_call) do |tp|
64
+ # tp.disable
65
+ # puts "=="
66
+ # puts tp.defined_class
67
+ # puts tp.method_id.inspect
68
+ # tp.enable
69
+ # end
70
+
71
+ # trace.enable
72
+
73
+ # a = rand
74
+ # "blahblah" << "blah #{a}"
75
+
76
+ foo()
77
+
78
+
metadata ADDED
@@ -0,0 +1,106 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: let_it_go
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - schneems
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2015-07-19 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.9'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.9'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rspec
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '10.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '10.0'
55
+ description: " Finds un-frozen string literals in your program "
56
+ email:
57
+ - richard.schneeman@gmail.com
58
+ executables: []
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - ".gitignore"
63
+ - ".rspec"
64
+ - ".travis.yml"
65
+ - CODE_OF_CONDUCT.md
66
+ - Gemfile
67
+ - LICENSE.txt
68
+ - README.md
69
+ - Rakefile
70
+ - bin/console
71
+ - bin/setup
72
+ - let_it_go.gemspec
73
+ - lib/let_it_go.rb
74
+ - lib/let_it_go/core_ext/array.rb
75
+ - lib/let_it_go/core_ext/pathname.rb
76
+ - lib/let_it_go/core_ext/string.rb
77
+ - lib/let_it_go/middleware/olaf.rb
78
+ - lib/let_it_go/version.rb
79
+ - lib/let_it_go/wtf_parser.rb
80
+ - lib/untitled.rb
81
+ homepage: https://github.com/schneems/let_it_go
82
+ licenses:
83
+ - MIT
84
+ metadata: {}
85
+ post_install_message:
86
+ rdoc_options: []
87
+ require_paths:
88
+ - lib
89
+ required_ruby_version: !ruby/object:Gem::Requirement
90
+ requirements:
91
+ - - ">="
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ required_rubygems_version: !ruby/object:Gem::Requirement
95
+ requirements:
96
+ - - ">="
97
+ - !ruby/object:Gem::Version
98
+ version: '0'
99
+ requirements: []
100
+ rubyforge_project:
101
+ rubygems_version: 2.4.5
102
+ signing_key:
103
+ specification_version: 4
104
+ summary: Finds un-frozen string literals in your program
105
+ test_files: []
106
+ has_rdoc: