pippi 0.0.1
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 +7 -0
- data/.gitignore +5 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +4 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +24 -0
- data/README.md +177 -0
- data/Rakefile +11 -0
- data/bin/pippi +7 -0
- data/doc/README +1 -0
- data/doc/docs.md +64 -0
- data/lib/pippi.rb +15 -0
- data/lib/pippi/auto_runner.rb +24 -0
- data/lib/pippi/check_loader.rb +23 -0
- data/lib/pippi/check_set_mapper.rb +35 -0
- data/lib/pippi/checks/check.rb +39 -0
- data/lib/pippi/checks/debug_check.rb +14 -0
- data/lib/pippi/checks/map_followed_by_flatten.rb +55 -0
- data/lib/pippi/checks/reverse_followed_by_each.rb +53 -0
- data/lib/pippi/checks/select_followed_by_first.rb +58 -0
- data/lib/pippi/checks/select_followed_by_size.rb +59 -0
- data/lib/pippi/context.rb +31 -0
- data/lib/pippi/exec_runner.rb +34 -0
- data/lib/pippi/problem.rb +24 -0
- data/lib/pippi/report.rb +27 -0
- data/lib/pippi/tasks.rb +41 -0
- data/lib/pippi/version.rb +3 -0
- data/pippi.gemspec +23 -0
- data/sample/map_followed_by_flatten.rb +6 -0
- data/test/check_test.rb +41 -0
- data/test/rails_core_extensions.rb +5 -0
- data/test/test_helper.rb +7 -0
- data/test/unit/map_followed_by_flatten_test.rb +38 -0
- data/test/unit/problem_test.rb +23 -0
- data/test/unit/report_test.rb +25 -0
- data/test/unit/reverse_followed_by_each_test.rb +29 -0
- data/test/unit/select_followed_by_first_test.rb +33 -0
- data/test/unit/select_followed_by_size_test.rb +33 -0
- data/vendor/cache/byebug-2.7.0.gem +0 -0
- data/vendor/cache/columnize-0.8.9.gem +0 -0
- data/vendor/cache/debugger-linecache-1.2.0.gem +0 -0
- data/vendor/cache/minitest-5.4.2.gem +0 -0
- data/vendor/cache/rake-10.1.0.gem +0 -0
- metadata +139 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 6b2991c3de0e47fb2502ca50193e9c6d1d1afef7
|
4
|
+
data.tar.gz: 16af3bb256083ed6f0b0f9faa2410e20110590a7
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 320cd3c32751f7835f3b8213b5d51841f27400f564161b7a4e55d38db747cec4984f8d31c81c9ff63a5186f509d868407e87146210d37d620c2397db3f36b1c0
|
7
|
+
data.tar.gz: 44110d0bca80145d567821a6202562dac0f0757911feb983ff404932c87ad6843077ada711b37754c9bb9d4dca3e0dc268b4f7fc59dcd9ffe70033c8b15c773e
|
data/.gitignore
ADDED
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
ruby-2.1.2
|
data/CHANGELOG.md
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
pippi (0.0.1)
|
5
|
+
|
6
|
+
GEM
|
7
|
+
remote: https://rubygems.org/
|
8
|
+
specs:
|
9
|
+
byebug (2.7.0)
|
10
|
+
columnize (~> 0.3)
|
11
|
+
debugger-linecache (~> 1.2)
|
12
|
+
columnize (0.8.9)
|
13
|
+
debugger-linecache (1.2.0)
|
14
|
+
minitest (5.4.2)
|
15
|
+
rake (10.1.0)
|
16
|
+
|
17
|
+
PLATFORMS
|
18
|
+
ruby
|
19
|
+
|
20
|
+
DEPENDENCIES
|
21
|
+
byebug (~> 2.7)
|
22
|
+
minitest (~> 5.0)
|
23
|
+
pippi!
|
24
|
+
rake (~> 10.1)
|
data/README.md
ADDED
@@ -0,0 +1,177 @@
|
|
1
|
+
Pippi is a utility for finding suboptimal Ruby class API usage.
|
2
|
+
|
3
|
+
<a href="http://thomasleecopeland.com/2014/10/22/finding-suboptimal-api-usage.html">Here's a project overview.</a>.
|
4
|
+
|
5
|
+
## Checks
|
6
|
+
|
7
|
+
### MapFollowedByFlatten
|
8
|
+
|
9
|
+
Don't use map followed by flatten; use flat_map instead
|
10
|
+
|
11
|
+
For example, rather than doing this:
|
12
|
+
|
13
|
+
```ruby
|
14
|
+
[1,2,3].map {|x| [x,x+1] }.flatten
|
15
|
+
```
|
16
|
+
|
17
|
+
Instead, consider doing this:
|
18
|
+
|
19
|
+
```ruby
|
20
|
+
[1,2,3].flat_map {|x| [x, x+1]}
|
21
|
+
```
|
22
|
+
|
23
|
+
### ReverseFollowedByEach
|
24
|
+
|
25
|
+
Don't use each followed by reverse; use reverse_each instead
|
26
|
+
|
27
|
+
For example, rather than doing this:
|
28
|
+
|
29
|
+
```ruby
|
30
|
+
[1,2,3].reverse.each {|x| x+1 }
|
31
|
+
```
|
32
|
+
|
33
|
+
Instead, consider doing this:
|
34
|
+
|
35
|
+
```ruby
|
36
|
+
[1,2,3].reverse_each {|x| x+1 }
|
37
|
+
```
|
38
|
+
|
39
|
+
### SelectFollowedByFirst
|
40
|
+
|
41
|
+
Don't use select followed by first; use detect instead
|
42
|
+
|
43
|
+
For example, rather than doing this:
|
44
|
+
|
45
|
+
```ruby
|
46
|
+
[1,2,3].select {|x| x > 1 }.first
|
47
|
+
```
|
48
|
+
|
49
|
+
Instead, consider doing this:
|
50
|
+
|
51
|
+
```ruby
|
52
|
+
[1,2,3].detect {|x| x > 1 }
|
53
|
+
```
|
54
|
+
|
55
|
+
### SelectFollowedBySize
|
56
|
+
|
57
|
+
Don't use select followed by size; use count instead
|
58
|
+
|
59
|
+
For example, rather than doing this:
|
60
|
+
|
61
|
+
```ruby
|
62
|
+
[1,2,3].select {|x| x > 1 }.size
|
63
|
+
```
|
64
|
+
|
65
|
+
Instead, consider doing this:
|
66
|
+
|
67
|
+
```ruby
|
68
|
+
[1,2,3].count {|x| x > 1 }
|
69
|
+
```
|
70
|
+
|
71
|
+
## Usage
|
72
|
+
|
73
|
+
### Inside Rails tests
|
74
|
+
|
75
|
+
See https://github.com/tcopeland/pippi_demo#pippi-demo
|
76
|
+
|
77
|
+
### From the command line:
|
78
|
+
|
79
|
+
Assuming you're using bundler:
|
80
|
+
|
81
|
+
```bash
|
82
|
+
# Add this to your project's Gemfile:
|
83
|
+
gem 'pippi'
|
84
|
+
# Run 'bundle', see some output
|
85
|
+
# To run a particular check:
|
86
|
+
bundle exec pippi tmp/tmpfile.rb MapFollowedByFlatten Foo.new.bar out.txt
|
87
|
+
# Or to run all the basic Pippi checks on your code and exercise it with MyClass.new.exercise_some_code:
|
88
|
+
bundle exec ruby -rpippi/auto_runner -e "MyClass.new.exercise_some_code"
|
89
|
+
```
|
90
|
+
|
91
|
+
## Ideas for other problems to detect:
|
92
|
+
|
93
|
+
```ruby
|
94
|
+
# Don't use select followed by compact, use select with the nil inside the block
|
95
|
+
# Use assert_nil rather than assert_equals
|
96
|
+
# wrong
|
97
|
+
assert_equals(nil, foo)
|
98
|
+
# right
|
99
|
+
assert_nil foo
|
100
|
+
|
101
|
+
# unnecessary assignment since String#strip! mutates receiver
|
102
|
+
# wrong
|
103
|
+
x = x.strip!
|
104
|
+
# right
|
105
|
+
x.strip!
|
106
|
+
|
107
|
+
# Use Pathname
|
108
|
+
# wrong
|
109
|
+
File.read(File.join(Rails.root, "config", "database.yml")
|
110
|
+
# right
|
111
|
+
Rails.root.join("config", "database.yml").read
|
112
|
+
|
113
|
+
# Use Kernel#tap
|
114
|
+
# wrong
|
115
|
+
x = [1,2]
|
116
|
+
x << 3
|
117
|
+
return x
|
118
|
+
# right
|
119
|
+
[1,2].tap {|y| y << 3 }
|
120
|
+
|
121
|
+
|
122
|
+
# Rails checks
|
123
|
+
|
124
|
+
# No need to call to_i on ActiveRecord::Base methods passed to route generators
|
125
|
+
# wrong
|
126
|
+
product_path(@product.to_i)
|
127
|
+
# right
|
128
|
+
product_path(@product)
|
129
|
+
|
130
|
+
# something with replacing x.map.compact with x.select.map
|
131
|
+
````
|
132
|
+
|
133
|
+
## Here are some things that Pippi is not well suited for
|
134
|
+
### Use self.new vs MyClass.new. This is not a good fit for Pippi because it involves a receiver usage that can be detected with static analysis.
|
135
|
+
#### wrong
|
136
|
+
class Foo
|
137
|
+
def self.bar
|
138
|
+
Foo.new
|
139
|
+
end
|
140
|
+
end
|
141
|
+
#### right
|
142
|
+
class Foo
|
143
|
+
def self.bar
|
144
|
+
self.new
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
|
149
|
+
## TODO
|
150
|
+
|
151
|
+
* Clean up this initial hacked out metaprogramming
|
152
|
+
* Do more checks
|
153
|
+
* Make writing rules nicer, without some much dorking around with methods. "select followed by first" could be specified with something like "Array#select => #first" and the rest left up to the framework.
|
154
|
+
|
155
|
+
## Developing
|
156
|
+
|
157
|
+
To see teacher output for a file `tmp/baz.rb`:
|
158
|
+
|
159
|
+
```bash
|
160
|
+
rm -f pippi_debug.log ; PIPPI_DEBUG=1 bundle exec pippi tmp/baz.rb DebugCheck Foo.new.bar tmp/out.txt ; cat pippi_debug.log
|
161
|
+
```
|
162
|
+
|
163
|
+
When trying to find issues in a project:
|
164
|
+
|
165
|
+
```bash
|
166
|
+
# in project directory (e.g., aasm)
|
167
|
+
rm -rf pippi_debug.log pippi.log .bundle/gems/pippi-0.0.1/ .bundle/cache/pippi-0.0.1.gem .bundle/specifications/pippi-0.0.1.gemspec && bundle update pippi --local && PIPPI_DEBUG=1 bundle exec ruby -rpippi/auto_runner -e "puts 'hi'" && grep -C 5 BOOM pippi_debug.log
|
168
|
+
# or to run some specs with pippi watching:
|
169
|
+
rm -rf pippi_debug.log pippi.log .bundle/gems/pippi-0.0.1/ .bundle/cache/pippi-0.0.1.gem .bundle/specifications/pippi-0.0.1.gemspec && bundle update pippi --local && PIPPI_DEBUG=1 bundle exec ruby -rpippi/auto_runner -Ispec spec/unit/*.rb
|
170
|
+
|
171
|
+
```
|
172
|
+
|
173
|
+
## Credits
|
174
|
+
|
175
|
+
* Thanks to <a href="https://www.livingsocial.com/">LivingSocial</a> for letting me develop and open source this utility.
|
176
|
+
* Thanks to Evan Phoenix for the idea of watching method invocations at runtime using metaprogramming rather than using `Tracepoint`.
|
177
|
+
* Thanks to Michael Bernstein (of Code Climate fame) for an inspirational discussion of code anaysis in general.
|
data/Rakefile
ADDED
data/bin/pippi
ADDED
data/doc/README
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
Docs
|
data/doc/docs.md
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
|
2
|
+
### MapFollowedByFlatten
|
3
|
+
|
4
|
+
Don't use map followed by flatten; use flat_map instead
|
5
|
+
|
6
|
+
For example, rather than doing this:
|
7
|
+
|
8
|
+
```ruby
|
9
|
+
[1,2,3].map {|x| [x,x+1] }.flatten
|
10
|
+
```
|
11
|
+
|
12
|
+
Instead, consider doing this:
|
13
|
+
|
14
|
+
```ruby
|
15
|
+
[1,2,3].flat_map {|x| [x, x+1]}
|
16
|
+
```
|
17
|
+
|
18
|
+
### ReverseFollowedByEach
|
19
|
+
|
20
|
+
Don't use each followed by reverse; use reverse_each instead
|
21
|
+
|
22
|
+
For example, rather than doing this:
|
23
|
+
|
24
|
+
```ruby
|
25
|
+
[1,2,3].reverse.each {|x| x+1 }
|
26
|
+
```
|
27
|
+
|
28
|
+
Instead, consider doing this:
|
29
|
+
|
30
|
+
```ruby
|
31
|
+
[1,2,3].reverse_each {|x| x+1 }
|
32
|
+
```
|
33
|
+
|
34
|
+
### SelectFollowedByFirst
|
35
|
+
|
36
|
+
Don't use select followed by first; use detect instead
|
37
|
+
|
38
|
+
For example, rather than doing this:
|
39
|
+
|
40
|
+
```ruby
|
41
|
+
[1,2,3].select {|x| x > 1 }.first
|
42
|
+
```
|
43
|
+
|
44
|
+
Instead, consider doing this:
|
45
|
+
|
46
|
+
```ruby
|
47
|
+
[1,2,3].detect {|x| x > 1 }
|
48
|
+
```
|
49
|
+
|
50
|
+
### SelectFollowedBySize
|
51
|
+
|
52
|
+
Don't use select followed by size; use count instead
|
53
|
+
|
54
|
+
For example, rather than doing this:
|
55
|
+
|
56
|
+
```ruby
|
57
|
+
[1,2,3].select {|x| x > 1 }.size
|
58
|
+
```
|
59
|
+
|
60
|
+
Instead, consider doing this:
|
61
|
+
|
62
|
+
```ruby
|
63
|
+
[1,2,3].count {|x| x > 1 }
|
64
|
+
```
|
data/lib/pippi.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
|
3
|
+
require 'pippi/context'
|
4
|
+
require 'pippi/check_set_mapper'
|
5
|
+
require 'pippi/report'
|
6
|
+
require "pippi/checks/check"
|
7
|
+
require 'pippi/problem'
|
8
|
+
require 'pippi/check_loader'
|
9
|
+
require 'pippi/exec_runner'
|
10
|
+
require 'pippi/auto_runner'
|
11
|
+
require 'pippi/checks/map_followed_by_flatten'
|
12
|
+
require 'pippi/checks/reverse_followed_by_each'
|
13
|
+
require 'pippi/checks/select_followed_by_first'
|
14
|
+
require 'pippi/checks/select_followed_by_size'
|
15
|
+
require 'pippi/checks/debug_check'
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Pippi
|
2
|
+
|
3
|
+
class AutoRunner
|
4
|
+
attr_reader :ctx
|
5
|
+
|
6
|
+
def initialize(opts={})
|
7
|
+
checkset = opts.fetch(:checkset, "basic")
|
8
|
+
@ctx = Pippi::Context.new
|
9
|
+
Pippi::CheckLoader.new(@ctx, checkset).checks.each(&:decorate)
|
10
|
+
at_exit { dump }
|
11
|
+
end
|
12
|
+
|
13
|
+
def dump
|
14
|
+
File.open("log/pippi.log", "w") do |outfile|
|
15
|
+
@ctx.report.problems.each do |problem|
|
16
|
+
outfile.syswrite("#{problem.to_text}\n")
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Pippi
|
2
|
+
|
3
|
+
class CheckLoader
|
4
|
+
|
5
|
+
attr_reader :ctx, :check_names
|
6
|
+
|
7
|
+
def initialize(ctx, check_names)
|
8
|
+
@ctx = ctx
|
9
|
+
@check_names = if check_names.kind_of?(String)
|
10
|
+
Pippi::CheckSetMapper.new(check_names).check_names
|
11
|
+
else
|
12
|
+
check_names
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def checks
|
17
|
+
check_names.map do |check_name|
|
18
|
+
Pippi::Checks.const_get(check_name).new(ctx)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Pippi
|
2
|
+
|
3
|
+
class CheckSetMapper
|
4
|
+
|
5
|
+
attr_reader :raw_check_specifier
|
6
|
+
|
7
|
+
def initialize(raw_check_specifier)
|
8
|
+
@raw_check_specifier = raw_check_specifier
|
9
|
+
end
|
10
|
+
|
11
|
+
def check_names
|
12
|
+
raw_check_specifier.split(",").map do |specifier|
|
13
|
+
predefined_sets[specifier] || specifier
|
14
|
+
end.flatten
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def predefined_sets
|
20
|
+
{
|
21
|
+
"basic" => [
|
22
|
+
"SelectFollowedByFirst",
|
23
|
+
"SelectFollowedBySize",
|
24
|
+
"ReverseFollowedByEach",
|
25
|
+
],
|
26
|
+
"training" => [
|
27
|
+
],
|
28
|
+
"buggy" => [
|
29
|
+
"MapFollowedByFlatten",
|
30
|
+
]
|
31
|
+
}
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Pippi::Checks
|
2
|
+
|
3
|
+
class Check
|
4
|
+
|
5
|
+
attr_accessor :ctx
|
6
|
+
|
7
|
+
def initialize(ctx)
|
8
|
+
@ctx = ctx
|
9
|
+
end
|
10
|
+
|
11
|
+
def array_mutator_methods
|
12
|
+
[:collect!, :compact!, :flatten!, :map!, :reject!, :reverse!, :rotate!, :select!, :shuffle!, :slice!, :sort!, :sort_by!, :uniq!]
|
13
|
+
end
|
14
|
+
|
15
|
+
def add_problem
|
16
|
+
problem_location = caller_locations.detect {|c| c.to_s !~ /byebug|lib\/pippi\/checks/ }
|
17
|
+
ctx.report.add(Pippi::Problem.new(:line_number => problem_location.lineno, :file_path => problem_location.path, :check_class => self.class))
|
18
|
+
end
|
19
|
+
|
20
|
+
def clear_fault_proc
|
21
|
+
Proc.new do
|
22
|
+
problem_location = caller_locations.detect {|c| c.to_s !~ /byebug|lib\/pippi\/checks/ }
|
23
|
+
self.class._pippi_check_select_followed_by_size.clear_fault(problem_location.lineno, problem_location.path)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def clear_fault(lineno, path)
|
28
|
+
ctx.report.remove(lineno, path, self.class)
|
29
|
+
end
|
30
|
+
|
31
|
+
def its_ok_watcher_proc(clazz, method_name)
|
32
|
+
Proc.new do
|
33
|
+
singleton_class.ancestors.detect {|x| x == clazz }.instance_eval { remove_method method_name }
|
34
|
+
super()
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
end
|