ruby-appraiser 1.0.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.
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: