Zum Inhalt springen

Formale Grammatikreferenz der CodeCharter-Regel-DSL

Formale Referenz der CodeCharter-DSL. Was der Parser akzeptiert, in EBNF-naher Notation.

Verhältnis zu anderen Seiten: diese Seite ist der formale Grammatik-Anhang. Für den alltäglichen DSL-Einsatz (Datei-Aufbau, Query-Formen, Collections-Übersicht) gibt es die Syntax-Übersicht. Alle abfragbaren Properties aller Modell-Typen stehen im Prädikat-Katalog.

Diese Seite ist die formale Wahrheit. Wenn Sie unsicher sind, ob ein Konstrukt erlaubt ist, schauen Sie hier nach. Für den alltäglichen Sprachgebrauch gibt es die kompakte Syntax-Übersicht, die nach Anwendungsfall sortiert ist.

Datei-Struktur

<file>            ::= <directive>* <query>
<directive>       ::= '@' <name> <directive-value>
<directive-value> ::= <string-literal> | <identifier>
<query>           ::= <query-syntax> | <fluent-syntax>

Eine .ccr-Datei enthält null oder mehr Direktiven, gefolgt von genau einer Query. Der Parser lehnt nachfolgenden Inhalt nicht ab: alles nach der ersten Query wird stillschweigend ignoriert. Schreiben Sie deshalb nur eine Query pro Datei.

Direktiven

Direktiven setzen Metadaten. Alle sind optional. Unbekannte Direktiv-Namen werden ignoriert, damit Regeln zukunftskompatibel bleiben.

Direktive Pflicht Wert Beschreibung
@name nein string Menschenlesbarer Titel der Regel. Default: Dateiname ohne Endung
@severity nein enum info, warn (auch warning), error. Default warn; unbekannte Werte fallen ebenfalls auf warn zurück
@category nein string Frei wählbare Gruppe (z.B. Async, Design). Default General
@description nein string Eine Zeile, was die Regel prüft
@recommendation nein string Eine Zeile, wie man es behebt

Strings nutzen C-Escapes für \", \\, \n, \r, \t, \0. Unbekannte Escapes bleiben unverändert stehen, damit "Handler\d*$" für Regex-Patterns natürlich bleibt.

@category ist Freitext: Die Engine validiert ihn nie gegen eine feste Liste, also können eigene Regeln jederzeit eine neue Kategorie einführen. Der eingebaute Regelkatalog gruppiert seine Regeln in Kategorien wie Design, Naming, Async, Style, Performance, Architecture, ErrorHandling, Documentation und Complexity; neue Kategorien kommen hinzu, sobald die eingebauten Regeln neue Bereiche der Codebasis abdecken. Die aktuelle, automatisch erzeugte Liste nach Kategorie steht unter Alle eingebauten Regeln.

Konfigurierbare Parameter (@param)

Eine Regel kann statt eines fest codierten Schwellenwerts einen oder mehrere einstellbare Parameter deklarieren:

<param-directive> ::= '@param' <id> ':' <param-type> ('=' <default>)?
                       <range-clause>? <aliases-clause>? <enum-clause>?

<param-type>      ::= 'int' | 'float' | 'bool' | 'string' | 'enum'
<range-clause>    ::= '[range:' <number> '..' <number> ']'
<aliases-clause>  ::= '[aliases:' <id> (',' <id>)* ']'
<enum-clause>     ::= '[enum:' <value> (',' <value>)* ']'
@param maxLines: int = 60 [range: 1..500]

Im Query-Body ist der Parameter ein ganz normaler Identifier: maxLines im Beispiel oben löst zu seinem konfigurierten oder default Wert auf und hat Vorrang vor allem außer einer gleichnamigen let-Variable oder Schleifenvariable:

Methods.Where(m => m.LinesOfCode > maxLines)

range schränkt den Default und jeden konfigurierten Wert eines int/float-Parameters auf die inklusiven Grenzen ein; enum listet die erlaubten Werte für einen string-Parameter, der als Enumeration genutzt wird; aliases fügt alternative Identifier-Namen hinzu, die der Query-Body austauschbar mit dem deklarierten Namen verwenden kann. Ein Wert, der gegen Typ, Bereich oder Enum-Vorgabe verstößt, wird beim Laden der Regel abgelehnt. Repository-Verantwortliche überschreiben den Default von außen, siehe params in der Konfigurationsdatei-Referenz.

Query-Syntax (LINQ-Form)

<query-syntax>    ::= 'from' <id> 'in' <id>
                      ( <let-clause> | <where-clause> )*
                      <orderby-clause>?
                      'select' <expression>

<let-clause>      ::= 'let' <id> '=' <expression>
<where-clause>    ::= 'where' <expression>
<orderby-clause>  ::= 'orderby' <expression> ('asc' | 'desc')?

Zwei semantische Hinweise:

  • orderby wird vom Parser akzeptiert, hat in dieser Form aber derzeit keine Wirkung: die Ergebnisse behalten die Reihenfolge der Collection. Zum Sortieren nutzen Sie stattdessen die Fluent-Methoden OrderBy/OrderByDescending.
  • Alle let-Klauseln werden vor allen where-Klauseln ausgewertet, auch wenn sie im Quelltext gemischt stehen.

Beispiel:

from t in Types
where t.Kind == "Class"
let methodCount = t.Methods.Count
where methodCount > 20
orderby methodCount desc
select t

Fluent-Syntax (Method-Form)

<fluent-syntax> ::= <id> <method-call>+
<method-call>   ::= '.' <id> ( '(' ( <argument> (',' <argument>)* )? ')' )?
<argument>      ::= <expression> | <lambda>
<lambda>        ::= <id> '=>' <expression>

Argumente werden durch Kommas getrennt. Aufrufe ohne Argumente wie .Count(), .Any() oder .ToLower() funktionieren sowohl als Schritte der äußeren Fluent-Kette als auch innerhalb von Ausdrücken oder Lambda-Bodys, etwa in einer where-Bedingung: m.Name.ToLower() wird als Methodenaufruf geparst, nicht als Zugriff auf ein Property.

Beispiel:

Methods.Where(m => m.IsAsync && m.Parameters.Count > 5)

Bei reinen Filtern treffen beide Formen dieselben Entitäten, sie sind aber nicht vollständig austauschbar: Sortieren kann nur die Fluent-Form (siehe den Hinweis zu orderby oben), und bei einem Auswertungsfehler überspringt die LINQ-Form nur das betroffene Element, während die Fluent-Form die ganze Regel ohne Findings abbricht. Bei mehreren where-Bedingungen liest sich die LINQ-Form meist besser. Bei kurzen Filter-Ketten ist Fluent oft kompakter.

Ausdrucksgrammatik

Operatoren von festester zu lockerster Bindung:

  1. Primär: Literale, Identifier, ( expr )
  2. Member-Access und Method-Call: a.b, a.b(args)
  3. Unär: !expr, -expr
  4. Multiplikativ: *, /, %
  5. Additiv: +, -
  6. Vergleich: ==, !=, >, >=, <, <=
  7. Logisches UND: &&
  8. Logisches ODER: ||

Literale die der Lexer kennt: Integer, Float, String ("..."), true, false.

Ein Identifier (<id>) beginnt mit einem Buchstaben oder Unterstrich, gefolgt von beliebig vielen Buchstaben, Ziffern oder Unterstrichen.

null ist kein Lexer-Literal, sondern wird bei der Auswertung als spezieller Identifier aufgelöst. Vergleiche wie t.BaseType == null sind daher erlaubt.

Whitespace und Kommentare

  • Whitespace zwischen Tokens ist bedeutungslos.
  • Kommentare beginnen mit // und gehen bis Zeilenende.

Zusicherungen der Sprache

Die DSL ist absichtlich klein und seiteneffektfrei:

  • Ein Query-Body liest das gecachte Code-Modell und sonst nichts.
  • Jedes Property ist ein Read, die Auswertung bleibt damit frei von mutablem State.
  • Die Auswertung läuft synchron über dem gecachten Code-Modell.

Wenn Sie ein Prädikat brauchen, das der Katalog noch nicht führt, melden Sie uns den Anwendungsfall bitte als kurze Anfrage, statt auf Reflection auszuweichen. Die DSL wächst absichtlich, nicht zufällig.

Wo geht's weiter