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