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 +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
|