let_it_go 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/.rspec +2 -0
- data/.travis.yml +3 -0
- data/CODE_OF_CONDUCT.md +13 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +148 -0
- data/Rakefile +1 -0
- data/bin/console +14 -0
- data/bin/setup +7 -0
- data/let_it_go.gemspec +26 -0
- data/lib/let_it_go.rb +200 -0
- data/lib/let_it_go/core_ext/array.rb +13 -0
- data/lib/let_it_go/core_ext/pathname.rb +1 -0
- data/lib/let_it_go/core_ext/string.rb +49 -0
- data/lib/let_it_go/middleware/olaf.rb +31 -0
- data/lib/let_it_go/version.rb +3 -0
- data/lib/let_it_go/wtf_parser.rb +200 -0
- data/lib/untitled.rb +78 -0
- metadata +106 -0
checksums.yaml
ADDED
@@ -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
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/CODE_OF_CONDUCT.md
ADDED
@@ -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
data/LICENSE.txt
ADDED
@@ -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.
|
data/README.md
ADDED
@@ -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
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/bin/console
ADDED
@@ -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
|
data/bin/setup
ADDED
data/let_it_go.gemspec
ADDED
@@ -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
|
data/lib/let_it_go.rb
ADDED
@@ -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,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
|
data/lib/untitled.rb
ADDED
@@ -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:
|