analyst 0.0.1 → 0.13.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/.gitignore +4 -1
- data/.rspec +2 -0
- data/CODE_OF_CONDUCT.md +13 -0
- data/README.md +1 -0
- data/analyst.gemspec +12 -0
- data/lib/analyst/analyzer.rb +162 -0
- data/lib/analyst/cli.rb +42 -0
- data/lib/analyst/entity_parser/association.rb +24 -0
- data/lib/analyst/entity_parser/entities/class.rb +92 -0
- data/lib/analyst/entity_parser/entities/empty.rb +13 -0
- data/lib/analyst/entity_parser/entities/entity.rb +29 -0
- data/lib/analyst/entity_parser/entities/method.rb +16 -0
- data/lib/analyst/entity_parser/entities/module.rb +31 -0
- data/lib/analyst/formatters/base.rb +33 -0
- data/lib/analyst/formatters/csv.rb +43 -0
- data/lib/analyst/formatters/html.rb +87 -0
- data/lib/analyst/formatters/html_index.rb +47 -0
- data/lib/analyst/formatters/templates/index.html.haml +92 -0
- data/lib/analyst/formatters/templates/output.html.haml +114 -0
- data/lib/analyst/formatters/text.rb +56 -0
- data/lib/analyst/fukuzatsu/analyzer.rb +162 -0
- data/lib/analyst/fukuzatsu/cli.rb +42 -0
- data/lib/analyst/fukuzatsu/entity_parser/association.rb +24 -0
- data/lib/analyst/fukuzatsu/entity_parser/entities/class.rb +92 -0
- data/lib/analyst/fukuzatsu/entity_parser/entities/empty.rb +13 -0
- data/lib/analyst/fukuzatsu/entity_parser/entities/entity.rb +29 -0
- data/lib/analyst/fukuzatsu/entity_parser/entities/method.rb +16 -0
- data/lib/analyst/fukuzatsu/entity_parser/entities/module.rb +31 -0
- data/lib/analyst/fukuzatsu/formatters/base.rb +33 -0
- data/lib/analyst/fukuzatsu/formatters/csv.rb +43 -0
- data/lib/analyst/fukuzatsu/formatters/html.rb +87 -0
- data/lib/analyst/fukuzatsu/formatters/html_index.rb +47 -0
- data/lib/analyst/fukuzatsu/formatters/templates/index.html.haml +92 -0
- data/lib/analyst/fukuzatsu/formatters/templates/output.html.haml +114 -0
- data/lib/analyst/fukuzatsu/formatters/text.rb +56 -0
- data/lib/analyst/fukuzatsu/line_of_code.rb +19 -0
- data/lib/analyst/fukuzatsu/parsed_file.rb +85 -0
- data/lib/analyst/fukuzatsu/parsed_method.rb +32 -0
- data/lib/analyst/fukuzatsu/parser_original.rb +76 -0
- data/lib/analyst/fukuzatsu/rethink/parser.rb +346 -0
- data/lib/analyst/fukuzatsu/version.rb +3 -0
- data/lib/analyst/line_of_code.rb +19 -0
- data/lib/analyst/parsed_file.rb +85 -0
- data/lib/analyst/parsed_method.rb +32 -0
- data/lib/analyst/parser.rb +76 -0
- data/lib/analyst/rethink/parser.rb +346 -0
- data/lib/analyst/version.rb +1 -1
- data/lib/analyst.rb +17 -2
- data/spec/analyzer_spec.rb +122 -0
- data/spec/cli_spec.rb +48 -0
- data/spec/fixtures/eg_class.rb +8 -0
- data/spec/fixtures/eg_mod_class.rb +2 -0
- data/spec/fixtures/eg_mod_class_2.rb +5 -0
- data/spec/fixtures/eg_module.rb +2 -0
- data/spec/fixtures/module_with_class.rb +9 -0
- data/spec/fixtures/multiple_methods.rb +7 -0
- data/spec/fixtures/nested_methods.rb +8 -0
- data/spec/fixtures/program_1.rb +19 -0
- data/spec/fixtures/program_2.rb +25 -0
- data/spec/fixtures/program_3.rb +66 -0
- data/spec/fixtures/program_4.rb +1 -0
- data/spec/fixtures/single_class.rb +9 -0
- data/spec/fixtures/single_method.rb +3 -0
- data/spec/formatters/csv_spec.rb +37 -0
- data/spec/formatters/html_index_spec.rb +36 -0
- data/spec/formatters/html_spec.rb +48 -0
- data/spec/formatters/text_spec.rb +39 -0
- data/spec/parsed_file_spec.rb +67 -0
- data/spec/parsed_method_spec.rb +34 -0
- data/spec/spec_helper.rb +7 -0
- metadata +229 -2
@@ -0,0 +1,47 @@
|
|
1
|
+
module Formatters
|
2
|
+
|
3
|
+
class HtmlIndex
|
4
|
+
|
5
|
+
include Formatters::Base
|
6
|
+
|
7
|
+
attr_reader :file_summary, :output_directory
|
8
|
+
|
9
|
+
def initialize(file_summary, output_directory=nil)
|
10
|
+
@file_summary = file_summary
|
11
|
+
@output_directory = output_directory
|
12
|
+
end
|
13
|
+
|
14
|
+
def content
|
15
|
+
Haml::Engine.new(output_template).render(
|
16
|
+
Object.new, {
|
17
|
+
file_summary: file_summary,
|
18
|
+
date: Time.now.strftime("%Y/%m/%d"),
|
19
|
+
time: Time.now.strftime("%l:%M %P")
|
20
|
+
}
|
21
|
+
)
|
22
|
+
end
|
23
|
+
|
24
|
+
def export
|
25
|
+
begin
|
26
|
+
File.open(path_to_results, 'w') {|outfile| outfile.write(content)}
|
27
|
+
rescue Exception => e
|
28
|
+
puts "Unable to write output: #{e} #{e.backtrace}"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def filename
|
33
|
+
"index.htm"
|
34
|
+
end
|
35
|
+
|
36
|
+
def output_path
|
37
|
+
FileUtils.mkpath(self.output_directory)
|
38
|
+
self.output_directory
|
39
|
+
end
|
40
|
+
|
41
|
+
def output_template
|
42
|
+
File.read(File.dirname(__FILE__) + "/templates/index.html.haml")
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
!!!
|
2
|
+
%html
|
3
|
+
%head
|
4
|
+
%title
|
5
|
+
Analyst
|
6
|
+
%link{href: "http://cdn.datatables.net/1.10.0/css/jquery.dataTables.css", rel: "stylesheet"}
|
7
|
+
%script{language: "javascript", src: "http://code.jquery.com/jquery-1.11.0.min.js", type: "text/javascript"}
|
8
|
+
%script{language: "javascript", src: "http://code.jquery.com/jquery-migrate-1.2.1.min.js", type: "text/javascript"}
|
9
|
+
%script{language: "javascript", src: "http://cdn.datatables.net/1.10.0/js/jquery.dataTables.js", type: "text/javascript"}
|
10
|
+
|
11
|
+
%style{media: "screen", type: "text/css"}
|
12
|
+
body { background: #593232; color: #fff; font-family: arial, sans-serif; padding: 2em; }
|
13
|
+
table { box-shadow: 0 5px 0 rgba(0,0,0,.8); background: #444; border-spacing: 0; border: 5px solid #000; border-radius: 25px; border-collapse: collapse; min-width: 50%; }
|
14
|
+
th.sorting_asc, th.sorting_desc { text-transform: uppercase !important; font-size: .8em !important; background-image: none !important; background: rgba(64, 41, 41, .5) !important;}
|
15
|
+
th.sorting { background-position: left !important; border-right: 1px solid #222; text-transform: uppercase !important; font-size: .8em !important}
|
16
|
+
tr.header th:first-child { border-radius: 6px 0 0 0; }
|
17
|
+
tr.header th:last-child { border-radius: 0 6px 0 0; }
|
18
|
+
tr.header th:only-child { border-radius: 6px 6px 0 0; }
|
19
|
+
tr.header { background-color: #222; }
|
20
|
+
tr.even { background: rgba(128, 128, 128, 0.5) !important;}
|
21
|
+
tr.odd { background: rgba(128, 128, 128, 0.25) !important}
|
22
|
+
tr.even:hover, tr.odd:hover { background: rgba(128, 128, 128, 0.75) !important;}
|
23
|
+
th { background: #000; text-align: left; padding: .5em; }
|
24
|
+
td { text-align: left; padding: .5em; padding-left: 1.25em !important;}
|
25
|
+
td.center { text-align: center; }
|
26
|
+
td.sorting_1 { background: none !important; padding-left: 1.25em !important; }
|
27
|
+
tfoot { background: #000; border-top: 10px solid #000; font-family: courier; margin-top: 4em; font-size: .75em; }
|
28
|
+
a:link, a:visited { color: #aaa }
|
29
|
+
div.file_meta { float: left; height: 3em; width: 30%; }
|
30
|
+
h1 { color:#fff; font-size: 1.25em; margin-top: .25em; }
|
31
|
+
h2 { color:#fff; font-size: .75em; margin-top: -1em; }
|
32
|
+
h3 { color:#fff; font-size: 1em; float: right; margin-top: 1em; }
|
33
|
+
td.sorting_1 { background: none !important; padding-left: 1.25em !important; }
|
34
|
+
div.dataTables_filter { margin-bottom: 2em !important; }
|
35
|
+
div.dataTables_filter label { color: #fff; }
|
36
|
+
div.dataTables_paginate { display: none !important; }
|
37
|
+
div.dataTables_info { display: none !important; }
|
38
|
+
|
39
|
+
%body
|
40
|
+
%table{class: "output-table"}
|
41
|
+
%thead
|
42
|
+
%tr.header
|
43
|
+
%th{colspan: 4}
|
44
|
+
.file_meta
|
45
|
+
%h1
|
46
|
+
Project Overview
|
47
|
+
%tr
|
48
|
+
%th
|
49
|
+
File
|
50
|
+
%th
|
51
|
+
Module/Class Name
|
52
|
+
%th
|
53
|
+
Complexity
|
54
|
+
%th
|
55
|
+
Details
|
56
|
+
%tbody
|
57
|
+
- file_summary.each do |summary|
|
58
|
+
%tr
|
59
|
+
%td
|
60
|
+
%a{href: "#{summary[:path_to_file]}.htm"}
|
61
|
+
= summary[:path_to_file]
|
62
|
+
%td
|
63
|
+
- if summary[:class_name].size > 25
|
64
|
+
= "..."
|
65
|
+
= summary[:class_name][-24..-1]
|
66
|
+
- else
|
67
|
+
= summary[:class_name]
|
68
|
+
%td
|
69
|
+
= summary[:complexity]
|
70
|
+
%td
|
71
|
+
%a{href: "#{summary[:path_to_file]}.htm"}
|
72
|
+
View Details
|
73
|
+
%tfoot
|
74
|
+
%tr
|
75
|
+
%td.center{colspan: 4}
|
76
|
+
%em
|
77
|
+
Analyzed on
|
78
|
+
= date
|
79
|
+
at
|
80
|
+
= time
|
81
|
+
by
|
82
|
+
%a{href: "https://gitlab.com/coraline/Analyst", target: "_new"}
|
83
|
+
Analyst
|
84
|
+
:javascript
|
85
|
+
$(document).ready(function(){
|
86
|
+
$('.output-table').dataTable({
|
87
|
+
bLengthChange: false,
|
88
|
+
iDisplayLength: 25000,
|
89
|
+
"order": [[2, "desc"]]
|
90
|
+
});
|
91
|
+
});
|
92
|
+
|
@@ -0,0 +1,114 @@
|
|
1
|
+
!!!
|
2
|
+
%html
|
3
|
+
%head
|
4
|
+
%title
|
5
|
+
Analyst:
|
6
|
+
= class_name
|
7
|
+
%link{href: "http://cdn.datatables.net/1.10.0/css/jquery.dataTables.css", rel: "stylesheet"}
|
8
|
+
%script{language: "javascript", src: "http://code.jquery.com/jquery-1.11.0.min.js", type: "text/javascript"}
|
9
|
+
%script{language: "javascript", src: "http://code.jquery.com/jquery-migrate-1.2.1.min.js", type: "text/javascript"}
|
10
|
+
%script{language: "javascript", src: "http://cdn.datatables.net/1.10.0/js/jquery.dataTables.js", type: "text/javascript"}
|
11
|
+
%script{language: "javascript", src: "http://cdnjs.cloudflare.com/ajax/libs/highlight.js/8.0/highlight.min.js", type: "text/javascript"}
|
12
|
+
:css
|
13
|
+
#{Rouge::Theme.find('thankful_eyes').render(scope: '.highlight')}
|
14
|
+
body { background: #593232; color: #fff; font-family: arial, sans-serif; padding: 2em; }
|
15
|
+
div.column { float: left; width: 45%; margin-left: 4%; }
|
16
|
+
div.file_listing { padding: .1em; border-radius: 5px; background: #000; width: 100%; border: 1px solid #000;}
|
17
|
+
div.file_meta { float: left; height: 3em; width: 30%; }
|
18
|
+
h1 { color:#fff; font-size: 1.25em; margin-top: .25em; }
|
19
|
+
h2 { color:#fff; font-size: .75em; margin-top: -1em; }
|
20
|
+
h3 { color:#fff; font-size: 1.1em;margin-top: 1em; }
|
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
|
+
a:link, a:visited { color: #aaa }
|
28
|
+
|
29
|
+
.file_listing 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%; }
|
30
|
+
.file_listing td { text-align: left; padding: .5em; padding-left: 1.25em !important;}
|
31
|
+
.file_listing td.gutter { background-color: rgb(109, 109, 109) !important; }
|
32
|
+
.file_listing tfoot { background: #000; border-top: 10px solid #000; font-family: courier; margin-top: 4em; font-size: .75em; }
|
33
|
+
.file_listing th { background: #000; text-align: left; padding: .5em; }
|
34
|
+
.file_listing tr.faint td { opacity: 0.5; font-style: italic; }
|
35
|
+
.file_listing tr.header { background-color: #222; }
|
36
|
+
.file_listing tr { background: rgba(0, 0, 0, 0.25) !important; border-bottom: 1px solid #000;}
|
37
|
+
.file_listing th { background: #000; text-align: left; padding: .5em;}
|
38
|
+
.file_listing td { text-align: left; padding: .5em; padding-left: 1.25em !important;}
|
39
|
+
.file_listing td.center { text-align: center; }
|
40
|
+
.file_listing td.sorting_1 { background: none !important; padding-left: 1.25em !important; }
|
41
|
+
.file_listing tfoot { background: #000; font-family: courier; margin-top: 4em; font-size: .75em; }
|
42
|
+
|
43
|
+
table { box-shadow: 0 5px 0 rgba(0,0,0,.8); background: #444; border-spacing: 0; border: 5px solid #000; border-radius: 25px; border-collapse: collapse; min-width: 50%; }
|
44
|
+
th.sorting_asc, th.sorting_desc { text-transform: uppercase !important; font-size: .8em !important; background-image: none !important; background: rgba(64, 41, 41, .5) !important;}
|
45
|
+
th.sorting { background-position: left !important; border-right: 1px solid #222; text-transform: uppercase !important; font-size: .8em !important}
|
46
|
+
tr.header th:first-child { border-radius: 6px 0 0 0; }
|
47
|
+
tr.header th:last-child { border-radius: 0 6px 0 0; }
|
48
|
+
tr.header th:only-child { border-radius: 6px 6px 0 0; }
|
49
|
+
tfoot tr { border-radius: 6px 6px 6px 6px; }
|
50
|
+
tr.header { background-color: #222; }
|
51
|
+
tr.even { background: rgba(128, 128, 128, 0.5) !important;}
|
52
|
+
tr.odd { background: rgba(128, 128, 128, 0.25) !important}
|
53
|
+
tr.even:hover, tr.odd:hover { background: rgba(128, 128, 128, 0.75) !important;}
|
54
|
+
th { background: #000; text-align: left; padding: .5em;}
|
55
|
+
td { text-align: left; padding: .5em; padding-left: 1.25em !important;}
|
56
|
+
td.center { text-align: center; }
|
57
|
+
td.sorting_1 { background: none !important; padding-left: 1.25em !important; }
|
58
|
+
tfoot { background: #000; font-family: courier; margin-top: 4em; font-size: .75em; }
|
59
|
+
|
60
|
+
h1 { color:#fff; font-size: 1.25em; margin-top: .25em; }
|
61
|
+
h2 { color:#fff; font-size: .75em; margin-top: -1em; }
|
62
|
+
h3 { color:#fff; font-size: 1em; float: right; margin-top: 1em; }
|
63
|
+
|
64
|
+
div.dataTables_filter { display: none !important; }
|
65
|
+
div.dataTables_paginate { display: none !important; }
|
66
|
+
div.dataTables_info { display: none !important; }
|
67
|
+
|
68
|
+
|
69
|
+
%body
|
70
|
+
.container
|
71
|
+
%table{class: "output-table"}
|
72
|
+
%thead
|
73
|
+
%tr.header
|
74
|
+
%th{colspan: 3}
|
75
|
+
.file_meta
|
76
|
+
%h1
|
77
|
+
= class_name
|
78
|
+
%h2
|
79
|
+
= path_to_file
|
80
|
+
.file_meta_right
|
81
|
+
%h3
|
82
|
+
= "Overall Complexity: #{complexity}"
|
83
|
+
%tr
|
84
|
+
= header
|
85
|
+
%tbody
|
86
|
+
= rows
|
87
|
+
%tfoot
|
88
|
+
%tr
|
89
|
+
%td.center{colspan: 3}
|
90
|
+
%em
|
91
|
+
Analyzed on
|
92
|
+
= date
|
93
|
+
at
|
94
|
+
= time
|
95
|
+
by
|
96
|
+
%a{href: "https://gitlab.com/coraline/Analyst", target: "_new"}
|
97
|
+
Analyst
|
98
|
+
%br
|
99
|
+
%input{onclick: "history.back(-1)", type: "button", value: "Back"}
|
100
|
+
|
101
|
+
%br
|
102
|
+
|
103
|
+
.file_listing
|
104
|
+
= source_lines
|
105
|
+
|
106
|
+
:javascript
|
107
|
+
$(document).ready(function(){
|
108
|
+
$('.output-table').dataTable({
|
109
|
+
bLengthChange: false,
|
110
|
+
iDisplayLength: 25000,
|
111
|
+
"order": [[2, "desc"]]
|
112
|
+
});
|
113
|
+
});
|
114
|
+
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module Formatters
|
2
|
+
|
3
|
+
require 'terminal-table'
|
4
|
+
require 'rainbow/ext/string'
|
5
|
+
|
6
|
+
class Text
|
7
|
+
|
8
|
+
include Formatters::Base
|
9
|
+
|
10
|
+
def self.has_index?
|
11
|
+
false
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.writes_to_file_system?
|
15
|
+
false
|
16
|
+
end
|
17
|
+
|
18
|
+
def color_for(row)
|
19
|
+
return :green if row.complexity == 0
|
20
|
+
return :yellow if row.complexity <= file.average_complexity
|
21
|
+
return :red if row.complexity > file.average_complexity
|
22
|
+
return :white
|
23
|
+
end
|
24
|
+
|
25
|
+
def header
|
26
|
+
["Class/Module", "Method", "Complexity"]
|
27
|
+
end
|
28
|
+
|
29
|
+
def export
|
30
|
+
return if rows.empty?
|
31
|
+
table = Terminal::Table.new(
|
32
|
+
title: file.path_to_file.color(:white),
|
33
|
+
headings: header,
|
34
|
+
rows: rows,
|
35
|
+
style: {width: 90}
|
36
|
+
)
|
37
|
+
table.align_column(3, :right)
|
38
|
+
puts table
|
39
|
+
end
|
40
|
+
|
41
|
+
def rows
|
42
|
+
file.methods.map do |method|
|
43
|
+
color = color_for(method)
|
44
|
+
[wrap("#{file.class_name}").color(color), wrap("#{method.name}".color(color)), "#{method.complexity}".color(color)]
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def wrap(string)
|
49
|
+
return string if string.length < 25
|
50
|
+
string[0..20] << "..."
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
|
@@ -0,0 +1,162 @@
|
|
1
|
+
require 'parser/current'
|
2
|
+
|
3
|
+
class Analyzer
|
4
|
+
|
5
|
+
CONDITIONALS = [:if, :or_asgn, :and_asgn, :or, :and]
|
6
|
+
|
7
|
+
attr_accessor :content, :class_name, :edges, :nodes, :exits
|
8
|
+
|
9
|
+
DEFAULT_CLASS_NAME = "Unknown"
|
10
|
+
|
11
|
+
def initialize(content)
|
12
|
+
self.content = content
|
13
|
+
self.edges = 0
|
14
|
+
self.nodes = 1
|
15
|
+
self.exits = 1
|
16
|
+
end
|
17
|
+
|
18
|
+
def complexity
|
19
|
+
return unless traverse(parsed)
|
20
|
+
self.edges - self.nodes + exits
|
21
|
+
end
|
22
|
+
|
23
|
+
def class_name
|
24
|
+
find_class(parsed) || DEFAULT_CLASS_NAME
|
25
|
+
end
|
26
|
+
|
27
|
+
def methods
|
28
|
+
@methods ||= methods_from(parsed)
|
29
|
+
end
|
30
|
+
|
31
|
+
def constants
|
32
|
+
@constants ||= constants_from(parsed)
|
33
|
+
end
|
34
|
+
|
35
|
+
def method_names
|
36
|
+
@method_names ||= method_names_from(parsed)
|
37
|
+
end
|
38
|
+
|
39
|
+
def extract_methods
|
40
|
+
@methods ||= methods_from(parsed)
|
41
|
+
end
|
42
|
+
|
43
|
+
def extract_class_name
|
44
|
+
return self.class_name if self.class_name && ! self.class_name.empty?
|
45
|
+
found = find_class(parsed)
|
46
|
+
self.class_name = ! found.empty? && found || DEFAULT_CLASS_NAME
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def method_list
|
52
|
+
@method_list ||= method_names
|
53
|
+
end
|
54
|
+
|
55
|
+
def method_names_from(node, found=[])
|
56
|
+
return found unless node.respond_to?(:type)
|
57
|
+
if node.type == :def || node.type == :defs
|
58
|
+
name = node.loc.name
|
59
|
+
found << content[name.begin_pos..name.end_pos - 1].to_sym
|
60
|
+
end
|
61
|
+
node.children.each do |child|
|
62
|
+
method_names_from(child, found) if parent_node?(child)
|
63
|
+
end
|
64
|
+
found
|
65
|
+
end
|
66
|
+
|
67
|
+
def constants_from(node, found=[])
|
68
|
+
if node.type == :const
|
69
|
+
expression = node.loc.expression
|
70
|
+
found << content[expression.begin_pos..expression.end_pos - 1]
|
71
|
+
end
|
72
|
+
node.children.each do |child|
|
73
|
+
constants_from(child, found) if parent_node?(child)
|
74
|
+
end
|
75
|
+
found.reject{ |constant| constant == class_name }
|
76
|
+
end
|
77
|
+
|
78
|
+
def extract_references_from(node, found=[])
|
79
|
+
return found unless node && node.respond_to?(:type)
|
80
|
+
if node.type == :send
|
81
|
+
reference = node.loc.expression
|
82
|
+
found << node.children.last
|
83
|
+
end
|
84
|
+
node.children.each do |child|
|
85
|
+
extract_references_from(child, found)
|
86
|
+
end
|
87
|
+
found.select{|name| method_list.include?(name)}
|
88
|
+
end
|
89
|
+
|
90
|
+
def text_at(start_pos, end_pos)
|
91
|
+
content[start_pos..end_pos - 1]
|
92
|
+
end
|
93
|
+
|
94
|
+
def find_class(node)
|
95
|
+
return unless node && node.respond_to?(:type)
|
96
|
+
concat = []
|
97
|
+
if node.type == :module || node.type == :class
|
98
|
+
concat << text_at(node.loc.name.begin_pos, node.loc.name.end_pos)
|
99
|
+
end
|
100
|
+
concat << node.children.map{|child| find_class(child)}.compact
|
101
|
+
concat.flatten.select(&:present?).join('::')
|
102
|
+
end
|
103
|
+
|
104
|
+
def extend_graph
|
105
|
+
self.edges += 2
|
106
|
+
self.nodes += 2
|
107
|
+
self.exits += 1
|
108
|
+
end
|
109
|
+
|
110
|
+
def methods_from(node, methods=[])
|
111
|
+
if node.type == :def || node.type == :defs
|
112
|
+
name = node.loc.name
|
113
|
+
expression = node.loc.expression
|
114
|
+
type = case(node.type)
|
115
|
+
when :defs
|
116
|
+
:class
|
117
|
+
when :def
|
118
|
+
:instance
|
119
|
+
when :class
|
120
|
+
:none
|
121
|
+
end
|
122
|
+
methods << ParsedMethod.new(
|
123
|
+
name: content[name.begin_pos..name.end_pos - 1],
|
124
|
+
content: content[expression.begin_pos..expression.end_pos - 1],
|
125
|
+
type: type,
|
126
|
+
refs: extract_references_from(node)
|
127
|
+
)
|
128
|
+
end
|
129
|
+
node.children.each do |child|
|
130
|
+
if parent_node?(child)
|
131
|
+
methods_from(child, methods)
|
132
|
+
end
|
133
|
+
end
|
134
|
+
methods.reject{ |m| m.name.empty? }
|
135
|
+
end
|
136
|
+
|
137
|
+
def parent_node?(node)
|
138
|
+
node.respond_to?(:type) || node.respond_to?(:children)
|
139
|
+
end
|
140
|
+
|
141
|
+
def parse!
|
142
|
+
traverse(parsed) && complexity
|
143
|
+
end
|
144
|
+
|
145
|
+
def parsed
|
146
|
+
@parsed ||= Parser::CurrentRuby.parse(content)
|
147
|
+
end
|
148
|
+
|
149
|
+
def traverse(node, accumulator=[], extract_methods=false)
|
150
|
+
accumulator << node.type
|
151
|
+
extend_graph if CONDITIONALS.include?(node.type)
|
152
|
+
node.children.each do |child|
|
153
|
+
if parent_node?(child)
|
154
|
+
accumulator << child.type
|
155
|
+
traverse(child, accumulator)
|
156
|
+
end
|
157
|
+
end
|
158
|
+
accumulator
|
159
|
+
end
|
160
|
+
|
161
|
+
end
|
162
|
+
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'thor'
|
2
|
+
require 'Analyst'
|
3
|
+
|
4
|
+
module Analyst
|
5
|
+
|
6
|
+
class CLI < Thor
|
7
|
+
|
8
|
+
desc_text = "Formats are text (default, to STDOUT), html, and csv. "
|
9
|
+
desc_text << "Example: fuku 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
|
+
method_option :threshold, :type => :numeric, :default => 0, :aliases => "-t"
|
14
|
+
|
15
|
+
def check(path="./")
|
16
|
+
parser = Analyst::Parser.new(
|
17
|
+
path,
|
18
|
+
formatter,
|
19
|
+
options['threshold']
|
20
|
+
)
|
21
|
+
parser.parse_files
|
22
|
+
parser.report
|
23
|
+
end
|
24
|
+
|
25
|
+
default_task :check
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def formatter
|
30
|
+
case options['format']
|
31
|
+
when 'html'
|
32
|
+
Formatters::Html
|
33
|
+
when 'csv'
|
34
|
+
Formatters::Csv
|
35
|
+
else
|
36
|
+
Formatters::Text
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Analyst
|
2
|
+
|
3
|
+
module EntityParser
|
4
|
+
|
5
|
+
class ClassRelation
|
6
|
+
attr_reader :type, :source, :target_class_name, :target
|
7
|
+
|
8
|
+
def initialize(type:, source:, target_class_name:)
|
9
|
+
@type = type
|
10
|
+
@source = source
|
11
|
+
@target_class_name = target_class_name
|
12
|
+
end
|
13
|
+
|
14
|
+
def resolve_target!(entity_list)
|
15
|
+
unless @target = entity_list.detect{ |entity| entity.full_name == self.target_class_name }
|
16
|
+
puts "WARNING: Couldn't find target: #{self.target_class_name}"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
@@ -0,0 +1,92 @@
|
|
1
|
+
#TODO add == to association
|
2
|
+
|
3
|
+
module Analyst
|
4
|
+
|
5
|
+
module EntityParser
|
6
|
+
module Entities
|
7
|
+
class ActiveModel < Entity
|
8
|
+
|
9
|
+
ASSOCIATIONS = [:belongs_to, :has_one, :has_many, :has_and_belongs_to_many]
|
10
|
+
|
11
|
+
attr_reader :associations
|
12
|
+
|
13
|
+
def initialize
|
14
|
+
@associations = []
|
15
|
+
end
|
16
|
+
|
17
|
+
def handle_send(node)
|
18
|
+
target, method_name, *args = node.children
|
19
|
+
if ASSOCIATIONS.include?(method_name)
|
20
|
+
add_association(method_name, args)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# When a class is reopened, merge its associations
|
25
|
+
def merge_associations_from(klass)
|
26
|
+
klass.associations.each do |association|
|
27
|
+
associations << Association.new(
|
28
|
+
type: association.type,
|
29
|
+
source: self,
|
30
|
+
target_class: association.target_class
|
31
|
+
)
|
32
|
+
end
|
33
|
+
associations.uniq!
|
34
|
+
end
|
35
|
+
|
36
|
+
def name
|
37
|
+
const_node_array(ast.children.first).join('::')
|
38
|
+
end
|
39
|
+
|
40
|
+
def full_name
|
41
|
+
parent.full_name.empty? ? name : parent.full_name + '::' + name
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def add_association(method_name, args)
|
47
|
+
target_class = value_from_hash_node(args.last, :class_name)
|
48
|
+
target_class ||= begin
|
49
|
+
symbol_node = args.first
|
50
|
+
symbol_name = symbol_node.children.first
|
51
|
+
symbol_name.pluralize.classify
|
52
|
+
end
|
53
|
+
association = Association.new(type: method_name, source: self, target_class: target_class)
|
54
|
+
associations << association
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
# takes a (const) node and returns an array specifying the fully-qualified
|
60
|
+
# constant name that it represents. ya know, so CoolModule::SubMod::SweetClass
|
61
|
+
# would be parsed to:
|
62
|
+
# (const
|
63
|
+
# (const
|
64
|
+
# (const nil :CoolModule) :SubMod) :SweetClass)
|
65
|
+
# and passing that node here would return [:CoolModule, :SubMod, :SweetClass]
|
66
|
+
def const_node_array(node)
|
67
|
+
return [] if node.nil?
|
68
|
+
raise "expected (const) node or nil, got (#{node.type})" unless node.type == :const
|
69
|
+
const_node_array(node.children.first) << node.children[1]
|
70
|
+
end
|
71
|
+
|
72
|
+
# Fetches value from hash node iff key is symbol and value is str
|
73
|
+
# Raises an exception if value is not str
|
74
|
+
# Returns nil if key is not found
|
75
|
+
def value_from_hash_node(node, key)
|
76
|
+
return unless node.type == :hash
|
77
|
+
pair = node.children.detect do |pair_node|
|
78
|
+
key_symbol_node = pair_node.children.first
|
79
|
+
key == key_symbol_node.children.first
|
80
|
+
end
|
81
|
+
if pair
|
82
|
+
value_node = pair.children.last
|
83
|
+
throw "Bad type. Expected (str), got (#{value_node.type})" unless value_node.type == :str
|
84
|
+
value_node.children.first
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|
89
|
+
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Analyst
|
2
|
+
module EntityParser
|
3
|
+
module Entities
|
4
|
+
class Entity
|
5
|
+
|
6
|
+
attr_reader :ast, :parent
|
7
|
+
|
8
|
+
def initialize(parent, ast)
|
9
|
+
@parent = parent
|
10
|
+
@ast = ast
|
11
|
+
end
|
12
|
+
|
13
|
+
def handle_send_node(node)
|
14
|
+
# raise "Subclass must implement handle_send_node"
|
15
|
+
# abstract method. btw, this feels wrong -- send should be an entity too. but for now, whatevs.
|
16
|
+
end
|
17
|
+
|
18
|
+
def full_name
|
19
|
+
throw "Subclass must implement #full_name"
|
20
|
+
end
|
21
|
+
|
22
|
+
def inspect
|
23
|
+
"\#<#{self.class}:#{object_id} full_name=#{full_name}>"
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|