Projekt-übergreifende RuboCop-Konfiguration

Wenn man im Team Software baut, dann einigt man sich oft auf Code-Konventionen, so auch wir. Diese zu überprüfen darf nicht Hauptbestandteil von Code-Reviews sein, sonst reiben sich alle Beteiligten aneinander auf. Dazu braucht es Tools, die diese Aufgabe möglichst automatisch übernehmen. In der Ruby-Welt hat sich dazu RuboCop etabliert.

Hat man sich im Team einmal auf einen Regelsatz geeinigt, dann möchte man diesen auch auf möglichst alle Projekte anwenden, trotzdem aber zentral an einer Stelle pflegen. Für dieses Problem möchte ich unsere Lösung vorstellen.

TL;DR: rubocop-nww bzw. lib/rubocop/rspec/inject.rb!

RuboCop

… ist für Rubyisten am Schnellsten an einem Beispiel erklärt:

$ gem install rubocop
# test.rb

# Ätzend, diese Foobar-Beispiele
a = 'foo'
b = 'bar'
c = 1
if x = a || b
  puts x
end
$ rubocop test.rb

Inspecting 1 file
W

Offenses:

main.rb:1:1: C: Use only ascii symbols in comments.
# Ätzend, diese Foobar-Beispiele
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
main.rb:4:1: W: Useless assignment to variable - c.
c = 1
^
main.rb:5:6: W: Assignment in condition - you probably meant to use ==.
if x = a || b
     ^

1 file inspected, 3 offenses detected

RuboCop scannt also spezifizierte Dateien oder ganze Verzeichisse nach Fehlermustern bzw. Konventionsabweichungen ab und präsentiert das Ergebnis ausführlich.

RuboCop stützt sich mit seinem Standardsatz an Cops (Regeln) auf den Ruby Style Guide, der passenderweise vom gleichen Autor stammt, nichtdestotrotz aber in der community durchaus anerkannt ist.

Nicht jedes Team kann sich mit allen Cops anfreunden. Im Beispiel sieht man, dass keine Umlaute in Kommentaren verwendet werden dürfen und auch Variablenzuweisungen in Bedingungen Tabu sind. Deswegen ist RuboCop natürlich konfigurierbar:

# .rubocop.yml
Style/AsciiComments:
  Enabled: false
Lint/AssignmentInCondition:
  Enabled: false

Diese Konfigurationsdatei kann man entweder im Projektverzeichnis oder in seinem Home-Verzeichnis ablegen.

Projektübergreifend und zentral konfigurieren

Natürlich muss die Konfiguration mit den Kollegen geteilt werden, deswegen ist man geneigt, sie einfach im Projektverzeichnis abzulegen und ins VCS einzuchecken. Das macht man dann für 30 Projekte und hofft, diese Konfiguration nie wieder ändern zu müssen.

Oder man legt sie im Home-Verzeichnis ab, dann gilt sie für alle Projekte, jede Änderung muss aber konsequent im Team kommuniziert und umgesetzt werden. Das ist also auch keine Option, das kann ja nur schief gehen.

Diese Crux haben wir mit einer RuboCop-Extension gelöst. rubocop-nww macht nichts weiter, als beim Start eine mitgelieferte Konfiguration einzulesen:

# https://github.com/Nix-wie-weg/rubocop-nww/blob/master/lib/rubocop/nww/inject.rb
require 'yaml'

module RuboCop
  module Nww
    # Because RuboCop doesn't yet support plugins, we have to monkey patch in a
    # bit of our configuration.
    module Inject
      DEFAULT_FILE = File.expand_path(
        '../../../../config/default.yml', __FILE__
      )

      def self.defaults!
        hash = YAML.load_file(DEFAULT_FILE)
        puts "configuration from #{DEFAULT_FILE}" if ConfigLoader.debug?
        config = ConfigLoader.merge_with_default(hash, DEFAULT_FILE)

        ConfigLoader.instance_variable_set(:@default_configuration, config)
      end
    end
  end
end

Dieser Code stammt 1:1 von rubocop-rspec, man muss also nicht immer das Rad neu erfinden ☺. Installiert wird die extension klassisch als Gem. Das bedeutet nicht nur, dass oft ein simples gem install rubocop-nww ausreicht. Das Team hat außerdem die Möglichkeit, über das Gemfile eines Projekts einen bestimmten Konfigurationsstand sicherzustellen:

# Gemfile
gem 'rubocop-nww', '>= 0.0.4', require: false

Ein kleiner Pferdefuß an dieser Lösung ist, dass die extension wiederum explizit geladen werden muss. Eine schmale ~/.rubocop.yml allerdings ist nicht viel Aufwand und muss vor allem bei zukünftigen Regeländerungen nicht angefasst werden:

# ~/.rubocop.yml
require: rubocop-nww

Darauf aufbauend ist trotzdem noch eine Projekt-spezifische Konfiguration problemlos möglich:

# <Projektverzeichnis>/.rubocop.yml

inherit_from: ~/.rubocop.yml

# Mit Ruby 2.2 sind UTF-8-Encoding-Angaben nicht mehr nötig
Style/Encoding:
  Enabled: false

Metrics/AbcSize:
  Max: 33
  Exclude:
    - spec/**/*

Rails/ActionFilter:
  Enabled: false