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
.Countstatt.Count(): Sub-Collections nutzenCountals 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, daToLowerkeine Property ist. Kind == "Class"ist case-sensitive: Großbuchstabe ist Pflicht.AccessModifier == "Public": wir folgen der C#-Pascal-Konvention.Parametersenthält keinenthis-Receiver bei Instanz-Methoden..First()und.First(x => ...)werfen, wenn nichts passt, genau wie LINQsFirst. 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
- DSL-Grammatik: wie die Sprache strukturell aufgebaut ist
- Regel-Beispiele: praktische Anwendungen der Prädikate
- Syntax-Übersicht: kompaktere Variante dieses Katalogs