kwala 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (67) hide show
  1. data/README +62 -0
  2. data/bin/kwala +39 -0
  3. data/lib/kwala.rb +59 -0
  4. data/lib/kwala/actions/code_change.rb +128 -0
  5. data/lib/kwala/actions/code_coverage.rb +303 -0
  6. data/lib/kwala/actions/code_duplication.rb +222 -0
  7. data/lib/kwala/actions/code_formatting.rb +358 -0
  8. data/lib/kwala/actions/cycle_detection.rb +118 -0
  9. data/lib/kwala/actions/cyclomatic_complexity.rb +159 -0
  10. data/lib/kwala/actions/dead_code.rb +68 -0
  11. data/lib/kwala/actions/executable_files_check.rb +111 -0
  12. data/lib/kwala/actions/flagged_comments.rb +128 -0
  13. data/lib/kwala/actions/gem_plugin.rb +31 -0
  14. data/lib/kwala/actions/loc_count.rb +153 -0
  15. data/lib/kwala/actions/multi_ruby_unit_test.rb +62 -0
  16. data/lib/kwala/actions/outside_links.rb +85 -0
  17. data/lib/kwala/actions/rails_migrate.rb +62 -0
  18. data/lib/kwala/actions/strange_requires.rb +106 -0
  19. data/lib/kwala/actions/syntax_check.rb +130 -0
  20. data/lib/kwala/actions/unit_test.rb +368 -0
  21. data/lib/kwala/build_action.rb +88 -0
  22. data/lib/kwala/build_context.rb +49 -0
  23. data/lib/kwala/command_line_runner.rb +184 -0
  24. data/lib/kwala/ext/demos.jar +0 -0
  25. data/lib/kwala/ext/pmd.jar +0 -0
  26. data/lib/kwala/ext/prefuse.jar +0 -0
  27. data/lib/kwala/extensions.rb +36 -0
  28. data/lib/kwala/lib/code_analyzer.rb +678 -0
  29. data/lib/kwala/lib/cycle_detector.rb +793 -0
  30. data/lib/kwala/lib/strange_requires_detector.rb +145 -0
  31. data/lib/kwala/notification.rb +45 -0
  32. data/lib/kwala/notifications/email.rb +54 -0
  33. data/lib/kwala/notifications/rss.rb +151 -0
  34. data/lib/kwala/project_builder_utils.rb +178 -0
  35. data/lib/kwala/templates/build_template.html +33 -0
  36. data/lib/kwala/templates/code_change_summary.html +27 -0
  37. data/lib/kwala/templates/code_coverage_detailed.html +25 -0
  38. data/lib/kwala/templates/code_coverage_summary.html +30 -0
  39. data/lib/kwala/templates/code_duplication_detailed.html +45 -0
  40. data/lib/kwala/templates/code_duplication_summary.html +9 -0
  41. data/lib/kwala/templates/code_formatting_detailed.html +27 -0
  42. data/lib/kwala/templates/code_formatting_summary.html +11 -0
  43. data/lib/kwala/templates/cycle_detection_detailed.html +20 -0
  44. data/lib/kwala/templates/cycle_detection_summary.html +7 -0
  45. data/lib/kwala/templates/cyclomatic_complexity_summary.html +27 -0
  46. data/lib/kwala/templates/executable_files_check_detailed.html +31 -0
  47. data/lib/kwala/templates/executable_files_check_summary.html +20 -0
  48. data/lib/kwala/templates/flagged_comments_detailed.html +22 -0
  49. data/lib/kwala/templates/flagged_comments_summary.html +10 -0
  50. data/lib/kwala/templates/loc_count_detailed.html +24 -0
  51. data/lib/kwala/templates/loc_count_summary.html +14 -0
  52. data/lib/kwala/templates/mdd/1.png +0 -0
  53. data/lib/kwala/templates/mdd/2.png +0 -0
  54. data/lib/kwala/templates/mdd/3.png +0 -0
  55. data/lib/kwala/templates/mdd/4.png +0 -0
  56. data/lib/kwala/templates/mdd/5.png +0 -0
  57. data/lib/kwala/templates/outside_links_summary.html +4 -0
  58. data/lib/kwala/templates/strange_requires_detailed.html +23 -0
  59. data/lib/kwala/templates/strange_requires_summary.html +13 -0
  60. data/lib/kwala/templates/style.css +95 -0
  61. data/lib/kwala/templates/syntax_check_detailed.html +21 -0
  62. data/lib/kwala/templates/syntax_check_summary.html +7 -0
  63. data/lib/kwala/templates/unit_test_detailed.html +66 -0
  64. data/lib/kwala/templates/unit_test_summary.html +70 -0
  65. data/test/tc_build_action.rb +32 -0
  66. data/test/tc_templates.rb +24 -0
  67. metadata +118 -0
@@ -0,0 +1,222 @@
1
+ # $Id: code_duplication.rb 29 2008-04-02 07:32:44Z zev $
2
+ #
3
+ # This file implements the logic to check for source code duplication.
4
+ #
5
+ # Requires the Java program PMD which uses the CPD tool.
6
+
7
+ # Copyright (c) 2006, Ubiquitous Business Technology (http://ubit.com)
8
+ # All rights reserved.
9
+ #
10
+ # Redistribution and use in source and binary forms, with or without
11
+ # modification, are permitted provided that the following conditions are
12
+ # met:
13
+ #
14
+ #
15
+ # * Redistributions of source code must retain the above copyright
16
+ # notice, this list of conditions and the following disclaimer.
17
+ #
18
+ # * Redistributions in binary form must reproduce the above
19
+ # copyright notice, this list of conditions and the following
20
+ # disclaimer in the documentation and/or other materials provided
21
+ # with the distribution.
22
+ #
23
+ # * Neither the name of Ubit nor the names of its
24
+ # contributors may be used to endorse or promote products derived
25
+ # from this software without specific prior written permission.
26
+ #
27
+ #
28
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
29
+ # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
30
+ # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
31
+ # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
32
+ # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
33
+ # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
34
+ # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
35
+ # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
36
+ # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
37
+ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
38
+ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
39
+ #
40
+ # == Author
41
+ # Zev Blut (zb@ubit.com)
42
+
43
+
44
+ class CodeDuplicationAction < BuildAction
45
+
46
+ def initialize
47
+ @duplicates = nil
48
+ @file_count = 0
49
+ end
50
+
51
+ def build_action(context)
52
+ temporarily_remove_symlinks(context.project_directory) do |dir|
53
+ @duplicates = find_duplicate_code(dir)
54
+ end
55
+ # Store file count for scoring
56
+ @file_count = context.ruby_files.size + context.test_files.size
57
+ end
58
+
59
+ def summary_display(context)
60
+ template = TemplateFile.new(self.class.summary_template_file)
61
+ context.amrita_data[:duplicate_code_results] = {
62
+ :duplicate_count => @duplicates.size,
63
+ :duplicates => @duplicates
64
+ }
65
+ context.amrita_data[:code_duplication_details] =
66
+ (Amrita::e(:a, :href=>"#{context.project_name}_code_duplication.html") { "Code Duplication Details" })
67
+ summary_expand(template, context)
68
+ end
69
+
70
+ def detailed_display(context)
71
+ det_res = expand_code_duplication_template(context.amrita_data[:duplicate_code_results])
72
+ det_file = "#{context.output_directory}/#{context.project_name}_code_duplication.html"
73
+ [det_file, det_res]
74
+ end
75
+
76
+ def score
77
+ # This should be based on code size ratio, for now using file count, but
78
+ # probably lines of code is better.
79
+ if @duplicates.size > @file_count
80
+ 1
81
+ else
82
+ score_scaling(@duplicates.size, @file_count)
83
+ end
84
+ end
85
+
86
+ def find_duplicate_code(dir=Dir.pwd)
87
+ require "rexml/document"
88
+ results = Array.new
89
+ Open3.popen3( cpd_command(dir) ) do |stdin, stdout, stderr|
90
+ xml = time_out_readlines(stdout)
91
+ if cpd_error?(stderr) || xml.first == "IO readlines timed out."
92
+ return results
93
+ end
94
+ doc = REXML::Document.new(xml.join("\n"))
95
+ doc.elements.each("pmd-cpd/duplication") do |dup|
96
+ results << {
97
+ :duplicate => {
98
+ :entry_num => results.size,
99
+ :lines => dup.attribute("lines").to_s,
100
+ :tokens => dup.attribute("tokens").to_s,
101
+ :code_fragement => highlight_code(dup.elements["codefragment"].cdatas.to_s),
102
+ :files => dup.get_elements("file").map { |f|
103
+ {
104
+ :path => f.attribute("path").to_s,
105
+ :line => f.attribute("line").to_s
106
+ }
107
+ },
108
+ }
109
+ }
110
+ end
111
+ end
112
+ results
113
+ end
114
+
115
+
116
+ def expand_code_duplication_template(data)
117
+ template = TemplateFile.new(self.class.detailed_template_file)
118
+ data[:duplicate_refs] = data[:duplicates].map do |d|
119
+ {
120
+ :entry_num => Amrita::a(:href=>"\##{d[:duplicate][:entry_num]}") {d[:duplicate][:entry_num]}
121
+ }
122
+ end
123
+ max_size = (data[:duplicates].size - 1)
124
+ data[:duplicates].each_with_index do |d, idx|
125
+ d[:duplicate][:entry_num] = Amrita::a(:name=>"#{d[:duplicate][:entry_num]}") {d[:duplicate][:entry_num]}
126
+ if idx > 0
127
+ d[:duplicate][:prev_entry_num] = Amrita::a(:href=>"\##{idx - 1}") { "Prev" }
128
+ end
129
+ if idx < max_size
130
+ d[:duplicate][:next_entry_num] = Amrita::a(:href=>"\##{idx + 1}") { "Next" }
131
+ end
132
+ end
133
+ ProjectBuilderUtils.expand_template(template, data)
134
+ end
135
+
136
+ def highlight_code(code)
137
+ # Check if enscript is installed.
138
+ cmd = "/usr/bin/enscript --color --language=html -Eruby --output=-"
139
+ out_data = nil
140
+ Open3.popen3(cmd) do |stdin, stdout, stderr|
141
+ stdin.puts code
142
+ stdin.close
143
+ out_data = time_out_readlines(stdout).join("")
144
+ #time_out_readlines(stderr)
145
+ end
146
+ if (out_data && ( m = /(<pre>.*<\/pre>)/im.match(out_data) ) )
147
+ code = Amrita::SanitizedString.new(m[1])
148
+ end
149
+
150
+ code
151
+ end
152
+
153
+ def cpd_command(dir)
154
+ "java -Xmx256m -cp #{cpd_jar} net.sourceforge.pmd.cpd.CPD" +
155
+ " --minimum-tokens #{minimum_tokens} --files #{dir}" +
156
+ " --language ruby --format net.sourceforge.pmd.cpd.XMLRenderer"
157
+ end
158
+
159
+ def cpd_jar
160
+ EXTERNAL_DIR + "pmd.jar"
161
+ end
162
+
163
+ def minimum_tokens
164
+ 50
165
+ end
166
+
167
+ def cpd_error?(stderr)
168
+ lines = stderr.readlines
169
+ if !lines.empty?
170
+ STDERR.puts "code_duplication error with cpd:", lines
171
+ true
172
+ else
173
+ false
174
+ end
175
+ end
176
+
177
+ def temporarily_remove_symlinks(dir)
178
+ symlinks = Hash.new
179
+
180
+ Find.find(dir) do |path|
181
+ if File.symlink?(path)
182
+ lnk = File.expand_path(path)
183
+
184
+ # Need to give it relative to lnk
185
+ src = File.expand_path(File.readlink(path), File.dirname(lnk))
186
+
187
+ # Only remove symlinks that are relative to dir. i.e. inside
188
+ # project to other parts of the project.
189
+
190
+ if src.index(dir) == 0
191
+
192
+ symlinks[lnk] = src
193
+
194
+ # Store if it is a directory
195
+ dircheck = File.directory?(path)
196
+
197
+ # Remove the link
198
+ FileUtils.rm_f(path)
199
+
200
+ if dircheck
201
+ # Prune must come after removing the file, because code
202
+ # after prune will not run.
203
+ Find.prune
204
+ end
205
+
206
+ end
207
+ end
208
+ end
209
+
210
+ yield dir
211
+ ensure
212
+
213
+ symlinks.each do |sym, source|
214
+ begin
215
+ FileUtils.ln_s(source, sym)
216
+ rescue => err
217
+ STDERR.puts "Cannot restore link:", err
218
+ end
219
+ end
220
+ end
221
+
222
+ end
@@ -0,0 +1,358 @@
1
+ #!/usr/bin/env ruby -w
2
+ # $Id: code_formatting.rb 29 2008-04-02 07:32:44Z zev $
3
+ #
4
+ # Checks for basic formatting violations.
5
+
6
+ # Copyright (c) 2006, Ubiquitous Business Technology (http://ubit.com)
7
+ # All rights reserved.
8
+ #
9
+ # Redistribution and use in source and binary forms, with or without
10
+ # modification, are permitted provided that the following conditions are
11
+ # met:
12
+ #
13
+ #
14
+ # * Redistributions of source code must retain the above copyright
15
+ # notice, this list of conditions and the following disclaimer.
16
+ #
17
+ # * Redistributions in binary form must reproduce the above
18
+ # copyright notice, this list of conditions and the following
19
+ # disclaimer in the documentation and/or other materials provided
20
+ # with the distribution.
21
+ #
22
+ # * Neither the name of Ubit nor the names of its
23
+ # contributors may be used to endorse or promote products derived
24
+ # from this software without specific prior written permission.
25
+ #
26
+ #
27
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
28
+ # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
29
+ # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
30
+ # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
31
+ # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
32
+ # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
33
+ # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
34
+ # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
35
+ # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
36
+ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
37
+ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
38
+ #
39
+ # == Author
40
+ # Zev Blut, Michael Reinsch, Pierre Baumard
41
+
42
+ require 'stringio'
43
+
44
+ # TODO: everything except CodeFormattingAction should be extracted from this file
45
+ # - for dependency tracking & tests, cf. Coding Guidelines
46
+ # - would also fix next issue
47
+
48
+ # XXX: cheat to remove the need to require kwala
49
+ # This should be fixed at a later time.
50
+ if !defined? BuildAction
51
+ class BuildAction
52
+ end
53
+ end
54
+
55
+
56
+ class CodeFormattingAction < BuildAction
57
+
58
+ def initialize(receiver=AmritaViolationReceiver.new)
59
+ @receiver = receiver
60
+ @detector = ViolationDetector.new(@receiver)
61
+ end
62
+
63
+ def build_action(context)
64
+ context.ruby_files.each do |file|
65
+ check_formatting(file)
66
+ end
67
+ end
68
+
69
+ def summary_display(context)
70
+ template = TemplateFile.new(self.class.summary_template_file)
71
+
72
+ if @receiver.empty?
73
+ context.amrita_data[:code_formatting_details] = "Code Formatting"
74
+
75
+ msg = "There are no formatting violations."
76
+ context.amrita_data[:code_formatting_results] = msg
77
+ else
78
+
79
+ context.amrita_data[:code_formatting_results] = {
80
+ :headers => ["Files"] << @receiver.summary.keys,
81
+ :values => [@receiver.files] << @receiver.summary.values,
82
+ }
83
+
84
+ url = "#{context.project_name}_code_formatting.html"
85
+ context.amrita_data[:code_formatting_details] = Amrita::a(:href => url)
86
+ end
87
+
88
+ summary_expand(template, context)
89
+ end
90
+
91
+ def detailed_display(context)
92
+ template = TemplateFile.new(self.class.detailed_template_file)
93
+
94
+ context.amrita_data[:code_formatting_details] = {
95
+ :entry => @receiver.details
96
+ }
97
+
98
+ det_res = ProjectBuilderUtils.expand_template(template,
99
+ context.amrita_data)
100
+
101
+ base_output_name = "#{ context.output_directory}/#{ context.project_name }"
102
+ det_file = base_output_name + "_code_formatting.html"
103
+
104
+ [det_file, det_res]
105
+ end
106
+
107
+ def score
108
+ return 10 if @receiver.empty?
109
+ errors = @receiver.summary.values.inject(0) {|s, v| s + v }
110
+ # For now made up mappings.
111
+ case errors
112
+ when 1..4 : 7
113
+ when 5..12 : 5
114
+ when 13..20 : 3
115
+ else
116
+ 1
117
+ end
118
+ end
119
+
120
+ # Checks the formatting of a file
121
+ def check_formatting(file)
122
+ @detector.check(file)
123
+ end
124
+
125
+ end
126
+
127
+
128
+ class ViolationDetector
129
+ extend InheritanceTracker
130
+
131
+ attr_accessor :violation_rec
132
+
133
+ def initialize(violation_rec)
134
+ @detectors = []
135
+ @violation_rec = violation_rec
136
+ end
137
+
138
+ # If detector needs the whole file instead of the lines of the file,
139
+ # then it must overwrite this method to return false.
140
+ def uses_lines?
141
+ true
142
+ end
143
+
144
+ # Add a specific instance of a detector to use when checking for
145
+ # violations.
146
+ def add_detector(detector)
147
+ @detectors << detector
148
+ end
149
+
150
+ # Return detectors used for checking. If no detectors were
151
+ # specifically added, then an instance of each subclass of
152
+ # ViolationDetector is returned.
153
+ def violation_detectors
154
+ if @detectors.empty?
155
+ @detectors = self.class.get_implementors.map { |k| k.new(violation_rec) }
156
+ end
157
+
158
+ @detectors
159
+ end
160
+
161
+ # Checks for violations in a file.
162
+ # == Parameters
163
+ # file:: The path to a file to check.
164
+ # Note: as Katagiri proposed we could make file an IO object to make
165
+ # this even more general and powerful.
166
+ def check(file)
167
+ lines = IO.readlines(file)
168
+
169
+ line_detectors, file_detectors = violation_detectors.partition do |vd|
170
+ vd.uses_lines?
171
+ end
172
+
173
+ line_detectors.each do |detector|
174
+ detector.check_lines(file, lines)
175
+ end
176
+
177
+ file_detectors.each do |detector|
178
+ detector.check_file(file)
179
+ end
180
+ end
181
+
182
+ #############################################################################
183
+ # Protected
184
+ #############################################################################
185
+ protected
186
+
187
+ # Overwrite this method to check against all lines in file.
188
+ # If you want to just check one line then use check_line.
189
+ # Return an array of violation, if no violations return an empty array.
190
+ def check_lines(file, lines)
191
+ viols = []
192
+
193
+ lines.each_with_index do |line, idx|
194
+ idx += 1
195
+ v = check_line(file, idx, line)
196
+ viols << v if v
197
+ end
198
+
199
+ viols
200
+ end
201
+
202
+ # Implement this method to check against a line in a file.
203
+ # Return either a violation instance or nil if no violation found.
204
+ def check_line(file, line_num, line)
205
+ end
206
+
207
+ # If uses_lines returns false implement this method to check against
208
+ # the path to a file.
209
+ def check_file(file)
210
+ end
211
+
212
+ end
213
+
214
+
215
+ class FileLineCountViolationDetector < ViolationDetector
216
+
217
+ MAX_LINE_COUNT = 1000
218
+
219
+ def check_lines(file, lines)
220
+ if lines.size > MAX_LINE_COUNT
221
+ @violation_rec.violation(file, "", "",
222
+ "File line count > #{MAX_LINE_COUNT}",
223
+ "Warning")
224
+ end
225
+ end
226
+
227
+ end
228
+
229
+
230
+ class TrailingWhiteSpaceViolationDetector < ViolationDetector
231
+ # cannot use \s because that adds \n which is valid.
232
+ # Just check for " " and tab for now.
233
+ TRAILING_WHITE_SPACE = /[ \t]$/
234
+
235
+ def check_line(file, line_num, line)
236
+ if TRAILING_WHITE_SPACE =~ line
237
+ @violation_rec.violation(file, line_num, line,
238
+ "Trailing White Space", "Warning")
239
+ end
240
+ end
241
+
242
+ end
243
+
244
+
245
+ class TabViolationDetector < ViolationDetector
246
+
247
+ TABS = /\t/
248
+
249
+ def check_line(file, line_num, line)
250
+ if TABS =~ line
251
+ @violation_rec.violation(file, line_num, line, "Tab", "Warning")
252
+ end
253
+ end
254
+
255
+ end
256
+
257
+
258
+ class LineLengthViolationDetector < ViolationDetector
259
+
260
+ MAX_LENGTH = 100
261
+ MAX_LENGTH_WITH_RETURN = MAX_LENGTH + 1 # with \n
262
+
263
+ def check_line(file, line_num, line)
264
+
265
+ if line.length > MAX_LENGTH_WITH_RETURN
266
+ @violation_rec.violation(file, line_num, line,
267
+ "Line length > #{MAX_LENGTH}",
268
+ "Warning")
269
+
270
+ end
271
+ end
272
+
273
+ end
274
+
275
+
276
+ class EOLBackslashViolationDetector < ViolationDetector
277
+
278
+ FINAL_BACKSLASH = /\\$/
279
+
280
+ def check_line(file, line_num, line)
281
+ if FINAL_BACKSLASH =~ line
282
+ @violation_rec.violation(file, line_num, line,
283
+ "Backslash at End of Line", "Warning")
284
+ end
285
+ end
286
+
287
+ end
288
+
289
+
290
+ class MissingFinalReturnViolationDetector < ViolationDetector
291
+
292
+ FINAL_RETURN = /\n$/
293
+
294
+ def check_lines(file, lines)
295
+ if FINAL_RETURN !~ lines.last
296
+ @violation_rec.violation(file, lines.size, lines.last,
297
+ "Missing Final Return", "Warning")
298
+ end
299
+ end
300
+
301
+ end
302
+
303
+
304
+ class AmritaViolationReceiver
305
+
306
+ attr_reader :summary, :details
307
+
308
+ def initialize
309
+ @summary = Hash.new(0)
310
+ @details = []
311
+ @seen_files = []
312
+ end
313
+
314
+ def violation(file, line_num, line, type, level)
315
+ @summary[type] += 1
316
+ @details << {
317
+ :file => file,
318
+ :type => type,
319
+ :line_num => line_num,
320
+ :line => line.inspect
321
+ }
322
+ @seen_files << file if !@seen_files.include?(file)
323
+ end
324
+
325
+ def empty?
326
+ @details.empty?
327
+ end
328
+
329
+ def files
330
+ @seen_files.size
331
+ end
332
+
333
+ end
334
+
335
+
336
+ if __FILE__ == $0
337
+
338
+ class ViolationPrinter
339
+ attr_reader :num
340
+
341
+ def initialize()
342
+ @num = 0
343
+ end
344
+
345
+ def violation(file, line_num, line, type, level)
346
+ puts "#{file} : #{line_num} - #{level} : #{type}"
347
+ @num += 1
348
+ end
349
+ end
350
+
351
+ vp = ViolationPrinter.new
352
+ ca = CodeFormattingAction.new(vp)
353
+ ARGV.each do |file|
354
+ res = ca.check_formatting(file)
355
+ end
356
+ puts "Number of violations: #{vp.num}"
357
+
358
+ end