snuffle 0.9.1 → 0.10.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 6b33ac8a8524d501103f6b30117995298fab5060
4
- data.tar.gz: 9f9e2c6e58f44b28fad06aaf7645ec213b6466aa
3
+ metadata.gz: 585cee4f627767538a301e5a3aba547e9bb98a8e
4
+ data.tar.gz: 8ba7bbd451845b2f0f583e66c5283e5ad68d7ea7
5
5
  SHA512:
6
- metadata.gz: 43437ee9b3eb9fae5729d66253d082ecb508d29e4d8e28559fd75de58d8ed51b8da347cdcef25e5ff9a456a96052a1acffc4ad9a46054254b2135dda540cb60a
7
- data.tar.gz: 9f1d620ee9123654237e532c3236da8493e6e492f4dcfe791de39746438f126a17017c6c6ea5d81a146e2bc9a9b68bea52297fe5e5f2a89d46681f6e75ffa152
6
+ metadata.gz: 256627b9c70a1aabea211f082cdf3489e4130aad75ba6905f52fd57f5b3d9d10e6c959d71702674580620510bc7504d9bf9e6dea5e3e132f96c1f18e5a6ebcfa
7
+ data.tar.gz: 9cff92572661670b3d6562aadff50266951b1addfe7e43e17ff026ef3758f4466da6b268c2f17b412881e0e178f8c5ccf4b8da1bef575e7db88740c2ee7486bb
data/README.md CHANGED
@@ -2,13 +2,18 @@
2
2
 
3
3
  Snuffle analyzes source code to identify "data clumps", clusters of attributes
4
4
  that are often used together. It uses this analysis to propose objects that
5
- may be extracted from a given class.
5
+ may be extracted from a given class. It also looks for objects that are hinted
6
+ at by method names and identifies them as "latent objects". For example, if you
7
+ have `home_address` and `work_address` methods in a User class, Snuffle will
8
+ tell you that you might want to extract those methods to a latent Address class.
9
+
10
+ Please note that Snuffle is still pre-release and will not be ready for serious
11
+ use until it hits version 1.0.0.
6
12
 
7
13
  ## TODO
8
14
 
9
15
  * Ignore data clumps called in "loose" class methods (e.g. attr_accessor)
10
- * Match on string concatenation
11
- * Consider weighting based on match type
16
+ * Output files in folder hierarchy that mirrors source files
12
17
 
13
18
  ## Installation
14
19
 
@@ -26,13 +31,10 @@ Or install it yourself as:
26
31
 
27
32
  ## Usage
28
33
 
29
- $ snuffle check example.rb
34
+ $ snuffle check lib/example.rb
30
35
 
31
- +----------------------------+------------+-----------------------------+
32
- | Filename | Host Class | Candidate Object Attributes |
33
- +----------------------------+------------+-----------------------------+
34
- | example.rb | Customer | company_name, customer_name |
35
- +----------------------------+------------+-----------------------------+
36
+ Checking lib/example.rb...
37
+ Results written to doc/snuffle/index.htm
36
38
 
37
39
  ## Contributing
38
40
 
data/lib/snuffle/cli.rb CHANGED
@@ -5,21 +5,17 @@ module Snuffle
5
5
 
6
6
  class CLI < Thor
7
7
 
8
- desc_text = "Formats are text (default, to STDOUT), html, and csv. "
9
- desc_text << "Example: snuffle check foo/ -f html"
10
-
11
- desc "check PATH_TO_FILE [-f FORMAT] [-t MAX_COMPLEXITY_ALLOWED]", desc_text
12
- method_option :format, :type => :string, :default => 'text', :aliases => "-f"
13
-
8
+ desc "snuffle check PATH_TO_FILES", "Example: snuffle app/models/"
14
9
  def check(path="./")
15
10
  summaries = []
16
11
  file_list(path).each do |path_to_file|
12
+ puts "Checking #{path_to_file}..."
17
13
  summary = Snuffle::SourceFile.new(path_to_file: path_to_file).summary
18
- report(summary, summary.source)
14
+ html_report(summary, summary.source)
19
15
  summaries << summary
20
16
  end
21
- create_html_index(summaries)
22
- puts results_files.join("\n")
17
+ create_html_index(summaries, path)
18
+ puts "Results written to #{results_files.last}"
23
19
  end
24
20
 
25
21
  default_task :check
@@ -39,32 +35,18 @@ module Snuffle
39
35
  def report(summary, source)
40
36
  text_report(summary)
41
37
  cvs_report(summary)
42
- html_report(summary, source)
43
- end
44
38
 
45
- def create_html_index(summaries)
46
- return unless options['format'] == 'html'
47
- results_files << Snuffle::Formatters::HtmlIndex.new(summaries).export
48
39
  end
49
40
 
50
- def cvs_report(summary)
51
- return unless options['format'] == 'csv'
52
- return unless summary.cohorts.count > 0
53
- results_files << Snuffle::Formatters::Csv.new(summary).export
41
+ def create_html_index(summaries, start_path)
42
+ results_files << Snuffle::Formatters::HtmlIndex.new(summaries, start_path).export
54
43
  end
55
44
 
56
45
  def html_report(summary, source)
57
- return unless options['format'] == 'html'
58
- return unless summary.cohorts.count > 0
46
+ return unless summary.cohorts.count > 0 || summary.latent_objects.count > 0
59
47
  results_files << Snuffle::Formatters::Html.new(summary, source).export
60
48
  end
61
49
 
62
- def text_report(summary)
63
- return unless options['format'] == 'text'
64
- puts
65
- puts Snuffle::Formatters::Text.new(summary).export
66
- end
67
-
68
50
  def results_files
69
51
  @results_files ||= []
70
52
  end
@@ -0,0 +1,17 @@
1
+ class Snuffle::Element::MethodDefinition
2
+
3
+ attr_accessor :node
4
+
5
+ def self.materialize(nodes=[])
6
+ nodes.each.map{|node| new(node) }
7
+ end
8
+
9
+ def initialize(node)
10
+ self.node = node
11
+ end
12
+
13
+ def method_name
14
+ node.name
15
+ end
16
+
17
+ end
@@ -10,9 +10,6 @@ module Snuffle
10
10
  end
11
11
 
12
12
  def rows
13
- # summary.object_candidates.map do |candidate|
14
- # [summary.path_to_file, summary.class_name, "##{candidate.join(" #")}"].join(',')
15
- # end
16
13
  summary.cohorts.group_by{|c| c.values}.map do |cohort|
17
14
  [summary.path_to_file, summary.class_name, cohort[0].join("; "), cohort[1].map(&:line_numbers).join("; ")].join(',')
18
15
  end
@@ -5,10 +5,11 @@ module Snuffle
5
5
 
6
6
  include Formatters::Base
7
7
 
8
- attr_accessor :summaries
8
+ attr_accessor :summaries, :start_path
9
9
 
10
- def initialize(summaries)
10
+ def initialize(summaries, start_path)
11
11
  self.summaries = summaries.sort{|a,b| a.cohorts.count <=> b.cohorts.count}.reverse
12
+ self.start_path = start_path
12
13
  end
13
14
 
14
15
  def header
@@ -18,7 +19,8 @@ module Snuffle
18
19
  def content
19
20
  Haml::Engine.new(output_template).render(
20
21
  Object.new, {
21
- summaries: summaries,
22
+ summaries: self.summaries,
23
+ start_path: self.start_path,
22
24
  date: Time.now.strftime("%Y/%m/%d"),
23
25
  time: Time.now.strftime("%l:%M %P")
24
26
  }
@@ -2,54 +2,93 @@
2
2
  %html
3
3
  %head
4
4
  %title
5
- Fukuzatsu
5
+ Snuffle
6
6
  %link{href: "http://cdn.datatables.net/1.10.0/css/jquery.dataTables.css", rel: "stylesheet"}
7
7
 
8
- %style{media: "screen", type: "text/css"}
9
- body { background: #49525a; color: #fff; font-family: arial, sans-serif; padding: 2em; }
8
+ :css
9
+ #{Rouge::Theme.find('thankful_eyes').render(scope: '.highlight')}
10
+ a:link, a:visited { color: #fff }
11
+ body { line-height: 1.5em; background: #49525a; color: #fff; font-family: arial, sans-serif; font-size: 14px; padding: 2em; }
12
+ div.column { float: left; width: 45%; }
13
+ div.file_listing { padding: .1em; border-radius: 5px; background: #000; width: 100%; border: 1px solid #000;}
14
+ div.file_meta { padding: 1em; border-radius: 5px; background: #440013; width: 98%; border: .5em solid #000;}
15
+ h1 { color:#fff; font-size: 1.25em; margin-top: .25em; }
16
+ h2 { color:#fff; font-size: .75em; margin-top: -1em; }
17
+ h3 { color:#fff; font-size: 1.1em;margin-top: 1em; }
18
+ h3.highlighted { background: rgba(170, 161, 57, .6); border-radius: 100px; padding: .25em; padding-left: 1em; color: #000;}
19
+ h3.highlighted-method { background: rgba(153, 51, 80, .6); border-radius: 100px; padding-left: 1em; }
20
+ li { margin-bottom: 1em;}
21
+ pre { line-height: 1.75em;}
22
+ pre.lineno { margin-top: -1.4em !important;}
23
+ span.highlighted { padding-left: 1em; display: inline-block; position: absolute; left: 0px; padding-right: 90%}
10
24
  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%; }
25
+ td { text-align: left; padding: .5em; padding-left: 1.25em !important;}
26
+ tfoot { background: #000; border-top: 10px solid #000; font-family: courier; margin-top: 4em; font-size: .75em; }
27
+ th { background: #000; text-align: left; padding: .5em; }
28
+ tr.even { background: rgba(128, 128, 128, 0.5) !important;}
29
+ tr.even:hover, tr.odd:hover { background: rgba(128, 128, 128, 0.75) !important;}
30
+ tr.faint td { opacity: 0.5; font-style: italic; }
31
+ tr.header { background-color: #222; }
11
32
  tr.header th:first-child { border-radius: 5px 0 0 0; }
12
33
  tr.header th:last-child { border-radius: 0 5px 0 0; }
13
34
  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
35
  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; }
36
+ .center { text-align: center; }
37
+ .clear { clear: both; }
38
+ .highlighted { background: rgba(170, 161, 57, .6); border-radius: 100px; }
39
+ .highlighted-method { background: rgba(153, 51, 80, .6); padding: .25em; border-radius: 100px; color: #fff; }
40
+ .indented {margin-left: 1em; }
41
+ .summary {padding: 1em; border-radius: 5px; background: rgb(41, 80, 109); width: 98%; border: .5em solid #000;}
42
+ .btn {
43
+ -webkit-border-radius: 28;
44
+ -moz-border-radius: 28;
45
+ border: none;
46
+ border-radius: 28px;
47
+ color: #ffffff;
48
+ background: #7d99af;
49
+ padding: 10px 20px 10px 20px;
50
+ text-decoration: none;
51
+ }
52
+
53
+ .btn:hover {
54
+ background: #4c708c;
55
+ border: none;
56
+ text-decoration: none;
57
+ }
58
+
28
59
 
29
60
  %body
61
+
62
+ .file_meta
63
+ %h1
64
+ Snuffle Analysis
65
+ %h2
66
+ = start_path
67
+
68
+ %br.clear
69
+
30
70
  %table{class: "output-table"}
71
+
31
72
  %thead
32
- %tr.header
33
- %th{colspan: 3}
34
- .file_meta
35
- %h1
36
- Snuffle Analysis
37
73
  %tr
38
74
  %th
39
75
  File
40
76
  %th
41
77
  Host Module/Class
42
78
  %th
43
- Object Candidates
79
+ Data Clumps
80
+ %th
81
+ Latent Objects
44
82
  %tbody
45
83
  - summaries.each_with_index do |summary, i|
46
- %tr{class: "#{i % 2 == 1 ? 'odd' : 'even'} #{summary.cohorts.count == 0 ? 'faint' : ''}"}
84
+ %tr{class: "#{i % 2 == 1 ? 'odd' : 'even'} #{summary.has_results? ? 'faint' : ''}"}
47
85
  %td
48
- - if summary.cohorts.count == 0
49
- = summary.path_to_file
50
- - else
86
+ - if summary.has_results?
51
87
  %a{href: "source/#{summary.class_filename}.htm"}
52
88
  = summary.path_to_file
89
+ - else
90
+ = summary.path_to_file
91
+
53
92
  %td
54
93
  - if summary.class_name.size > 30
55
94
  = "..."
@@ -58,14 +97,17 @@
58
97
  = summary.class_name
59
98
  %td
60
99
  = 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
100
+ %td
101
+ = summary.latent_objects.count
102
+
103
+ %br.clear
104
+
105
+ %p.center
106
+ %em
107
+ Analyzed on
108
+ = date
109
+ at
110
+ = time
111
+ by
112
+ %a{href: "https://gitlab.com/coraline/snuffle", target: "_new"}
113
+ Snuffle
@@ -7,50 +7,198 @@
7
7
  %link{href: "http://cdn.datatables.net/1.10.0/css/jquery.dataTables.css", rel: "stylesheet"}
8
8
  %script{language: "javascript", src: "http://code.jquery.com/jquery-1.11.0.min.js", type: "text/javascript"}
9
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')
10
+ %script{language: "javascript", src: "http://cdnjs.cloudflare.com/ajax/libs/highlight.js/8.0/highlight.min.js", type: "text/javascript"}
11
+ :css
12
+ #{Rouge::Theme.find('thankful_eyes').render(scope: '.highlight')}
13
+ a:link, a:visited { color: #fff }
14
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%; }
15
+ div.column { float: left; width: 45%; }
16
+ div.file_listing { padding: .1em; border-radius: 5px; background: #000; width: 100%; border: 1px solid #000;}
17
+ div.file_meta { padding: 1em; border-radius: 5px; background: #440013; width: 98%; border: .5em solid #000;}
19
18
  h1 { color:#fff; font-size: 1.25em; margin-top: .25em; }
20
19
  h2 { color:#fff; font-size: .75em; margin-top: -1em; }
21
20
  h3 { color:#fff; font-size: 1.1em;margin-top: 1em; }
22
- = ".indented {margin-left: 4em; }"
23
- %body
21
+ h3.highlighted { background: rgba(170, 161, 57, .6); border-radius: 100px; padding: .25em; padding-left: 1em; color: #000;}
22
+ h3.highlighted-method { background: rgba(153, 51, 80, .6); border-radius: 100px; padding-left: 1em; }
23
+ li { margin-bottom: 1em;}
24
+ pre { line-height: 1.75em;}
25
+ pre.lineno { margin-top: -1.4em !important;}
26
+ span.highlighted { padding-left: 1em; display: inline-block; position: absolute; left: 0px; padding-right: 90%}
27
+ 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%; }
28
+ td { text-align: left; padding: .5em; padding-left: 1.25em !important;}
29
+ tfoot { background: #000; border-top: 10px solid #000; font-family: courier; margin-top: 4em; font-size: .75em; }
30
+ th { background: #000; text-align: left; padding: .5em; }
31
+ tr.even { background: rgba(128, 128, 128, 0.5) !important;}
32
+ tr.even:hover, tr.odd:hover { background: rgba(128, 128, 128, 0.75) !important;}
33
+ tr.faint td { opacity: 0.5; font-style: italic; }
34
+ tr.header { background-color: #222; }
35
+ tr.header th:first-child { border-radius: 5px 0 0 0; }
36
+ tr.header th:last-child { border-radius: 0 5px 0 0; }
37
+ tr.header th:only-child { border-radius: 5px 5px 0 0; }
38
+ tr.odd { background: rgba(128, 128, 128, 0.25) !important}
39
+ .btn.float-right { float: right; margin-top: -4em;}
40
+ .center { text-align: center; }
41
+ .clear { clear: both; }
42
+ .highlighted { background: rgba(170, 161, 57, .6); border-radius: 100px; }
43
+ .highlighted-method { background: rgba(153, 51, 80, .6); padding: .25em; border-radius: 100px; color: #fff; }
44
+ .indented {margin-left: 1em; }
45
+ .summary {padding: 1em; border-radius: 5px; background: rgb(41, 80, 109); width: 98%; border: .5em solid #000;}
46
+ .btn {
47
+ -webkit-border-radius: 28;
48
+ -moz-border-radius: 28;
49
+ border: none;
50
+ border-radius: 28px;
51
+ color: #ffffff;
52
+ background: #7d99af;
53
+ padding: 10px 20px 10px 20px;
54
+ text-decoration: none;
55
+ }
56
+
57
+ .btn:hover {
58
+ background: #4c708c;
59
+ border: none;
60
+ text-decoration: none;
61
+ }
24
62
 
25
63
  .file_meta
26
64
  %h1
27
65
  = summary.class_name
28
66
  %h2
29
67
  = summary.path_to_file
68
+ %input.btn.float-right{onclick: "history.back(-1)", type: "button", value: "Back"}
30
69
 
31
- %h3.indented
32
- Candidate object attributes:
70
+ %br.clear
33
71
 
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(', :')}"
72
+ %div.summary
73
+ %div.column
74
+ %h3.indented.highlighted
75
+ Data Clumps:
76
+ - if summary.cohorts.count == 0
77
+ %em.indented None
78
+ - else
79
+ %ul.indented
80
+ - summary.cohorts.group_by{|c| c.values.sort }.each do |values, cohorts|
81
+ - if cohorts.count > 0
82
+ %li
83
+ = values.map{|c| ".#{c}" }.join(", ")
84
+ %br
85
+ (line
86
+ = ":#{cohorts.map(&:line_numbers).join(', :')}"
87
+ )
88
+ %div.column
89
+ %h3.indented.highlighted-method
90
+ Possible Latent Objects:
91
+ - if summary.latent_objects.count == 0
92
+ %em.indented None
93
+ - else
94
+ %ul.indented
95
+ - summary.latent_objects.each do |latent_object|
96
+ %li
97
+ = latent_object.object_candidate.titleize
98
+ %br
99
+ (
100
+ = ".#{latent_object.source_methods.join(', .')}"
101
+ )
102
+ %br.clear
41
103
 
42
- = source_lines
104
+ %br.clear
105
+
106
+ .file_listing
107
+ = source_lines
43
108
 
44
109
  %br
45
- %input{onclick: "history.back(-1)", type: "button", value: "Back"}
110
+ %input.btn{onclick: "history.back(-1)", type: "button", value: "Back"}
111
+
112
+ %p.center
113
+ %em
114
+ Analyzed on
115
+ = date
116
+ at
117
+ = time
118
+ by
119
+ %a{href: "https://gitlab.com/coraline/snuffle", target: "_new"}
120
+ Snuffle
46
121
 
47
122
  :javascript
48
123
 
49
- var line_numbers = #{summary.cohorts.map(&:line_numbers).flatten};
124
+ /*
125
+ * jQuery Highlight plugin
126
+ *
127
+ * Based on highlight v3 by Johann Burkard
128
+ * http://johannburkard.de/blog/programming/javascript/highlight-javascript-text-higlighting-jquery-plugin.html
129
+ *
130
+ * Copyright (c) 2009 Bartek Szopka
131
+ *
132
+ * Licensed under MIT license.
133
+ *
134
+ */
50
135
 
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"};}))
136
+ jQuery.extend({
137
+ highlight: function (node, re, nodeName, className) {
138
+ if (node.nodeType === 3) {
139
+ var match = node.data.match(re);
140
+ if (match) {
141
+ var highlight = document.createElement(nodeName || 'span');
142
+ highlight.className = className || 'highlight';
143
+ var wordNode = node.splitText(match.index);
144
+ wordNode.splitText(match[0].length);
145
+ var wordClone = wordNode.cloneNode(true);
146
+ highlight.appendChild(wordClone);
147
+ wordNode.parentNode.replaceChild(highlight, wordNode);
148
+ return 1; //skip added node in parent
149
+ }
150
+ } else if ((node.nodeType === 1 && node.childNodes) && // only element nodes that have children
151
+ !/(script|style)/i.test(node.tagName) && // ignore script and style nodes
152
+ !(node.tagName === nodeName.toUpperCase() && node.className === className)) { // skip if already highlighted
153
+ for (var i = 0; i < node.childNodes.length; i++) {
154
+ i += jQuery.highlight(node.childNodes[i], re, nodeName, className);
155
+ }
156
+ }
157
+ return 0;
158
+ }
159
+ });
52
160
 
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
- }
161
+ jQuery.fn.unhighlight = function (options) {
162
+ var settings = { className: 'highlight', element: 'span' };
163
+ jQuery.extend(settings, options);
164
+
165
+ return this.find(settings.element + "." + settings.className).each(function () {
166
+ var parent = this.parentNode;
167
+ parent.replaceChild(this.firstChild, this);
168
+ parent.normalize();
169
+ }).end();
170
+ };
171
+
172
+ jQuery.fn.highlight = function (words, options) {
173
+ var settings = { className: 'highlight', element: 'span', caseSensitive: false, wordsOnly: false };
174
+ jQuery.extend(settings, options);
175
+
176
+ if (words.constructor === String) {
177
+ words = [words];
178
+ }
179
+ words = jQuery.grep(words, function(word, i){
180
+ return word != '';
181
+ });
182
+ words = jQuery.map(words, function(word, i) {
183
+ return word.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
184
+ });
185
+ if (words.length == 0) { return this; };
186
+
187
+ var flag = settings.caseSensitive ? "" : "i";
188
+ var pattern = "(" + words.join("|") + ")";
189
+ if (settings.wordsOnly) {
190
+ pattern = "\\b" + pattern + "\\b";
191
+ }
192
+ var re = new RegExp(pattern, flag);
193
+
194
+ return this.each(function () {
195
+ jQuery.highlight(this, re, settings.element, settings.className);
196
+ });
197
+ };
198
+
199
+ var line_numbers = #{summary.cohorts.map(&:line_numbers).flatten};
200
+ var method_names = #{summary.latent_objects.map(&:object_candidate).flatten};
201
+
202
+ $('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"};}));
56
203
 
204
+ $('.nf').highlight(method_names, { className: 'highlighted-method'});
@@ -0,0 +1,41 @@
1
+ class Snuffle::LatentObject
2
+
3
+ include PoroPlus
4
+
5
+ attr_accessor :object_candidate, :source_methods
6
+
7
+ DUPLICATE_THRESHOLD = 1
8
+ STOPWORDS = [
9
+ "the", "be", "to", "of", "and", "a", "in", "that", "have", "I", "it", "for",
10
+ "not", "on", "with", "he", "as", "you", "do", "at", "this", "but", "his",
11
+ "by", "from", "they", "we", "say", "her", "she", "or", "an", "will", "my",
12
+ "one", "all", "would", "there", "their", "what", "so", "up", "out", "if",
13
+ "about", "who", "get", "which", "go", "me", "when", "make", "can", "like",
14
+ "time", "no", "just", "him", "know", "take", "into", "else", "other", "again",
15
+ "your", "good", "some", "could", "them", "see", "other", "than", "then",
16
+ "now", "look", "only", "come", "its", "over", "think", "also", "back", "else",
17
+ "after", "use", "two", "how", "our", "work", "first", "well", "way", "even",
18
+ "new", "want", "because", "any", "these", "give", "day", "most", "us", "call"
19
+ ]
20
+
21
+ def self.from(nodes)
22
+ potential_objects_with_methods(nodes).map do |k,v|
23
+ new(object_candidate: k, source_methods: v)
24
+ end
25
+ end
26
+
27
+ def self.potential_objects_with_methods(nodes, threshold=DUPLICATE_THRESHOLD)
28
+ method_candidates = Snuffle::Element::MethodDefinition.materialize(nodes.methods)
29
+ extract_candidates(method_candidates).select{|k,v| v.count > threshold }
30
+ end
31
+
32
+ def self.extract_candidates(methods)
33
+ methods.map(&:method_name).inject({}) do |words, method_name|
34
+ atoms = method_name.split('_') - STOPWORDS
35
+ atoms.each{ |word| words[word] ||= []; words[word] << method_name }
36
+ words
37
+ end
38
+ end
39
+
40
+ end
41
+
data/lib/snuffle/node.rb CHANGED
@@ -11,6 +11,7 @@ module Snuffle
11
11
  scope :by_type, lambda{|type| where(:type => type)}
12
12
  scope :with_parent, lambda{|parent_id| where(parent_id: parent_id) }
13
13
  scope :hashes, {type: :hash}
14
+ scope :methods, {is_method: true}
14
15
 
15
16
  def self.nil
16
17
  new(type: :nil)
@@ -33,6 +34,10 @@ module Snuffle
33
34
  Snuffle::Node.where(parent_id: self.id)
34
35
  end
35
36
 
37
+ def is_method
38
+ self.type == :def || self.type == :defs
39
+ end
40
+
36
41
  def inspect
37
42
  {
38
43
  id: self.id,
@@ -23,6 +23,10 @@ module Snuffle
23
23
  @cohorts ||= Cohort.from(self.nodes)
24
24
  end
25
25
 
26
+ def latent_objects
27
+ @latent_objects ||= LatentObject.from(self.nodes)
28
+ end
29
+
26
30
  def source
27
31
  return @source if @source
28
32
  end_pos = 0
@@ -42,6 +46,7 @@ module Snuffle
42
46
  class_name: class_name,
43
47
  path_to_file: self.path_to_file,
44
48
  cohorts: cohorts,
49
+ latent_objects: latent_objects,
45
50
  source: self.source
46
51
  )
47
52
  end
@@ -75,7 +80,7 @@ module Snuffle
75
80
  extracted_node = Snuffle::Node.new(
76
81
  type: ast_node.type,
77
82
  parent_id: parent_id,
78
- name: name_from(ast_node),
83
+ name: name,
79
84
  line_numbers: lines.map(&:line_number)
80
85
  )
81
86
  else
@@ -98,9 +103,7 @@ module Snuffle
98
103
  if name_coords = node.loc.name
99
104
  name = source[name_coords.begin_pos, name_coords.end_pos - 1]
100
105
  return unless name =~ /[a-zA-Z]/
101
- return name
102
- else
103
- "?"
106
+ return name.gsub(/^([A-Za-z0-9\_\?\!]+)(.+)$/m, '\1')
104
107
  end
105
108
  else
106
109
  return name_from(node.children.last)
@@ -2,12 +2,16 @@ module Snuffle
2
2
 
3
3
  class Summary
4
4
  include PoroPlus
5
- attr_accessor :class_name, :path_to_file, :cohorts, :source
5
+ attr_accessor :class_name, :path_to_file, :cohorts, :latent_objects, :source
6
6
 
7
7
  def class_filename
8
8
  self.class_name.downcase.gsub(' ', '_')
9
9
  end
10
10
 
11
+ def has_results?
12
+ self.cohorts.count != 0 || self.latent_objects.count != 0
13
+ end
14
+
11
15
  end
12
16
 
13
17
  end
@@ -1,3 +1,3 @@
1
1
  module Snuffle
2
- VERSION = "0.9.1"
2
+ VERSION = "0.10.1"
3
3
  end
data/lib/snuffle.rb CHANGED
@@ -12,11 +12,13 @@ require_relative "snuffle/formatters/csv"
12
12
  require_relative "snuffle/formatters/html"
13
13
  require_relative "snuffle/formatters/html_index"
14
14
  require_relative "snuffle/formatters/text"
15
+ require_relative "snuffle/latent_object"
15
16
  require_relative "snuffle/line_of_code"
16
17
  require_relative "snuffle/node"
17
18
  require_relative "snuffle/source_file"
18
19
  require_relative "snuffle/summary"
19
20
  require_relative "snuffle/elements/hash"
21
+ require_relative "snuffle/elements/method_definition"
20
22
  require_relative "snuffle/util/histogram"
21
23
 
22
24
  module Snuffle
data/snuffle.gemspec CHANGED
@@ -10,7 +10,7 @@ Gem::Specification.new do |spec|
10
10
  spec.email = ["coraline@idolhands.com"]
11
11
  spec.summary = %q{Snuffle detects data clumps in your Ruby code.}
12
12
  spec.description = %q{Snuffle detects data clumps and other hints of extractable objects in your Ruby code.}
13
- spec.homepage = ""
13
+ spec.homepage = "https://gitlab.com/coraline/snuffle/tree/master"
14
14
  spec.license = "MIT"
15
15
 
16
16
  spec.files = `git ls-files -z`.split("\x0")
@@ -0,0 +1,24 @@
1
+ class Account
2
+
3
+ attr_accessor :user
4
+
5
+ def initialize(user)
6
+ self.user = user
7
+ end
8
+
9
+ def status
10
+ end
11
+
12
+ def active?
13
+ end
14
+
15
+ def user_name
16
+ end
17
+
18
+ def user_address
19
+ end
20
+
21
+ def user_email
22
+ end
23
+
24
+ end
@@ -6,6 +6,9 @@ class Customer
6
6
 
7
7
  MY_CONSTANT = "TheOtherZachIsThePrimaryZach"
8
8
 
9
+ def self.api_root
10
+ end
11
+
9
12
  def my_condition
10
13
  puts "MAGIC" if true == false
11
14
  end
@@ -0,0 +1,14 @@
1
+ class LoadedAttrAccessor
2
+
3
+ attr_accessor :name, :email, :password
4
+ attr_accessor :city, :state, :postal_code
5
+
6
+ def address
7
+ {
8
+ city: city,
9
+ state: state,
10
+ postal_code: postal_code
11
+ }
12
+ end
13
+
14
+ end
@@ -0,0 +1,39 @@
1
+ require 'spec_helper'
2
+
3
+ describe Snuffle::LatentObject do
4
+
5
+ let(:program_2) { Snuffle::SourceFile.new(path_to_file: "spec/fixtures/latent_object_fixture.rb") }
6
+
7
+ describe ".from" do
8
+
9
+ let(:results) { Snuffle::LatentObject.from(program_2.nodes) }
10
+
11
+ it "returns an array of LatentObject instances" do
12
+ expect(results.first.class.name).to eq "Snuffle::LatentObject"
13
+ end
14
+
15
+ it "returns instances with object candidates" do
16
+ expect(results.first.object_candidate).to eq "user"
17
+ end
18
+
19
+ it "returns instances with source methods" do
20
+ expect(results.first.source_methods).to eq ["user_name", "user_address", "user_email"]
21
+ end
22
+
23
+ end
24
+
25
+ describe ".potential_objects_with_methods" do
26
+
27
+ let(:results) { Snuffle::LatentObject.potential_objects_with_methods(program_2.nodes) }
28
+
29
+ it "finds repeated words " do
30
+ expect(results.keys).to eq(["user"])
31
+ end
32
+
33
+ it "finds methods that repeated words appear in" do
34
+ expect(results['user']).to eq(["user_name", "user_address", "user_email"])
35
+ end
36
+
37
+ end
38
+
39
+ end
@@ -0,0 +1,18 @@
1
+ require "spec_helper"
2
+
3
+ Snuffle::Node.class_variable_set(:@@objects, [])
4
+
5
+ describe Snuffle::Node do
6
+
7
+ let(:source) { Snuffle::SourceFile.new }
8
+ let(:parent) { Snuffle::Node.new(id: 1, parent_id: :root, type: :hash) }
9
+ let(:child_1) { Snuffle::Node.new(id: 2, parent_id: 1, type: :hash) }
10
+ let(:child_2) { Snuffle::Node.new(id: 3, parent_id: 1, type: :hash) }
11
+
12
+ describe ".nil" do
13
+ it "returns a default object" do
14
+ expect(Snuffle::Node.nil.type).to eq(:nil)
15
+ end
16
+ end
17
+
18
+ end
@@ -3,31 +3,49 @@ require 'pry'
3
3
 
4
4
  describe Snuffle::SourceFile do
5
5
 
6
- let(:program_2) {Snuffle::SourceFile.new(path_to_file: "spec/fixtures/program_2.rb") }
7
- let(:program_3) {Snuffle::SourceFile.new(path_to_file: "spec/fixtures/program_3.rb") }
6
+ let(:program_2) { Snuffle::SourceFile.new(path_to_file: "spec/fixtures/program_2.rb") }
7
+ let(:program_3) { Snuffle::SourceFile.new(path_to_file: "spec/fixtures/program_3.rb") }
8
+ let(:program_4) { Snuffle::SourceFile.new(path_to_file: "spec/fixtures/program_4.rb") }
8
9
 
9
- describe "#object_candidates" do
10
+ describe "#cohorts" do
10
11
 
11
12
  it "does not match hash values with non-hash values" do
12
13
  attr_accessor_args = ['city', 'postal_code', 'state']
13
- values = program_2.summary.object_candidates
14
+ values = program_2.summary.cohorts.map(&:values)
14
15
  expect(values.include?(attr_accessor_args)).to be_falsey
15
16
  end
16
17
 
17
18
  it "matches elements with the same type" do
18
19
  args = ['company_name', 'customer_name']
19
- values = program_3.summary.object_candidates
20
+ values = program_3.summary.cohorts.map(&:values)
20
21
  expect(values.include?(args)).to be_truthy
21
22
  end
22
23
 
24
+ xit "does not match loose class method calls" do
25
+ values = program_4.summary.cohorts.map(&:values)
26
+ expect(values.empty?).to be_truthy
27
+ end
28
+
29
+ end
30
+
31
+ describe "#name_from" do
32
+
33
+ let(:program) { Snuffle::SourceFile.new(path_to_file: "spec/fixtures/latent_object_fixture.rb") }
34
+ let(:node) { program.send(:ast).children[2].children[1] }
35
+
36
+ it "pulls the name of a method" do
37
+ expect(program.send(:name_from, node)).to eq('initialize')
38
+ end
39
+
23
40
  end
24
41
 
25
42
  describe "#class_name" do
26
43
 
27
- let(:top_level) { "require 'something'; class Foo; def bar; puts 'hi'; end; end"}
44
+ let(:top_level) { "require 'something'; class Foo; def bar; puts 'hi'; end; end"}
28
45
  let(:namespaced_1) { "require 'something'; class Foo::Bar; def bar; puts 'hi'; end; end"}
29
46
  let(:namespaced_2) { "require 'something'; module Foo; module Bar; class Baz; def bar; puts 'hi'; end; end; end; end"}
30
- let(:source_file) { Snuffle::SourceFile.new }
47
+ let(:namespaced_3) { "class Foo; module Bar; class Baz; class << self; end; end; end; end" }
48
+ let(:source_file) { Snuffle::SourceFile.new }
31
49
 
32
50
  it "picks up a non-nested class name" do
33
51
  source_file.source = top_level
@@ -44,7 +62,11 @@ describe Snuffle::SourceFile do
44
62
  expect(source_file.class_name).to eq("Foo::Bar::Baz")
45
63
  end
46
64
 
47
- end
65
+ xit "picks up crazily nested names" do
66
+ source_file.source = namespaced_3
67
+ expect(source_file.class_name).to eq("Foo::Bar::Baz")
68
+ end
48
69
 
70
+ end
49
71
 
50
72
  end
data/test.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  require 'snuffle'
2
2
 
3
3
  def source_file
4
- @source_file ||= Snuffle::SourceFile.new(path_to_file: "spec/fixtures/program_2.rb")
4
+ @source_file ||= Snuffle::SourceFile.new(path_to_file: "spec/fixtures/latent_object_fixture.rb")
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: snuffle
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.1
4
+ version: 0.10.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Coraline Ada Ehmke
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2014-07-17 00:00:00.000000000 Z
12
+ date: 2014-07-19 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: parser
@@ -200,6 +200,7 @@ files:
200
200
  - lib/snuffle/cli.rb
201
201
  - lib/snuffle/cohort.rb
202
202
  - lib/snuffle/elements/hash.rb
203
+ - lib/snuffle/elements/method_definition.rb
203
204
  - lib/snuffle/formatters/base.rb
204
205
  - lib/snuffle/formatters/csv.rb
205
206
  - lib/snuffle/formatters/html.rb
@@ -207,6 +208,7 @@ files:
207
208
  - lib/snuffle/formatters/templates/index.html.haml
208
209
  - lib/snuffle/formatters/templates/output.html.haml
209
210
  - lib/snuffle/formatters/text.rb
211
+ - lib/snuffle/latent_object.rb
210
212
  - lib/snuffle/line_of_code.rb
211
213
  - lib/snuffle/node.rb
212
214
  - lib/snuffle/source_file.rb
@@ -215,16 +217,20 @@ files:
215
217
  - lib/snuffle/version.rb
216
218
  - snuffle.gemspec
217
219
  - spec/fixtures/account.rb
220
+ - spec/fixtures/latent_object_fixture.rb
218
221
  - spec/fixtures/program_1.rb
219
222
  - spec/fixtures/program_2.rb
220
223
  - spec/fixtures/program_3.rb
224
+ - spec/fixtures/program_4.rb
221
225
  - spec/snuffle/elements/hash_spec.rb
226
+ - spec/snuffle/latent_object_spec.rb
222
227
  - spec/snuffle/line_of_code_spec.rb
228
+ - spec/snuffle/node_spec.rb
223
229
  - spec/snuffle/source_file_spec.rb
224
230
  - spec/snuffle/util/histogram_spec.rb
225
231
  - spec/spec_helper.rb
226
232
  - test.rb
227
- homepage: ''
233
+ homepage: https://gitlab.com/coraline/snuffle/tree/master
228
234
  licenses:
229
235
  - MIT
230
236
  metadata: {}
@@ -250,11 +256,15 @@ specification_version: 4
250
256
  summary: Snuffle detects data clumps in your Ruby code.
251
257
  test_files:
252
258
  - spec/fixtures/account.rb
259
+ - spec/fixtures/latent_object_fixture.rb
253
260
  - spec/fixtures/program_1.rb
254
261
  - spec/fixtures/program_2.rb
255
262
  - spec/fixtures/program_3.rb
263
+ - spec/fixtures/program_4.rb
256
264
  - spec/snuffle/elements/hash_spec.rb
265
+ - spec/snuffle/latent_object_spec.rb
257
266
  - spec/snuffle/line_of_code_spec.rb
267
+ - spec/snuffle/node_spec.rb
258
268
  - spec/snuffle/source_file_spec.rb
259
269
  - spec/snuffle/util/histogram_spec.rb
260
270
  - spec/spec_helper.rb