analyst 0.0.1 → 0.13.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (72) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +4 -1
  3. data/.rspec +2 -0
  4. data/CODE_OF_CONDUCT.md +13 -0
  5. data/README.md +1 -0
  6. data/analyst.gemspec +12 -0
  7. data/lib/analyst/analyzer.rb +162 -0
  8. data/lib/analyst/cli.rb +42 -0
  9. data/lib/analyst/entity_parser/association.rb +24 -0
  10. data/lib/analyst/entity_parser/entities/class.rb +92 -0
  11. data/lib/analyst/entity_parser/entities/empty.rb +13 -0
  12. data/lib/analyst/entity_parser/entities/entity.rb +29 -0
  13. data/lib/analyst/entity_parser/entities/method.rb +16 -0
  14. data/lib/analyst/entity_parser/entities/module.rb +31 -0
  15. data/lib/analyst/formatters/base.rb +33 -0
  16. data/lib/analyst/formatters/csv.rb +43 -0
  17. data/lib/analyst/formatters/html.rb +87 -0
  18. data/lib/analyst/formatters/html_index.rb +47 -0
  19. data/lib/analyst/formatters/templates/index.html.haml +92 -0
  20. data/lib/analyst/formatters/templates/output.html.haml +114 -0
  21. data/lib/analyst/formatters/text.rb +56 -0
  22. data/lib/analyst/fukuzatsu/analyzer.rb +162 -0
  23. data/lib/analyst/fukuzatsu/cli.rb +42 -0
  24. data/lib/analyst/fukuzatsu/entity_parser/association.rb +24 -0
  25. data/lib/analyst/fukuzatsu/entity_parser/entities/class.rb +92 -0
  26. data/lib/analyst/fukuzatsu/entity_parser/entities/empty.rb +13 -0
  27. data/lib/analyst/fukuzatsu/entity_parser/entities/entity.rb +29 -0
  28. data/lib/analyst/fukuzatsu/entity_parser/entities/method.rb +16 -0
  29. data/lib/analyst/fukuzatsu/entity_parser/entities/module.rb +31 -0
  30. data/lib/analyst/fukuzatsu/formatters/base.rb +33 -0
  31. data/lib/analyst/fukuzatsu/formatters/csv.rb +43 -0
  32. data/lib/analyst/fukuzatsu/formatters/html.rb +87 -0
  33. data/lib/analyst/fukuzatsu/formatters/html_index.rb +47 -0
  34. data/lib/analyst/fukuzatsu/formatters/templates/index.html.haml +92 -0
  35. data/lib/analyst/fukuzatsu/formatters/templates/output.html.haml +114 -0
  36. data/lib/analyst/fukuzatsu/formatters/text.rb +56 -0
  37. data/lib/analyst/fukuzatsu/line_of_code.rb +19 -0
  38. data/lib/analyst/fukuzatsu/parsed_file.rb +85 -0
  39. data/lib/analyst/fukuzatsu/parsed_method.rb +32 -0
  40. data/lib/analyst/fukuzatsu/parser_original.rb +76 -0
  41. data/lib/analyst/fukuzatsu/rethink/parser.rb +346 -0
  42. data/lib/analyst/fukuzatsu/version.rb +3 -0
  43. data/lib/analyst/line_of_code.rb +19 -0
  44. data/lib/analyst/parsed_file.rb +85 -0
  45. data/lib/analyst/parsed_method.rb +32 -0
  46. data/lib/analyst/parser.rb +76 -0
  47. data/lib/analyst/rethink/parser.rb +346 -0
  48. data/lib/analyst/version.rb +1 -1
  49. data/lib/analyst.rb +17 -2
  50. data/spec/analyzer_spec.rb +122 -0
  51. data/spec/cli_spec.rb +48 -0
  52. data/spec/fixtures/eg_class.rb +8 -0
  53. data/spec/fixtures/eg_mod_class.rb +2 -0
  54. data/spec/fixtures/eg_mod_class_2.rb +5 -0
  55. data/spec/fixtures/eg_module.rb +2 -0
  56. data/spec/fixtures/module_with_class.rb +9 -0
  57. data/spec/fixtures/multiple_methods.rb +7 -0
  58. data/spec/fixtures/nested_methods.rb +8 -0
  59. data/spec/fixtures/program_1.rb +19 -0
  60. data/spec/fixtures/program_2.rb +25 -0
  61. data/spec/fixtures/program_3.rb +66 -0
  62. data/spec/fixtures/program_4.rb +1 -0
  63. data/spec/fixtures/single_class.rb +9 -0
  64. data/spec/fixtures/single_method.rb +3 -0
  65. data/spec/formatters/csv_spec.rb +37 -0
  66. data/spec/formatters/html_index_spec.rb +36 -0
  67. data/spec/formatters/html_spec.rb +48 -0
  68. data/spec/formatters/text_spec.rb +39 -0
  69. data/spec/parsed_file_spec.rb +67 -0
  70. data/spec/parsed_method_spec.rb +34 -0
  71. data/spec/spec_helper.rb +7 -0
  72. 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,13 @@
1
+ module Analyst
2
+
3
+ module EntityParser
4
+ module Entities
5
+ class Empty < Entity
6
+ def full_name
7
+ ""
8
+ end
9
+ end
10
+ end
11
+ end
12
+
13
+ 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
@@ -0,0 +1,16 @@
1
+ module Analyst
2
+
3
+ module EntityParser
4
+ module Entities
5
+ class Method < Entity
6
+ def name
7
+ ast.children.first.to_s
8
+ end
9
+ def full_name
10
+ parent.full_name + '#' + name
11
+ end
12
+ end
13
+ end
14
+ end
15
+
16
+ end