snuffle 0.9.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 +23 -0
- data/.rspec +2 -0
- data/CODE_OF_CONDUCT.md +12 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +45 -0
- data/Rakefile +2 -0
- data/bin/snuffle +3 -0
- data/lib/snuffle.rb +23 -0
- data/lib/snuffle/cli.rb +74 -0
- data/lib/snuffle/cohort.rb +42 -0
- data/lib/snuffle/elements/hash.rb +37 -0
- data/lib/snuffle/formatters/base.rb +57 -0
- data/lib/snuffle/formatters/csv.rb +31 -0
- data/lib/snuffle/formatters/html.rb +48 -0
- data/lib/snuffle/formatters/html_index.rb +39 -0
- data/lib/snuffle/formatters/templates/index.html.haml +71 -0
- data/lib/snuffle/formatters/templates/output.html.haml +56 -0
- data/lib/snuffle/formatters/text.rb +30 -0
- data/lib/snuffle/line_of_code.rb +23 -0
- data/lib/snuffle/node.rb +46 -0
- data/lib/snuffle/source_file.rb +112 -0
- data/lib/snuffle/summary.rb +13 -0
- data/lib/snuffle/util/histogram.rb +17 -0
- data/lib/snuffle/version.rb +3 -0
- data/snuffle.gemspec +35 -0
- data/spec/fixtures/account.rb +1390 -0
- data/spec/fixtures/program_1.rb +31 -0
- data/spec/fixtures/program_2.rb +67 -0
- data/spec/fixtures/program_3.rb +68 -0
- data/spec/snuffle/elements/hash_spec.rb +16 -0
- data/spec/snuffle/line_of_code_spec.rb +38 -0
- data/spec/snuffle/source_file_spec.rb +50 -0
- data/spec/snuffle/util/histogram_spec.rb +25 -0
- data/spec/spec_helper.rb +7 -0
- data/test.rb +5 -0
- metadata +260 -0
@@ -0,0 +1,39 @@
|
|
1
|
+
module Snuffle
|
2
|
+
module Formatters
|
3
|
+
|
4
|
+
class HtmlIndex
|
5
|
+
|
6
|
+
include Formatters::Base
|
7
|
+
|
8
|
+
attr_accessor :summaries
|
9
|
+
|
10
|
+
def initialize(summaries)
|
11
|
+
self.summaries = summaries.sort{|a,b| a.cohorts.count <=> b.cohorts.count}.reverse
|
12
|
+
end
|
13
|
+
|
14
|
+
def header
|
15
|
+
["File", "Class", "Object Candidates"].map{|col| "<th>#{col.titleize}</th>"}.join("\r\n")
|
16
|
+
end
|
17
|
+
|
18
|
+
def content
|
19
|
+
Haml::Engine.new(output_template).render(
|
20
|
+
Object.new, {
|
21
|
+
summaries: summaries,
|
22
|
+
date: Time.now.strftime("%Y/%m/%d"),
|
23
|
+
time: Time.now.strftime("%l:%M %P")
|
24
|
+
}
|
25
|
+
)
|
26
|
+
end
|
27
|
+
|
28
|
+
def filename
|
29
|
+
"index.htm"
|
30
|
+
end
|
31
|
+
|
32
|
+
def output_template
|
33
|
+
File.read(File.dirname(__FILE__) + "/templates/index.html.haml")
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
!!!
|
2
|
+
%html
|
3
|
+
%head
|
4
|
+
%title
|
5
|
+
Fukuzatsu
|
6
|
+
%link{href: "http://cdn.datatables.net/1.10.0/css/jquery.dataTables.css", rel: "stylesheet"}
|
7
|
+
|
8
|
+
%style{media: "screen", type: "text/css"}
|
9
|
+
body { background: #49525a; color: #fff; font-family: arial, sans-serif; padding: 2em; }
|
10
|
+
table { width: 100%; box-shadow: 0 5px 0 rgba(0,0,0,.8); border-spacing: 0; border: 5px solid #000; border-radius: 5px; border-collapse: collapse; min-width: 50%; }
|
11
|
+
tr.header th:first-child { border-radius: 5px 0 0 0; }
|
12
|
+
tr.header th:last-child { border-radius: 0 5px 0 0; }
|
13
|
+
tr.header th:only-child { border-radius: 5px 5px 0 0; }
|
14
|
+
tr.header { background-color: #222; }
|
15
|
+
tr.even { background: rgba(128, 128, 128, 0.5) !important;}
|
16
|
+
tr.odd { background: rgba(128, 128, 128, 0.25) !important}
|
17
|
+
tr.even:hover, tr.odd:hover { background: rgba(128, 128, 128, 0.75) !important;}
|
18
|
+
tr.faint td { opacity: 0.5; font-style: italic; }
|
19
|
+
th { background: #000; text-align: left; padding: .5em; }
|
20
|
+
td { text-align: left; padding: .5em; padding-left: 1.25em !important;}
|
21
|
+
td.center { text-align: center; }
|
22
|
+
tfoot { background: #000; border-top: 10px solid #000; font-family: courier; margin-top: 4em; font-size: .75em; }
|
23
|
+
a:link, a:visited { color: #fff }
|
24
|
+
div.file_meta { float: left; height: 3em; width: 30%; }
|
25
|
+
h1 { color:#fff; font-size: 1.25em; margin-top: .25em; }
|
26
|
+
h2 { color:#fff; font-size: .75em; margin-top: -1em; }
|
27
|
+
h3 { color:#fff; font-size: 1em; float: right; margin-top: 1em; }
|
28
|
+
|
29
|
+
%body
|
30
|
+
%table{class: "output-table"}
|
31
|
+
%thead
|
32
|
+
%tr.header
|
33
|
+
%th{colspan: 3}
|
34
|
+
.file_meta
|
35
|
+
%h1
|
36
|
+
Snuffle Analysis
|
37
|
+
%tr
|
38
|
+
%th
|
39
|
+
File
|
40
|
+
%th
|
41
|
+
Host Module/Class
|
42
|
+
%th
|
43
|
+
Object Candidates
|
44
|
+
%tbody
|
45
|
+
- summaries.each_with_index do |summary, i|
|
46
|
+
%tr{class: "#{i % 2 == 1 ? 'odd' : 'even'} #{summary.cohorts.count == 0 ? 'faint' : ''}"}
|
47
|
+
%td
|
48
|
+
- if summary.cohorts.count == 0
|
49
|
+
= summary.path_to_file
|
50
|
+
- else
|
51
|
+
%a{href: "source/#{summary.class_filename}.htm"}
|
52
|
+
= summary.path_to_file
|
53
|
+
%td
|
54
|
+
- if summary.class_name.size > 30
|
55
|
+
= "..."
|
56
|
+
= summary.class_name[-29..-1]
|
57
|
+
- else
|
58
|
+
= summary.class_name
|
59
|
+
%td
|
60
|
+
= summary.cohorts.count
|
61
|
+
%tfoot
|
62
|
+
%tr
|
63
|
+
%td.center{colspan: 3}
|
64
|
+
%em
|
65
|
+
Analyzed on
|
66
|
+
= date
|
67
|
+
at
|
68
|
+
= time
|
69
|
+
by
|
70
|
+
%a{href: "https://gitlab.com/coraline/snuffle", target: "_new"}
|
71
|
+
Snuffle
|
@@ -0,0 +1,56 @@
|
|
1
|
+
!!!
|
2
|
+
%html
|
3
|
+
%head
|
4
|
+
%title
|
5
|
+
Snuffle:
|
6
|
+
= summary.class_name
|
7
|
+
%link{href: "http://cdn.datatables.net/1.10.0/css/jquery.dataTables.css", rel: "stylesheet"}
|
8
|
+
%script{language: "javascript", src: "http://code.jquery.com/jquery-1.11.0.min.js", type: "text/javascript"}
|
9
|
+
%script{language: "javascript", src: "http://code.jquery.com/jquery-migrate-1.2.1.min.js", type: "text/javascript"}
|
10
|
+
%script{language: "javascript", src: "http://cdn.datatables.net/1.10.0/js/jquery.dataTables.js", type: "text/javascript"}
|
11
|
+
|
12
|
+
%style{media: "screen", type: "text/css"}
|
13
|
+
= Rouge::Theme.find('thankful_eyes').render(scope: '.highlight')
|
14
|
+
body { line-height: 1.5em; background: #49525a; color: #fff; font-family: arial, sans-serif; font-size: 14px; padding: 2em; }
|
15
|
+
pre.lineno { margin-top: -1.4em !important;}
|
16
|
+
pre { line-height: 1.75em;}
|
17
|
+
span.highlighted { background: rgba(200, 0, 0, .4); padding-left: 1em; border-radius: 100px; display: inline-block; position: absolute; left: 0px; padding-right: 90%}
|
18
|
+
div.file_meta { padding: 1em; border-radius: 5px; background: #000; height: 3em; width: 98%; }
|
19
|
+
h1 { color:#fff; font-size: 1.25em; margin-top: .25em; }
|
20
|
+
h2 { color:#fff; font-size: .75em; margin-top: -1em; }
|
21
|
+
h3 { color:#fff; font-size: 1.1em;margin-top: 1em; }
|
22
|
+
= ".indented {margin-left: 4em; }"
|
23
|
+
%body
|
24
|
+
|
25
|
+
.file_meta
|
26
|
+
%h1
|
27
|
+
= summary.class_name
|
28
|
+
%h2
|
29
|
+
= summary.path_to_file
|
30
|
+
|
31
|
+
%h3.indented
|
32
|
+
Candidate object attributes:
|
33
|
+
|
34
|
+
%ul.indented
|
35
|
+
- summary.cohorts.group_by{|c| c.values.sort }.each do |values, cohorts|
|
36
|
+
- if cohorts.count > 0
|
37
|
+
%li
|
38
|
+
= values.map{|c| "##{c}" }.join(", ")
|
39
|
+
%br
|
40
|
+
= ":#{cohorts.map(&:line_numbers).join(', :')}"
|
41
|
+
|
42
|
+
= source_lines
|
43
|
+
|
44
|
+
%br
|
45
|
+
%input{onclick: "history.back(-1)", type: "button", value: "Back"}
|
46
|
+
|
47
|
+
:javascript
|
48
|
+
|
49
|
+
var line_numbers = #{summary.cohorts.map(&:line_numbers).flatten};
|
50
|
+
|
51
|
+
$('pre.lineno').html($('pre.lineno').html().split(/\s+/).map(function(val){ if (line_numbers.indexOf(parseInt(val)) > -1) { return "<span class='highlighted'>" + val + "</span>\n" } else { return "<span class='foo'>" + val + "</span>\n"};}))
|
52
|
+
|
53
|
+
for (i = 0; i <= line_numbers; i ++) {
|
54
|
+
$('.code pre').html($('.code pre').html().split(/\s+/)[i].html("<span class='highlighted'>" + i + "</span>\n"))
|
55
|
+
}
|
56
|
+
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'text-table'
|
2
|
+
|
3
|
+
module Snuffle
|
4
|
+
module Formatters
|
5
|
+
|
6
|
+
class Text
|
7
|
+
|
8
|
+
include Formatters::Base
|
9
|
+
|
10
|
+
def header
|
11
|
+
columns.map(&:titleize)
|
12
|
+
end
|
13
|
+
|
14
|
+
def export
|
15
|
+
table = ::Text::Table.new
|
16
|
+
table.head = header
|
17
|
+
table.rows = rows
|
18
|
+
table.to_s
|
19
|
+
end
|
20
|
+
|
21
|
+
def rows
|
22
|
+
summary.cohorts.group_by{|c| c.values}.map do |cohort|
|
23
|
+
[summary.path_to_file, summary.class_name, "#{cohort[0].join(', ')}", cohort[1].map(&:line_numbers).join(", ")]
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Snuffle
|
2
|
+
|
3
|
+
class LineOfCode
|
4
|
+
|
5
|
+
include PoroPlus
|
6
|
+
include Ephemeral::Base
|
7
|
+
|
8
|
+
attr_accessor :line_number, :range, :content
|
9
|
+
|
10
|
+
def self.containing(locs, start_index, end_index)
|
11
|
+
locs.inject([]) do |a, loc|
|
12
|
+
a << loc if loc.in_range?(start_index) || loc.in_range?(end_index)
|
13
|
+
a
|
14
|
+
end.compact
|
15
|
+
end
|
16
|
+
|
17
|
+
def in_range?(index)
|
18
|
+
self.range.include?(index)
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
data/lib/snuffle/node.rb
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
module Snuffle
|
2
|
+
|
3
|
+
class Node
|
4
|
+
|
5
|
+
include Ephemeral::Base
|
6
|
+
include PoroPlus
|
7
|
+
|
8
|
+
attr_accessor :id, :name, :type, :child_ids, :parent_id, :line_numbers
|
9
|
+
|
10
|
+
scope :by_id, lambda{|id| where(:id => id)}
|
11
|
+
scope :by_type, lambda{|type| where(:type => type)}
|
12
|
+
scope :with_parent, lambda{|parent_id| where(parent_id: parent_id) }
|
13
|
+
scope :hashes, {type: :hash}
|
14
|
+
|
15
|
+
def self.nil
|
16
|
+
new(type: :nil)
|
17
|
+
end
|
18
|
+
|
19
|
+
def initialize(*args, &block)
|
20
|
+
@id = SecureRandom.uuid
|
21
|
+
super
|
22
|
+
end
|
23
|
+
|
24
|
+
def parent
|
25
|
+
Snuffle::Node.where(id: self.parent_id).first
|
26
|
+
end
|
27
|
+
|
28
|
+
def siblings
|
29
|
+
@siblings ||= Snuffle::Node.by_type(self.type).to_a - [self]
|
30
|
+
end
|
31
|
+
|
32
|
+
def children
|
33
|
+
Snuffle::Node.where(parent_id: self.id)
|
34
|
+
end
|
35
|
+
|
36
|
+
def inspect
|
37
|
+
{
|
38
|
+
id: self.id,
|
39
|
+
type: self.type,
|
40
|
+
parent_id: self.parent_id,
|
41
|
+
child_ids: self.child_ids
|
42
|
+
}.to_s
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
# TODO factor out poroplus here
|
2
|
+
|
3
|
+
require "parser/current"
|
4
|
+
|
5
|
+
module Snuffle
|
6
|
+
|
7
|
+
class SourceFile
|
8
|
+
|
9
|
+
include PoroPlus
|
10
|
+
|
11
|
+
attr_accessor :path_to_file, :source, :lines_of_code
|
12
|
+
|
13
|
+
def class_name
|
14
|
+
return @class_name if @class_name
|
15
|
+
@class_name = find_class(ast) || "?"
|
16
|
+
end
|
17
|
+
|
18
|
+
def nodes
|
19
|
+
@nodes ||= extract_nodes_from(ast)
|
20
|
+
end
|
21
|
+
|
22
|
+
def cohorts
|
23
|
+
@cohorts ||= Cohort.from(self.nodes)
|
24
|
+
end
|
25
|
+
|
26
|
+
def source
|
27
|
+
return @source if @source
|
28
|
+
end_pos = 0
|
29
|
+
self.lines_of_code = []
|
30
|
+
@source = File.readlines(self.path_to_file).each_with_index do |line, index|
|
31
|
+
start_pos = end_pos + 1
|
32
|
+
end_pos += line.size
|
33
|
+
self.lines_of_code << LineOfCode.new(line_number: index + 1, range: (start_pos..end_pos))
|
34
|
+
line
|
35
|
+
end.join
|
36
|
+
end
|
37
|
+
|
38
|
+
def summary
|
39
|
+
Summary.new(
|
40
|
+
source_file: self,
|
41
|
+
source: self.source,
|
42
|
+
class_name: class_name,
|
43
|
+
path_to_file: self.path_to_file,
|
44
|
+
cohorts: cohorts,
|
45
|
+
source: self.source
|
46
|
+
)
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def text_at(start_pos, end_pos)
|
52
|
+
source[start_pos..end_pos - 1]
|
53
|
+
end
|
54
|
+
|
55
|
+
def find_class(node)
|
56
|
+
return unless node.respond_to?(:type)
|
57
|
+
concat = []
|
58
|
+
if node.type == :module || node.type == :class
|
59
|
+
concat << text_at(node.loc.name.begin_pos, node.loc.name.end_pos)
|
60
|
+
end
|
61
|
+
unless node.type == :class
|
62
|
+
concat << node.children.map{|child| find_class(child)}.compact
|
63
|
+
end
|
64
|
+
concat.flatten.select(&:present?).join('::')
|
65
|
+
end
|
66
|
+
|
67
|
+
def ast
|
68
|
+
@ast ||= Parser::CurrentRuby.parse(source)
|
69
|
+
end
|
70
|
+
|
71
|
+
def extract_nodes_from(ast_node, nodes=Ephemeral::Collection.new("Snuffle::Node"), parent_id=:root)
|
72
|
+
if name = name_from(ast_node)
|
73
|
+
if ast_node.respond_to?(:type)
|
74
|
+
lines = LineOfCode.containing(lines_of_code, ast_node.loc.expression.begin_pos, ast_node.loc.expression.end_pos)
|
75
|
+
extracted_node = Snuffle::Node.new(
|
76
|
+
type: ast_node.type,
|
77
|
+
parent_id: parent_id,
|
78
|
+
name: name_from(ast_node),
|
79
|
+
line_numbers: lines.map(&:line_number)
|
80
|
+
)
|
81
|
+
else
|
82
|
+
extracted_node = Snuffle::Node.new(
|
83
|
+
type: :nil,
|
84
|
+
parent_id: parent_id,
|
85
|
+
name: name
|
86
|
+
)
|
87
|
+
end
|
88
|
+
nodes << extracted_node
|
89
|
+
ast_node.children.each{|child| extract_nodes_from(child, nodes, extracted_node.id)} if ast_node.respond_to?(:children)
|
90
|
+
end
|
91
|
+
nodes
|
92
|
+
end
|
93
|
+
|
94
|
+
def name_from(node)
|
95
|
+
return if node.nil?
|
96
|
+
return node unless node.respond_to?(:children)
|
97
|
+
if node.respond_to?(:loc) && node.loc.respond_to?(:name)
|
98
|
+
if name_coords = node.loc.name
|
99
|
+
name = source[name_coords.begin_pos, name_coords.end_pos - 1]
|
100
|
+
return unless name =~ /[a-zA-Z]/
|
101
|
+
return name
|
102
|
+
else
|
103
|
+
"?"
|
104
|
+
end
|
105
|
+
else
|
106
|
+
return name_from(node.children.last)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
end
|
111
|
+
|
112
|
+
end
|
data/snuffle.gemspec
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'snuffle/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "snuffle"
|
8
|
+
spec.version = Snuffle::VERSION
|
9
|
+
spec.authors = ["Coraline Ada Ehmke", "Kerri Miller"]
|
10
|
+
spec.email = ["coraline@idolhands.com"]
|
11
|
+
spec.summary = %q{Snuffle detects data clumps in your Ruby code.}
|
12
|
+
spec.description = %q{Snuffle detects data clumps and other hints of extractable objects in your Ruby code.}
|
13
|
+
spec.homepage = ""
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_dependency "parser"
|
22
|
+
spec.add_dependency "thor"
|
23
|
+
spec.add_dependency "ephemeral", "~> 2.3.2"
|
24
|
+
spec.add_dependency "poro_plus"
|
25
|
+
spec.add_dependency "rouge"
|
26
|
+
spec.add_dependency "text-table"
|
27
|
+
spec.add_dependency "haml"
|
28
|
+
|
29
|
+
spec.add_development_dependency "bundler", "~> 1.6"
|
30
|
+
spec.add_development_dependency "rake"
|
31
|
+
spec.add_development_dependency "rspec"
|
32
|
+
spec.add_development_dependency "simplecov"
|
33
|
+
spec.add_development_dependency "pry"
|
34
|
+
|
35
|
+
end
|