Skip to content

Syntax-Übersicht

Die DSL als kompakte Referenz.

Verhältnis zu anderen Seiten: diese Seite behandelt den alltäglichen DSL-Einsatz: Datei-Aufbau, Query-Formen und die Collections-Übersicht. Die formale EBNF-Grammatik steht in der DSL-Grammatik. Den vollständigen Property-Katalog aller Modell-Typen finden Sie im Prädikat-Katalog.

Die CodeCharter-DSL ist an LINQ angelehnt. Wenn Sie C# kennen, kennen Sie fast alles, was Sie brauchen.

Datei-Anatomie

@name "..."                // optional, Default: Dateiname
@severity warn             // optional, Default: warn (info | warn | error)
@category "..."            // optional, Default: "General"
@description "..."         // optional
@recommendation "..."      // optional

// der Query-Body (keine Leerzeile erforderlich)
<linq-expression>

Alle Direktiven sind optional. Der Regel-Slug in den Findings ist der Dateiname ohne .ccr-Endung. @name ist nur der menschenlesbare Anzeigetitel. Kommentare nutzen //; es gibt keine Block-Kommentare.

Query-Formen

Query-Syntax

from t in Types
where t.Kind == "Class"
where t.IsAbstract
select t

Method-Syntax

Types.Where(t => t.Kind == "Class" && t.IsAbstract)

Beide ergeben das gleiche Ergebnis. Bei mehr als einer Bedingung ist Query-Syntax meist lesbarer.

Die Query-Syntax unterstützt zusätzlich let-Klauseln, um Zwischenwerte zu benennen. Eine orderby-Klausel (optional gefolgt von asc oder desc) wird akzeptiert, ändert die Reihenfolge der Ergebnisse aber nicht.

Top-Level-Collections

Collection Element Hinweis
Types TypeModel
Methods MethodModel Konstruktoren sind nicht enthalten; sie sind als Constructors auf einem TypeModel erreichbar
Properties PropertyModel
Fields FieldModel
Events EventModel
Namespaces NamespaceModel
Assemblies AssemblyModel
TypeDependencies TypeDependency
AllBodies IHasBodySyntax Jede Methode, jeder Konstruktor und jeder Property-Accessor über alle Typen, inklusive verschachtelter Typen; für Regeln, die Statement-Level-Facts (Catches, Invocations, MemberAccesses, Literals, BinaryExpressions, LocalDeclarations) ohne Nesting-Grenze auswerten
Files FileModel Eine pro Quelldatei: using-Direktiven und Kommentar-Trivia

AllBodies existiert ausschließlich als Top-Level-Root-Collection; es gibt keine AllBodies-Sub-Collection auf einer Methode. Um die Statement-Level-Facts eines bestimmten Typs oder einer bestimmten Methode zu prüfen, filtern Sie die Root-Collection über die Properties DeclaringType und Name des umschließenden Members.

Operatoren

LINQ-Standard:

==  !=  <  <=  >  >=
&&  ||  !
+ - * / %

Strings:

.StartsWith("...")
.EndsWith("...")
.Contains("...")
.Matches("regex")           // Regex-Match
.ToLower()
.ToUpper()
.Length                     // Property, nicht Methode

Collections:

.Any(x => ...)
.All(x => ...)
.Count                      // Property, nicht Methode
.Where(x => ...).Count
.Select(x => ...)
.SelectMany(x => ...)
.First(x => ...)
.Contains(value)
.Distinct()
.Take(n)
.OrderBy(x => ...)
.OrderByDescending(x => ...)
.Sum(x => ...)  .Average(x => ...)  .Min(x => ...)  .Max(x => ...)

Beide Listen sind abgeschlossen: nur die oben gezeigten Methoden existieren. Andere Methoden, die Sie aus C# erwarten würden, etwa Trim, Replace, GroupBy, Skip oder FirstOrDefault, werden nicht unterstützt und schlagen zur Auswertungszeit fehl. Die Folge hängt von der Query-Form ab: die Query-Form (LINQ) überspringt nur das fehlschlagende Element, die Method-Form (fluent) bricht die gesamte Regel ohne Findings ab. Details stehen in der DSL-Grammatik.

Spezielle Sub-Modelle

BaseType.FullName

where t.BaseType.FullName.Contains("ControllerBase")

BaseType kann null sein (zum Beispiel bei Interfaces oder Typen ohne deklarierte Basisklasse). Vergleiche gegen null (t.BaseType != null) werden unterstützt, und ein Member-Zugriff auf einen null-Wert liefert sicher null, statt einen Fehler auszulösen.

BaseType ist nur gesetzt, wenn der Basistyp im analysierten Code deklariert ist, bei Framework-Basistypen also null. Um einen Framework-Basistyp zu treffen oder einen Basistyp irgendwo in der Vererbungskette, nutzen Sie DeclaredBaseTypeName (nur der direkte Basistyp) oder BaseTypeNames (die gesamte Kette, siehe unten).

BaseTypeNames.Contains(...)

# Jeden Typ ausnehmen, der in beliebiger Tiefe von System.Exception erbt
where !t.BaseTypeNames.Contains("System.Exception")

BaseTypeNames enthält die voll qualifizierten Namen der gesamten Vererbungskette, den direkten plus jeden transitiven Basistyp, bis (exklusive) System.Object. Es überschreitet die Framework-Grenze, sodass "System.Exception" enthalten ist, obwohl dieser Typ nicht Teil des analysierten Codes ist. Bevorzugen Sie es gegenüber DeclaredBaseTypeName, wenn eine Ausnahme auch indirekte Unterklassen abdecken muss (z.B. DomainException : ExceptionBase mit ExceptionBase : Exception); nutzen Sie DeclaredBaseTypeName nur, wenn Sie bewusst ausschließlich den direkten Basistyp meinen.

Parameters.Any(...)

where m.Parameters.Any(p => p.TypeShortName == "CancellationToken")

m.Syntax, Regeln auf Anweisungsebene

m.Syntax navigiert die Anweisungen im Methodenrumpf, also Schleifen, try/catch, throw, Aufrufe, über dieselben Namen, die C# für diese Konstrukte verwendet. Descendants erreicht ein Konstrukt in beliebiger Tiefe, ein Konstruktname filtert die Menge, benannte Teile (.Block, .Condition, .Expression, ...) gehen hinein.

Methods.Where(m =>
  m.Syntax.Descendants.CatchClause.Any(c =>
    c.Block.Descendants.ThrowStatement.Any(t =>
      t.Expression.Text == c.Declaration.Identifier.Text)))

Die vollständige Oberfläche (lesbare Member, gängige Konstruktnamen und die Grenzen) steht im Prädikat-Katalog.

Vollständiger Property-Katalog

Die vollständige Liste aller Properties auf TypeModel, MethodModel, ParameterModel, PropertyModel, FieldModel, EventModel und den typisierten Statement-Level-Facts (CatchClauseModel, InvocationModel, MemberAccessModel, LiteralModel, BinaryExpressionModel, LocalDeclarationModel, einschließlich Collection-Helfer) steht im Prädikat-Katalog.