Relationship to other pages: this is the formal grammar appendix. For everyday DSL usage (file anatomy, query forms, collection overview) see Syntax overview. For all queryable properties on each model type see Predicate catalog.
This page is the formal source of truth. When you are unsure whether a construct is allowed, look here. For everyday language use the more compact Syntax overview, which is organised by use case.
File shape
<file> ::= <directive>* <query>
<directive> ::= '@' <name> <directive-value>
<directive-value> ::= <string-literal> | <identifier>
<query> ::= <query-syntax> | <fluent-syntax>
A .ccr file contains zero or more directives followed by exactly one
query. The parser does not reject trailing content: anything after the
first query is silently ignored, so keep one query per file.
Directives
Directives set metadata. All of them are optional. Unknown directive names are ignored so rules stay forward-compatible.
| Directive | Required | Value | Description |
|---|---|---|---|
@name |
no | string | Human-readable rule title. Default: file name without extension |
@severity |
no | enum | info, warn (or warning), error. Default warn; unrecognised values also fall back to warn |
@category |
no | string | Free-form grouping (e.g. Async, Design). Default General |
@description |
no | string | One-line description of what the rule checks |
@recommendation |
no | string | One-line description of how to fix a finding |
Strings use C-style escapes for \", \\, \n, \r, \t, \0.
Unknown escape sequences are preserved verbatim so "Handler\d*$"
reads natural for regex authors.
@category is free text — the engine never validates it against a fixed
list, so your own rules can introduce a new category name at any time. The
built-in rule catalog groups its rules into categories such as Design,
Naming, Async, Style, Performance, Architecture, ErrorHandling,
Documentation, and Complexity, and new categories appear as the
built-in rules grow to cover new areas of the codebase. See
All built-in rules for the current, generated
list grouped by category.
Configurable parameters (@param)
A rule can declare one or more tunable parameters instead of hard-coding a threshold:
<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]
Inside the query body the parameter is a normal identifier — maxLines in
the example above resolves to its configured or default value, taking
precedence over everything except a let-bound or loop variable of the same
name:
Methods.Where(m => m.LinesOfCode > maxLines)
range constrains an int/float default and any configured override to
the inclusive bounds; enum lists the allowed values for a string
parameter used as an enumeration; aliases adds alternative identifier
names the query body can use interchangeably with the declared name. A
value that violates its type, range, or enum constraint is rejected when
the rule loads. Repository maintainers override the default from outside
the rule file — see params in the
configuration file reference.
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')?
Two semantic notes:
orderbyis accepted by the parser but currently has no effect in this form: results keep the collection order. To sort, use the fluentOrderBy/OrderByDescendingmethods instead.- All
letclauses are evaluated before anywhereclause, even if they are interleaved in the source text.
Example:
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>
Arguments are separated by commas. Zero-argument calls such as
.Count(), .Any(), or .ToLower() work both as steps of the
top-level fluent chain and inside expressions or lambda bodies, for
example in a where clause: m.Name.ToLower() is parsed as a method
call, not as member access.
Example:
Methods.Where(m => m.IsAsync && m.Parameters.Count > 5)
For plain filters both forms match the same entities, but they are
not fully interchangeable: only the fluent form can sort (see the
orderby note above), and when an evaluation error occurs, the LINQ
form skips just the failing item while the fluent form aborts the
whole rule without findings. With multiple where clauses the LINQ
form usually reads better. For short filter chains, fluent is often
more compact.
Expression grammar
Operator precedence from tightest to loosest binding:
- Primary: literals, identifiers,
( expr ) - Member access and method call:
a.b,a.b(args) - Unary:
!expr,-expr - Multiplicative:
*,/,% - Additive:
+,- - Comparison:
==,!=,>,>=,<,<= - Logical AND:
&& - Logical OR:
||
Literals recognised by the lexer: integers, floats, strings ("..."),
true, false.
An identifier (<id>) starts with a letter or underscore, followed by
any number of letters, digits, or underscores.
null is not a lexer literal; it is resolved as a special identifier
during evaluation, so comparisons such as t.BaseType == null are
legal.
Whitespace and comments
- Whitespace between tokens is insignificant.
- Comments start with
//and run to the end of the line.
Guarantees of the language
The DSL is deliberately small and side-effect-free:
- A query body reads the cached code model and only that.
- Every property is a read, so evaluation stays free of mutable state.
- Evaluation is synchronous over the cached code model.
If you need a predicate the catalog does not list yet, please tell us the use case rather than reaching for reflection. The DSL grows on purpose, not by accident.
Where to go next
- Predicate catalog: full list of properties available on each entity.
- Syntax overview: compact, use-case-oriented variant of this page.
- Rule examples: practical applications of the grammar.