Skip to content

Best Practices

Was gute eigene Regeln von schlechten unterscheidet.

Regeln zu schreiben, die in einem Team produktiv sind, ist eine Disziplin für sich. Hier die wichtigsten Faustregeln.

1. Eine Regel pro Datei

rules/
├── no-manager-suffix.ccr
├── controller-must-end-controller.ccr
└── domain-must-not-reference-web.ccr

Das .ccr-Format erzwingt das bereits technisch: jede Datei enthält genau einen Satz Direktiven und genau eine Query. Die Disziplin liegt im Zuschnitt: Widerstehen Sie der Versuchung, eine breite Query zu schreiben, die mehrere Konventionen auf einmal prüft.

Pro Datei eine @name, eine Severity, eine Verantwortung. Das macht Diffs lesbar und Suppressions zielgerichtet.

2. Sprechende Dateinamen

Der Dateiname (ohne Endung) ist die Rule-ID: Sie taucht in den JSON- und SARIF-Reports auf und ist das, was Sie in Suppression-Kommentare schreiben. rule-42.ccr nutzt niemandem. controller-must-end-controller.ccr sagt direkt, was los ist.

Konvention: kebab-case, Verb-Phrase wenn möglich.

3. Klein anfangen mit Severity info

Eine neue Regel direkt auf error zu stellen produziert Reibung. Stattdessen:

  1. Regel auf info setzen.
  2. Eine Woche laufen lassen, schauen, welche Stellen erfasst werden.
  3. Fixen oder Suppressions setzen für die existierenden Stellen.
  4. Auf warn hochstufen.
  5. Wenn das Team sie als Konvention etabliert hat: error.

Vorsicht bei Tippfehlern: Ein unbekannter @severity-Wert fällt stillschweigend auf warn zurück; ein vertipptes eror lässt den Build also nicht fehlschlagen.

4. @recommendation ehrlich schreiben

@recommendation "Add 'CancellationToken cancellationToken = default' as the last parameter"

Nicht:

@recommendation "Fix this"

Die Empfehlung ist das, was Entwickler im Build-Log, in CI-Annotations und im JSON-Report lesen. Wenn sie nutzlos ist, ist die ganze Regel nutzlos; Entwickler deaktivieren sie, statt sie zu befolgen.

5. Keine Dopplung zu Roslyn

Wenn Microsoft.CodeAnalysis.NetAnalyzers eine Regel schon abdeckt (CA1822, CA2007, ...), nochmal mit CodeCharter zu prüfen ist Lärm. Lieber:

  • Roslyn-Analyzer für sprachen-nahe Themen
  • CodeCharter für team-spezifische Konventionen und Architektur

6. Architektur-Regeln spezifisch halten

// Gut, spezifisch
@name "Domain layer must not reference Web layer"

from t in Types
where t.Namespace.FullName.StartsWith("Acme.Domain")
where t.UsedTypes.Any(u => u.Namespace.FullName.StartsWith("Acme.Web"))
select t
// Schlecht, zu generisch
@name "Layers must not be violated"

from t in Types
where t.UsedTypes.Any(u => true)   // what?
select t

Eine Regel pro konkrete Architektur-Beziehung. Wenn Sie fünf Layer haben, dann fünf Regeln, nicht eine, die alle zusammenwirft.

7. Performance im Auge behalten

Sub-Queries auf großen Collections sind teuer:

// Teuer wenn 5000 Typen
from t in Types
where t.UsedTypes.Any(u => u.Methods.Any(m => m.IsAsync))
select t

Wenn Sie so etwas brauchen, prüfen Sie vorher, ob Sie gleich auf der äußeren Collection landen können:

// Direkt
from m in Methods
where m.IsAsync
where m.DeclaringType.UsedByTypes.Any(...)
select m

8. Die typisierten Statement-Level-Facts bevorzugen wenn vorhanden

Verbreitete Code-Smells wie direkte DateTime.Now/DateTime.UtcNow-Nutzung oder Fire-and-Forget-Async-Aufrufe stehen bereits als typisierte Facts an jedem Methoden-/Konstruktor-/Property-Accessor-Body zur Verfügung (m.MemberAccesses, m.Invocations, m.Catches, usw.), erreichbar über die abgeflachte AllBodies-Collection. Versuchen Sie nicht, diese Prüfungen mit einer eigenen m.Syntax-Traversierung nachzubauen, sondern lesen Sie den typisierten Fact direkt:

AllBodies.SelectMany(m => m.MemberAccesses.Where(a => (a.Name == "Now" || a.Name == "UtcNow")
    && (a.ResolvedType.FullName == "System.DateTime" || a.ResolvedType.FullName == "System.DateTimeOffset")))

Wenn Sie ein Finding weiter eingrenzen wollen, filtern Sie zuerst auf den Eigenschaften des umschließenden Members. Jedes Element unter AllBodies trägt SourceFile und DeclaringType:

AllBodies
    .Where(m => !m.SourceFile.Contains("Tests"))
    .SelectMany(m => m.Invocations.Where(i => i.IsUnobservedStatement
        && (i.ResolvedType.FullName == "System.Threading.Tasks.Task"
            || i.ResolvedType.FullName.StartsWith("System.Threading.Tasks.Task<"))))

Die typisierten Facts liefern direkt die Stelle. Schneller, lesbarer und zuverlässiger als die gleiche Prüfung von Hand anzunähern.

9. Keine Magic Strings für Namespaces

Wenn Sie Namespace-Prefixes hartcodieren müssen, zentralisieren Sie sie in einem gemeinsamen Konventions-Dokument oder halten Sie sie in einer kleinen Gruppe von Architektur-Regeln, die Sie zusammen reviewen:

// In jeder Architektur-Regel:
where t.Namespace.FullName.StartsWith("Acme.Domain")
where u.Namespace.FullName.StartsWith("Acme.Web")

Bei einer Umbenennung müssen Sie alle betroffenen Regel-Dateien anpassen. Wenn Sie diese Regeln nebeneinander halten, wird die Änderung in einem einzigen Diff sichtbar.

10. Suppressions mit Begründung

Suppressions sind einfache Kommentare im Code, auf der Zeile des Findings oder der Zeile darüber:

// codecharter-disable generic-exception-catch
// codecharter-disable-next-line no-manager-suffix

Ein nacktes // codecharter-disable ohne Rule-ID unterdrückt alle Regeln für die ganze Datei; setzen Sie es sparsam ein. Machen Sie es zur Team-Konvention, nach der Rule-ID eine kurze Begründung anzuhängen:

// codecharter-disable generic-exception-catch legacy interop boundary

Zusätzliche Wörter nach der Rule-ID ignoriert der Matcher; die Begründung wandert also mit der Suppression mit. Suppressions ohne Begründung reißen ein.

11. Regeln mit Specs testen

Legen Sie neben jede <rule-name>.ccr eine <rule-name>.spec.md mit Code-Beispielen, die die Regel treffen muss, und Beispielen, die sie nicht treffen darf. codecharter test rules/ prüft sie dann mit derselben Engine, die auch im Build läuft. codecharter test --scaffold pfad/zur/regel.ccr erzeugt eine Start-Spec neben der Regel. Eine Regel ohne Spec ist eine Regel, deren genaues Verhalten niemand festgehalten hat.

Anti-Patterns

  • Regeln, die nur select t ohne where haben. Erzeugt ein Finding pro Element der Collection, meist sinnlos.
  • Regeln, die fast jede Datei treffen. Die Severity ist falsch, oder die Regel passt nicht zum Team.
  • Hunderte Suppressions auf einer Regel. Wenn 80% der Findings unterdrückt werden, ist die Regel falsch.

Reviews

Behandeln Sie Regel-Änderungen wie Code-Änderungen, durch PR-Review. Eine neue Regel verändert das Verhalten des Build-Systems für alle; das verdient eine Diskussion.