The Selector is the first part of a rule — the from clause or the initial
collection access. It determines which kind of code element the rule
operates on.
Top-Level Collections
You can use any of these directly as a starting point:
| Collection | Element type | Description |
|---|---|---|
Types |
TypeModel |
All classes, structs, interfaces, records, enums |
Methods |
MethodModel |
All methods; constructors are not included — reach them via t.Constructors on a type |
Properties |
PropertyModel |
All properties |
Fields |
FieldModel |
All fields |
Events |
EventModel |
All events |
Namespaces |
NamespaceModel |
All namespaces |
Assemblies |
AssemblyModel |
All analyzed assemblies |
TypeDependencies |
TypeDependency |
Coupling relationships between types |
Files |
FileModel |
One per source file: using directives and comment trivia |
Two Syntax Styles
The DSL supports both from x in Y (query syntax) and Y.Where(x => ...)
(method syntax). Both produce the same result, with one difference in error
behavior: if evaluating an element fails (for example a mistyped property),
query syntax skips only that element, while method syntax aborts the whole
rule and reports no violations.
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)
When to use which? With multiple where clauses, query syntax is more
readable. With a single condition or an aggregation (.Count > 10, where
Count is a property), method syntax is shorter.
Per-Element Properties
Each element type has properties you can use in where clauses and
sub-queries.
TypeModel (excerpt)
t.Name // simple name
t.FullName // with namespace
t.Namespace // namespace object; use t.Namespace.FullName for the name
t.Kind // "Class", "Interface", "Struct", "Enum", "Record"
t.IsAbstract // bool
t.IsSealed // bool
t.IsStatic // bool
t.IsRecord // bool
t.LinesOfCode // int
t.NumberOfMethods // int
t.Methods // collection of MethodModel
t.Constructors // collection of MethodModel
t.NestedTypes // collection of TypeModel
t.DerivedTypes // collection of TypeModel
t.UsedTypes // which types does this type reference
t.UsedByTypes // which types reference this one
t.BaseType.FullName // parent class
MethodModel (excerpt)
m.Name // method name
m.AccessModifier // "Public", "Private", "Protected", "Internal"
m.IsAsync // bool
m.IsStatic // bool
m.IsOverride // bool
m.IsInterfaceImplementation // bool
m.Parameters // collection of ParameterModel
m.CalledMethods // which methods does this one call
m.CalledByMethods // which methods call this one
m.LinesOfCode // int
m.DeclaringType // TypeModel
The full property and Kind reference lives in the Predicate catalog; when using an AI assistant, the get_authoring_docs MCP tool (topic predicates) returns the same reference.
Composition with Any / All / Count
Sub-queries on collection properties. For Any / All you'll usually pass a
lambda (Any(m => ...)), but a zero-argument call like t.Methods.Any() is
also valid and simply checks whether the collection has any elements.
Count is available both as a property (t.Methods.Count) and as a call
(t.Methods.Count()).
// Classes with at least one async method without CancellationToken
from t in Types
where t.Methods.Any(m =>
m.IsAsync &&
!m.Parameters.Any(p => p.TypeShortName == "CancellationToken"))
select t
// Classes with more than 10 public methods
from t in Types
where t.Methods.Where(m => m.AccessModifier == "Public").Count > 10
select t
Statement-Level Facts as a Selector
Some patterns live inside a method or constructor body — a catch clause, a
member access, a literal, a binary expression. These are exposed as typed
facts on every body (m.Catches, m.MemberAccesses, m.Literals,
m.BinaryExpressions, m.Invocations, m.LocalDeclarations), reachable from
the flattened AllBodies collection so a rule sees every method, constructor,
and property accessor across every type without a nesting boundary:
@name "Direct DateTime usage"
@severity warn
@category "Testability"
AllBodies.SelectMany(m => m.MemberAccesses.Where(a => (a.Name == "Now" || a.Name == "UtcNow")
&& (a.ResolvedType.FullName == "System.DateTime" || a.ResolvedType.FullName == "System.DateTimeOffset")))
The full member catalog per fact type lives in the Predicate catalog.
What's Next
- Suppressions — when a location should be deliberately excluded
- Best Practices — good vs. bad Selector strategies