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