pippi 0.0.4 → 0.0.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +3 -0
- data/README.md +108 -39
- data/bin/pippi +2 -2
- data/doc/docs.md +38 -20
- data/lib/pippi.rb +2 -1
- data/lib/pippi/auto_runner.rb +3 -7
- data/lib/pippi/check_loader.rb +2 -5
- data/lib/pippi/check_set_mapper.rb +2 -3
- data/lib/pippi/checks/assert_with_nil.rb +5 -6
- data/lib/pippi/checks/check.rb +10 -8
- data/lib/pippi/checks/debug_check.rb +1 -4
- data/lib/pippi/checks/map_followed_by_flatten.rb +7 -9
- data/lib/pippi/checks/reverse_followed_by_each.rb +6 -8
- data/lib/pippi/checks/select_followed_by_empty.rb +55 -0
- data/lib/pippi/checks/select_followed_by_first.rb +7 -9
- data/lib/pippi/checks/select_followed_by_size.rb +6 -12
- data/lib/pippi/context.rb +3 -7
- data/lib/pippi/exec_runner.rb +12 -7
- data/lib/pippi/problem.rb +0 -1
- data/lib/pippi/report.rb +2 -6
- data/lib/pippi/tasks.rb +11 -13
- data/lib/pippi/version.rb +2 -2
- metadata +4 -37
- data/.gitignore +0 -6
- data/.ruby-version +0 -1
- data/Gemfile +0 -3
- data/Gemfile.lock +0 -24
- data/Rakefile +0 -11
- data/pippi.gemspec +0 -23
- data/sample/map_followed_by_flatten.rb +0 -6
- data/test/check_test.rb +0 -41
- data/test/rails_core_extensions.rb +0 -13
- data/test/test_helper.rb +0 -7
- data/test/unit/assert_with_nil_test.rb +0 -50
- data/test/unit/check_set_mapper_test.rb +0 -17
- data/test/unit/map_followed_by_flatten_test.rb +0 -38
- data/test/unit/problem_test.rb +0 -23
- data/test/unit/report_test.rb +0 -25
- data/test/unit/reverse_followed_by_each_test.rb +0 -29
- data/test/unit/select_followed_by_first_test.rb +0 -33
- data/test/unit/select_followed_by_size_test.rb +0 -47
- 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
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b69820f23560f62844335dba76692a5b29c40523
|
4
|
+
data.tar.gz: bfdc9ccdd89aab53afc19f82c247c55e32f56eef
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 11480a7cce7761a9c2aeff77f31829872f79e577945082e1f6b7975f851b1968991d07c17eea8bbb3d28ae0e1eeceb0223edb99e578458e4fe987633a53b3ff1
|
7
|
+
data.tar.gz: 361ff7703eb9af5c59ea6a3385c6e2d8def03be9a2b94b4a2b6342e7689055fa4bc9497b2a2b8f85d5b797dfcf0ae4ad311a7eaec2989bf6a4eaabc2e3a9a887
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
@@ -2,7 +2,54 @@
|
|
2
2
|
|
3
3
|
Pippi is a utility for finding suboptimal Ruby class API usage.
|
4
4
|
|
5
|
-
|
5
|
+
Consider this little array:
|
6
|
+
|
7
|
+
```ruby
|
8
|
+
[1,2,3]
|
9
|
+
```
|
10
|
+
|
11
|
+
Now suppose we want to find the first element in that array that's greater than one. We can use Array#select, which returns another Array, and then use Array#first:
|
12
|
+
|
13
|
+
```ruby
|
14
|
+
[1,2,3].select {|x| x > 1 }.first
|
15
|
+
```
|
16
|
+
|
17
|
+
Of course that's terribly inefficient. Since we only need one element we don't need to select all elements that match the predicate. We should use Array#detect instead:
|
18
|
+
|
19
|
+
```ruby
|
20
|
+
[1,2,3].detect {|x| x > 1}
|
21
|
+
```
|
22
|
+
|
23
|
+
A change like this is a small optimization, but they can add up. More importantly, they communicate the intent of the programmer; the use of Array#detect makes it clear that we're just looking for the first item to match the predicate.
|
24
|
+
|
25
|
+
This sort of thing can be be found during a code review, or maybe when you're just poking around the code. But why not have a tool find it instead? Thus, pippi. Pippi observes code while it's running - by hooking into your test suite execution - and reports misuse of class-level APIs.
|
26
|
+
|
27
|
+
There are many nifty Ruby static analysis tools - flay, reek, flog, etc. This is not like those. It doesn't parse source code; it doesn't examine an abstract syntax tree or even sequences of MRI instructions. So it cannot find the types of issues that those tools can find. Instead, it's focused on runtime analysis; that is, method calls and method call sequences.
|
28
|
+
|
29
|
+
Here's an important caveat: pippi is not, and more importantly cannot, be free of false positives. That's because of the halting problem. Pippi finds suboptimal API usage based on data flows as driven by a project's test suite. There may be alternate data flows where this API usage is correct. For example, in the code below, if rand < 0.5 is true, then the Array will be mutated and the program cannot correctly be simplified by replacing "select followed by first" with "detect":
|
30
|
+
|
31
|
+
```ruby
|
32
|
+
x = [1,2,3].select {|y| y > 1 }
|
33
|
+
x.reject! {|y| y > 2} if rand < 0.5
|
34
|
+
x.first
|
35
|
+
```
|
36
|
+
|
37
|
+
There are various techniques that eliminate many of these false positives. For example, after flagging an issue, pippi watches subsequent method invocations and if those indicate the initial problem report was in error it'll remove the problem from the report.
|
38
|
+
|
39
|
+
Pippi is entirely dependent on the test suite to execute code in order to find problems. If a project's test code coverage is small, pippi probably won't find much.
|
40
|
+
|
41
|
+
Here's how pippi stacks up using the [Aaron Quint](https://twitter.com/aq) [Ruby Performance Character Profiles](https://www.youtube.com/watch?v=cOaVIeX6qGg&t=8m50s) system:
|
42
|
+
|
43
|
+
* Specificity - very specific, finds actual detailed usages of bad code
|
44
|
+
* Impact - very impactful, slows things down lots
|
45
|
+
* Difficulty of Operator Use - easy to install, just a new gemfile entry
|
46
|
+
* Readability - results are easy to read
|
47
|
+
* Realtimedness - finds stuff right away
|
48
|
+
* Special Abilities - ?
|
49
|
+
|
50
|
+
Finally, why "pippi"? Because Pippi Longstocking was a Thing-Finder, and pippi finds things.
|
51
|
+
|
52
|
+
[![Build Status](http://img.shields.io/travis/tcopeland/pippi.svg)](http://travis-ci.org/tcopeland/pippi)
|
6
53
|
|
7
54
|
## Usage
|
8
55
|
|
@@ -22,11 +69,31 @@ end
|
|
22
69
|
USE_PIPPI=true bundle exec rake test:units && cat log/pippi.log
|
23
70
|
```
|
24
71
|
|
72
|
+
* You can also select a different checkset:
|
73
|
+
|
74
|
+
```text
|
75
|
+
USE_PIPPI=true PIPPI_CHECKSET=training bundle exec rake test:units && cat log/pippi.log
|
76
|
+
```
|
77
|
+
|
78
|
+
|
25
79
|
Here's a [demo Rails application](https://github.com/tcopeland/pippi_demo#pippi-demo).
|
26
80
|
|
27
81
|
### Rails with rspec
|
28
82
|
|
29
|
-
|
83
|
+
* Add `gem 'pippi'` to the `test` group in your project's `Gemfile`
|
84
|
+
* Add this to the bottom of `spec_helper.rb`:
|
85
|
+
|
86
|
+
```ruby
|
87
|
+
if ENV['USE_PIPPI'].present?
|
88
|
+
Pippi::AutoRunner.new(:checkset => ENV['PIPPI_CHECKSET'] || "basic")
|
89
|
+
end
|
90
|
+
```
|
91
|
+
* Run it:
|
92
|
+
|
93
|
+
```text
|
94
|
+
USE_PIPPI=true bundle exec rake spec && cat log/pippi.log
|
95
|
+
```
|
96
|
+
|
30
97
|
|
31
98
|
### From the command line:
|
32
99
|
|
@@ -43,106 +110,108 @@ bundle exec ruby -rpippi/auto_runner -e "MyClass.new.exercise_some_code"
|
|
43
110
|
```
|
44
111
|
|
45
112
|
|
46
|
-
|
47
113
|
## Checksets
|
48
114
|
|
49
115
|
Pippi has the concept of "checksets" which are, well, sets of checks. The current checksets are listed below.
|
50
116
|
|
51
|
-
|
117
|
+
### basic
|
52
118
|
|
53
|
-
|
119
|
+
#### ReverseFollowedByEach
|
54
120
|
|
55
|
-
|
56
|
-
* SelectFollowedBySize
|
57
|
-
* ReverseFollowedByEach
|
121
|
+
Don't use each followed by reverse; use reverse_each instead
|
58
122
|
|
59
|
-
|
123
|
+
For example, rather than doing this:
|
60
124
|
|
61
|
-
|
62
|
-
|
125
|
+
```ruby
|
126
|
+
[1,2,3].reverse.each {|x| x+1 }
|
127
|
+
```
|
63
128
|
|
64
|
-
|
129
|
+
Instead, consider doing this:
|
65
130
|
|
66
|
-
|
131
|
+
```ruby
|
132
|
+
[1,2,3].reverse_each {|x| x+1 }
|
133
|
+
```
|
67
134
|
|
68
|
-
|
135
|
+
#### SelectFollowedByEmpty
|
136
|
+
|
137
|
+
Don't use select followed by empty?; use none? instead
|
69
138
|
|
70
139
|
For example, rather than doing this:
|
71
140
|
|
72
141
|
```ruby
|
73
|
-
x
|
142
|
+
[1,2,3].select {|x| x > 1 }.empty?
|
74
143
|
```
|
75
144
|
|
76
145
|
Instead, consider doing this:
|
77
146
|
|
78
147
|
```ruby
|
79
|
-
x
|
148
|
+
[1,2,3].none? {|x| x > 1 }
|
80
149
|
```
|
81
150
|
|
82
|
-
|
151
|
+
#### SelectFollowedByFirst
|
83
152
|
|
84
|
-
Don't use
|
153
|
+
Don't use select followed by first; use detect instead
|
85
154
|
|
86
155
|
For example, rather than doing this:
|
87
156
|
|
88
157
|
```ruby
|
89
|
-
[1,2,3].
|
158
|
+
[1,2,3].select {|x| x > 1 }.first
|
90
159
|
```
|
91
160
|
|
92
161
|
Instead, consider doing this:
|
93
162
|
|
94
163
|
```ruby
|
95
|
-
[1,2,3].
|
164
|
+
[1,2,3].detect {|x| x > 1 }
|
96
165
|
```
|
97
166
|
|
98
|
-
|
167
|
+
#### SelectFollowedBySize
|
99
168
|
|
100
|
-
Don't use
|
169
|
+
Don't use select followed by size; use count instead
|
101
170
|
|
102
171
|
For example, rather than doing this:
|
103
172
|
|
104
173
|
```ruby
|
105
|
-
[1,2,3].
|
174
|
+
[1,2,3].select {|x| x > 1 }.size
|
106
175
|
```
|
107
176
|
|
108
177
|
Instead, consider doing this:
|
109
178
|
|
110
179
|
```ruby
|
111
|
-
[1,2,3].
|
180
|
+
[1,2,3].count {|x| x > 1 }
|
112
181
|
```
|
182
|
+
### buggy
|
113
183
|
|
114
|
-
|
184
|
+
#### AssertWithNil
|
115
185
|
|
116
|
-
Don't use
|
186
|
+
Don't use assert_equal with nil as a first argument; use assert_nil instead
|
117
187
|
|
118
188
|
For example, rather than doing this:
|
119
189
|
|
120
190
|
```ruby
|
121
|
-
|
191
|
+
x = nil ; assert_equal(nil, x)
|
122
192
|
```
|
123
193
|
|
124
194
|
Instead, consider doing this:
|
125
195
|
|
126
196
|
```ruby
|
127
|
-
|
197
|
+
x = nil ; assert_nil(x)
|
128
198
|
```
|
129
199
|
|
130
|
-
|
200
|
+
#### MapFollowedByFlatten
|
131
201
|
|
132
|
-
Don't use
|
202
|
+
Don't use map followed by flatten; use flat_map instead
|
133
203
|
|
134
204
|
For example, rather than doing this:
|
135
205
|
|
136
206
|
```ruby
|
137
|
-
[1,2,3].
|
207
|
+
[1,2,3].map {|x| [x,x+1] }.flatten
|
138
208
|
```
|
139
209
|
|
140
210
|
Instead, consider doing this:
|
141
211
|
|
142
212
|
```ruby
|
143
|
-
[1,2,3].
|
213
|
+
[1,2,3].flat_map {|x| [x, x+1]}
|
144
214
|
```
|
145
|
-
|
146
215
|
## Ideas for other problems to detect:
|
147
216
|
|
148
217
|
```ruby
|
@@ -226,7 +295,6 @@ When trying to find issues in a project:
|
|
226
295
|
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
|
227
296
|
# or to run some specs with pippi watching:
|
228
297
|
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
|
229
|
-
|
230
298
|
```
|
231
299
|
|
232
300
|
## How to do a release
|
@@ -241,8 +309,9 @@ rm -rf pippi_debug.log pippi.log .bundle/gems/pippi-0.0.1/ .bundle/cache/pippi-0
|
|
241
309
|
|
242
310
|
## Credits
|
243
311
|
|
244
|
-
* Christopher Schramm bugfixes in fault proc clearing
|
245
|
-
* [Evan Phoenix](https://twitter.com/evanphx) for the idea of watching method invocations at runtime using metaprogramming rather than using `Tracepoint`.
|
246
|
-
* Igor Kapkov documentation fixes
|
247
|
-
* LivingSocial](https://www.livingsocial.com/) for letting me develop and open source this utility.
|
248
|
-
* [Michael Bernstein](https://twitter.com/mrb_bk) (of [CodeClimate](https://codeclimate.com/) fame) for an inspirational discussion of code anaysis in general.
|
312
|
+
* Christopher Schramm([@cschramm](https://github.com/cschramm)) bugfixes in fault proc clearing
|
313
|
+
* [Evan Phoenix](https://twitter.com/evanphx)([@evanphx](https://github.com/evanphx)) for the idea of watching method invocations at runtime using metaprogramming rather than using `Tracepoint`.
|
314
|
+
* [Igor Kapkov](https://twitter.com/igasgeek)([@igas](https://github.com/igas)) documentation fixes
|
315
|
+
* [LivingSocial](https://www.livingsocial.com/) for letting me develop and open source this utility.
|
316
|
+
* [Michael Bernstein](https://twitter.com/mrb_bk)([@mrb](https://github.com/mrb)) (of [CodeClimate](https://codeclimate.com/) fame) for an inspirational discussion of code anaysis in general.
|
317
|
+
* [Olle Jonsson](https://twitter.com/olleolleolle)([@olleolleolle](https://github.com/olleolleolle)) rubocop fixes
|
data/bin/pippi
CHANGED
data/doc/docs.md
CHANGED
@@ -1,80 +1,98 @@
|
|
1
|
+
### basic
|
1
2
|
|
2
|
-
|
3
|
+
#### ReverseFollowedByEach
|
3
4
|
|
4
|
-
Don't use
|
5
|
+
Don't use each followed by reverse; use reverse_each instead
|
5
6
|
|
6
7
|
For example, rather than doing this:
|
7
8
|
|
8
9
|
```ruby
|
9
|
-
x
|
10
|
+
[1,2,3].reverse.each {|x| x+1 }
|
10
11
|
```
|
11
12
|
|
12
13
|
Instead, consider doing this:
|
13
14
|
|
14
15
|
```ruby
|
15
|
-
x
|
16
|
+
[1,2,3].reverse_each {|x| x+1 }
|
16
17
|
```
|
17
18
|
|
18
|
-
|
19
|
+
#### SelectFollowedByEmpty
|
19
20
|
|
20
|
-
Don't use
|
21
|
+
Don't use select followed by empty?; use none? instead
|
21
22
|
|
22
23
|
For example, rather than doing this:
|
23
24
|
|
24
25
|
```ruby
|
25
|
-
[1,2,3].
|
26
|
+
[1,2,3].select {|x| x > 1 }.empty?
|
26
27
|
```
|
27
28
|
|
28
29
|
Instead, consider doing this:
|
29
30
|
|
30
31
|
```ruby
|
31
|
-
[1,2,3].
|
32
|
+
[1,2,3].none? {|x| x > 1 }
|
32
33
|
```
|
33
34
|
|
34
|
-
|
35
|
+
#### SelectFollowedByFirst
|
35
36
|
|
36
|
-
Don't use
|
37
|
+
Don't use select followed by first; use detect instead
|
37
38
|
|
38
39
|
For example, rather than doing this:
|
39
40
|
|
40
41
|
```ruby
|
41
|
-
[1,2,3].
|
42
|
+
[1,2,3].select {|x| x > 1 }.first
|
42
43
|
```
|
43
44
|
|
44
45
|
Instead, consider doing this:
|
45
46
|
|
46
47
|
```ruby
|
47
|
-
[1,2,3].
|
48
|
+
[1,2,3].detect {|x| x > 1 }
|
48
49
|
```
|
49
50
|
|
50
|
-
|
51
|
+
#### SelectFollowedBySize
|
51
52
|
|
52
|
-
Don't use select followed by
|
53
|
+
Don't use select followed by size; use count instead
|
53
54
|
|
54
55
|
For example, rather than doing this:
|
55
56
|
|
56
57
|
```ruby
|
57
|
-
[1,2,3].select {|x| x > 1 }.
|
58
|
+
[1,2,3].select {|x| x > 1 }.size
|
58
59
|
```
|
59
60
|
|
60
61
|
Instead, consider doing this:
|
61
62
|
|
62
63
|
```ruby
|
63
|
-
[1,2,3].
|
64
|
+
[1,2,3].count {|x| x > 1 }
|
64
65
|
```
|
66
|
+
### buggy
|
65
67
|
|
66
|
-
|
68
|
+
#### AssertWithNil
|
67
69
|
|
68
|
-
Don't use
|
70
|
+
Don't use assert_equal with nil as a first argument; use assert_nil instead
|
69
71
|
|
70
72
|
For example, rather than doing this:
|
71
73
|
|
72
74
|
```ruby
|
73
|
-
|
75
|
+
x = nil ; assert_equal(nil, x)
|
74
76
|
```
|
75
77
|
|
76
78
|
Instead, consider doing this:
|
77
79
|
|
78
80
|
```ruby
|
79
|
-
|
81
|
+
x = nil ; assert_nil(x)
|
82
|
+
```
|
83
|
+
|
84
|
+
#### MapFollowedByFlatten
|
85
|
+
|
86
|
+
Don't use map followed by flatten; use flat_map instead
|
87
|
+
|
88
|
+
For example, rather than doing this:
|
89
|
+
|
90
|
+
```ruby
|
91
|
+
[1,2,3].map {|x| [x,x+1] }.flatten
|
92
|
+
```
|
93
|
+
|
94
|
+
Instead, consider doing this:
|
95
|
+
|
96
|
+
```ruby
|
97
|
+
[1,2,3].flat_map {|x| [x, x+1]}
|
80
98
|
```
|
data/lib/pippi.rb
CHANGED
@@ -3,7 +3,7 @@ require 'rubygems'
|
|
3
3
|
require 'pippi/context'
|
4
4
|
require 'pippi/check_set_mapper'
|
5
5
|
require 'pippi/report'
|
6
|
-
require
|
6
|
+
require 'pippi/checks/check'
|
7
7
|
require 'pippi/problem'
|
8
8
|
require 'pippi/check_loader'
|
9
9
|
require 'pippi/exec_runner'
|
@@ -12,5 +12,6 @@ require 'pippi/checks/map_followed_by_flatten'
|
|
12
12
|
require 'pippi/checks/reverse_followed_by_each'
|
13
13
|
require 'pippi/checks/select_followed_by_first'
|
14
14
|
require 'pippi/checks/select_followed_by_size'
|
15
|
+
require 'pippi/checks/select_followed_by_empty'
|
15
16
|
require 'pippi/checks/assert_with_nil'
|
16
17
|
require 'pippi/checks/debug_check'
|
data/lib/pippi/auto_runner.rb
CHANGED
@@ -1,24 +1,20 @@
|
|
1
1
|
module Pippi
|
2
|
-
|
3
2
|
class AutoRunner
|
4
3
|
attr_reader :ctx
|
5
4
|
|
6
|
-
def initialize(opts={})
|
7
|
-
checkset = opts.fetch(:checkset,
|
5
|
+
def initialize(opts = {})
|
6
|
+
checkset = opts.fetch(:checkset, 'basic')
|
8
7
|
@ctx = Pippi::Context.new
|
9
8
|
Pippi::CheckLoader.new(@ctx, checkset).checks.each(&:decorate)
|
10
9
|
at_exit { dump }
|
11
10
|
end
|
12
11
|
|
13
12
|
def dump
|
14
|
-
File.open(
|
13
|
+
File.open('log/pippi.log', 'w') do |outfile|
|
15
14
|
@ctx.report.problems.each do |problem|
|
16
15
|
outfile.syswrite("#{problem.to_text}\n")
|
17
16
|
end
|
18
17
|
end
|
19
18
|
end
|
20
|
-
|
21
19
|
end
|
22
|
-
|
23
20
|
end
|
24
|
-
|
data/lib/pippi/check_loader.rb
CHANGED
@@ -1,13 +1,11 @@
|
|
1
1
|
module Pippi
|
2
|
-
|
3
2
|
class CheckLoader
|
4
|
-
|
5
3
|
attr_reader :ctx, :check_names
|
6
4
|
|
7
5
|
def initialize(ctx, check_names)
|
8
6
|
@ctx = ctx
|
9
|
-
@check_names = if check_names.
|
10
|
-
|
7
|
+
@check_names = if check_names.is_a?(String)
|
8
|
+
Pippi::CheckSetMapper.new(check_names).check_names
|
11
9
|
else
|
12
10
|
check_names
|
13
11
|
end
|
@@ -18,6 +16,5 @@ module Pippi
|
|
18
16
|
Pippi::Checks.const_get(check_name).new(ctx)
|
19
17
|
end
|
20
18
|
end
|
21
|
-
|
22
19
|
end
|
23
20
|
end
|