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
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f928fc2a5aaf979b7d338ba06a0a588cd07cf98e
|
4
|
+
data.tar.gz: 3108a32bef9afb6a87a26023da2b7e5e751ab774
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c9a8c882c5e080ae9612f7531cbac8037c703b3ca0a73206c07db9de3e94c16021c41a7f62add65a854cefda1d2c8d8746bb72ea08125a3e84875864cb66f75c
|
7
|
+
data.tar.gz: d7d3a8615db9efe5c88471b7f30dc6eedb5bf3e5caff532f0ff689abc2621a67ac9e02c12309b03d3f941ba4280ca41ff2da7f6faed5c0164f16ec7673624d7e
|
data/.gitignore
CHANGED
data/.rspec
ADDED
data/CODE_OF_CONDUCT.md
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
# CODE OF CONDUCT
|
2
|
+
|
3
|
+
This project has adopted version 0.4 of the [Contributor Covenant](https://github.com/bantik/contributor_covenant/).
|
4
|
+
|
5
|
+
As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities.
|
6
|
+
|
7
|
+
If any participant in this project has issues or takes exception with a contribution, they are obligated to provide constructive feedback and never resort to personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct.
|
8
|
+
|
9
|
+
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team.
|
10
|
+
|
11
|
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers.
|
12
|
+
|
13
|
+
We promise to extend courtesy and respect to everyone involved in this project regardless of gender, gender identity, sexual orientation, ability or disability, ethnicity, religion, or level of experience.
|
data/README.md
CHANGED
@@ -21,6 +21,7 @@ Or install it yourself as:
|
|
21
21
|
TODO: Write usage instructions here
|
22
22
|
|
23
23
|
## Contributing
|
24
|
+
Please note that this project is released with a [Contributor Code of Conduct](https://gitlab.com/coraline/analyst/blob/master/CODE_OF_CONDUCT.md). By participating in this project you agree to abide by its terms.
|
24
25
|
|
25
26
|
1. Fork it ( https://github.com/[my-github-username]/analyst/fork )
|
26
27
|
2. Create your feature branch (`git checkout -b my-new-feature`)
|
data/analyst.gemspec
CHANGED
@@ -18,6 +18,18 @@ Gem::Specification.new do |spec|
|
|
18
18
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
19
|
spec.require_paths = ["lib"]
|
20
20
|
|
21
|
+
spec.add_dependency "ephemeral"
|
22
|
+
spec.add_dependency "poro_plus"
|
23
|
+
spec.add_dependency "haml"
|
24
|
+
spec.add_dependency "parser"
|
25
|
+
spec.add_dependency "rainbow"
|
26
|
+
spec.add_dependency "rouge"
|
27
|
+
spec.add_dependency "terminal-table"
|
28
|
+
spec.add_dependency "thor"
|
29
|
+
|
21
30
|
spec.add_development_dependency "bundler", "~> 1.6"
|
22
31
|
spec.add_development_dependency "rake"
|
32
|
+
spec.add_development_dependency "rspec"
|
33
|
+
spec.add_development_dependency "simplecov"
|
34
|
+
|
23
35
|
end
|
@@ -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
|
+
|
data/lib/analyst/cli.rb
ADDED
@@ -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
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Analyst
|
2
|
+
|
3
|
+
module EntityParser
|
4
|
+
module Entities
|
5
|
+
class Module < Entity
|
6
|
+
def name
|
7
|
+
const_node_array(ast.children.first).join('::')
|
8
|
+
end
|
9
|
+
def full_name
|
10
|
+
parent.full_name.empty? ? name : parent.full_name + '::' + name
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
# takes a (const) node and returns an array specifying the fully-qualified
|
16
|
+
# constant name that it represents. ya know, so CoolModule::SubMod::SweetClass
|
17
|
+
# would be parsed to:
|
18
|
+
# (const
|
19
|
+
# (const
|
20
|
+
# (const nil :CoolModule) :SubMod) :SweetClass)
|
21
|
+
# and passing that node here would return [:CoolModule, :SubMod, :SweetClass]
|
22
|
+
def const_node_array(node)
|
23
|
+
return [] if node.nil?
|
24
|
+
raise "expected (const) node or nil, got (#{node.type})" unless node.type == :const
|
25
|
+
const_node_array(node.children.first) << node.children[1]
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Formatters
|
2
|
+
|
3
|
+
module Base
|
4
|
+
|
5
|
+
def self.included(klass)
|
6
|
+
klass.send(:attr_accessor, :file)
|
7
|
+
klass.send(:attr_accessor, :source)
|
8
|
+
klass.send(:attr_accessor, :output_directory)
|
9
|
+
end
|
10
|
+
|
11
|
+
def initialize(file, output_directory=nil, source="")
|
12
|
+
self.file = file
|
13
|
+
self.source = source
|
14
|
+
self.output_directory = output_directory
|
15
|
+
end
|
16
|
+
|
17
|
+
def filename
|
18
|
+
File.basename(self.file.path_to_file) + file_extension
|
19
|
+
end
|
20
|
+
|
21
|
+
def output_path
|
22
|
+
output_path = File.dirname(File.join(self.output_directory, self.file.path_to_file))
|
23
|
+
FileUtils.mkpath(output_path)
|
24
|
+
output_path
|
25
|
+
end
|
26
|
+
|
27
|
+
def path_to_results
|
28
|
+
File.join(output_path, filename)
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module Formatters
|
2
|
+
|
3
|
+
class Csv
|
4
|
+
|
5
|
+
include Formatters::Base
|
6
|
+
|
7
|
+
def self.has_index?
|
8
|
+
false
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.writes_to_file_system?
|
12
|
+
true
|
13
|
+
end
|
14
|
+
|
15
|
+
def content
|
16
|
+
rows + "\r\n"
|
17
|
+
end
|
18
|
+
|
19
|
+
def export
|
20
|
+
begin
|
21
|
+
File.open(path_to_results, 'a') {|outfile| outfile.write(content)}
|
22
|
+
rescue Exception => e
|
23
|
+
puts "Unable to write output: #{e} #{e.backtrace}"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def file_extension
|
28
|
+
".csv"
|
29
|
+
end
|
30
|
+
|
31
|
+
def path_to_results
|
32
|
+
File.join(output_directory, "results#{file_extension}")
|
33
|
+
end
|
34
|
+
|
35
|
+
def rows
|
36
|
+
file.methods.map do |method|
|
37
|
+
"#{file.path_to_file},#{file.class_name},#{method.name},#{method.complexity}"
|
38
|
+
end.join("\r\n")
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
require 'rouge'
|
2
|
+
|
3
|
+
module Formatters
|
4
|
+
|
5
|
+
class Html
|
6
|
+
|
7
|
+
include Formatters::Base
|
8
|
+
|
9
|
+
def self.has_index?
|
10
|
+
true
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.writes_to_file_system?
|
14
|
+
true
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.index_class
|
18
|
+
Formatters::HtmlIndex
|
19
|
+
end
|
20
|
+
|
21
|
+
def columns
|
22
|
+
["class", "method", "complexity"]
|
23
|
+
end
|
24
|
+
|
25
|
+
def content
|
26
|
+
Haml::Engine.new(output_template).render(
|
27
|
+
Object.new, {
|
28
|
+
header: header,
|
29
|
+
rows: rows,
|
30
|
+
source_lines: preprocessed,
|
31
|
+
class_name: file.class_name,
|
32
|
+
complexity: file.complexity,
|
33
|
+
path_to_file: file.path_to_file,
|
34
|
+
date: Time.now.strftime("%Y/%m/%d"),
|
35
|
+
time: Time.now.strftime("%l:%M %P")
|
36
|
+
}
|
37
|
+
)
|
38
|
+
end
|
39
|
+
|
40
|
+
def export
|
41
|
+
begin
|
42
|
+
File.open(path_to_results, 'w') {|outfile| outfile.write(content)}
|
43
|
+
rescue Exception => e
|
44
|
+
puts "Unable to write output: #{e} #{e.backtrace}"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def formatter
|
49
|
+
Rouge::Formatters::HTML.new(line_numbers: true)
|
50
|
+
end
|
51
|
+
|
52
|
+
def header
|
53
|
+
columns.map{|col| "<th>#{col.titleize}</th>"}.join("\r\n")
|
54
|
+
end
|
55
|
+
|
56
|
+
def lexer
|
57
|
+
lexer = Rouge::Lexers::Ruby.new
|
58
|
+
end
|
59
|
+
|
60
|
+
def output_template
|
61
|
+
File.read(File.dirname(__FILE__) + "/templates/output.html.haml")
|
62
|
+
end
|
63
|
+
|
64
|
+
def preprocessed
|
65
|
+
formatter.format(lexer.lex(source))
|
66
|
+
end
|
67
|
+
|
68
|
+
def rows
|
69
|
+
i = 0
|
70
|
+
file.methods.inject([]) do |a, method|
|
71
|
+
i += 1
|
72
|
+
a << "<tr class='#{i % 2 == 1 ? 'even' : 'odd'}'>"
|
73
|
+
a << " <td>#{file.class_name}</td>"
|
74
|
+
a << " <td>#{method.name}</td>"
|
75
|
+
a << " <td>#{method.complexity}</td>"
|
76
|
+
a << "</tr>"
|
77
|
+
a
|
78
|
+
end.join("\r\n")
|
79
|
+
end
|
80
|
+
|
81
|
+
def file_extension
|
82
|
+
".htm"
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
86
|
+
|
87
|
+
end
|