diff --git a/bin/rubocop-git b/bin/rubocop-git index 7691b0f..dda0f2b 100755 --- a/bin/rubocop-git +++ b/bin/rubocop-git @@ -1,3 +1,5 @@ #!/usr/bin/env ruby -require 'rubocop/git' +require 'rubocop/git/cli' + +RuboCop::Git::CLI.new.run diff --git a/hound.yml b/hound.yml new file mode 100644 index 0000000..dfbe0c5 --- /dev/null +++ b/hound.yml @@ -0,0 +1,235 @@ +AccessorMethodName: + Enabled: false + +Alias: + Enabled: false + +ArrayJoin: + Enabled: false + +AsciiComments: + Enabled: false + +AsciiIdentifiers: + Enabled: false + +Attr: + Enabled: false + +BlockNesting: + Enabled: false + +CaseEquality: + Enabled: false + +CharacterLiteral: + Enabled: false + +ClassLength: + Enabled: false + +ClassVars: + Enabled: false + +CollectionMethods: + PreferredMethods: + find: detect + reduce: inject + collect: map + find_all: select + +ColonMethodCall: + Enabled: false + +CommentAnnotation: + Enabled: false + +CyclomaticComplexity: + Enabled: false + +Delegate: + Enabled: false + +DeprecatedHashMethods: + Enabled: false + +Documentation: + Enabled: false + +DotPosition: + EnforcedStyle: trailing + +DoubleNegation: + Enabled: false + +EmptyLiteral: + Enabled: false + +Encoding: + Enabled: false + +EvenOdd: + Enabled: false + +FileName: + Enabled: false + +FlipFlop: + Enabled: false + +FormatString: + Enabled: false + +GlobalVars: + Enabled: false + +IfUnlessModifier: + Enabled: false + +IfWithSemicolon: + Enabled: false + +Lambda: + Enabled: false + +LambdaCall: + Enabled: false + +LineEndConcatenation: + Enabled: false + +LineLength: + Max: 80 + +MethodLength: + Enabled: false + +ModuleFunction: + Enabled: false + +NegatedIf: + Enabled: false + +NegatedWhile: + Enabled: false + +NilComparison: + Enabled: false + +Not: + Enabled: false + +NumericLiterals: + Enabled: false + +OneLineConditional: + Enabled: false + +OpMethod: + Enabled: false + +ParameterLists: + Enabled: false + +PercentLiteralDelimiters: + PreferredDelimiters: + '%': '{}' + +PerlBackrefs: + Enabled: false + +PredicateName: + NamePrefixBlacklist: + - is_ + +Proc: + Enabled: false + +RaiseArgs: + Enabled: false + +RegexpLiteral: + Enabled: false + +SelfAssignment: + Enabled: false + +SingleLineBlockParams: + Enabled: false + +SingleLineMethods: + Enabled: false + +SignalException: + Enabled: false + +SpecialGlobalVars: + Enabled: false + +VariableInterpolation: + Enabled: false + +TrailingComma: + Enabled: false + +TrivialAccessors: + Enabled: false + +VariableInterpolation: + Enabled: false + +WhenThen: + Enabled: false + +WhileUntilModifier: + Enabled: false + +WordArray: + Enabled: false + +# Lint + +AmbiguousOperator: + Enabled: false + +AmbiguousRegexpLiteral: + Enabled: false + +AssignmentInCondition: + Enabled: false + +ConditionPosition: + Enabled: false + +DeprecatedClassMethods: + Enabled: false + +ElseLayout: + Enabled: false + +HandleExceptions: + Enabled: false + +InvalidCharacterLiteral: + Enabled: false + +LiteralInCondition: + Enabled: false + +LiteralInInterpolation: + Enabled: false + +Loop: + Enabled: false + +ParenthesesAsGroupedExpression: + Enabled: false + +RequireParentheses: + Enabled: false + +UnderscorePrefixedVariableName: + Enabled: false + +Void: + Enabled: false diff --git a/lib/rubocop/git.rb b/lib/rubocop/git.rb index e4726cc..03b9409 100644 --- a/lib/rubocop/git.rb +++ b/lib/rubocop/git.rb @@ -1,7 +1,20 @@ require 'rubocop/git/version' +require 'rubocop' +require 'active_support/core_ext/module/attribute_accessors' module RuboCop module Git - # Your code goes here... + autoload :FileCollection, 'rubocop/git/file_collection' + autoload :FileViolation, 'rubocop/git/file_violation' + autoload :Line, 'rubocop/git/line' + autoload :ModifiedFile, 'rubocop/git/modified_file' + autoload :Patch, 'rubocop/git/patch' + autoload :PseudoPullRequest, 'rubocop/git/pseudo_pull_request' + autoload :PseudoResource, 'rubocop/git/pseudo_resource' + autoload :Runner, 'rubocop/git/runner' + autoload :StyleChecker, 'rubocop/git/style_checker' + autoload :StyleGuide, 'rubocop/git/style_guide' + + mattr_accessor :config_path end end diff --git a/lib/rubocop/git/cli.rb b/lib/rubocop/git/cli.rb new file mode 100644 index 0000000..d271708 --- /dev/null +++ b/lib/rubocop/git/cli.rb @@ -0,0 +1,42 @@ +require 'rubocop/git' +require 'optparse' + +module RuboCop + module Git + class CLI + def run(args = ARGV) + options = parse_arguments(args) + Runner.new.run(options) + end + + private + + def parse_arguments(args) + options = {} + + OptionParser.new do |opt| + opt.on('-c', '--config FILE', + 'Specify configuration file') do |config| + options[:config] = config + end + + opt.on('--cached', 'git diff --cached') do + options[:cached] = true + end + + opt.on('--staged', 'synonym of --cached') do + options[:cached] = true + end + + opt.on('--hound', 'Hound compatibility mode') do + options[:hound] = true + end + + opt.parse(args) + end + + options + end + end + end +end diff --git a/lib/rubocop/git/file_collection.rb b/lib/rubocop/git/file_collection.rb new file mode 100644 index 0000000..db0ca01 --- /dev/null +++ b/lib/rubocop/git/file_collection.rb @@ -0,0 +1,18 @@ +module RuboCop::Git +# copy from https://github.com/thoughtbot/hound/blob/a6a8d3f/app/models/file_collection.rb +class FileCollection + IGNORED_FILES = ['db/schema.rb'] + + attr_reader :files + + def initialize(files) + @files = files + end + + def relevant_files + files.reject do |file| + file.removed? || !file.ruby? || IGNORED_FILES.include?(file.filename) + end + end +end +end diff --git a/lib/rubocop/git/file_violation.rb b/lib/rubocop/git/file_violation.rb new file mode 100644 index 0000000..10e01f9 --- /dev/null +++ b/lib/rubocop/git/file_violation.rb @@ -0,0 +1,5 @@ +module RuboCop::Git +# copy from https://github.com/thoughtbot/hound/blob/a6a8d3f/app/models/file_violation.rb +class FileViolation < Struct.new(:filename, :offenses) +end +end diff --git a/lib/rubocop/git/line.rb b/lib/rubocop/git/line.rb new file mode 100644 index 0000000..c733578 --- /dev/null +++ b/lib/rubocop/git/line.rb @@ -0,0 +1,8 @@ +module RuboCop::Git +# copy from https://github.com/thoughtbot/hound/blob/a6a8d3f/app/models/line.rb +class Line < Struct.new(:content, :line_number, :patch_position) + def ==(other_line) + content == other_line.content + end +end +end diff --git a/lib/rubocop/git/modified_file.rb b/lib/rubocop/git/modified_file.rb new file mode 100644 index 0000000..a976650 --- /dev/null +++ b/lib/rubocop/git/modified_file.rb @@ -0,0 +1,58 @@ +require 'active_support/core_ext/object/try.rb' +require 'linguist' + +module RuboCop::Git +# copy from https://github.com/thoughtbot/hound/blob/a6a8d3f/app/models/modified_file.rb +class ModifiedFile + def initialize(file, pull_request) + @file = file + @pull_request = pull_request + end + + def relevant_line?(line_number) + modified_lines.detect do |modified_line| + modified_line.line_number == line_number + end + end + + def filename + @file.filename + end + + def removed? + @file.status == 'removed' + end + + def ruby? + language == 'Ruby' + end + + def contents + @contents ||= begin + unless removed? + @pull_request.file_contents(filename) + end + end + end + + def modified_lines + @modified_lines ||= patch.additions + end + + def modified_line_at(line_number) + modified_lines.detect do |modified_line| + modified_line.line_number == line_number + end + end + + private + + def language + @language ||= Linguist::Language.detect(filename, contents).try(:name) + end + + def patch + Patch.new(@file.patch) + end +end +end diff --git a/lib/rubocop/git/patch.rb b/lib/rubocop/git/patch.rb new file mode 100644 index 0000000..4f0de2f --- /dev/null +++ b/lib/rubocop/git/patch.rb @@ -0,0 +1,36 @@ +module RuboCop::Git +# copy from https://github.com/thoughtbot/hound/blob/a6a8d3f/app/models/patch.rb +class Patch + RANGE_INFORMATION_LINE = /^@@ .+\+(?\d+),/ + MODIFIED_LINE = /^\+(?!\+|\+)/ + NOT_REMOVED_LINE = /^[^-]/ + + def initialize(body) + @body = body || '' + end + + def additions + line_number = 0 + + lines.each_with_index.inject([]) do |additions, (content, patch_position)| + case content + when RANGE_INFORMATION_LINE + line_number = Regexp.last_match[:line_number].to_i + when MODIFIED_LINE + additions << Line.new(content, line_number, patch_position) + line_number += 1 + when NOT_REMOVED_LINE + line_number += 1 + end + + additions + end + end + + private + + def lines + @body.lines + end +end +end diff --git a/lib/rubocop/git/pseudo_pull_request.rb b/lib/rubocop/git/pseudo_pull_request.rb new file mode 100644 index 0000000..75039bb --- /dev/null +++ b/lib/rubocop/git/pseudo_pull_request.rb @@ -0,0 +1,34 @@ +module RuboCop + module Git + # ref. https://github.com/thoughtbot/hound/blob/a6a8d3f/app/models/pull_request.rb + class PseudoPullRequest + HOUND_CONFIG_FILE = '.hound.yml' + + def initialize(files, options) + @files = files + @options = options + end + + def pull_request_files + @files.map do |file| + ModifiedFile.new(file, self) + end + end + + def file_contents(filename) + if @options[:cached] + `git show ':#{filename}'` + else + File.read(filename) + end + end + + def config + return unless @options[:hound] + File.read(HOUND_CONFIG_FILE) + rescue Errno::ENOENT + nil + end + end + end +end diff --git a/lib/rubocop/git/pseudo_resource.rb b/lib/rubocop/git/pseudo_resource.rb new file mode 100644 index 0000000..ecfe617 --- /dev/null +++ b/lib/rubocop/git/pseudo_resource.rb @@ -0,0 +1,6 @@ +module RuboCop + module Git + class PseudoResource < Struct.new(:filename, :status, :patch) + end + end +end diff --git a/lib/rubocop/git/runner.rb b/lib/rubocop/git/runner.rb new file mode 100644 index 0000000..f8a6532 --- /dev/null +++ b/lib/rubocop/git/runner.rb @@ -0,0 +1,87 @@ +module RuboCop + module Git + # ref. https://github.com/thoughtbot/hound/blob/a6a8d3f/app/services/build_runner.rb + class Runner + DEFAULT_CONFIG_FILE = '.rubocop.yml' + HOUND_DEFAULT_CONFIG_FILE = + File.expand_path('../../../../hound.yml', __FILE__) + + def run(options) + @options = options + @files = parse_diff(git_diff(options[:cached])) + + set_config_path(options[:config], options[:hound]) + display_violations($stdout) + end + + private + + def violations + @violations ||= style_checker.violations + end + + def style_checker + StyleChecker.new(modified_files, pull_request.config) + end + + def modified_files + collection = FileCollection.new(pull_request.pull_request_files) + collection.relevant_files + end + + def pull_request + @pull_request ||= PseudoPullRequest.new(@files, @options) + end + + def git_diff(cached) + if cached + `git diff --cached --diff-filter=AM` + else + `git diff --diff-filter=AM` + end + end + + def parse_diff(diff) + files = [] + in_patch = false + + diff.each_line do |line| + case line + when %r{^diff --git a/(.*) b/\1$} + files << PseudoResource.new($1, 'modified', '') + in_patch = false + when /^@@/ + in_patch = true + end + + files.last.patch << line if in_patch + end + + files + end + + def set_config_path(config, hound) + RuboCop::Git.config_path = + if hound + HOUND_DEFAULT_CONFIG_FILE + else + config || DEFAULT_CONFIG_FILE + end + end + + def display_violations(io) + formatter = Rubocop::Formatter::ClangStyleFormatter.new(io) + formatter.started(nil) + + violations.map do |violation| + formatter.file_finished( + violation.filename, + violation.offenses.compact.sort.freeze + ) + end + + formatter.finished(@files.map(&:filename).freeze) + end + end + end +end diff --git a/lib/rubocop/git/style_checker.rb b/lib/rubocop/git/style_checker.rb new file mode 100644 index 0000000..0e22e69 --- /dev/null +++ b/lib/rubocop/git/style_checker.rb @@ -0,0 +1,36 @@ +module RuboCop::Git +# ref. https://github.com/thoughtbot/hound/blob/a6a8d3f/app/models/style_checker.rb +class StyleChecker + def initialize(modified_files, custom_config = nil) + @modified_files = modified_files + @custom_config = custom_config + end + + def violations + file_violations = @modified_files.map do |modified_file| + FileViolation.new(modified_file.filename, offenses(modified_file)) + end + + file_violations.select do |file_violation| + file_violation.offenses.any? + end + end + + private + + def offenses(modified_file) + violations = style_guide.violations(modified_file) + violations_on_changed_lines(modified_file, violations) + end + + def violations_on_changed_lines(modified_file, violations) + violations.select do |violation| + modified_file.relevant_line?(violation.line) + end + end + + def style_guide + @style_guide ||= StyleGuide.new(@custom_config) + end +end +end diff --git a/lib/rubocop/git/style_guide.rb b/lib/rubocop/git/style_guide.rb new file mode 100644 index 0000000..9c38d69 --- /dev/null +++ b/lib/rubocop/git/style_guide.rb @@ -0,0 +1,41 @@ +module RuboCop::Git +# ref. https://github.com/thoughtbot/hound/blob/a6a8d3f/app/models/style_guide.rb +class StyleGuide + def initialize(override_config_content = nil) + @override_config_content = override_config_content + end + + def violations(file) + parsed_source = parse_source(file) + team = Rubocop::Cop::Team.new(Rubocop::Cop::Cop.all, configuration) + commissioner = Rubocop::Cop::Commissioner.new(team.cops, []) + commissioner.investigate(parsed_source) + end + + private + + def parse_source(file) + Rubocop::SourceParser.parse(file.contents, file.filename) + end + + def configuration + config = Rubocop::ConfigLoader + .configuration_from_file(RuboCop::Git.config_path) + + if override_config + config = Rubocop::Config.new( + Rubocop::ConfigLoader.merge(config, override_config), + '' + ) + end + + config + end + + def override_config + if @override_config_content + Rubocop::Config.new(YAML.load(@override_config_content)) + end + end +end +end diff --git a/rubocop-git.gemspec b/rubocop-git.gemspec index d4cc925..32d3b53 100644 --- a/rubocop-git.gemspec +++ b/rubocop-git.gemspec @@ -20,4 +20,8 @@ Gem::Specification.new do |spec| spec.add_development_dependency 'bundler', '~> 1.6' spec.add_development_dependency 'rake' + + spec.add_dependency 'activesupport', '>= 3.0.0' + spec.add_dependency 'github-linguist', '~> 2.12.0' + spec.add_dependency 'rubocop', '0.22.0' end