ruby-appraiser 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ # encoding: utf-8
2
+ source 'https://rubygems.org'
3
+
4
+ # Specify your gem's dependencies in ruby-appraiser.gemspec
5
+ gemspec
data/LICENSE-APACHE.md ADDED
@@ -0,0 +1,13 @@
1
+ Copyright 2013 Simply Measured
2
+
3
+ Licensed under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License.
5
+ You may obtain a copy of the License at
6
+
7
+ http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ Unless required by applicable law or agreed to in writing, software
10
+ distributed under the License is distributed on an "AS IS" BASIS,
11
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ See the License for the specific language governing permissions and
13
+ limitations under the License.
data/LICENSE-MIT.md ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Simply Measured
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/LICENSE.md ADDED
@@ -0,0 +1,4 @@
1
+ This project is dual-licensed under [MIT][] and [APACHE][].
2
+
3
+ [MIT]: LICENSE-MIT.md
4
+ [APACHE]: LICENSE-APACHE.md
data/README.md ADDED
@@ -0,0 +1,77 @@
1
+ RubyAppraiser
2
+ =============
3
+
4
+ So you have a big project and you want to improve the code quality? Sweet. Too
5
+ bad you'll get a million errors when you run [rubocop][], [reek][], or
6
+ [flog][], so it'll annoy you with information overload until you get fed up &
7
+ turn it off.
8
+
9
+ Enter: RubyAppraiser, a generic interface for attaching code-quality tools
10
+ that limits their output to the lines you're changing, which allows you to use
11
+ these tools to gradually heal projects. Add a pre-commit hook that rejects
12
+ defective contributions, level up to require entire touched files to be fixed,
13
+ or run several code-quality tools in a single command.
14
+
15
+ The filters currently provided are:
16
+
17
+ - all - (default) show all defects
18
+ - authored - all uncommitted defects
19
+ - staged - all staged defects
20
+ - touched - all defects in files that have been touched
21
+
22
+ Usage:
23
+ ------
24
+
25
+ 1. Include one or more adapters in your `Gemfile` or as development
26
+ dependencies of your gem. They'll make sure their dependencies (including
27
+ `ruby-appraiser` itself) are taken care of.
28
+
29
+ ```ruby
30
+ gem 'ruby-appraiser-rubocop'
31
+ gem 'ruby-appraiser-reek'
32
+ ```
33
+
34
+ 2. Execute the appraiser:
35
+
36
+ ```sh
37
+ bundle exec ruby-appraiser --mode=authored reek rubocop
38
+ ```
39
+
40
+ The script will exit 0 IFF there are no matching defects from any of your
41
+ coverage tools. The tools themselves will respect any project-wide settings or
42
+ config files.
43
+
44
+ ```
45
+ $ bundle exec ruby-appraiser --help
46
+ Usage: ruby-appraiser [inspector...] [options]
47
+ -v, --[no-]verbose Run verbosely
48
+ --list List available adapters
49
+ --silent Silence output
50
+ --mode=MODE Set the mode. [staged,authored,touched,all]
51
+ --git-hook Output a git hook with current comand to STDOUT
52
+ --all Run all available adapters.
53
+ ```
54
+
55
+ Contributing:
56
+ -------------
57
+
58
+ 1. Write an adapter! Take a look at the existing adapters for help.
59
+
60
+ ```ruby
61
+ class Foo < RubyAppraiser::Adapter
62
+ def appraise
63
+ # ...
64
+ add_defect( file, line, description )
65
+ # ...
66
+ end
67
+ end
68
+ ```
69
+
70
+ License
71
+ -------
72
+ See [LICENSE][]
73
+
74
+ [LICENSE]: LICENSE.md
75
+ [rubocop]: https://github.com/bbatslov/rubocop
76
+ [reek]: https://github.com/troessner/reek
77
+ [flog]: https://github.com/seattlerb/flog
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ # encoding: utf-8
2
+ require 'bundler/gem_tasks'
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: utf-8
3
+
4
+ $LOAD_PATH.unshift(File.dirname(File.realpath(__FILE__)) + '/../lib')
5
+
6
+ require 'ruby-appraiser'
7
+ require 'benchmark'
8
+
9
+ cli = RubyAppraiser::CLI.new
10
+ success = nil
11
+
12
+ time = Benchmark::realtime do
13
+ success = cli.run
14
+ end
15
+
16
+ puts "Finished in #{time} seconds" if cli.options[:verbose]
17
+ exit(success ? 0 : 1)
@@ -0,0 +1,19 @@
1
+ # encoding: utf-8
2
+
3
+ require 'ruby-appraiser'
4
+
5
+ class RubyAppraiser::Adapter::LineLength < RubyAppraiser::Adapter
6
+ def appraise
7
+ source_files.each do |source_file|
8
+ File.open(source_file) do |source|
9
+ source.lines.each_with_index do |line, number|
10
+ line_length = line.chomp.length
11
+ if line_length > 80
12
+ add_defect(source_file, number,
13
+ "Line too long [#{line_length}/80]")
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,88 @@
1
+ # encoding: utf-8
2
+
3
+ require 'set'
4
+ require 'forwardable'
5
+
6
+ class RubyAppraiser::Adapter
7
+ class << self
8
+ def inherited(base)
9
+ registry << base
10
+ end
11
+
12
+ def registry
13
+ @registry ||= Set.new
14
+ end
15
+
16
+ def adapter_type(type = nil)
17
+ @adapter_type = type.to_s unless type.nil?
18
+
19
+ @adapter_type or
20
+ self.name.split('::').last.
21
+ sub(/Adapter$/, '').
22
+ gsub(/[A-Z]+/, '-\0').
23
+ sub(/^-/, '').
24
+ downcase
25
+ end
26
+
27
+ def find(query)
28
+ registry.detect do |adapter|
29
+ adapter.adapter_type == query
30
+ end
31
+ end
32
+
33
+ def get(query)
34
+ find(query) or
35
+ attempt_require_adapter(query, "ruby-appraiser/adapter/#{query}") or
36
+ attempt_require_adapter(query, "ruby-appraiser/#{query}") or
37
+ attempt_require_adapter(query, "ruby-appraiser-#{query}")
38
+ end
39
+
40
+ def all
41
+ # load relevant gems
42
+ scanner_pattern = /^(ruby-appraiser-([a-zA-Z0-9_\-]+))/
43
+ (`gem list --local`).scan(scanner_pattern) do |gem_name, adapter_type|
44
+ attempt_require_adapter(adapter_type, gem_name)
45
+ end
46
+
47
+ # look in the adapter directory
48
+ Dir::glob(File::expand_path('../adapter/*.rb', __FILE__)) do |filepath|
49
+ require filepath
50
+ end
51
+
52
+ # return the registry
53
+ registry
54
+ end
55
+
56
+ def attempt_require_adapter(name, path)
57
+ require path and find(name)
58
+ rescue LoadError
59
+ false
60
+ end
61
+
62
+ def find!(query)
63
+ find(query) or raise ArgumentError, "Adapter '#{query}' not found."
64
+ end
65
+ end
66
+
67
+ def initialize(appraisal, options)
68
+ @appraisal = appraisal
69
+ @options = options.dup
70
+ end
71
+
72
+ def appraise
73
+ raise NotImplementedError,
74
+ "#{self.class.name} does not implement appraise."
75
+ end
76
+
77
+ extend Forwardable
78
+
79
+ def_delegators :@appraisal, :source_files,
80
+ :project_root,
81
+ :relative_path,
82
+ :add_defect,
83
+ :authored_lines,
84
+ :touched_files,
85
+ :relevant_files,
86
+ :relevant_lines
87
+ end
88
+
@@ -0,0 +1,126 @@
1
+ # encoding: utf-8
2
+
3
+ class RubyAppraiser
4
+ class Appraisal
5
+ def initialize(options = {})
6
+ @options = options.dup.freeze
7
+ end
8
+
9
+ def mode
10
+ @options.fetch(:mode) { 'all' }
11
+ end
12
+
13
+ def options
14
+ @options.dup
15
+ end
16
+
17
+ def run!
18
+ appraisers.each do |appraiser|
19
+ appraiser.appraise
20
+ end unless relevant_files.empty?
21
+
22
+ @has_run = true
23
+ end
24
+
25
+ def success?
26
+ run! unless @has_run
27
+
28
+ defects.empty?
29
+ end
30
+
31
+ def defects
32
+ @defects ||= Set.new
33
+ end
34
+
35
+ def adapters
36
+ @adapters ||= Set.new
37
+ end
38
+
39
+ def appraisers
40
+ adapters.map do |adapter|
41
+ adapter.new(self, options)
42
+ end
43
+ end
44
+
45
+ def add_adapter(name)
46
+ Adapter::get(name).tap do |adapter|
47
+ adapter and self.adapters << adapter
48
+ end
49
+ end
50
+
51
+ def source_files
52
+ Dir::glob(File::expand_path('**/*', project_root)).select do |filepath|
53
+ File::file? filepath and RubyAppraiser::rubytype? filepath
54
+ end.map { |path| relative_path path }
55
+ end
56
+
57
+ def add_defect(defect, *args)
58
+ unless defect.kind_of? Defect
59
+ defect = Defect.new(defect, *args)
60
+ end
61
+ defects << defect if match?(defect.location)
62
+ end
63
+
64
+ def to_s
65
+ defects.to_a.sort.map(&:to_s).join($/)
66
+ end
67
+
68
+ def summary
69
+ "#{defects.count} defects detected."
70
+ end
71
+
72
+ def project_root
73
+ @project_root ||= (`git rev-parse --show-toplevel`).chomp
74
+ end
75
+
76
+ def relative_path(path)
77
+ full_path = File::expand_path(path, project_root)
78
+ full_path[(project_root.length + 1)..-1]
79
+ end
80
+
81
+ protected
82
+
83
+ def match?(location)
84
+ file, line = *location
85
+ relevant_lines[file].include? line
86
+ end
87
+
88
+ def relevant_files
89
+ relevant_lines.keys
90
+ end
91
+
92
+ def relevant_lines
93
+ case mode
94
+ when 'staged' then staged_authored_lines
95
+ when 'authored' then authored_lines
96
+ when 'touched' then all_lines_in touched_files
97
+ when 'all' then all_lines_in source_files
98
+ else raise ArgumentError, "Unsupported mode #{mode}."
99
+ end
100
+ end
101
+
102
+ # return a hash.
103
+ # key is a filename
104
+ # value is a truebot
105
+ def all_lines_in(files)
106
+ infinite_set = (0 .. (1.0 / 0))
107
+ files.reduce(Hash.new { [] }) do |memo, file|
108
+ memo.merge!(file => infinite_set)
109
+ end
110
+ end
111
+
112
+ def touched_files
113
+ @touched_files ||= authored_lines.keys
114
+ end
115
+
116
+ def authored_lines
117
+ @authored_lines ||=
118
+ RubyAppraiser::Git::authored_lines
119
+ end
120
+
121
+ def staged_authored_lines
122
+ @staged_authored_lines ||=
123
+ RubyAppraiser::Git::authored_lines(staged: true)
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,98 @@
1
+ # encoding: utf-8
2
+
3
+ require 'ruby-appraiser'
4
+ require 'optparse'
5
+ require 'set'
6
+
7
+ class RubyAppraiser
8
+ class CLI
9
+ def initialize(args = ARGV)
10
+ @argv = ARGV.dup
11
+ args = @argv.dup # needed for --git-hook
12
+ @options = {}
13
+ adapters = Set.new
14
+
15
+ OptionParser.new do |opts|
16
+ opts.banner = "Usage: #{File::basename($0)} [inspector...] [options]"
17
+ opts.on('-v', '--[no-]verbose', 'Run verbosely') do |verbose|
18
+ @options[:verbose] = verbose
19
+ end
20
+ opts.on('--list', 'List available adapters') do |list|
21
+ puts available_adapters
22
+ exit 1
23
+ end
24
+ opts.on('--silent', 'Silence output') do |silent|
25
+ @options[:silent] = true
26
+ end
27
+ opts.on('--mode=MODE',
28
+ 'Set the mode. ' +
29
+ '[staged,authored,touched,all]') do |mode|
30
+ @options[:mode] = mode
31
+ end
32
+ opts.on('--git-hook',
33
+ 'Output a git hook with current comand to STDOUT') do
34
+ command = $0
35
+ if (`which #{File::basename(command)}`).chomp == command
36
+ command = File::basename(command)
37
+ end
38
+ be = 'bundle exec'
39
+ hook_args = @argv.select { |arg| arg != '--git-hook' }
40
+
41
+ indented_git_hook = <<-EOGITHOOK
42
+ #!/bin/bash
43
+ echo -e "\\033[0;36mRuby Appraiser: running\\033[0m"
44
+
45
+ bundle exec #{command} #{hook_args.join(' ')}
46
+
47
+ result_code=$?
48
+ if [ $result_code -gt "0" ]; then
49
+ echo -en "\\033[0;31m" # RED
50
+ echo "[✘] Ruby Appraiser found newly-created defects and "
51
+ echo " has blocked your commit."
52
+ echo " Fix the defects and commit again."
53
+ echo " To bypass, commit again with --no-verify."
54
+ echo -en "\\033[0m" # RESET
55
+ exit $result_code
56
+ else
57
+ echo -en "\\033[0;32m" # GREEN
58
+ echo "[✔] Ruby Appraiser ok"
59
+ echo -en "\\033[0m" #RESET
60
+ fi
61
+ EOGITHOOK
62
+
63
+ indent = indented_git_hook.scan(/^\s*/).map(&:length).min
64
+ puts indented_git_hook.lines.map {|l| l[indent..-1]}.join
65
+ exit 1
66
+ end
67
+ opts.on('--all', 'Run all available adapters.') do
68
+ adapters += available_adapters
69
+ end
70
+ end.parse!(args)
71
+
72
+ @appraisal = RubyAppraiser::Appraisal.new(options)
73
+ adapters += args
74
+ adapters.each do |adapter|
75
+ @appraisal.add_adapter(adapter) or
76
+ raise "Unknown adapter '#{adapter}'"
77
+ end
78
+ Dir::chdir((`git rev-parse --show-toplevel`).chomp)
79
+ end
80
+
81
+ def options
82
+ @options.dup
83
+ end
84
+
85
+ def run
86
+ @appraisal.run!
87
+ puts @appraisal unless @options[:silent]
88
+ puts @appraisal.summary if @options[:verbose]
89
+ @appraisal.success?
90
+ rescue Object
91
+ puts "#{@appraisal.class.name} caught #{$!} at #{$!.backtrace.first}."
92
+ end
93
+
94
+ def available_adapters
95
+ @available_adapters ||= RubyAppraiser::Adapter::all.map(&:adapter_type)
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,33 @@
1
+ # encoding: utf-8
2
+
3
+ class RubyAppraiser
4
+ class Defect
5
+ def initialize(file, line, description)
6
+ @location = [file, line.to_i].freeze
7
+ @description = description.dup.freeze
8
+ end
9
+
10
+ attr_reader :location
11
+ attr_reader :description
12
+
13
+ def file
14
+ @location[0]
15
+ end
16
+
17
+ def line
18
+ @location[1]
19
+ end
20
+
21
+ def to_s
22
+ "#{file}:#{line} #{description}"
23
+ end
24
+
25
+ def ==(other)
26
+ self.to_s == other.to_s
27
+ end
28
+
29
+ def <=>(other)
30
+ self.to_s <=> other.to_s
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,41 @@
1
+ # encoding: utf-8
2
+
3
+ class RubyAppraiser
4
+ module Git
5
+ extend self
6
+
7
+ def authored_lines(options = {})
8
+ diff_command = ['git diff']
9
+ if options[:range]
10
+ diff_command << options[:range]
11
+ else
12
+ diff_command << (options[:staged] ? '--cached' : 'HEAD')
13
+ end
14
+
15
+ diff_output = IO.popen(diff_command.join(' ')) { |io| io.read }
16
+
17
+ current_path, current_line = nil, nil
18
+ authored_lines = Hash.new { |hash, key| hash[key] = [] }
19
+
20
+ diff_output.lines do |line|
21
+ case line
22
+ when /^---/ then next
23
+ when /^\+\+\+ (?:b\/)?(.*)/
24
+ current_path = Regexp::last_match(1)
25
+ when /-[0-9]+(?:,[0-9]+)? \+([0-9]+)((,[0-9]+)?)/
26
+ current_line = Regexp::last_match(1).to_i
27
+ else
28
+ next if line.start_with? '-'
29
+ authored_lines[current_path] << current_line if line[0] == '+'
30
+ current_line += 1 unless current_line.nil?
31
+ end
32
+ end
33
+
34
+ authored_lines.default_proc = Proc.new { [] }
35
+ authored_lines.reject do |filepath, lines|
36
+ not File::file? filepath or
37
+ not RubyAppraiser::rubytype? filepath
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,5 @@
1
+ # encoding: utf-8
2
+
3
+ class RubyAppraiser
4
+ VERSION = '1.0.0'
5
+ end
@@ -0,0 +1,64 @@
1
+ # encoding: utf-8
2
+
3
+ require 'ruby-appraiser/version'
4
+
5
+ class RubyAppraiser
6
+
7
+ autoload :Adapter, 'ruby-appraiser/adapter'
8
+ autoload :Appraisal, 'ruby-appraiser/appraisal'
9
+ autoload :CLI, 'ruby-appraiser/cli'
10
+ autoload :Defect, 'ruby-appraiser/defect'
11
+ autoload :Git, 'ruby-appraiser/git'
12
+
13
+ def initialize(options)
14
+ @options = options.dup
15
+ end
16
+
17
+ def options
18
+ @options.dup
19
+ end
20
+
21
+
22
+ def appraisal
23
+ unless @appraisal
24
+ @appraisal = Appraisal.new(options)
25
+
26
+ unless @appraisal.relevant_files.empty?
27
+ appraisers(@appraisal).each(&:appraise)
28
+ end
29
+ end
30
+
31
+ @appraisal
32
+ end
33
+
34
+ class << self
35
+ def rubytypes
36
+ %w(
37
+ *.rb
38
+ *.gemspec
39
+ Capfile
40
+ Gemfile
41
+ Rakefile
42
+ )
43
+ end
44
+
45
+ def rubytype?(filepath)
46
+ # true if the extension matches
47
+ filename = File::basename(filepath)
48
+ return true if rubytypes.any? do |rubytype|
49
+ File::fnmatch(rubytype, filename)
50
+ end
51
+
52
+ # true if file has a ruby shebang
53
+ begin
54
+ return true if File.open(filepath) do |file|
55
+ file.read(20).chomp =~ /\A#\!.+ruby/
56
+ end
57
+ rescue Errno::ENOENT
58
+ rescue ArgumentError # invalid byte sequence
59
+ end
60
+
61
+ false
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,19 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'ruby-appraiser/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = 'ruby-appraiser'
8
+ gem.version = RubyAppraiser::VERSION
9
+ gem.authors = ['Ryan Biesemeyer']
10
+ gem.email = ['ryan@simplymeasured.com']
11
+ gem.description = 'Run multiple code-quality tools against staged changes'
12
+ gem.summary = 'A Common interface/executor for code quality tools'
13
+ gem.homepage = 'https://github.com/simplymeasured'
14
+
15
+ gem.files = `git ls-files | grep -v '^ruby-appraiser-'`.split($/)
16
+ gem.executables = gem.files.grep(%r{^bin/}).map { |f| File.basename(f) }
17
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
+ gem.require_paths = ['lib']
19
+ end
metadata ADDED
@@ -0,0 +1,64 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ruby-appraiser
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Ryan Biesemeyer
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-06-01 00:00:00.000000000 Z
13
+ dependencies: []
14
+ description: Run multiple code-quality tools against staged changes
15
+ email:
16
+ - ryan@simplymeasured.com
17
+ executables:
18
+ - ruby-appraiser
19
+ extensions: []
20
+ extra_rdoc_files: []
21
+ files:
22
+ - .gitignore
23
+ - Gemfile
24
+ - LICENSE-APACHE.md
25
+ - LICENSE-MIT.md
26
+ - LICENSE.md
27
+ - README.md
28
+ - Rakefile
29
+ - bin/ruby-appraiser
30
+ - lib/ruby-appraiser.rb
31
+ - lib/ruby-appraiser/adapter.rb
32
+ - lib/ruby-appraiser/adapter/line-length.rb
33
+ - lib/ruby-appraiser/appraisal.rb
34
+ - lib/ruby-appraiser/cli.rb
35
+ - lib/ruby-appraiser/defect.rb
36
+ - lib/ruby-appraiser/git.rb
37
+ - lib/ruby-appraiser/version.rb
38
+ - ruby-appraiser.gemspec
39
+ homepage: https://github.com/simplymeasured
40
+ licenses: []
41
+ post_install_message:
42
+ rdoc_options: []
43
+ require_paths:
44
+ - lib
45
+ required_ruby_version: !ruby/object:Gem::Requirement
46
+ none: false
47
+ requirements:
48
+ - - ! '>='
49
+ - !ruby/object:Gem::Version
50
+ version: '0'
51
+ required_rubygems_version: !ruby/object:Gem::Requirement
52
+ none: false
53
+ requirements:
54
+ - - ! '>='
55
+ - !ruby/object:Gem::Version
56
+ version: '0'
57
+ requirements: []
58
+ rubyforge_project:
59
+ rubygems_version: 1.8.24
60
+ signing_key:
61
+ specification_version: 3
62
+ summary: A Common interface/executor for code quality tools
63
+ test_files: []
64
+ has_rdoc: