pippi 0.0.4 → 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +3 -0
  3. data/README.md +108 -39
  4. data/bin/pippi +2 -2
  5. data/doc/docs.md +38 -20
  6. data/lib/pippi.rb +2 -1
  7. data/lib/pippi/auto_runner.rb +3 -7
  8. data/lib/pippi/check_loader.rb +2 -5
  9. data/lib/pippi/check_set_mapper.rb +2 -3
  10. data/lib/pippi/checks/assert_with_nil.rb +5 -6
  11. data/lib/pippi/checks/check.rb +10 -8
  12. data/lib/pippi/checks/debug_check.rb +1 -4
  13. data/lib/pippi/checks/map_followed_by_flatten.rb +7 -9
  14. data/lib/pippi/checks/reverse_followed_by_each.rb +6 -8
  15. data/lib/pippi/checks/select_followed_by_empty.rb +55 -0
  16. data/lib/pippi/checks/select_followed_by_first.rb +7 -9
  17. data/lib/pippi/checks/select_followed_by_size.rb +6 -12
  18. data/lib/pippi/context.rb +3 -7
  19. data/lib/pippi/exec_runner.rb +12 -7
  20. data/lib/pippi/problem.rb +0 -1
  21. data/lib/pippi/report.rb +2 -6
  22. data/lib/pippi/tasks.rb +11 -13
  23. data/lib/pippi/version.rb +2 -2
  24. metadata +4 -37
  25. data/.gitignore +0 -6
  26. data/.ruby-version +0 -1
  27. data/Gemfile +0 -3
  28. data/Gemfile.lock +0 -24
  29. data/Rakefile +0 -11
  30. data/pippi.gemspec +0 -23
  31. data/sample/map_followed_by_flatten.rb +0 -6
  32. data/test/check_test.rb +0 -41
  33. data/test/rails_core_extensions.rb +0 -13
  34. data/test/test_helper.rb +0 -7
  35. data/test/unit/assert_with_nil_test.rb +0 -50
  36. data/test/unit/check_set_mapper_test.rb +0 -17
  37. data/test/unit/map_followed_by_flatten_test.rb +0 -38
  38. data/test/unit/problem_test.rb +0 -23
  39. data/test/unit/report_test.rb +0 -25
  40. data/test/unit/reverse_followed_by_each_test.rb +0 -29
  41. data/test/unit/select_followed_by_first_test.rb +0 -33
  42. data/test/unit/select_followed_by_size_test.rb +0 -47
  43. data/vendor/cache/byebug-2.7.0.gem +0 -0
  44. data/vendor/cache/columnize-0.8.9.gem +0 -0
  45. data/vendor/cache/debugger-linecache-1.2.0.gem +0 -0
  46. data/vendor/cache/minitest-5.4.2.gem +0 -0
  47. 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: 95752be642c66d479b293d0887592472cbb7a6d4
4
- data.tar.gz: 80f13c0e7329c6909e2729161f09f3489d6eb8e9
3
+ metadata.gz: b69820f23560f62844335dba76692a5b29c40523
4
+ data.tar.gz: bfdc9ccdd89aab53afc19f82c247c55e32f56eef
5
5
  SHA512:
6
- metadata.gz: 3544e1ec0e442f38f8cfb52fe05a46eb6f504af6e9ff4f25665cf1930d1f618660a8adbd5e6bb0c19c750f46b542d2afa39ff42973d67441372a1aa0cc233b50
7
- data.tar.gz: a56988007f42d5ee982887c5be19a8635fea55c0813193710dec41317f0125801658f87fb09a4a27e319c6f751844219362d8bc100f47d86c909b44ab76cfbc1
6
+ metadata.gz: 11480a7cce7761a9c2aeff77f31829872f79e577945082e1f6b7975f851b1968991d07c17eea8bbb3d28ae0e1eeceb0223edb99e578458e4fe987633a53b3ff1
7
+ data.tar.gz: 361ff7703eb9af5c59ea6a3385c6e2d8def03be9a2b94b4a2b6342e7689055fa4bc9497b2a2b8f85d5b797dfcf0ae4ad311a7eaec2989bf6a4eaabc2e3a9a887
@@ -1,3 +1,6 @@
1
+ Nov 5, 2014 - 0.0.5:
2
+ Added SelectFollowedByEmpty
3
+
1
4
  Oct 30, 2014 - 0.0.4:
2
5
  Bugfixes around method delegation
3
6
 
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
- [Here's a project overview](http://thomasleecopeland.com/2014/10/22/finding-suboptimal-api-usage.html).
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
- TODO and FIXME
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
- Maybe we should have a dedicated "test" checkset? Let me know what you think at https://twitter.com/tcopeland, thanks!
117
+ ### basic
52
118
 
53
- ### Basic
119
+ #### ReverseFollowedByEach
54
120
 
55
- * SelectFollowedByFirst
56
- * SelectFollowedBySize
57
- * ReverseFollowedByEach
121
+ Don't use each followed by reverse; use reverse_each instead
58
122
 
59
- ### Buggy
123
+ For example, rather than doing this:
60
124
 
61
- * AssertWithNil
62
- * MapFollowedByFlatten
125
+ ```ruby
126
+ [1,2,3].reverse.each {|x| x+1 }
127
+ ```
63
128
 
64
- ## Checks
129
+ Instead, consider doing this:
65
130
 
66
- ### AssertWithNil
131
+ ```ruby
132
+ [1,2,3].reverse_each {|x| x+1 }
133
+ ```
67
134
 
68
- Don't use assert_equal with nil as a first argument; use assert_nil instead
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 = nil ; assert_equal(nil, 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 = nil ; assert_nil(x)
148
+ [1,2,3].none? {|x| x > 1 }
80
149
  ```
81
150
 
82
- ### MapFollowedByFlatten
151
+ #### SelectFollowedByFirst
83
152
 
84
- Don't use map followed by flatten; use flat_map instead
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].map {|x| [x,x+1] }.flatten
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].flat_map {|x| [x, x+1]}
164
+ [1,2,3].detect {|x| x > 1 }
96
165
  ```
97
166
 
98
- ### ReverseFollowedByEach
167
+ #### SelectFollowedBySize
99
168
 
100
- Don't use each followed by reverse; use reverse_each instead
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].reverse.each {|x| x+1 }
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].reverse_each {|x| x+1 }
180
+ [1,2,3].count {|x| x > 1 }
112
181
  ```
182
+ ### buggy
113
183
 
114
- ### SelectFollowedByFirst
184
+ #### AssertWithNil
115
185
 
116
- Don't use select followed by first; use detect instead
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
- [1,2,3].select {|x| x > 1 }.first
191
+ x = nil ; assert_equal(nil, x)
122
192
  ```
123
193
 
124
194
  Instead, consider doing this:
125
195
 
126
196
  ```ruby
127
- [1,2,3].detect {|x| x > 1 }
197
+ x = nil ; assert_nil(x)
128
198
  ```
129
199
 
130
- ### SelectFollowedBySize
200
+ #### MapFollowedByFlatten
131
201
 
132
- Don't use select followed by size; use count instead
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].select {|x| x > 1 }.size
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].count {|x| x > 1 }
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
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- $:.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
3
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
4
4
 
5
5
  require 'pippi'
6
6
 
7
- Pippi::ExecRunner.new(ARGV.dup).run
7
+ Pippi::ExecRunner.new(ARGV.dup).run
@@ -1,80 +1,98 @@
1
+ ### basic
1
2
 
2
- ### AssertWithNil
3
+ #### ReverseFollowedByEach
3
4
 
4
- Don't use assert_equal with nil as a first argument; use assert_nil instead
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 = nil ; assert_equal(nil, 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 = nil ; assert_nil(x)
16
+ [1,2,3].reverse_each {|x| x+1 }
16
17
  ```
17
18
 
18
- ### MapFollowedByFlatten
19
+ #### SelectFollowedByEmpty
19
20
 
20
- Don't use map followed by flatten; use flat_map instead
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].map {|x| [x,x+1] }.flatten
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].flat_map {|x| [x, x+1]}
32
+ [1,2,3].none? {|x| x > 1 }
32
33
  ```
33
34
 
34
- ### ReverseFollowedByEach
35
+ #### SelectFollowedByFirst
35
36
 
36
- Don't use each followed by reverse; use reverse_each instead
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].reverse.each {|x| x+1 }
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].reverse_each {|x| x+1 }
48
+ [1,2,3].detect {|x| x > 1 }
48
49
  ```
49
50
 
50
- ### SelectFollowedByFirst
51
+ #### SelectFollowedBySize
51
52
 
52
- Don't use select followed by first; use detect instead
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 }.first
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].detect {|x| x > 1 }
64
+ [1,2,3].count {|x| x > 1 }
64
65
  ```
66
+ ### buggy
65
67
 
66
- ### SelectFollowedBySize
68
+ #### AssertWithNil
67
69
 
68
- Don't use select followed by size; use count instead
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
- [1,2,3].select {|x| x > 1 }.size
75
+ x = nil ; assert_equal(nil, x)
74
76
  ```
75
77
 
76
78
  Instead, consider doing this:
77
79
 
78
80
  ```ruby
79
- [1,2,3].count {|x| x > 1 }
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
  ```
@@ -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 "pippi/checks/check"
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'
@@ -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, "basic")
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("log/pippi.log", "w") do |outfile|
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
-
@@ -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.kind_of?(String)
10
- Pippi::CheckSetMapper.new(check_names).check_names
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