snuffle 0.9.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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
@@ -0,0 +1,13 @@
1
+ module Snuffle
2
+
3
+ class Summary
4
+ include PoroPlus
5
+ attr_accessor :class_name, :path_to_file, :cohorts, :source
6
+
7
+ def class_filename
8
+ self.class_name.downcase.gsub(' ', '_')
9
+ end
10
+
11
+ end
12
+
13
+ end
@@ -0,0 +1,17 @@
1
+ module Snuffle
2
+ module Util
3
+
4
+ class Histogram
5
+
6
+ def self.from(arrays)
7
+ arrays.flatten.inject({}) do |h, value|
8
+ h[value] ||= 0;
9
+ h[value] += 1
10
+ h
11
+ end
12
+ end
13
+
14
+ end
15
+
16
+ end
17
+ end
@@ -0,0 +1,3 @@
1
+ module Snuffle
2
+ VERSION = "0.9.1"
3
+ 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