snuffle 0.9.1 → 0.10.1

Sign up to get free protection for your applications and to get access to all the features.
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