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