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