p8-metric_fu 0.9.0.1 → 0.9.0.2

Sign up to get free protection for your applications and to get access to all the features.
data/README CHANGED
@@ -154,4 +154,4 @@ You can also change the minimum churn count like so:
154
154
 
155
155
  ****Thanks****
156
156
 
157
- I'd like to thank the authors of Saikuro, Flog, Rcov, CruiseControl.rb, Flay, Reek, Roodi and Rails for creating such excellent open source products. Also Andre Arko, Petrik de Heus, Sean Soper, Erik St Martin, Andy Gregorowicz, Bastien, Michael Schubert, Kurtis Seebaldt, Jacob Kjeldahl, Toby Tripp, Paul Gross, and Chirdeep Shetty for their help and advice.
157
+ I'd like to thank the authors of Saikuro, Flog, Rcov, CruiseControl.rb, Flay, Reek, Roodi and Rails for creating such excellent open source products. Also Andre Arko, Petrik de Heus, Sean Soper, Erik St Martin, Andy Gregorowicz, Bastien, Michael Schubert, Kurtis Seebaldt, Toby Tripp, Paul Gross, and Chirdeep Shetty for their help and advice.
data/Rakefile CHANGED
@@ -1,28 +1,11 @@
1
1
  require 'rake'
2
2
  require 'rake/rdoctask'
3
3
  require 'spec/rake/spectask'
4
- require 'lib/metric_fu'
4
+ require File.join(File.dirname(__FILE__), 'lib', 'metric_fu')
5
5
 
6
6
  desc "Run all specs in spec directory"
7
7
  Spec::Rake::SpecTask.new(:spec) do |t|
8
8
  t.spec_files = FileList['spec/**/*_spec.rb']
9
9
  end
10
10
 
11
- MetricFu::Configuration.run do |config|
12
- end
13
-
14
- namespace :metrics do
15
- desc "Generate all reports"
16
- task :all do
17
- MetricFu.metrics.each {|metric| MetricFu.report.add(metric) }
18
- MetricFu.report.save_output(MetricFu.report.to_yaml,
19
- MetricFu.base_directory,
20
- 'report.yml')
21
- MetricFu.report.save_templatized_report
22
- if MetricFu.report.open_in_browser?
23
- MetricFu.report.show_in_browser(MetricFu.output_directory)
24
- end
25
- end
26
- end
27
-
28
- task :default => [:"metrics:all"]
11
+ task :default => [:"metrics:all"]
data/TODO CHANGED
@@ -10,5 +10,4 @@
10
10
  * Generate metrics:* rake tasks for each of CodeMetric's descendants
11
11
  * Update flog specs so that they actually run flog
12
12
  * Add flay specs that run flay
13
- * Convert readme to markdown and rename to README.mkdn so github will render it
14
- * Saikuro.rb falls over if tries to parse an empty file. Fair enough. We shouldn't feed it empty files
13
+ * Convert readme to markdown and rename to README.mkdn so github will render it
@@ -1,17 +1,3 @@
1
- # Load a few things to make our lives easier elsewhere.
2
- module MetricFu
3
- LIB_ROOT = File.dirname(__FILE__)
4
- end
5
- base_dir = File.join(MetricFu::LIB_ROOT, 'base')
6
- generator_dir = File.join(MetricFu::LIB_ROOT, 'generators')
7
- template_dir = File.join(MetricFu::LIB_ROOT, 'templates')
8
-
9
- # We need to require these two things first because our other classes
10
- # depend on them.
11
- require File.join(base_dir, 'report')
12
- require File.join(base_dir, 'generator')
13
-
14
- # Now load everything else that's in the directory
15
- Dir[File.join(base_dir, '*.rb')].each{|l| require l }
16
- Dir[File.join(generator_dir, '*.rb')].each {|l| require l }
17
- Dir[File.join(template_dir, 'standard/*.rb')].each {|l| require l}
1
+ require File.join(File.dirname(__FILE__), 'metric_fu', 'base') #require first because of dependecies
2
+ require File.join(File.dirname(__FILE__), 'tasks', 'metric_fu')
3
+ Dir[File.join(File.dirname(__FILE__), 'metric_fu/*.rb')].each{|l| require l }
@@ -0,0 +1,88 @@
1
+ module MetricFu
2
+
3
+ def self.generate_churn_report
4
+ Churn.generate_report(MetricFu.churn)
5
+ system("open #{Churn.metric_dir}/index.html") if open_in_browser?
6
+ end
7
+
8
+ class Churn < Base::Generator
9
+
10
+ def initialize(options={})
11
+ @base_dir = File.join(MetricFu::BASE_DIRECTORY, template_name)
12
+ if File.exist?(".git")
13
+ @source_control = Git.new(options[:start_date])
14
+ elsif File.exist?(".svn")
15
+ @source_control = Svn.new(options[:start_date])
16
+ else
17
+ raise "Churning requires a subversion or git repo"
18
+ end
19
+
20
+ @minimum_churn_count = options[:minimum_churn_count] || 5
21
+ end
22
+
23
+ def analyze
24
+ @changes = parse_log_for_changes.reject! {|file, change_count| change_count < @minimum_churn_count}
25
+ end
26
+
27
+ private
28
+
29
+ def parse_log_for_changes
30
+ changes = {}
31
+
32
+ logs = @source_control.get_logs
33
+ logs.each do |line|
34
+ changes[line] ? changes[line] += 1 : changes[line] = 1
35
+ end
36
+ changes
37
+ end
38
+
39
+
40
+ class SourceControl
41
+ def initialize(start_date=nil)
42
+ @start_date = start_date
43
+ end
44
+
45
+ private
46
+ def require_rails_env
47
+ # not sure if the following works because active_support might only be in vendor/rails
48
+ # require 'activesupport'
49
+ require RAILS_ROOT + '/config/environment'
50
+ end
51
+ end
52
+
53
+ class Git < SourceControl
54
+ def get_logs
55
+ `git log #{date_range} --name-only --pretty=format:`.split(/\n/).reject{|line| line == ""}
56
+ end
57
+
58
+ private
59
+ def date_range
60
+ if @start_date
61
+ require_rails_env
62
+ "--after=#{@start_date.call.strftime('%Y-%m-%d')}"
63
+ end
64
+ end
65
+
66
+ end
67
+
68
+ class Svn < SourceControl
69
+ def get_logs
70
+ `svn log #{date_range} --verbose`.split(/\n/).map { |line| clean_up_svn_line(line) }.compact
71
+ end
72
+
73
+ private
74
+ def date_range
75
+ if @start_date
76
+ require_rails_env
77
+ "--revision {#{@start_date.call.strftime('%Y-%m-%d')}}:{#{Time.now.strftime('%Y-%m-%d')}}"
78
+ end
79
+ end
80
+
81
+ def clean_up_svn_line(line)
82
+ m = line.match(/\W*[A,M]\W+(\/.*)\b/)
83
+ m ? m[1] : nil
84
+ end
85
+ end
86
+
87
+ end
88
+ end
@@ -0,0 +1,17 @@
1
+ module MetricFu
2
+
3
+ def self.generate_flay_report
4
+ Flay.generate_report
5
+ system("open #{Flay.metric_dir}/index.html") if open_in_browser?
6
+ end
7
+
8
+ class Flay < Base::Generator
9
+
10
+ def analyze
11
+ files_to_flay = MetricFu.flay[:dirs_to_flay].map{|dir| Dir[File.join(dir, "**/*.rb")] }
12
+ output = `flay #{files_to_flay.join(" ")}`
13
+ @matches = output.chomp.split("\n\n").map{|m| m.split("\n ") }
14
+ end
15
+
16
+ end
17
+ end
@@ -0,0 +1,129 @@
1
+ module MetricFu
2
+
3
+ def self.generate_flog_report
4
+ Flog::Generator.generate_report
5
+ system("open #{Flog::Generator.metric_dir}/index.html") if open_in_browser?
6
+ end
7
+
8
+ module Flog
9
+ class Generator < Base::Generator
10
+ def generate_report
11
+ @base_dir = self.class.metric_dir
12
+ pages = []
13
+ flog_results.each do |filename|
14
+ page = Base.parse(open(filename, "r") { |f| f.read })
15
+ if page
16
+ page.filename = filename
17
+ pages << page
18
+ end
19
+ end
20
+ generate_pages(pages)
21
+ end
22
+
23
+ def generate_pages(pages)
24
+ pages.each do |page|
25
+ unless MetricFu::MD5Tracker.file_already_counted?(page.filename)
26
+ generate_page(page)
27
+ end
28
+ end
29
+ save_html(ERB.new(File.read(template_file)).result(binding))
30
+ end
31
+
32
+ def generate_page(page)
33
+ save_html(page.to_html, page.path)
34
+ end
35
+
36
+ def flog_results
37
+ Dir.glob("#{@base_dir}/**/*.txt")
38
+ end
39
+
40
+ def self.template_name
41
+ "flog"
42
+ end
43
+ end
44
+
45
+ SCORE_FORMAT = "%0.2f"
46
+
47
+ class Base
48
+ METHOD_LINE_REGEX = /([A-Za-z]+#.*):\s\((\d+\.\d+)\)/
49
+ OPERATOR_LINE_REGEX = /\s+(\d+\.\d+):\s(.*)$/
50
+
51
+ class << self
52
+
53
+ def parse(text)
54
+ score = text[/\w+ = (\d+\.\d+)/, 1]
55
+ return nil unless score
56
+ page = Page.new(score)
57
+
58
+ text.each_line do |method_line|
59
+ if match = method_line.match(METHOD_LINE_REGEX)
60
+ page.scanned_methods << ScannedMethod.new(match[1], match[2])
61
+ end
62
+
63
+ if match = method_line.match(OPERATOR_LINE_REGEX)
64
+ return if page.scanned_methods.empty?
65
+ page.scanned_methods.last.operators << Operator.new(match[1], match[2])
66
+ end
67
+ end
68
+ page
69
+ end
70
+
71
+ end
72
+ end
73
+
74
+ class Page < MetricFu::Base::Generator
75
+ attr_accessor :filename, :score, :scanned_methods
76
+
77
+ def initialize(score, scanned_methods = [])
78
+ @score = score.to_f
79
+ @scanned_methods = scanned_methods
80
+ end
81
+
82
+ def path
83
+ @path ||= File.basename(filename, ".txt") + '.html'
84
+ end
85
+
86
+ def to_html
87
+ ERB.new(File.read(template_file)).result(binding)
88
+ end
89
+
90
+ def average_score
91
+ return 0 if scanned_methods.length == 0
92
+ sum = 0
93
+ scanned_methods.each do |m|
94
+ sum += m.score
95
+ end
96
+ sum / scanned_methods.length
97
+ end
98
+
99
+ def highest_score
100
+ scanned_methods.inject(0) do |highest, m|
101
+ m.score > highest ? m.score : highest
102
+ end
103
+ end
104
+
105
+ def template_name
106
+ 'flog_page'
107
+ end
108
+ end
109
+
110
+ class Operator
111
+ attr_accessor :score, :operator
112
+
113
+ def initialize(score, operator)
114
+ @score = score.to_f
115
+ @operator = operator
116
+ end
117
+ end
118
+
119
+ class ScannedMethod
120
+ attr_accessor :name, :score, :operators
121
+
122
+ def initialize(name, score, operators = [])
123
+ @name = name
124
+ @score = score.to_f
125
+ @operators = operators
126
+ end
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,52 @@
1
+ require 'digest/md5'
2
+ require 'fileutils'
3
+
4
+ module MetricFu
5
+ class MD5Tracker
6
+
7
+ @@unchanged_md5s = []
8
+
9
+ class << self
10
+ def md5_dir(path_to_file, base_dir)
11
+ File.join(base_dir,
12
+ path_to_file.split('/')[0..-2].join('/'))
13
+ end
14
+
15
+ def md5_file(path_to_file, base_dir)
16
+ File.join(md5_dir(path_to_file, base_dir),
17
+ path_to_file.split('/').last.sub(/\.[a-z]+/, '.md5'))
18
+ end
19
+
20
+ def track(path_to_file, base_dir)
21
+ md5 = Digest::MD5.hexdigest(File.read(path_to_file))
22
+ FileUtils.mkdir_p(md5_dir(path_to_file, base_dir), :verbose => false)
23
+ f = File.new(md5_file(path_to_file, base_dir), "w")
24
+ f.puts(md5)
25
+ f.close
26
+ md5
27
+ end
28
+
29
+ def file_changed?(path_to_file, base_dir)
30
+ orig_md5_file = md5_file(path_to_file, base_dir)
31
+ return !!track(path_to_file, base_dir) unless File.exist?(orig_md5_file)
32
+
33
+ current_md5 = ""
34
+ file = File.open(orig_md5_file, 'r')
35
+ file.each_line { |line| current_md5 << line }
36
+ file.close
37
+ current_md5.chomp!
38
+
39
+ new_md5 = Digest::MD5.hexdigest(File.read(path_to_file))
40
+ new_md5.chomp!
41
+
42
+ @@unchanged_md5s << path_to_file if new_md5 == current_md5
43
+
44
+ return new_md5 != current_md5
45
+ end
46
+
47
+ def file_already_counted?(path_to_file)
48
+ return @@unchanged_md5s.include?(path_to_file)
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,1214 @@
1
+ # $Id: saikuro 33 2006-12-07 16:09:55Z zev $
2
+ # Version 0.2
3
+ # == Usage
4
+ #
5
+ # ruby saikuro.rb [ -h ] [-o output_directory] [-f type] [ -c, -t ]
6
+ # [ -y, -w, -e, -k, -s, -d - number ] ( -p file | -i directory )
7
+ #
8
+ # == Help
9
+ #
10
+ # -o, --output_directory (directory) : A directory to ouput the results in.
11
+ # The current directory is used if this option is not passed.
12
+ #
13
+ # -h, --help : This help message.
14
+ #
15
+ # -f, --formater (html | text) : The format to output the results in.
16
+ # The default is html
17
+ #
18
+ # -c, --cyclo : Compute the cyclomatic complexity of the input.
19
+ #
20
+ # -t, --token : Count the number of tokens per line of the input.
21
+ #
22
+ # -y, --filter_cyclo (number) : Filter the output to only include methods
23
+ # whose cyclomatic complexity are greater than the passed number.
24
+ #
25
+ # -w, --warn_cyclo (number) : Highlight with a warning methods whose
26
+ # cyclomatic complexity are greather than or equal to the passed number.
27
+ #
28
+ #
29
+ # -e, --error_cyclo (number) : Highligh with an error methods whose
30
+ # cyclomatic complexity are greather than or equal to the passed number.
31
+ #
32
+ #
33
+ # -k, --filter_token (number) : Filter the output to only include lines
34
+ # whose token count are greater than the passed number.
35
+ #
36
+ #
37
+ # -s, --warn_token (number) : Highlight with a warning lines whose
38
+ # token count are greater than or equal to the passed number.
39
+ #
40
+ #
41
+ # -d, --error_token (number) : Highlight with an error lines whose
42
+ # token count are greater than or equal to the passed number.
43
+ #
44
+ #
45
+ # -p, --parse_file (file) : A file to use as input.
46
+ #
47
+ # -i, --input_directory (directory) : All ruby files found recursively
48
+ # inside the directory are passed as input.
49
+
50
+ # == License
51
+ # Saikruo uses the BSD license.
52
+ #
53
+ # Copyright (c) 2005, Ubiquitous Business Technology (http://ubit.com)
54
+ # All rights reserved.
55
+ #
56
+ # Redistribution and use in source and binary forms, with or without
57
+ # modification, are permitted provided that the following conditions are
58
+ # met:
59
+ #
60
+ #
61
+ # * Redistributions of source code must retain the above copyright
62
+ # notice, this list of conditions and the following disclaimer.
63
+ #
64
+ # * Redistributions in binary form must reproduce the above
65
+ # copyright notice, this list of conditions and the following
66
+ # disclaimer in the documentation and/or other materials provided
67
+ # with the distribution.
68
+ #
69
+ # * Neither the name of Ubiquitous Business Technology nor the names
70
+ # of its contributors may be used to endorse or promote products
71
+ # derived from this software without specific prior written
72
+ # permission.
73
+ #
74
+ #
75
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
76
+ # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
77
+ # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
78
+ # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
79
+ # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
80
+ # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
81
+ # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
82
+ # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
83
+ # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
84
+ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
85
+ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
86
+ #
87
+ # == Author
88
+ # Zev Blut (zb@ubit.com)
89
+
90
+ require 'irb/ruby-lex'
91
+ require 'yaml'
92
+
93
+ # States to watch for
94
+ # once in def get the token after space, because it may also
95
+ # be something like + or << for operator overloading.
96
+
97
+ # Counts the number of tokens in each line.
98
+ class TokenCounter
99
+ include RubyToken
100
+
101
+ attr_reader :current_file
102
+
103
+ def initialize
104
+ @files = Hash.new
105
+ @tokens_per_line = Hash.new(0)
106
+ @current_file = ""
107
+ end
108
+
109
+ # Mark file to associate with the token count.
110
+ def set_current_file(file)
111
+ @current_file = file
112
+ @tokens_per_line = Hash.new(0)
113
+ @files[@current_file] = @tokens_per_line
114
+ end
115
+
116
+ # Iterate through all tracked files, passing the
117
+ # the provided formater the token counts.
118
+ def list_tokens_per_line(formater)
119
+ formater.start_count(@files.size)
120
+ @files.each do |fname, tok_per_line|
121
+ formater.start_file(fname)
122
+ tok_per_line.sort.each do |line,num|
123
+ formater.line_token_count(line,num)
124
+ end
125
+ formater.end_file
126
+ end
127
+ end
128
+
129
+ # Count the token for the passed line.
130
+ def count_token(line_no,token)
131
+ case token
132
+ when TkSPACE, TkNL, TkRD_COMMENT
133
+ # Do not count these as tokens
134
+ when TkCOMMENT
135
+ # Ignore this only for comments in a statement?
136
+ # Ignore TkCOLON,TkCOLON2 and operators? like "." etc..
137
+ when TkRBRACK, TkRPAREN, TkRBRACE
138
+ # Ignore the closing of an array/index/hash/paren
139
+ # The opening is counted, but no more.
140
+ # Thus [], () {} is counted as 1 token not 2.
141
+ else
142
+ # may want to filter out comments...
143
+ @tokens_per_line[line_no] += 1
144
+ end
145
+ end
146
+
147
+ end
148
+
149
+ # Main class and structure used to compute the
150
+ # cyclomatic complexity of Ruby programs.
151
+ class ParseState
152
+ include RubyToken
153
+ attr_accessor :name, :children, :complexity, :parent, :lines
154
+
155
+ @@top_state = nil
156
+ def ParseState.make_top_state()
157
+ @@top_state = ParseState.new(nil)
158
+ @@top_state.name = "__top__"
159
+ @@top_state
160
+ end
161
+
162
+ @@token_counter = TokenCounter.new
163
+ def ParseState.set_token_counter(counter)
164
+ @@token_counter = counter
165
+ end
166
+ def ParseState.get_token_counter
167
+ @@token_counter
168
+ end
169
+
170
+ def initialize(lexer,parent=nil)
171
+ @name = ""
172
+ @children = Array.new
173
+ @complexity = 0
174
+ @parent = parent
175
+ @lexer = lexer
176
+ @run = true
177
+ # To catch one line def statements, We always have one line.
178
+ @lines = 0
179
+ @last_token_line_and_char = Array.new
180
+ end
181
+
182
+ def top_state?
183
+ self == @@top_state
184
+ end
185
+
186
+ def lexer=(lexer)
187
+ @run = true
188
+ @lexer = lexer
189
+ end
190
+
191
+ def make_state(type,parent = nil)
192
+ cstate = type.new(@lexer,self)
193
+ parent.children<< cstate
194
+ cstate
195
+ end
196
+
197
+ def calc_complexity
198
+ complexity = @complexity
199
+ children.each do |child|
200
+ complexity += child.calc_complexity
201
+ end
202
+ complexity
203
+ end
204
+
205
+ def calc_lines
206
+ lines = @lines
207
+ children.each do |child|
208
+ lines += child.calc_lines
209
+ end
210
+ lines
211
+ end
212
+
213
+ def compute_state(formater)
214
+ if top_state?
215
+ compute_state_for_global(formater)
216
+ end
217
+
218
+ @children.each do |s|
219
+ s.compute_state(formater)
220
+ end
221
+ end
222
+
223
+ def compute_state_for_global(formater)
224
+ global_def, @children = @children.partition do |s|
225
+ !s.kind_of?(ParseClass)
226
+ end
227
+ return if global_def.empty?
228
+ gx = global_def.inject(0) { |c,s| s.calc_complexity }
229
+ gl = global_def.inject(0) { |c,s| s.calc_lines }
230
+ formater.start_class_compute_state("Global", "", gx, gl)
231
+ global_def.each do |s|
232
+ s.compute_state(formater)
233
+ end
234
+ formater.end_class_compute_state("")
235
+ end
236
+
237
+ # Count the tokens parsed if true else ignore them.
238
+ def count_tokens?
239
+ true
240
+ end
241
+
242
+ def parse
243
+ while @run do
244
+ tok = @lexer.token
245
+ @run = false if tok.nil?
246
+ if lexer_loop?(tok)
247
+ STDERR.puts "Lexer loop at line : #{@lexer.line_no} char #{@lexer.char_no}."
248
+ @run = false
249
+ end
250
+ @last_token_line_and_char<< [@lexer.line_no.to_i, @lexer.char_no.to_i, tok]
251
+ if $VERBOSE
252
+ puts "DEBUG: #{@lexer.line_no} #{tok.class}:#{tok.name if tok.respond_to?(:name)}"
253
+ end
254
+ @@token_counter.count_token(@lexer.line_no, tok) if count_tokens?
255
+ parse_token(tok)
256
+ end
257
+ end
258
+
259
+ # Ruby-Lexer can go into a loop if the file does not end with a newline.
260
+ def lexer_loop?(token)
261
+ return false if @last_token_line_and_char.empty?
262
+ loop_flag = false
263
+ last = @last_token_line_and_char.last
264
+ line = last[0]
265
+ char = last[1]
266
+ ltok = last[2]
267
+
268
+ if ( (line == @lexer.line_no.to_i) &&
269
+ (char == @lexer.char_no.to_i) &&
270
+ (ltok.class == token.class) )
271
+ # We are potentially in a loop
272
+ if @last_token_line_and_char.size >= 3
273
+ loop_flag = true
274
+ end
275
+ else
276
+ # Not in a loop so clear stack
277
+ @last_token_line_and_char = Array.new
278
+ end
279
+
280
+ loop_flag
281
+ end
282
+
283
+ def do_begin_token(token)
284
+ make_state(EndableParseState, self)
285
+ end
286
+
287
+ def do_class_token(token)
288
+ make_state(ParseClass,self)
289
+ end
290
+
291
+ def do_module_token(token)
292
+ make_state(ParseModule,self)
293
+ end
294
+
295
+ def do_def_token(token)
296
+ make_state(ParseDef,self)
297
+ end
298
+
299
+ def do_constant_token(token)
300
+ nil
301
+ end
302
+
303
+ def do_identifier_token(token)
304
+ if (token.name == "__END__" && token.char_no.to_i == 0)
305
+ # The Ruby code has stopped and the rest is data so cease parsing.
306
+ @run = false
307
+ end
308
+ nil
309
+ end
310
+
311
+ def do_right_brace_token(token)
312
+ nil
313
+ end
314
+
315
+ def do_end_token(token)
316
+ end_debug
317
+ nil
318
+ end
319
+
320
+ def do_block_token(token)
321
+ make_state(ParseBlock,self)
322
+ end
323
+
324
+ def do_conditional_token(token)
325
+ make_state(ParseCond,self)
326
+ end
327
+
328
+ def do_conditional_do_control_token(token)
329
+ make_state(ParseDoCond,self)
330
+ end
331
+
332
+ def do_case_token(token)
333
+ make_state(EndableParseState, self)
334
+ end
335
+
336
+ def do_one_line_conditional_token(token)
337
+ # This is an if with no end
338
+ @complexity += 1
339
+ #STDOUT.puts "got IF_MOD: #{self.to_yaml}" if $VERBOSE
340
+ #if state.type != "class" && state.type != "def" && state.type != "cond"
341
+ #STDOUT.puts "Changing IF_MOD Parent" if $VERBOSE
342
+ #state = state.parent
343
+ #@run = false
344
+ nil
345
+ end
346
+
347
+ def do_else_token(token)
348
+ STDOUT.puts "Ignored/Unknown Token:#{token.class}" if $VERBOSE
349
+ nil
350
+ end
351
+
352
+ def do_comment_token(token)
353
+ make_state(ParseComment, self)
354
+ end
355
+
356
+ def do_symbol_token(token)
357
+ make_state(ParseSymbol, self)
358
+ end
359
+
360
+ def parse_token(token)
361
+ state = nil
362
+ case token
363
+ when TkCLASS
364
+ state = do_class_token(token)
365
+ when TkMODULE
366
+ state = do_module_token(token)
367
+ when TkDEF
368
+ state = do_def_token(token)
369
+ when TkCONSTANT
370
+ # Nothing to do with a constant at top level?
371
+ state = do_constant_token(token)
372
+ when TkIDENTIFIER,TkFID
373
+ # Nothing to do at top level?
374
+ state = do_identifier_token(token)
375
+ when TkRBRACE
376
+ # Nothing to do at top level
377
+ state = do_right_brace_token(token)
378
+ when TkEND
379
+ state = do_end_token(token)
380
+ # At top level this might be an error...
381
+ when TkDO,TkfLBRACE
382
+ state = do_block_token(token)
383
+ when TkIF,TkUNLESS
384
+ state = do_conditional_token(token)
385
+ when TkWHILE,TkUNTIL,TkFOR
386
+ state = do_conditional_do_control_token(token)
387
+ when TkELSIF #,TkELSE
388
+ @complexity += 1
389
+ when TkELSE
390
+ # Else does not increase complexity
391
+ when TkCASE
392
+ state = do_case_token(token)
393
+ when TkWHEN
394
+ @complexity += 1
395
+ when TkBEGIN
396
+ state = do_begin_token(token)
397
+ when TkRESCUE
398
+ # Maybe this should add complexity and not begin
399
+ @complexity += 1
400
+ when TkIF_MOD, TkUNLESS_MOD, TkUNTIL_MOD, TkWHILE_MOD, TkQUESTION
401
+ state = do_one_line_conditional_token(token)
402
+ when TkNL
403
+ #
404
+ @lines += 1
405
+ when TkRETURN
406
+ # Early returns do not increase complexity as the condition that
407
+ # calls the return is the one that increases it.
408
+ when TkCOMMENT
409
+ state = do_comment_token(token)
410
+ when TkSYMBEG
411
+ state = do_symbol_token(token)
412
+ when TkError
413
+ STDOUT.puts "Lexer received an error for line #{@lexer.line_no} char #{@lexer.char_no}"
414
+ else
415
+ state = do_else_token(token)
416
+ end
417
+ state.parse if state
418
+ end
419
+
420
+ def end_debug
421
+ STDOUT.puts "got an end: #{@name} in #{self.class.name}" if $VERBOSE
422
+ if @parent.nil?
423
+ STDOUT.puts "DEBUG: Line #{@lexer.line_no}"
424
+ STDOUT.puts "DEBUG: #{@name}; #{self.class}"
425
+ # to_yaml can cause an infinite loop?
426
+ #STDOUT.puts "TOP: #{@@top_state.to_yaml}"
427
+ #STDOUT.puts "TOP: #{@@top_state.inspect}"
428
+
429
+ # This may not be an error?
430
+ #exit 1
431
+ end
432
+ end
433
+
434
+ end
435
+
436
+ # Read and consume tokens in comments until a new line.
437
+ class ParseComment < ParseState
438
+
439
+ # While in a comment state do not count the tokens.
440
+ def count_tokens?
441
+ false
442
+ end
443
+
444
+ def parse_token(token)
445
+ if token.is_a?(TkNL)
446
+ @lines += 1
447
+ @run = false
448
+ end
449
+ end
450
+ end
451
+
452
+ class ParseSymbol < ParseState
453
+ def initialize(lexer, parent = nil)
454
+ super
455
+ STDOUT.puts "STARTING SYMBOL" if $VERBOSE
456
+ end
457
+
458
+ def parse_token(token)
459
+ STDOUT.puts "Symbol's token is #{token.class}" if $VERBOSE
460
+ # Consume the next token and stop
461
+ @run = false
462
+ nil
463
+ end
464
+ end
465
+
466
+ class EndableParseState < ParseState
467
+ def initialize(lexer,parent=nil)
468
+ super(lexer,parent)
469
+ STDOUT.puts "Starting #{self.class}" if $VERBOSE
470
+ end
471
+
472
+ def do_end_token(token)
473
+ end_debug
474
+ @run = false
475
+ nil
476
+ end
477
+ end
478
+
479
+ class ParseClass < EndableParseState
480
+ def initialize(lexer,parent=nil)
481
+ super(lexer,parent)
482
+ @type_name = "Class"
483
+ end
484
+
485
+ def do_constant_token(token)
486
+ @name = token.name if @name.empty?
487
+ nil
488
+ end
489
+
490
+ def compute_state(formater)
491
+ # Seperate the Module and Class Children out
492
+ cnm_children, @children = @children.partition do |child|
493
+ child.kind_of?(ParseClass)
494
+ end
495
+
496
+ formater.start_class_compute_state(@type_name,@name,self.calc_complexity,self.calc_lines)
497
+ super(formater)
498
+ formater.end_class_compute_state(@name)
499
+
500
+ cnm_children.each do |child|
501
+ child.name = @name + "::" + child.name
502
+ child.compute_state(formater)
503
+ end
504
+ end
505
+ end
506
+
507
+ class ParseModule < ParseClass
508
+ def initialize(lexer,parent=nil)
509
+ super(lexer,parent)
510
+ @type_name = "Module"
511
+ end
512
+ end
513
+
514
+ class ParseDef < EndableParseState
515
+
516
+ def initialize(lexer,parent=nil)
517
+ super(lexer,parent)
518
+ @complexity = 1
519
+ @looking_for_name = true
520
+ @first_space = true
521
+ end
522
+
523
+ # This way I don't need to list all possible overload
524
+ # tokens.
525
+ def create_def_name(token)
526
+ case token
527
+ when TkSPACE
528
+ # mark first space so we can stop at next space
529
+ if @first_space
530
+ @first_space = false
531
+ else
532
+ @looking_for_name = false
533
+ end
534
+ when TkNL,TkLPAREN,TkfLPAREN,TkSEMICOLON
535
+ # we can also stop at a new line or left parenthesis
536
+ @looking_for_name = false
537
+ when TkDOT
538
+ @name<< "."
539
+ when TkCOLON2
540
+ @name<< "::"
541
+ when TkASSIGN
542
+ @name<< "="
543
+ when TkfLBRACK
544
+ @name<< "["
545
+ when TkRBRACK
546
+ @name<< "]"
547
+ else
548
+ begin
549
+ @name<< token.name.to_s
550
+ rescue Exception => err
551
+ #what is this?
552
+ STDOUT.puts @@token_counter.current_file
553
+ STDOUT.puts @name
554
+ STDOUT.puts token.inspect
555
+ STDOUT.puts err.message
556
+ exit 1
557
+ end
558
+ end
559
+ end
560
+
561
+ def parse_token(token)
562
+ if @looking_for_name
563
+ create_def_name(token)
564
+ end
565
+ super(token)
566
+ end
567
+
568
+ def compute_state(formater)
569
+ formater.def_compute_state(@name, self.calc_complexity, self.calc_lines)
570
+ super(formater)
571
+ end
572
+ end
573
+
574
+ class ParseCond < EndableParseState
575
+ def initialize(lexer,parent=nil)
576
+ super(lexer,parent)
577
+ @complexity = 1
578
+ end
579
+ end
580
+
581
+ class ParseDoCond < ParseCond
582
+ def initialize(lexer,parent=nil)
583
+ super(lexer,parent)
584
+ @looking_for_new_line = true
585
+ end
586
+
587
+ # Need to consume the do that can appear at the
588
+ # end of these control structures.
589
+ def parse_token(token)
590
+ if @looking_for_new_line
591
+ if token.is_a?(TkDO)
592
+ nil
593
+ else
594
+ if token.is_a?(TkNL)
595
+ @looking_for_new_line = false
596
+ end
597
+ super(token)
598
+ end
599
+ else
600
+ super(token)
601
+ end
602
+ end
603
+
604
+ end
605
+
606
+ class ParseBlock < EndableParseState
607
+
608
+ def initialize(lexer,parent=nil)
609
+ super(lexer,parent)
610
+ @complexity = 1
611
+ @lbraces = Array.new
612
+ end
613
+
614
+ # Because the token for a block and hash right brace is the same,
615
+ # we need to track the hash left braces to determine when an end is
616
+ # encountered.
617
+ def parse_token(token)
618
+ if token.is_a?(TkLBRACE)
619
+ @lbraces.push(true)
620
+ elsif token.is_a?(TkRBRACE)
621
+ if @lbraces.empty?
622
+ do_right_brace_token(token)
623
+ #do_end_token(token)
624
+ else
625
+ @lbraces.pop
626
+ end
627
+ else
628
+ super(token)
629
+ end
630
+ end
631
+
632
+ def do_right_brace_token(token)
633
+ # we are done ? what about a hash in a block :-/
634
+ @run = false
635
+ nil
636
+ end
637
+
638
+ end
639
+
640
+ # ------------ END Analyzer logic ------------------------------------
641
+
642
+ class Filter
643
+ attr_accessor :limit, :error, :warn
644
+
645
+ def initialize(limit = -1, error = 11, warn = 8)
646
+ @limit = limit
647
+ @error = error
648
+ @warn = warn
649
+ end
650
+
651
+ def ignore?(count)
652
+ count < @limit
653
+ end
654
+
655
+ def warn?(count)
656
+ count >= @warn
657
+ end
658
+
659
+ def error?(count)
660
+ count >= @error
661
+ end
662
+
663
+ end
664
+
665
+
666
+ class BaseFormater
667
+ attr_accessor :warnings, :errors, :current
668
+
669
+ def initialize(out, filter = nil)
670
+ @out = out
671
+ @filter = filter
672
+ reset_data
673
+ end
674
+
675
+ def warn_error?(num, marker)
676
+ klass = ""
677
+
678
+ if @filter.error?(num)
679
+ klass = ' class="error"'
680
+ @errors<< [@current, marker, num]
681
+ elsif @filter.warn?(num)
682
+ klass = ' class="warning"'
683
+ @warnings<< [@current, marker, num]
684
+ end
685
+
686
+ klass
687
+ end
688
+
689
+ def reset_data
690
+ @warnings = Array.new
691
+ @errors = Array.new
692
+ @current = ""
693
+ end
694
+
695
+ end
696
+
697
+ class TokenCounterFormater < BaseFormater
698
+
699
+ def start(new_out=nil)
700
+ reset_data
701
+ @out = new_out if new_out
702
+ @out.puts "Token Count"
703
+ end
704
+
705
+ def start_count(number_of_files)
706
+ @out.puts "Counting tokens for #{number_of_files} files."
707
+ end
708
+
709
+ def start_file(file_name)
710
+ @current = file_name
711
+ @out.puts "File:#{file_name}"
712
+ end
713
+
714
+ def line_token_count(line_number,number_of_tokens)
715
+ return if @filter.ignore?(number_of_tokens)
716
+ warn_error?(number_of_tokens, line_number)
717
+ @out.puts "Line:#{line_number} ; Tokens : #{number_of_tokens}"
718
+ end
719
+
720
+ def end_file
721
+ @out.puts ""
722
+ end
723
+
724
+ def end_count
725
+ end
726
+
727
+ def end
728
+ end
729
+
730
+ end
731
+
732
+ module HTMLStyleSheet
733
+ def HTMLStyleSheet.style_sheet
734
+ out = StringIO.new
735
+
736
+ out.puts "<style>"
737
+ out.puts 'body {'
738
+ out.puts ' margin: 20px;'
739
+ out.puts ' padding: 0;'
740
+ out.puts ' font-size: 12px;'
741
+ out.puts ' font-family: bitstream vera sans, verdana, arial, sans serif;'
742
+ out.puts ' background-color: #efefef;'
743
+ out.puts '}'
744
+ out.puts ''
745
+ out.puts 'table { '
746
+ out.puts ' border-collapse: collapse;'
747
+ out.puts ' /*border-spacing: 0;*/'
748
+ out.puts ' border: 1px solid #666;'
749
+ out.puts ' background-color: #fff;'
750
+ out.puts ' margin-bottom: 20px;'
751
+ out.puts '}'
752
+ out.puts ''
753
+ out.puts 'table, th, th+th, td, td+td {'
754
+ out.puts ' border: 1px solid #ccc;'
755
+ out.puts '}'
756
+ out.puts ''
757
+ out.puts 'table th {'
758
+ out.puts ' font-size: 12px;'
759
+ out.puts ' color: #fc0;'
760
+ out.puts ' padding: 4px 0;'
761
+ out.puts ' background-color: #336;'
762
+ out.puts '}'
763
+ out.puts ''
764
+ out.puts 'th, td {'
765
+ out.puts ' padding: 4px 10px;'
766
+ out.puts '}'
767
+ out.puts ''
768
+ out.puts 'td { '
769
+ out.puts ' font-size: 13px;'
770
+ out.puts '}'
771
+ out.puts ''
772
+ out.puts '.class_name {'
773
+ out.puts ' font-size: 17px;'
774
+ out.puts ' margin: 20px 0 0;'
775
+ out.puts '}'
776
+ out.puts ''
777
+ out.puts '.class_complexity {'
778
+ out.puts 'margin: 0 auto;'
779
+ out.puts '}'
780
+ out.puts ''
781
+ out.puts '.class_complexity>.class_complexity {'
782
+ out.puts ' margin: 0;'
783
+ out.puts '}'
784
+ out.puts ''
785
+ out.puts '.class_total_complexity, .class_total_lines, .start_token_count, .file_count {'
786
+ out.puts ' font-size: 13px;'
787
+ out.puts ' font-weight: bold;'
788
+ out.puts '}'
789
+ out.puts ''
790
+ out.puts '.class_total_complexity, .class_total_lines {'
791
+ out.puts ' color: #c00;'
792
+ out.puts '}'
793
+ out.puts ''
794
+ out.puts '.start_token_count, .file_count {'
795
+ out.puts ' color: #333;'
796
+ out.puts '}'
797
+ out.puts ''
798
+ out.puts '.warning {'
799
+ out.puts ' background-color: yellow;'
800
+ out.puts '}'
801
+ out.puts ''
802
+ out.puts '.error {'
803
+ out.puts ' background-color: #f00;'
804
+ out.puts '}'
805
+ out.puts "</style>"
806
+
807
+ out.string
808
+ end
809
+
810
+ def style_sheet
811
+ HTMLStyleSheet.style_sheet
812
+ end
813
+ end
814
+
815
+
816
+ class HTMLTokenCounterFormater < TokenCounterFormater
817
+ include HTMLStyleSheet
818
+
819
+ def start(new_out=nil)
820
+ reset_data
821
+ @out = new_out if new_out
822
+ @out.puts "<html>"
823
+ @out.puts style_sheet
824
+ @out.puts "<body>"
825
+ end
826
+
827
+ def start_count(number_of_files)
828
+ @out.puts "<div class=\"start_token_count\">"
829
+ @out.puts "Number of files: #{number_of_files}"
830
+ @out.puts "</div>"
831
+ end
832
+
833
+ def start_file(file_name)
834
+ @current = file_name
835
+ @out.puts "<div class=\"file_count\">"
836
+ @out.puts "<p class=\"file_name\">"
837
+ @out.puts "File: #{file_name}"
838
+ @out.puts "</p>"
839
+ @out.puts "<table width=\"100%\" border=\"1\">"
840
+ @out.puts "<tr><th>Line</th><th>Tokens</th></tr>"
841
+ end
842
+
843
+ def line_token_count(line_number,number_of_tokens)
844
+ return if @filter.ignore?(number_of_tokens)
845
+ klass = warn_error?(number_of_tokens, line_number)
846
+ @out.puts "<tr><td>#{line_number}</td><td#{klass}>#{number_of_tokens}</td></tr>"
847
+ end
848
+
849
+ def end_file
850
+ @out.puts "</table>"
851
+ end
852
+
853
+ def end_count
854
+ end
855
+
856
+ def end
857
+ @out.puts "</body>"
858
+ @out.puts "</html>"
859
+ end
860
+ end
861
+
862
+ class ParseStateFormater < BaseFormater
863
+
864
+ def start(new_out=nil)
865
+ reset_data
866
+ @out = new_out if new_out
867
+ end
868
+
869
+ def end
870
+ end
871
+
872
+ def start_class_compute_state(type_name,name,complexity,lines)
873
+ @current = name
874
+ @out.puts "-- START #{name} --"
875
+ @out.puts "Type:#{type_name} Name:#{name} Complexity:#{complexity} Lines:#{lines}"
876
+ end
877
+
878
+ def end_class_compute_state(name)
879
+ @out.puts "-- END #{name} --"
880
+ end
881
+
882
+ def def_compute_state(name,complexity,lines)
883
+ return if @filter.ignore?(complexity)
884
+ warn_error?(complexity, name)
885
+ @out.puts "Type:Def Name:#{name} Complexity:#{complexity} Lines:#{lines}"
886
+ end
887
+
888
+ end
889
+
890
+
891
+
892
+ class StateHTMLComplexityFormater < ParseStateFormater
893
+ include HTMLStyleSheet
894
+
895
+ def start(new_out=nil)
896
+ reset_data
897
+ @out = new_out if new_out
898
+ @out.puts "<html><head><title>Cyclometric Complexity</title></head>"
899
+ @out.puts style_sheet
900
+ @out.puts "<body>"
901
+ end
902
+
903
+ def end
904
+ @out.puts "</body>"
905
+ @out.puts "</html>"
906
+ end
907
+
908
+ def start_class_compute_state(type_name,name,complexity,lines)
909
+ @current = name
910
+ @out.puts "<div class=\"class_complexity\">"
911
+ @out.puts "<h2 class=\"class_name\">#{type_name} : #{name}</h2>"
912
+ @out.puts "<div class=\"class_total_complexity\">Total Complexity: #{complexity}</div>"
913
+ @out.puts "<div class=\"class_total_lines\">Total Lines: #{lines}</div>"
914
+ @out.puts "<table width=\"100%\" border=\"1\">"
915
+ @out.puts "<tr><th>Method</th><th>Complexity</th><th># Lines</th></tr>"
916
+ end
917
+
918
+ def end_class_compute_state(name)
919
+ @out.puts "</table>"
920
+ @out.puts "</div>"
921
+ end
922
+
923
+ def def_compute_state(name, complexity, lines)
924
+ return if @filter.ignore?(complexity)
925
+ klass = warn_error?(complexity, name)
926
+ @out.puts "<tr><td>#{name}</td><td#{klass}>#{complexity}</td><td>#{lines}</td></tr>"
927
+ end
928
+
929
+ end
930
+
931
+
932
+ module ResultIndexGenerator
933
+ def summarize_errors_and_warnings(enw, header)
934
+ return "" if enw.empty?
935
+ f = StringIO.new
936
+ erval = Hash.new { |h,k| h[k] = Array.new }
937
+ wval = Hash.new { |h,k| h[k] = Array.new }
938
+
939
+ enw.each do |fname, warnings, errors|
940
+ errors.each do |c,m,v|
941
+ erval[v] << [fname, c, m]
942
+ end
943
+ warnings.each do |c,m,v|
944
+ wval[v] << [fname, c, m]
945
+ end
946
+ end
947
+
948
+ f.puts "<h2 class=\"class_name\">Errors and Warnings</h2>"
949
+ f.puts "<table width=\"100%\" border=\"1\">"
950
+ f.puts header
951
+
952
+ f.puts print_summary_table_rows(erval, "error")
953
+ f.puts print_summary_table_rows(wval, "warning")
954
+ f.puts "</table>"
955
+
956
+ f.string
957
+ end
958
+
959
+ def print_summary_table_rows(ewvals, klass_type)
960
+ f = StringIO.new
961
+ ewvals.sort { |a,b| b <=> a}.each do |v, vals|
962
+ vals.sort.each do |fname, c, m|
963
+ f.puts "<tr><td><a href=\"./#{fname}\">#{c}</a></td><td>#{m}</td>"
964
+ f.puts "<td class=\"#{klass_type}\">#{v}</td></tr>"
965
+ end
966
+ end
967
+ f.string
968
+ end
969
+
970
+ def list_analyzed_files(files)
971
+ f = StringIO.new
972
+ f.puts "<h2 class=\"class_name\">Analyzed Files</h2>"
973
+ f.puts "<ul>"
974
+ files.each do |fname, warnings, errors|
975
+ readname = fname.split("_")[0...-1].join("_")
976
+ f.puts "<li>"
977
+ f.puts "<p class=\"file_name\"><a href=\"./#{fname}\">#{readname}</a>"
978
+ f.puts "</li>"
979
+ end
980
+ f.puts "</ul>"
981
+ f.string
982
+ end
983
+
984
+ def write_index(files, filename, title, header)
985
+ return if files.empty?
986
+
987
+ File.open(filename,"w") do |f|
988
+ f.puts "<html><head><title>#{title}</title></head>"
989
+ f.puts "#{HTMLStyleSheet.style_sheet}\n<body>"
990
+ f.puts "<h1>#{title}</h1>"
991
+
992
+ enw = files.find_all { |fn,w,e| (!w.empty? || !e.empty?) }
993
+
994
+ f.puts summarize_errors_and_warnings(enw, header)
995
+
996
+ f.puts "<hr/>"
997
+ f.puts list_analyzed_files(files)
998
+ f.puts "</body></html>"
999
+ end
1000
+ end
1001
+
1002
+ def write_cyclo_index(files, output_dir)
1003
+ header = "<tr><th>Class</th><th>Method</th><th>Complexity</th></tr>"
1004
+ write_index(files,
1005
+ "#{output_dir}/index_cyclo.html",
1006
+ "Index for cyclomatic complexity",
1007
+ header)
1008
+ end
1009
+
1010
+ def write_token_index(files, output_dir)
1011
+ header = "<tr><th>File</th><th>Line #</th><th>Tokens</th></tr>"
1012
+ write_index(files,
1013
+ "#{output_dir}/index_token.html",
1014
+ "Index for tokens per line",
1015
+ header)
1016
+ end
1017
+
1018
+ end
1019
+
1020
+ module Saikuro
1021
+ def Saikuro.analyze(files, state_formater, token_count_formater, output_dir)
1022
+
1023
+ idx_states = Array.new
1024
+ idx_tokens = Array.new
1025
+
1026
+ # parse each file
1027
+ files.each do |file|
1028
+ begin
1029
+ STDOUT.puts "Parsing #{file}"
1030
+ # create top state
1031
+ top = ParseState.make_top_state
1032
+ STDOUT.puts "TOP State made" if $VERBOSE
1033
+ token_counter = TokenCounter.new
1034
+ ParseState.set_token_counter(token_counter)
1035
+ token_counter.set_current_file(file)
1036
+
1037
+ STDOUT.puts "Setting up Lexer" if $VERBOSE
1038
+ lexer = RubyLex.new
1039
+ # Turn of this, because it aborts when a syntax error is found...
1040
+ lexer.exception_on_syntax_error = false
1041
+ lexer.set_input(File.new(file,"r"))
1042
+ top.lexer = lexer
1043
+ STDOUT.puts "Parsing" if $VERBOSE
1044
+ top.parse
1045
+
1046
+
1047
+ fdir_path = seperate_file_from_path(file)
1048
+ FileUtils.makedirs("#{output_dir}/#{fdir_path}")
1049
+
1050
+ if state_formater
1051
+ # output results
1052
+ state_io = StringIO.new
1053
+ state_formater.start(state_io)
1054
+ top.compute_state(state_formater)
1055
+ state_formater.end
1056
+
1057
+ fname = "#{file}_cyclo.html"
1058
+ puts "writing cyclomatic #{file}" if $VERBOSE
1059
+ File.open("#{output_dir}/#{fname}","w") do |f|
1060
+ f.write state_io.string
1061
+ end
1062
+ idx_states<< [
1063
+ fname,
1064
+ state_formater.warnings.dup,
1065
+ state_formater.errors.dup,
1066
+ ]
1067
+ end
1068
+
1069
+ if token_count_formater
1070
+ token_io = StringIO.new
1071
+ token_count_formater.start(token_io)
1072
+ token_counter.list_tokens_per_line(token_count_formater)
1073
+ token_count_formater.end
1074
+
1075
+ fname = "#{file}_token.html"
1076
+ puts "writing token #{file}" if $VERBOSE
1077
+ File.open("#{output_dir}/#{fname}","w") do |f|
1078
+ f.write token_io.string
1079
+ end
1080
+ idx_tokens<< [
1081
+ fname,
1082
+ token_count_formater.warnings.dup,
1083
+ token_count_formater.errors.dup,
1084
+ ]
1085
+ end
1086
+
1087
+ rescue RubyLex::SyntaxError => synerr
1088
+ STDOUT.puts "Lexer error for file #{file} on line #{lexer.line_no}"
1089
+ STDOUT.puts "#{synerr.class.name} : #{synerr.message}"
1090
+ rescue StandardError => err
1091
+ STDOUT.puts "Error while parsing file : #{file}"
1092
+ STDOUT.puts err.class,err.message,err.backtrace.join("\n")
1093
+ rescue Exception => ex
1094
+ STDOUT.puts "Error while parsing file : #{file}"
1095
+ STDOUT.puts ex.class,ex.message,ex.backtrace.join("\n")
1096
+ end
1097
+ end
1098
+
1099
+ [idx_states, idx_tokens]
1100
+ end
1101
+ end
1102
+
1103
+ if __FILE__ == $0
1104
+ require 'stringio'
1105
+ require 'getoptlong'
1106
+ require 'fileutils'
1107
+ require 'find'
1108
+ require 'rdoc/ri/ri_paths'
1109
+ require 'rdoc/usage'
1110
+
1111
+ include ResultIndexGenerator
1112
+
1113
+ #Returns the path without the file
1114
+ def seperate_file_from_path(path)
1115
+ res = path.split("/")
1116
+ if res.size == 1
1117
+ ""
1118
+ else
1119
+ res[0..res.size - 2].join("/")
1120
+ end
1121
+ end
1122
+
1123
+ def get_ruby_files(input_path)
1124
+ files = Array.new
1125
+ input_path.split("|").each do |path|
1126
+ Find.find(path.strip) do |f|
1127
+ files << f if !FileTest.directory?(f) && f =~ /\.rb$/
1128
+ end
1129
+ end
1130
+ files
1131
+ end
1132
+
1133
+ files = Array.new
1134
+ output_dir = "./"
1135
+ formater = "html"
1136
+ state_filter = Filter.new(5)
1137
+ token_filter = Filter.new(10, 25, 50)
1138
+ comp_state = comp_token = false
1139
+ begin
1140
+ opt = GetoptLong.new(
1141
+ ["-o","--output_directory", GetoptLong::REQUIRED_ARGUMENT],
1142
+ ["-h","--help", GetoptLong::NO_ARGUMENT],
1143
+ ["-f","--formater", GetoptLong::REQUIRED_ARGUMENT],
1144
+ ["-c","--cyclo", GetoptLong::NO_ARGUMENT],
1145
+ ["-t","--token", GetoptLong::NO_ARGUMENT],
1146
+ ["-y","--filter_cyclo", GetoptLong::REQUIRED_ARGUMENT],
1147
+ ["-k","--filter_token", GetoptLong::REQUIRED_ARGUMENT],
1148
+ ["-w","--warn_cyclo", GetoptLong::REQUIRED_ARGUMENT],
1149
+ ["-s","--warn_token", GetoptLong::REQUIRED_ARGUMENT],
1150
+ ["-e","--error_cyclo", GetoptLong::REQUIRED_ARGUMENT],
1151
+ ["-d","--error_token", GetoptLong::REQUIRED_ARGUMENT],
1152
+ ["-p","--parse_file", GetoptLong::REQUIRED_ARGUMENT],
1153
+ ["-i","--input_directory", GetoptLong::REQUIRED_ARGUMENT],
1154
+ ["-v","--verbose", GetoptLong::NO_ARGUMENT]
1155
+ )
1156
+
1157
+ opt.each do |arg,val|
1158
+ case arg
1159
+ when "-o"
1160
+ output_dir = val
1161
+ when "-h"
1162
+ RDoc.usage('help')
1163
+ when "-f"
1164
+ formater = val
1165
+ when "-c"
1166
+ comp_state = true
1167
+ when "-t"
1168
+ comp_token = true
1169
+ when "-k"
1170
+ token_filter.limit = val.to_i
1171
+ when "-s"
1172
+ token_filter.warn = val.to_i
1173
+ when "-d"
1174
+ token_filter.error = val.to_i
1175
+ when "-y"
1176
+ state_filter.limit = val.to_i
1177
+ when "-w"
1178
+ state_filter.warn = val.to_i
1179
+ when "-e"
1180
+ state_filter.error = val.to_i
1181
+ when "-p"
1182
+ files<< val
1183
+ when "-i"
1184
+ files.concat(get_ruby_files(val))
1185
+ when "-v"
1186
+ STDOUT.puts "Verbose mode on"
1187
+ $VERBOSE = true
1188
+ end
1189
+
1190
+ end
1191
+ RDoc.usage if !comp_state && !comp_token
1192
+ rescue => err
1193
+ RDoc.usage
1194
+ end
1195
+
1196
+ if formater =~ /html/i
1197
+ state_formater = StateHTMLComplexityFormater.new(STDOUT,state_filter)
1198
+ token_count_formater = HTMLTokenCounterFormater.new(STDOUT,token_filter)
1199
+ else
1200
+ state_formater = ParseStateFormater.new(STDOUT,state_filter)
1201
+ token_count_formater = TokenCounterFormater.new(STDOUT,token_filter)
1202
+ end
1203
+
1204
+ state_formater = nil if !comp_state
1205
+ token_count_formater = nil if !comp_token
1206
+
1207
+ idx_states, idx_tokens = Saikuro.analyze(files,
1208
+ state_formater,
1209
+ token_count_formater,
1210
+ output_dir)
1211
+
1212
+ write_cyclo_index(idx_states, output_dir)
1213
+ write_token_index(idx_tokens, output_dir)
1214
+ end