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 +4 -4
- data/README.md +11 -9
- data/lib/snuffle/cli.rb +8 -26
- data/lib/snuffle/elements/method_definition.rb +17 -0
- data/lib/snuffle/formatters/csv.rb +0 -3
- data/lib/snuffle/formatters/html_index.rb +5 -3
- data/lib/snuffle/formatters/templates/index.html.haml +79 -37
- data/lib/snuffle/formatters/templates/output.html.haml +174 -26
- data/lib/snuffle/latent_object.rb +41 -0
- data/lib/snuffle/node.rb +5 -0
- data/lib/snuffle/source_file.rb +7 -4
- data/lib/snuffle/summary.rb +5 -1
- data/lib/snuffle/version.rb +1 -1
- data/lib/snuffle.rb +2 -0
- data/snuffle.gemspec +1 -1
- data/spec/fixtures/latent_object_fixture.rb +24 -0
- data/spec/fixtures/program_2.rb +3 -0
- data/spec/fixtures/program_4.rb +14 -0
- data/spec/snuffle/latent_object_spec.rb +39 -0
- data/spec/snuffle/node_spec.rb +18 -0
- data/spec/snuffle/source_file_spec.rb +30 -8
- data/test.rb +1 -1
- metadata +13 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 585cee4f627767538a301e5a3aba547e9bb98a8e
|
4
|
+
data.tar.gz: 8ba7bbd451845b2f0f583e66c5283e5ad68d7ea7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
*
|
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
|
-
|
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
|
-
|
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
|
-
|
14
|
+
html_report(summary, summary.source)
|
19
15
|
summaries << summary
|
20
16
|
end
|
21
|
-
create_html_index(summaries)
|
22
|
-
puts results_files.
|
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
|
51
|
-
|
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
|
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
|
@@ -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
|
-
|
5
|
+
Snuffle
|
6
6
|
%link{href: "http://cdn.datatables.net/1.10.0/css/jquery.dataTables.css", rel: "stylesheet"}
|
7
7
|
|
8
|
-
|
9
|
-
|
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
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
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
|
-
|
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.
|
84
|
+
%tr{class: "#{i % 2 == 1 ? 'odd' : 'even'} #{summary.has_results? ? 'faint' : ''}"}
|
47
85
|
%td
|
48
|
-
- if summary.
|
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
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
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://
|
11
|
-
|
12
|
-
|
13
|
-
|
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
|
-
|
16
|
-
|
17
|
-
|
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
|
-
|
23
|
-
|
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
|
-
%
|
32
|
-
Candidate object attributes:
|
70
|
+
%br.clear
|
33
71
|
|
34
|
-
%
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
54
|
-
|
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,
|
data/lib/snuffle/source_file.rb
CHANGED
@@ -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:
|
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)
|
data/lib/snuffle/summary.rb
CHANGED
@@ -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
|
data/lib/snuffle/version.rb
CHANGED
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")
|
data/spec/fixtures/program_2.rb
CHANGED
@@ -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 "#
|
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.
|
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.
|
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)
|
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(:
|
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
|
-
|
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
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.
|
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-
|
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
|