kwala 0.9.0

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