pippi 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +5 -0
  3. data/.ruby-version +1 -0
  4. data/CHANGELOG.md +4 -0
  5. data/Gemfile +3 -0
  6. data/Gemfile.lock +24 -0
  7. data/README.md +177 -0
  8. data/Rakefile +11 -0
  9. data/bin/pippi +7 -0
  10. data/doc/README +1 -0
  11. data/doc/docs.md +64 -0
  12. data/lib/pippi.rb +15 -0
  13. data/lib/pippi/auto_runner.rb +24 -0
  14. data/lib/pippi/check_loader.rb +23 -0
  15. data/lib/pippi/check_set_mapper.rb +35 -0
  16. data/lib/pippi/checks/check.rb +39 -0
  17. data/lib/pippi/checks/debug_check.rb +14 -0
  18. data/lib/pippi/checks/map_followed_by_flatten.rb +55 -0
  19. data/lib/pippi/checks/reverse_followed_by_each.rb +53 -0
  20. data/lib/pippi/checks/select_followed_by_first.rb +58 -0
  21. data/lib/pippi/checks/select_followed_by_size.rb +59 -0
  22. data/lib/pippi/context.rb +31 -0
  23. data/lib/pippi/exec_runner.rb +34 -0
  24. data/lib/pippi/problem.rb +24 -0
  25. data/lib/pippi/report.rb +27 -0
  26. data/lib/pippi/tasks.rb +41 -0
  27. data/lib/pippi/version.rb +3 -0
  28. data/pippi.gemspec +23 -0
  29. data/sample/map_followed_by_flatten.rb +6 -0
  30. data/test/check_test.rb +41 -0
  31. data/test/rails_core_extensions.rb +5 -0
  32. data/test/test_helper.rb +7 -0
  33. data/test/unit/map_followed_by_flatten_test.rb +38 -0
  34. data/test/unit/problem_test.rb +23 -0
  35. data/test/unit/report_test.rb +25 -0
  36. data/test/unit/reverse_followed_by_each_test.rb +29 -0
  37. data/test/unit/select_followed_by_first_test.rb +33 -0
  38. data/test/unit/select_followed_by_size_test.rb +33 -0
  39. data/vendor/cache/byebug-2.7.0.gem +0 -0
  40. data/vendor/cache/columnize-0.8.9.gem +0 -0
  41. data/vendor/cache/debugger-linecache-1.2.0.gem +0 -0
  42. data/vendor/cache/minitest-5.4.2.gem +0 -0
  43. data/vendor/cache/rake-10.1.0.gem +0 -0
  44. 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
@@ -0,0 +1,5 @@
1
+ tmp/
2
+ pippi.log
3
+ pippi_debug.log
4
+ pippi*.gem
5
+ out.txt
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ ruby-2.1.2
data/CHANGELOG.md ADDED
@@ -0,0 +1,4 @@
1
+ Oct 22, 2014 - 0.0.1:
2
+ Initial release.
3
+
4
+
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec
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
@@ -0,0 +1,11 @@
1
+ require 'rake/testtask'
2
+ require 'rubygems'
3
+ require 'bundler/setup'
4
+ require 'pippi/tasks'
5
+
6
+ Rake::TestTask.new do |t|
7
+ t.libs << "test"
8
+ t.pattern = "test/unit/**_test.rb"
9
+ end
10
+
11
+ task :default => [:test]
data/bin/pippi ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $:.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
4
+
5
+ require 'pippi'
6
+
7
+ Pippi::ExecRunner.new(ARGV.dup).run
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