Zum Inhalt springen

Prädikat-Katalog zum Abfragen von Code-Entitäten in Regeln

Was Sie an jeder Code-Entität abfragen können. Vollständige Liste der Properties pro Model-Typ.

CodeCharter analysiert Ihre Solution einmal und stellt der DSL ein gecachtes Code-Modell zur Verfügung. Diese Seite listet, was Sie darauf abfragen können, sortiert nach Entitäts-Typ.

Wenn Sie eine Regel formulieren, wandern Sie von einer Root-Collection ausgehend durch das Modell. Property-Reads, Method-Aufrufe, LINQ-Ketten, am Ende sammeln Sie die Treffer ein.

Root-Collections

Jede Query startet bei einer dieser Sammlungen.

Root Element Inhalt
Types TypeModel Alle Klassen, Structs, Interfaces, Enums, Records
Methods MethodModel Alle Methoden über alle Typen (Konstruktoren sind nicht enthalten, die stehen in TypeModel.Constructors). Operator-Überladungen und Finalizer sind hier ganz normale Einträge: m.Name liefert "op_Addition", "op_Equality" usw. für einen überladenen Operator und "Finalize" für einen ~Type()-Destruktor, nicht die Quelltext-Schreibweise
Properties PropertyModel Alle Properties
Fields FieldModel Alle Felder
Events EventModel Alle Events
Namespaces NamespaceModel Alle Namespaces
Assemblies AssemblyModel Eines pro Projekt
TypeDependencies TypeDependency Gerichtete Type-zu-Type-Abhängigkeiten
Files FileModel Eine pro Quelldatei: using-Direktiven und Kommentar-Trivia
Diagnostics DiagnosticModel Programmweite Analyzer-Findings, siehe Sicherheits-Diagnostics
AllTypesFlattened TypeModel Jeder Typ inklusive verschachtelter Typen in beliebiger Tiefe (Types endet bei Top-Level-Typen)
AllMethodsFlattened MethodModel Jede Methode über AllTypesFlattened, inklusive verschachtelter Typen
AllBodies siehe unten Jede Methode und Property mit Rumpf (inklusive Accessors) über AllTypesFlattened, direkt bereit für .Catches/.Invocations/usw. (siehe Vorgefertigte Collections auf Body-Ebene), ohne selbst über Types und Methods zu navigieren
AllLiteralSources siehe unten Jede Methode, Property und jedes Feld, das Literal-Ausdrücke tragen kann, über AllTypesFlattened, direkt bereit für .Literals
AllConstructorInjectableCreations ObjectCreationModel Jedes new-Ausdruck in einem Konstruktor-Body oder Field-Initializer, über AllTypesFlattened, nach denselben Ausnahmen wie die New-in-Constructor-Regel (Werttypen, Records, BCL-/Framework-Typen, Suffixe wie Exception/Args/Options)
DocumentableDeclarations siehe unten Jeder Typ, jede Methode, Property, jedes Feld und Event über AllTypesFlattened, als eine Sequenz: für eine Regel, die XML-Doc-Abdeckung einheitlich statt getrennt pro Entitätsart prüft

TypeModel

Identität:

Name                       string         # ohne Namespace
FullName                   string         # Namespace.Type
Namespace                  NamespaceModel # Objekt, kein String: für den String t.Namespace.FullName nutzen
Kind                       string         # "Class" | "Interface" | "Struct" | "Enum" | "Record" | "Delegate"
SourceFile                 string
LineNumber                 int

Modifiers:

IsAbstract                 bool
IsSealed                   bool
IsStatic                   bool
IsPartial                  bool
IsGeneric                  bool
IsRecord                   bool
IsValueType                bool           # true bei struct, record struct, enum; false bei class, record class/record, interface, delegate; einziger Weg, einen record struct von einem record class zu unterscheiden, da beide Kind == "Record" melden
AccessModifier             string         # "Public" | "Internal" | "Private" | "Protected" | "ProtectedInternal" | "PrivateProtected"

Strukturelle Metriken:

LinesOfCode                int
NumberOfMethods            int
NumberOfFields             int
NumberOfProperties         int
NumberOfDerivedTypes       int
WeightedMethodsPerClass    int
DepthOfInheritance         int
LackOfCohesion             double
ResponseForClass           int
Instability                double
MaintainabilityIndex       double
CouplingEfferent           int
CouplingAfferent           int

Abstractness und DistanceFromMainSequence sind Namespace-Metriken; Sie finden sie auf NamespaceModel, nicht hier.

Beziehungen:

BaseType                   TypeModel      # nur gesetzt, wenn der Basistyp im analysierten Code deklariert ist; bei Framework-Basistypen null
DeclaredBaseTypeName       string         # voll qualifizierter Name des DIREKTEN Basistyps, z.B. "System.Exception" (auch für Framework-Typen gesetzt); null ohne expliziten Basistyp
BaseTypeNames              collection<string>   # voll qualifizierte Namen der GESAMTEN Vererbungskette (direkt + transitiv) bis System.Object; überschreitet die Framework-Grenze, also für transitive Basistyp-Prüfungen verwenden
Methods                    collection<MethodModel>   # bei einem Typ mit Kind == "Delegate" genau ein synthetisierter "Invoke"-Eintrag mit dem echten ReturnType/Parameters des Delegates; der einzige Weg zur Signatur eines Delegates
Constructors               collection<MethodModel>
Properties                 collection<PropertyModel>
Fields                     collection<FieldModel>
Events                     collection<EventModel>
NestedTypes                collection<TypeModel>
DerivedTypes               collection<TypeModel>
UsedTypes                  collection<TypeModel>
UsedByTypes                collection<TypeModel>
ImplementedInterfaces      collection<TypeModel>
ImplementedInterfaceNames  collection<string>   # voll qualifizierte Namen aller implementierten Interfaces (direkt oder geerbt), auf die generische Definition des Interfaces reduziert, z.B. "System.IEquatable<T>"
ImplementedInterfaceConstructedNames  collection<string>   # gleiche Menge und Reihenfolge wie ImplementedInterfaceNames, behält aber die konstruierten Typargumente jedes Interfaces, z.B. "System.IEquatable<Foo>"; damit lässt sich IEquatable<Bar> von IEquatable<Foo> unterscheiden, was ImplementedInterfaceNames nicht kann
Attributes                 collection<AttributeModel>
TypeParameters             collection<TypeParameterModel>   # generische Typparameter, z.B. das T in class Box<T>; leer wenn IsGeneric false ist
Reaches(pattern, mode?)     bool           # siehe "Erreichbarkeit" unten
ReachedBy(pattern, mode?)   bool           # siehe "Erreichbarkeit" unten
InboundReferenceCount()     int            # siehe "Erreichbarkeit" unten
Syntax                      node           # die Syntax der TYPDEKLARATION selbst, nicht die Rümpfe ihrer Member, dafür AllBodies bzw. das eigene Syntax eines Members verwenden. Bei einem partial-Typ verankert an derselben Deklaration, an der bereits SourceFile/LineNumber verankern (der zuletzt verarbeitete Teil), spiegelt also nie die anderen Teile wider. Leer für Enum- und Delegate-Deklarationen. Siehe "Syntax (Navigation auf Anweisungsebene)" unten.

Erreichbarkeit

t.Reaches(pattern, mode?) und t.ReachedBy(pattern, mode?) beantworten eine Frage, die die Member-Collections UsedTypes/UsedByTypes oben nicht beantworten können: ob ein gerichteter Pfad aus Kanten t mit einem Typ verbindet, der auf pattern passt, egal über wie viele Zwischenschritte. Reaches läuft von t nach außen, ReachedBy ist die Umkehrung, wahr wenn ein zu pattern passender Typ t erreichen kann. Ein Typ erreicht kein Pattern, auf das er selbst passt, außer er sitzt auf einem echten Zyklus.

pattern ist ein Glob, keine Regex: * trifft eine beliebige Zeichenfolge, ? trifft genau ein Zeichen, verglichen mit dem vollständigen Namen des Kandidatentyps. Fügen Sie hier keine Regex ein: Ein Pattern mit ^, \ oder $ wird beim Linting gemeldet, da das Regex-Syntax ist, die in einem Glob nichts bedeutet. Für eine echte Regex verwenden Sie Matches("regex") (siehe String-Helfer).

Das optionale Argument mode wählt die Kantenmenge, über die der Pfad berechnet wird. Der Standard und der gebräuchlichste Modus ist "declared": genau die sechs Kantenarten, die ein Scan des Quelltexts zur Kompilierzeit erkennen kann, also Vererbung, Interface-Implementierung, Member-/Parameter-/Rückgabetyp-Nutzung, ein aufgelöster Aufruf, ein angewendetes Attribut und ein Objekterzeugungsausdruck. Das ist eine reine Kompilierzeit-Frage, ein declared-Treffer ist also immer eine echte Abhängigkeitskette, was diesen Modus zur richtigen Wahl für eine Verbotsregel macht, etwa "Domain darf Infrastructure nicht erreichen":

from t in Types
where t.FullName.Matches("App.Domain\\..*")
where t.Reaches("App.Infra.*", "declared")
select t

Wenn App.Domain.OrderService nur ein Interface IRepository hält und App.Infra.SqlRepository es an anderer Stelle implementiert, bleibt declared korrekt stumm, das ist die DIP-konforme Form, und declared folgt Dependency-Injection- Verdrahtung nie von einem Interface zu seiner aufgelösten Implementierung. Das ist auch seine blinde Stelle: Reflection, Serializer, Source-Generatoren und string-basierte DI-Auflösung sind für declared unsichtbar, sodass es eine Anforderungsregel ("X muss Y erreichen") auf dieselbe Art unterschätzen kann. Die Menge der Modusnamen ist geschlossen: gültig sind nur "declared" und "dispatch", wobei "dispatch" zusätzlich an jeder aufgelösten Aufrufstelle Interface- und virtuellem Dispatch folgt; jeder andere String ist ein Autorenfehler und wird beim Linting gemeldet.

t.InboundReferenceCount() ist das Tiefe-1-Gegenstück zu diesen Prädikaten: die Zahl der verschiedenen Solution-Typen mit mindestens einer direkten deklarierten Kante auf t, über dieselben sechs Kantenarten. Es nimmt keine Argumente und zählt nur direkte Verweise, keine transitive Erreichbarkeit. t.AccessModifier != "Public" && t.InboundReferenceCount() == 0 meldet einen internen Typ, den nichts in der Solution referenziert.

MethodModel

Name                       string
FullName                   string
AccessModifier             string
IsAsync                    bool
IsStatic                   bool
IsAbstract                 bool
IsSealed                   bool
IsOverride                 bool
IsInterfaceImplementation  bool
IsConstructor              bool
IsExtensionMethod          bool
IsVirtual                  bool
IsNew                      bool           # mit dem `new`-Modifizierer deklariert, verdeckt ein gleichnamiges Basismember (Member Hiding, kein Overriding); aus der Deklarationssyntax gelesen, da Roslyn kein direktes "is new"-Symbol-Flag hat
ReturnType                 string
ReturnTypeShortName        string
IsReturnTypeClass          bool
IsReturnTypeInterface      bool
IsReturnTypeRecord         bool
ResolvedReturnType         TypeInfo
Parameters                 collection<ParameterModel>
LinesOfCode                int
CognitiveComplexity        int
CyclomaticComplexity       int
NestingDepth               int
NumberOfLocalVariables     int
NumberOfParameters         int
NumberOfOverloads          int
Overloads                  collection<MethodModel>
DeclaringType              TypeModel
CalledMethods              collection<MethodModel>
CalledByMethods            collection<MethodModel>
Attributes                 collection<AttributeModel>
TypeParameters             collection<TypeParameterModel>   # die eigenen generischen Typparameter der Methode, z.B. das T in void Map<T>(); nicht die des deklarierenden Typs; leer bei nicht-generischen Methoden
SourceFile                 string
LineNumber                 int
Syntax                     node           # der Methodenrumpf, für Regeln auf Anweisungsebene, siehe unten

Syntax (Navigation auf Anweisungsebene)

Die Properties oben beschreiben eine Methode von außen. Mit m.Syntax fragen Sie die Anweisungen im Rumpf ab, also Schleifen, try/catch, throw, Aufrufe und so weiter. Sie starten bei m.Syntax und gehen hinunter zum Konstrukt, das Sie brauchen.

m.Syntax ist ein node. Das Lesen eines Members liefert wieder einen node, ein node set (eine Menge von Knoten) oder ein token (ein Blatt wie ein Bezeichner).

Member eines node

Jeder node, auch m.Syntax selbst, bietet genau diese:

Kind          string     # der Konstruktname, z. B. "CatchClause" (siehe "Konstrukt-Kinds")
Text          string     # der wörtliche Quelltext dieses Konstrukts
Line          int        # 1-basierte Startzeile
Column        int        # 1-basierte Startspalte
Descendants   node set   # jedes Konstrukt unterhalb dieses Knotens, beliebig tief
Children      node set   # nur die direkten Kind-Konstrukte (eine Ebene tiefer)
ResolvedType  TypeInfo   # der semantisch aufgelöste Typ dieses Knotens, oder null
ResolvedSymbol string    # der voll qualifizierte Name des gebundenen Symbols, oder null
<slot>        node | node set | token   # ein benannter Teil, siehe "Kind-Slots je Konstrukt"

Ein token (zum Beispiel der Identifier einer catch-Deklaration) bietet nur Kind, Text, Line und Column. Es ist ein Blatt: es hat keine Descendants, Children oder Kind-Slots.

Member eines node set

Ein node set ist eine Collection. Es bietet Count und dieselben geschlossenen Collection-Helfer (.Any, .Where, .Count, .Select, ...) wie jede andere Collection, dazu:

Count        int        # Anzahl der Knoten in der Menge
Descendants  node set   # alle Nachfahren aller Knoten der Menge
Children     node set   # direkte Kinder aller Knoten der Menge
<Kind>       node set   # die Knoten dieser Art, z. B. .Descendants.CatchClause

.<Kind> filtert über den Konstruktnamen (siehe unten). Ein Name, der kein bekanntes Konstrukt ist, wird als Fehler gemeldet, nicht als stilles leeres Ergebnis.

Konstrukt-Kinds

Die exakten Namen, die Sie als Kind-Filter (.Descendants.<Kind>) oder im Vergleich mit Kind verwenden. Es sind die Namen der C#-Sprachkonstrukte.

Statements    Block  ExpressionStatement  LocalDeclarationStatement
              IfStatement  ElseClause  SwitchStatement  SwitchSection
              ForStatement  ForEachStatement  WhileStatement  DoStatement
              TryStatement  ThrowStatement  ReturnStatement  YieldStatement
              UsingStatement  LockStatement
              BreakStatement  ContinueStatement  GotoStatement

Clauses       CatchClause  CatchDeclaration  CatchFilterClause  FinallyClause

Expressions   InvocationExpression  MemberAccessExpression  ObjectCreationExpression
              BinaryExpression  AssignmentExpression  ConditionalExpression
              CastExpression  AwaitExpression  IdentifierName
              LiteralExpression  NumericLiteralExpression  StringLiteralExpression
              ArgumentList  Argument

Declarations  VariableDeclaration  VariableDeclarator  EqualsValueClause

Sie folgen C#s eigener Grammatik; jedes andere Konstrukt der Sprache ist über seinen Grammatiknamen erreichbar, egal wie tief verschachtelt. Die Grammatik ist umfangreich, die Kinds oben sind die, die Regeln normalerweise brauchen.

Kind-Slots je Konstrukt

Die benannten Teile eines Konstrukts. Jeder liefert einen node, ein node set oder ein token. Ein Slot, der im Quelltext fehlt (ein if ohne else, ein bloßes throw;, ein catch {} ohne Deklaration), liefert leer und ist sicher lesbar.

TryStatement              Block (node)         Catches (node set)    Finally (node)
CatchClause               Declaration (node)   Filter (node)         Block (node)
CatchDeclaration          Type (node)          Identifier (token)
CatchFilterClause         FilterExpression (node)
FinallyClause             Block (node)
IfStatement               Condition (node)     Statement (node)      Else (node)
ElseClause                Statement (node)
ForStatement              Declaration (node)   Condition (node)      Incrementors (node set)   Statement (node)
ForEachStatement          Type (node)          Identifier (token)    Expression (node)         Statement (node)
WhileStatement            Condition (node)     Statement (node)
DoStatement               Statement (node)     Condition (node)
SwitchStatement           Expression (node)    Sections (node set)
SwitchSection             Labels (node set)    Statements (node set)
ThrowStatement            Expression (node)
ReturnStatement           Expression (node)
Block                     Statements (node set)
ExpressionStatement       Expression (node)
LocalDeclarationStatement Declaration (node)
UsingStatement            Declaration (node)   Expression (node)     Statement (node)
LockStatement             Expression (node)    Statement (node)
InvocationExpression      Expression (node)    ArgumentList (node)
ArgumentList              Arguments (node set)
Argument                  Expression (node)
MemberAccessExpression    Expression (node)    Name (node)
ObjectCreationExpression  Type (node)          ArgumentList (node)   Initializer (node)
BinaryExpression          Left (node)          Right (node)
AssignmentExpression      Left (node)          Right (node)
ConditionalExpression     Condition (node)     WhenTrue (node)       WhenFalse (node)
CastExpression            Type (node)          Expression (node)
AwaitExpression           Expression (node)
IdentifierName            Identifier (token)
LiteralExpression         Token (token)
VariableDeclaration       Type (node)          Variables (node set)
VariableDeclarator        Identifier (token)   Initializer (node)
EqualsValueClause         Value (node)

Beispiel

throw ex; anstreichen, das den ursprünglichen Fehlerort verwirft:

@name "Rethrow loses the original location"
@severity error
@category "ErrorHandling"
@recommendation "Use a bare throw; to keep the original stack trace"

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

Das Finding zeigt auf die genaue throw-Anweisung, auch wenn sie tief in if-Zweigen oder Schleifen steckt, nicht auf die ganze Methode.

Typen und Symbole auflösen

Standardmäßig trifft m.Syntax die geschriebene Form des Codes. Wenn Sie den echten Typ hinter einem Ausdruck treffen möchten, unabhängig von Alias-Usings oder Teilnamen, lesen Sie ResolvedType (ein TypeInfo) oder ResolvedSymbol (der voll qualifizierte Name des Symbols). Beide sind null, wenn keine Auflösung möglich ist, also entsprechend absichern.

# MD5/SHA1 über ihren echten Typ treffen, egal wie der Code sie schreibt:
Methods.Where(m =>
  m.Syntax.Descendants.ObjectCreationExpression.Any(n =>
    n.ResolvedType.FullName == "System.Security.Cryptography.MD5"))

ResolvedSymbol liefert bei einer fluent aufgerufenen Extension-Methode die reduzierte Form, qualifiziert über den statischen Typ des Empfängers, nie über die deklarierende statische Klasse. xs.Count() löst zu "System.Collections.Generic.IEnumerable<int>.Count<int>()" auf, nicht zu "System.Linq.Enumerable.Count<int>(...)"; logger.LogError(...) löst zu "Microsoft.Extensions.Logging.ILogger.LogError(...)" auf, nicht zu einem LoggerExtensions.-Präfix. Pinnen Sie die exakt gerenderte Zeichenkette für die gewünschte Überladung (Parameterlisten sind Teil des Anzeigenamens), statt ein Präfix zu erraten.

Grenzen

m.Syntax verfolgt keinen Wert über Anweisungen hinweg (var e2 = ex; throw e2; liest sich als andere Variable) und bleibt auf einen Methodenrumpf beschränkt. Für Prüfungen, die über mehrere Methoden gehen, nutzen Sie die strukturellen Properties oben. Syntax ist leer bei Membern ohne Rumpf (abstrakte oder Interface-Methoden); die Navigation liefert dann keine Treffer.

Vorgefertigte Collections auf Body-Ebene

Die rohe Syntax mit .Descendants.<Kind> zu durchlaufen deckt alles ab, aber für die häufigsten Muster stellt MethodModel sie bereits vorextrahiert und typisiert zur Verfügung: Das erspart den Baum-Durchlauf. Dieselben Collections stehen auch auf den Accessor-Bodies einer Property zur Verfügung (PropertyModel.Catches, PropertyModel.Invocations usw., leer bei einer auto-implementierten Property), und Literals/ObjectCreations gibt es zusätzlich auf FieldModel für den Initializer-Ausdruck.

Catches               collection<CatchClauseModel>
Invocations           collection<InvocationModel>
ObjectCreations       collection<ObjectCreationModel>
MemberAccesses        collection<MemberAccessModel>
Literals              collection<LiteralModel>
BinaryExpressions     collection<BinaryExpressionModel>
LocalDeclarations     collection<LocalDeclarationModel>
UnusedParameters      collection<ParameterModel>   # Parameter dieser Methode, die im Body nie referenziert werden, nach denselben Ausnahmen wie die eingebaute Unused-Parameter-Regel (Unterstrich-Namen, CancellationToken, die (sender, e)-Event-Handler-Form); leer bei abstract/virtual/override/partial/explicit-interface/leeren Methoden
Shape                 MethodShapeModel             # grobe Kontrollfluss-Form, siehe unten

CatchClauseModel: eine einzelne catch-Klausel.

IsEmpty                    bool     # keine Statements im Block: schluckt die Exception stillschweigend
ExceptionType               TypeInfo # aufgelöster gefangener Typ, null bei bloßem `catch { }` oder wenn unauflösbar
RethrowsOriginal            bool     # ein bloßes `throw;` irgendwo im Block (nicht `throw ex;`)
SourceFile, Line, Column

InvocationModel: ein einzelner Methodenaufruf.

Target                      string   # wörtlicher aufgerufener Ausdruck, z.B. "_repo.Save"
ResolvedType                 TypeInfo # aufgelöster Rückgabetyp der aufgerufenen Methode
Arguments                   collection<string>   # wörtlicher Argumenttext, in Aufrufreihenfolge
IsUnobservedStatement        bool     # dieser Aufruf IST das ganze Statement: nicht awaited, zugewiesen, returned oder als Argument übergeben
SourceFile, Line, Column

Fire-and-forget-Prüfung: i.IsUnobservedStatement && i.ResolvedType.FullName.StartsWith("System.Threading.Tasks.Task").

ObjectCreationModel: ein new Foo(...) oder target-typed new(...).

ResolvedType                 TypeInfo   # aufgelöster erzeugter Typ, null wenn unauflösbar
SourceFile, Line, Column

MemberAccessModel: ein Member-Zugriff wie DateTime.Now.

Name                        string     # einfacher Name des zugegriffenen Members, z.B. "Now"
ResolvedType                 TypeInfo
SourceFile, Line, Column

LiteralModel: ein Literal-Ausdruck.

Value                        string   # wörtlicher Quelltext des Literal-Tokens
IsNumeric                    bool
IsNamedConstant               bool     # innerhalb einer const/static-readonly-Field-Initialisierung, einer const-Lokalen oder einer Enum-Member-Deklaration
IsTrivialMagicExemptValue     bool     # numerischer Wert ist 0, 1 oder 2, über alle Suffix-/Hex-/Binär-/Real-Formen hinweg
IsInMagicNumberExcludedContext bool    # einer von: Named-Constant-Kontext, Attribut-Argument, Array-Index
SourceFile, Line, Column

BinaryExpressionModel: a == b, x + y und Ähnliches.

Operator                     string   # z.B. "==", "!=", "+", "&&"
Left, Right                  BinaryOperandModel
IsInNestedScope               bool     # verschachtelt in einem Lambda-Body oder einem LINQ-Query-Ausdruck
IsConcatenationRoot           bool     # dieses `+` ist der oberste Knoten seiner String-Konkatenationskette
ConcatenationOperatorCount   int      # Anzahl der `+`-Operatoren in der hier verwurzelten Kette
SourceFile, Line, Column

BinaryOperandModel (jede Seite von Left/Right):

ResolvedType                 TypeInfo   # auch null für das `null`-Literal
IsNull                       bool
IsStringLiteral               bool     # ein String-Literal, nicht nur ein Wert, der zu System.String auflöst

LocalDeclarationModel: eine lokale Variablendeklaration.

IsVar                        bool
DeclaredTypeName              string   # wörtlicher deklarierter Typtext, z.B. "Foo" oder "var"
DeclaredType                  TypeInfo # aufgelöster deklarierter Typ; bei `var` der inferierte Typ
InitializerKind               string   # "ObjectCreation" | "Cast" | "None" | "Other"
InitializerType                TypeInfo
SourceFile, Line, Column

MethodShapeModel (m.Shape): die grobe Kontrollfluss-Form eines Bodys.

BodyStatementCount            int      # Top-Level-Statements direkt im Body-Block
BodyLinesOfCode                int      # nicht-leere Zeilen im Body-Block
FirstStatementIsGuardIf        bool     # erstes ausführbares Statement (nach führenden lokalen Deklarationen) ist ein if ohne else
FirstGuardIfBodyLineRatio      double   # Anteil (0.0-1.0) der Body-Zeilen innerhalb dieses Guard-if; 0, wenn das Flag oben false ist

Beispiel für fehlende Guard-Klausel: m.Shape.BodyStatementCount >= 3 && m.Shape.FirstStatementIsGuardIf && m.Shape.BodyLinesOfCode > 5 && m.Shape.FirstGuardIfBodyLineRatio > 0.7.

ParameterModel

Name                       string
Type                       string         # voll qualifizierter Typ-Name
TypeShortName              string         # z.B. "CancellationToken"
HasDefaultValue            bool
IsParams                   bool
IsOut                      bool
IsRef                      bool           # ref, inklusive ref readonly
IsIn                       bool           # in (schreibgeschützt per Referenz)
IsTypeInterface            bool
IsTypeClass                bool
IsTypeRecord               bool
IsTypeEnum                 bool
IsTypeStruct               bool
IsTypePrimitive            bool
IsTypeAbstract             bool
ResolvedType               TypeInfo
TypeBaseTypeNames          collection<string>   # Basistyp-Namen des Parameter-Typs
Attributes                 collection<AttributeModel>

PropertyModel

Name                       string
Type                       string         # Typ-Name
AccessModifier             string
IsStatic                   bool
IsAutoProperty             bool
IsAbstract                 bool
IsVirtual                  bool
IsOverride                 bool
HasGetter                  bool
HasSetter                  bool           # set oder init
HasInitOnlySetter          bool           # true, wenn der Setter init-only ist
IsInterfaceImplementation  bool
IsNew                      bool           # mit dem `new`-Modifizierer deklariert, verdeckt ein gleichnamiges Basismember (Member Hiding, kein Overriding); aus der Deklarationssyntax gelesen, da Roslyn kein direktes "is new"-Symbol-Flag hat
DeclaringType              TypeModel
Attributes                 collection<AttributeModel>
ResolvedType               TypeInfo
SourceFile                 string
LineNumber                 int

FieldModel

Name                       string
Type                       string         # Typ-Name
AccessModifier             string
IsStatic                   bool
IsReadonly                 bool           # kleines "o" beachten: Property-Namen sind case-sensitive
IsConst                    bool
IsVolatile                 bool           # mit `volatile` deklariert; Double-Checked-Locking-Regeln nutzen das, um ein bereits sicher publiziertes Feld nicht zu melden
DeclaringType              TypeModel
Attributes                 collection<AttributeModel>
ResolvedType               TypeInfo
SourceFile                 string
LineNumber                 int

EventModel

Name                       string
DelegateType               string         # Delegate-Typ des Events
IsStatic                   bool
DeclaringType              TypeModel
SourceFile                 string
LineNumber                 int

NamespaceModel

Name                       string         # letztes Segment
FullName                   string
Assembly                   AssemblyModel
Types                      collection<TypeModel>   # direkt enthaltene Typen
NumberOfTypes              int
Abstractness               double
Instability                double
DistanceFromMainSequence   double

AssemblyModel

Name                       string
Version                    string
TargetFramework            string
IsTestProject              bool         # ermittelt über Build-Property, Test-Framework-Referenz oder Namenssuffix
Namespaces                 collection<NamespaceModel>

TypeDependency

Source                     TypeModel
Target                     TypeModel
Kind                       string         # "Inherits" | "Implements" | "Uses"

AttributeModel

Name                       string         # behält das compilersichtbare "Attribute"-Suffix, z.B. "ObsoleteAttribute"
FullName                   string         # z.B. "System.ObsoleteAttribute"
ArgumentValues             collection<string>   # positionale/Konstruktor-Argumentwerte, in Reihenfolge, als TypedConstant.ToCSharpString()-Anzeigestring
NamedArguments             collection<AttributeArgumentModel>   # benannte Argumente, in Quelltext-Deklarationsreihenfolge

Jeder Argumentwert wird mit Roslyns TypedConstant.ToCSharpString() gerendert: eine String-Konstante erscheint in Anführungszeichen ("reason"), eine Zahl oder ein Bool als Literal (1, true), ein Enum-Member qualifiziert mit seinem deklarierenden Typ (N.Color.Green), ein typeof(...)-Argument als typeof(string), und ein Array-Argument als geschweifte Elementliste ({1, 2, 3}). Jeder gerenderte Wert ist auf 256 Zeichen begrenzt (mit einem abschließenden "…"-Marker), und ein überlanges Array-Argument (mehr als 32 Elemente oder eine Gesamt-Elementlänge über der Grenze) rendert nur ein begrenztes Präfix seiner Elemente (die Liste endet dann auf , …}), damit ein böswilliges mehrere Megabyte großes Attribut-Literal das analysierte Modell nicht aufblähen kann. ArgumentValues und NamedArguments sind immer leere Collections, nie null, wenn die Attribut-Anwendung keine Argumente dieser Art liefert.

Die DSL kennt kein Zip/ElementAt, um zwei parallele Collections über den Index zu korrelieren. Deshalb paart NamedArguments den Namen jedes Arguments mit seinem Wert in einem einzigen AttributeArgumentModel-Eintrag, statt getrennte Namens-/Werte-Collections offenzulegen.

AttributeArgumentModel

Ein einzelnes benanntes Argument einer Attribut-Anwendung (z.B. das Skip = "reason" in [Fact(Skip = "reason")]).

Name                       string         # der Name des benannten Arguments, wie im Quelltext geschrieben
Value                      string         # der gerenderte Wert des benannten Arguments, gleiches Format und 256-Zeichen-Limit wie AttributeModel.ArgumentValues

Einen Platzhalter-Skip-Grund erkennen: m.Attributes.Any(a => a.Name == "FactAttribute" && a.NamedArguments.Any(na => na.Name == "Skip" && na.Value == "\"TODO\"")).

TypeParameterModel

Verfügbar über TypeParameters auf TypeModel und MethodModel: ein generischer Typparameter wie das T in class Box<T> oder void Map<T>().

Name                       string         # z.B. "T"
Ordinal                    int            # nullbasierte Position in der Typparameter-Liste des deklarierenden Typs/der Methode
Variance                   string         # "None" (invariant) | "In" (kontravariant, z.B. IComparer<in T>) | "Out" (kovariant, z.B. IEnumerable<out T>)
HasReferenceTypeConstraint bool           # class-Constraint
HasValueTypeConstraint     bool           # struct-Constraint
HasNotNullConstraint       bool           # notnull-Constraint
HasConstructorConstraint   bool           # new()-Constraint
ConstraintTypeNames        collection<string>   # Namen der Basisklassen-/Interface-Constraints, in Deklarationsreihenfolge

TypeInfo

Kommt aus ResolvedType / ResolvedReturnType zurück. Anders als TypeModel funktioniert es auch für Typen außerhalb Ihrer Solution:

FullName                   string
ShortName                  string
IsClass                    bool           # Klassen ohne Records
IsRecord                   bool
IsInterface                bool
IsEnum                     bool
IsStruct                   bool
IsPrimitive                bool
IsAbstract                 bool
IsTypeParameter            bool           # ein noch nicht aufgelöster generischer Typparameter wie `T` in `List<T>`
IsSealed                   bool           # auch true für Structs, Enums und Delegates (nicht für statische Klassen)

String-Helfer

Wenn Sie ein string-Property lesen, können Sie verketten:

.StartsWith("...")
.EndsWith("...")
.Contains("...")
.Matches("regex")
.ToLower()
.ToUpper()
.Substring(start)          # bis zum Ende des Strings
.Substring(start, length)
.Length                    # Property, nicht Methode

Vergleiche sind case-sensitive sofern der Helper nichts anderes sagt.

Diese Liste ist abgeschlossen: Zur Verfügung stehen genau die oben aufgeführten Methoden. Der Aufruf einer anderen String-Methode, die Sie aus C# kennen, etwa Trim oder Replace, schlägt zur Auswertungszeit fehl.

+ funktioniert auch zwischen Strings (die vollständige Operatoren-Liste steht in Syntax-Übersicht), sodass Sie einen Vergleichswert aus Teilen zusammensetzen können, z.B. "Get" + p.Name == m.Name, um eine GetFoo()-Methode neben einer Foo-Property zu erkennen (CA1721):

from t in Types
from m in t.Methods
where t.Properties.Any(p => m.Name == "Get" + p.Name && m.Parameters.Count == 0)
select m

Collection-Helfer

Alle Root-Collections und Sub-Collections (Methods, Parameters, Properties, ...) unterstützen genau diese Methoden:

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

Auch diese Liste ist abgeschlossen: Der Aufruf einer Methode außerhalb dieser Liste schlägt zur Auswertungszeit fehl. Das gilt auch für gängige LINQ-Operatoren wie GroupBy, Skip oder FirstOrDefault. Die Folge hängt von der Query-Form ab: die LINQ-Form überspringt nur das fehlschlagende Element, die Fluent-Form bricht die gesamte Regel ohne Findings ab. Details stehen in der DSL-Grammatik.

Häufige Fallen

  • .Count statt .Count(): Sub-Collections nutzen Count als Property.
  • Die Klammern entscheiden, nicht die Anzahl der Argumente, ob ein Aufruf ein Methodenaufruf ist: .Any(), .Count() und .ToLower() funktionieren als Methodenaufruf sowohl als Schritt der Fluent-Kette als auch in geschachtelten Ausdrücken (m.Name.ToLower()). Lassen Sie die Klammern weg (m.Name.ToLower), wird das als Property-Zugriff gelesen und schlägt fehl, da ToLower keine Property ist.
  • Kind == "Class" ist case-sensitive: Großbuchstabe ist Pflicht.
  • AccessModifier == "Public": wir folgen der C#-Pascal-Konvention.
  • Parameters enthält keinen this-Receiver bei Instanz-Methoden.
  • .First() und .First(x => ...) werfen, wenn nichts passt, genau wie LINQs First. Sichern Sie mit .Any(...) ab, oder prüfen Sie .Where(...).Count > 0, statt anzunehmen, ein leeres Ergebnis komme stillschweigend zurück.

Ein Property anfragen

Wenn Ihr Use-Case ein Property braucht, das dieser Katalog noch nicht führt, sagen Sie uns Bescheid. Die DSL wächst entlang echtem Bedarf.

Wo geht's weiter