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.
- checksums.yaml +7 -0
- data/.gitignore +5 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +4 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +24 -0
- data/README.md +177 -0
- data/Rakefile +11 -0
- data/bin/pippi +7 -0
- data/doc/README +1 -0
- data/doc/docs.md +64 -0
- data/lib/pippi.rb +15 -0
- data/lib/pippi/auto_runner.rb +24 -0
- data/lib/pippi/check_loader.rb +23 -0
- data/lib/pippi/check_set_mapper.rb +35 -0
- data/lib/pippi/checks/check.rb +39 -0
- data/lib/pippi/checks/debug_check.rb +14 -0
- data/lib/pippi/checks/map_followed_by_flatten.rb +55 -0
- data/lib/pippi/checks/reverse_followed_by_each.rb +53 -0
- data/lib/pippi/checks/select_followed_by_first.rb +58 -0
- data/lib/pippi/checks/select_followed_by_size.rb +59 -0
- data/lib/pippi/context.rb +31 -0
- data/lib/pippi/exec_runner.rb +34 -0
- data/lib/pippi/problem.rb +24 -0
- data/lib/pippi/report.rb +27 -0
- data/lib/pippi/tasks.rb +41 -0
- data/lib/pippi/version.rb +3 -0
- data/pippi.gemspec +23 -0
- data/sample/map_followed_by_flatten.rb +6 -0
- data/test/check_test.rb +41 -0
- data/test/rails_core_extensions.rb +5 -0
- data/test/test_helper.rb +7 -0
- data/test/unit/map_followed_by_flatten_test.rb +38 -0
- data/test/unit/problem_test.rb +23 -0
- data/test/unit/report_test.rb +25 -0
- data/test/unit/reverse_followed_by_each_test.rb +29 -0
- data/test/unit/select_followed_by_first_test.rb +33 -0
- data/test/unit/select_followed_by_size_test.rb +33 -0
- data/vendor/cache/byebug-2.7.0.gem +0 -0
- data/vendor/cache/columnize-0.8.9.gem +0 -0
- data/vendor/cache/debugger-linecache-1.2.0.gem +0 -0
- data/vendor/cache/minitest-5.4.2.gem +0 -0
- data/vendor/cache/rake-10.1.0.gem +0 -0
- 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
|
data/lib/pippi/report.rb
ADDED
@@ -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
|
data/lib/pippi/tasks.rb
ADDED
@@ -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
|
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
|