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.
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