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
@@ -0,0 +1,14 @@
1
+ module Pippi::Checks
2
+
3
+ class DebugCheck < Check
4
+
5
+ ["line", "class", "end", "call", "return", "c_call", "c_return", "b_call", "b_return", "raise", "thread_begin", "thread_end"].each do |event_name|
6
+ define_method "#{event_name}_event" do |tp|
7
+ return if tp.path =~ %r{lib/pippi}
8
+ ctx.debug_logger.warn "#{event_name}_event in #{tp.defined_class}##{tp.method_id} at line #{tp.lineno} of #{tp.path}"
9
+ ctx.debug_logger.warn " return_value #{tp.return_value}" rescue nil
10
+ end
11
+ end
12
+
13
+ end
14
+ end
@@ -0,0 +1,55 @@
1
+ module Pippi::Checks
2
+
3
+ class MapFollowedByFlatten < Check
4
+
5
+ module MyFlatten
6
+ def flatten(depth=nil)
7
+ result = super(depth)
8
+ if depth && depth == 1
9
+ self.class._pippi_check_map_followed_by_flatten.add_problem
10
+ end
11
+ result
12
+ end
13
+ end
14
+
15
+ module MyMap
16
+ def map(&blk)
17
+ result = super
18
+ if self.class._pippi_check_map_followed_by_flatten.nil?
19
+ # Ignore Array subclasses since map or flatten may have difference meanings
20
+ else
21
+ result.extend MyFlatten
22
+ self.class._pippi_check_map_followed_by_flatten.array_mutator_methods.each do |this_means_its_ok_sym|
23
+ result.define_singleton_method(this_means_its_ok_sym, self.class._pippi_check_map_followed_by_flatten.its_ok_watcher_proc(MyFlatten, :flatten))
24
+ end
25
+ end
26
+ result
27
+ end
28
+ end
29
+
30
+ def decorate
31
+ Array.class_exec(self) do |my_check|
32
+ # How to do this without a class instance variable?
33
+ @_pippi_check_map_followed_by_flatten = my_check
34
+ def self._pippi_check_map_followed_by_flatten
35
+ @_pippi_check_map_followed_by_flatten
36
+ end
37
+ end
38
+ Array.prepend MyMap
39
+ end
40
+
41
+ class Documentation
42
+ def description
43
+ "Don't use map followed by flatten; use flat_map instead"
44
+ end
45
+ def sample
46
+ "[1,2,3].map {|x| [x,x+1] }.flatten"
47
+ end
48
+ def instead_use
49
+ "[1,2,3].flat_map {|x| [x, x+1]}"
50
+ end
51
+ end
52
+
53
+ end
54
+
55
+ end
@@ -0,0 +1,53 @@
1
+ module Pippi::Checks
2
+
3
+ class ReverseFollowedByEach < Check
4
+
5
+ module MyEach
6
+ def each
7
+ result = super()
8
+ self.class._pippi_check_reverse_followed_by_each.add_problem
9
+ result
10
+ end
11
+ end
12
+
13
+ module MyReverse
14
+ def reverse
15
+ result = super
16
+ if self.class._pippi_check_reverse_followed_by_each.nil?
17
+ # Ignore Array subclasses since reverse or each may have difference meanings
18
+ else
19
+ result.singleton_class.prepend MyEach
20
+ self.class._pippi_check_reverse_followed_by_each.array_mutator_methods.each do |this_means_its_ok_sym|
21
+ result.define_singleton_method(this_means_its_ok_sym, self.class._pippi_check_reverse_followed_by_each.its_ok_watcher_proc(MyEach, :each))
22
+ end
23
+ end
24
+ result
25
+ end
26
+ end
27
+
28
+ def decorate
29
+ Array.class_exec(self) do |my_check|
30
+ # How to do this without a class instance variable?
31
+ @_pippi_check_reverse_followed_by_each = my_check
32
+ def self._pippi_check_reverse_followed_by_each
33
+ @_pippi_check_reverse_followed_by_each
34
+ end
35
+ end
36
+ Array.prepend MyReverse
37
+ end
38
+
39
+ class Documentation
40
+ def description
41
+ "Don't use each followed by reverse; use reverse_each instead"
42
+ end
43
+ def sample
44
+ "[1,2,3].reverse.each {|x| x+1 }"
45
+ end
46
+ def instead_use
47
+ "[1,2,3].reverse_each {|x| x+1 }"
48
+ end
49
+ end
50
+
51
+ end
52
+
53
+ end
@@ -0,0 +1,58 @@
1
+ module Pippi::Checks
2
+
3
+ class SelectFollowedByFirst < Check
4
+
5
+ module MyFirst
6
+ def first(elements=nil)
7
+ result = if elements
8
+ super(elements)
9
+ else
10
+ super()
11
+ end
12
+ unless elements
13
+ self.class._pippi_check_select_followed_by_first.add_problem
14
+ end
15
+ result
16
+ end
17
+ end
18
+
19
+ module MySelect
20
+ def select(&blk)
21
+ result = super
22
+ if self.class._pippi_check_select_followed_by_first.nil?
23
+ # Ignore Array subclasses since select or first may have difference meanings
24
+ else
25
+ result.extend MyFirst
26
+ self.class._pippi_check_select_followed_by_first.array_mutator_methods.each do |this_means_its_ok_sym|
27
+ result.define_singleton_method(this_means_its_ok_sym, self.class._pippi_check_select_followed_by_first.its_ok_watcher_proc(MyFirst, :first))
28
+ end
29
+ end
30
+ result
31
+ end
32
+ end
33
+
34
+ def decorate
35
+ Array.class_exec(self) do |my_check|
36
+ @_pippi_check_select_followed_by_first = my_check
37
+ def self._pippi_check_select_followed_by_first
38
+ @_pippi_check_select_followed_by_first
39
+ end
40
+ end
41
+ Array.prepend MySelect
42
+ end
43
+
44
+ class Documentation
45
+ def description
46
+ "Don't use select followed by first; use detect instead"
47
+ end
48
+ def sample
49
+ "[1,2,3].select {|x| x > 1 }.first"
50
+ end
51
+ def instead_use
52
+ "[1,2,3].detect {|x| x > 1 }"
53
+ end
54
+ end
55
+
56
+ end
57
+
58
+ end
@@ -0,0 +1,59 @@
1
+ module Pippi::Checks
2
+
3
+ class SelectFollowedBySize < Check
4
+
5
+ module MySize
6
+ def size
7
+ result = super()
8
+ self.class._pippi_check_select_followed_by_size.add_problem
9
+ self.class._pippi_check_select_followed_by_size.method_names_that_indicate_this_is_being_used_as_a_collection.each do |this_means_its_ok_sym|
10
+ define_singleton_method(this_means_its_ok_sym, self.class._pippi_check_select_followed_by_size.clear_fault_proc)
11
+ end
12
+ result
13
+ end
14
+ end
15
+
16
+ module MySelect
17
+ def select(&blk)
18
+ result = super
19
+ if self.class._pippi_check_select_followed_by_size.nil?
20
+ # Ignore Array subclasses since select or size may have difference meanings
21
+ else
22
+ result.extend MySize
23
+ self.class._pippi_check_select_followed_by_size.array_mutator_methods.each do |this_means_its_ok_sym|
24
+ result.define_singleton_method(this_means_its_ok_sym, self.class._pippi_check_select_followed_by_size.its_ok_watcher_proc(MySize, :size))
25
+ end
26
+ end
27
+ result
28
+ end
29
+ end
30
+
31
+ def method_names_that_indicate_this_is_being_used_as_a_collection
32
+ [:collect!, :compact!, :flatten!, :map!, :reject!, :reverse!, :rotate!, :select!, :shuffle!, :slice!, :sort!, :sort_by!, :uniq!, :collect, :compact, :flatten, :map, :reject, :reverse, :rotate, :select, :shuffle, :slice, :sort, :sort_by, :uniq]
33
+ end
34
+
35
+ def decorate
36
+ Array.class_exec(self) do |my_check|
37
+ @_pippi_check_select_followed_by_size = my_check
38
+ def self._pippi_check_select_followed_by_size
39
+ @_pippi_check_select_followed_by_size
40
+ end
41
+ end
42
+ Array.prepend MySelect
43
+ end
44
+
45
+ class Documentation
46
+ def description
47
+ "Don't use select followed by size; use count instead"
48
+ end
49
+ def sample
50
+ "[1,2,3].select {|x| x > 1 }.size"
51
+ end
52
+ def instead_use
53
+ "[1,2,3].count {|x| x > 1 }"
54
+ end
55
+ end
56
+
57
+ end
58
+
59
+ end
@@ -0,0 +1,31 @@
1
+ module Pippi
2
+
3
+ class Context
4
+
5
+ class DebugLogger
6
+ def warn(str)
7
+ File.open("pippi_debug.log", "a") do |f|
8
+ f.syswrite("#{str}\n")
9
+ end
10
+ end
11
+ end
12
+
13
+ class NullLogger
14
+ def warn(str)
15
+ end
16
+ end
17
+
18
+ attr_reader :report, :debug_logger
19
+
20
+ def initialize
21
+ @report = Pippi::Report.new
22
+ @debug_logger = if ENV['PIPPI_DEBUG']
23
+ Pippi::Context::DebugLogger.new
24
+ else
25
+ Pippi::Context::NullLogger.new
26
+ end
27
+ end
28
+
29
+ end
30
+
31
+ end
@@ -0,0 +1,34 @@
1
+ module Pippi
2
+
3
+ class ExecRunner
4
+
5
+ attr_accessor :codefile, :check_name, :code_to_eval, :output_file_name
6
+
7
+ def initialize(args)
8
+ @codefile = args[0]
9
+ @check_name = args[1]
10
+ @code_to_eval = args[2]
11
+ @output_file_name = args[3]
12
+ end
13
+
14
+ def run
15
+ ctx = Pippi::Context.new
16
+ CheckLoader.new(ctx, check_name).checks.each do |check|
17
+ check.decorate
18
+ end
19
+ load "#{codefile}"
20
+ eval code_to_eval
21
+ dump_report ctx
22
+ end
23
+
24
+ def dump_report(ctx)
25
+ File.open(output_file_name, "w") do |outfile|
26
+ ctx.report.problems.each do |problem|
27
+ outfile.syswrite("#{problem.to_text}\n")
28
+ end
29
+ end
30
+ end
31
+
32
+ end
33
+
34
+ end
@@ -0,0 +1,24 @@
1
+ module Pippi
2
+ class Problem
3
+
4
+ attr_accessor :file_path, :line_number, :check_class
5
+
6
+ def initialize(opts)
7
+ @file_path = opts[:file_path]
8
+ @check_class = opts[:check_class]
9
+ @line_number = opts[:line_number]
10
+ end
11
+
12
+ # TODO probably need various reporting formats
13
+ def to_text
14
+ "#{file_path},#{check_class.name.split('::').last},#{line_number}"
15
+ end
16
+
17
+ # TODO correct method?
18
+ def eql?(other)
19
+ file_path == other.file_path &&
20
+ check_class == other.check_class &&
21
+ line_number == other.line_number
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,27 @@
1
+ module Pippi
2
+
3
+ class Report
4
+
5
+ attr_reader :problems
6
+
7
+ def initialize
8
+ @problems = []
9
+ end
10
+
11
+ def add(problem)
12
+ @problems << problem unless duplicate_report?(problem)
13
+ end
14
+
15
+ def remove(lineno, path, clazz)
16
+ @problems.reject! {|p| p.line_number == lineno && p.file_path == path && p.check_class == clazz }
17
+ end
18
+
19
+ private
20
+
21
+ def duplicate_report?(candidate)
22
+ !problems.detect {|existing| existing.eql?(candidate) }.nil?
23
+ end
24
+
25
+ end
26
+
27
+ end
@@ -0,0 +1,41 @@
1
+ require 'pippi'
2
+
3
+ module Pippi
4
+ class Documentation
5
+ def generate
6
+ str = ""
7
+ [Pippi::Checks::SelectFollowedBySize::Documentation,
8
+ Pippi::Checks::SelectFollowedByFirst::Documentation,
9
+ Pippi::Checks::ReverseFollowedByEach::Documentation,
10
+ Pippi::Checks::MapFollowedByFlatten::Documentation
11
+ ].sort {|a,b| a.name <=> b.name }.each do |clz|
12
+ obj = clz.new
13
+ str << %Q{
14
+ ### #{clz.name.to_s.split('::')[2]}
15
+
16
+ #{obj.description}
17
+
18
+ For example, rather than doing this:
19
+
20
+ \`\`\`ruby
21
+ #{obj.sample}
22
+ \`\`\`
23
+
24
+ Instead, consider doing this:
25
+
26
+ \`\`\`ruby
27
+ #{obj.instead_use}
28
+ \`\`\`
29
+ }
30
+ end
31
+ File.open("doc/docs.md", "w") {|f| f.syswrite(str) }
32
+ end
33
+ end
34
+ end
35
+
36
+ namespace :pippi do
37
+ desc "Generate check documentation"
38
+ task :generate_docs do
39
+ Pippi::Documentation.new.generate
40
+ end
41
+ end
@@ -0,0 +1,3 @@
1
+ module Pippi
2
+ VERSION = "0.0.1"
3
+ end
data/pippi.gemspec ADDED
@@ -0,0 +1,23 @@
1
+ $:.push File.expand_path("../lib", __FILE__)
2
+
3
+ require 'pippi/version'
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = 'pippi'
7
+ s.version = Pippi::VERSION
8
+ s.authors = ["Tom Copeland"]
9
+ s.email = ["tom@thomasleecopeland.com"]
10
+ s.homepage = "https://github.com/tcopeland/pippi"
11
+ s.summary = "A Ruby runtime code analyzer"
12
+ s.description = "Pippi is a utility for locating suboptimal Ruby class API usage."
13
+ s.license = "MIT"
14
+ s.rubyforge_project = "none"
15
+ s.files = `git ls-files`.split("\n")
16
+ s.test_files = `git ls-files test/*`.split("\n")
17
+ s.executables = "pippi"
18
+ s.require_paths = ["lib"]
19
+ s.add_development_dependency 'rake', '~> 10.1'
20
+ s.add_development_dependency 'minitest', '~> 5.0'
21
+ s.add_development_dependency 'byebug', '~> 2.7'
22
+ s.required_ruby_version = '>= 2.0.0'
23
+ end
@@ -0,0 +1,6 @@
1
+ class Foo
2
+ def bar
3
+ x = [1,2,3]
4
+ x.map {|y| [y, y+1] }.flatten
5
+ end
6
+ end