Skip to content

DSL grammar

Formal reference for the CodeCharter DSL. What the parser accepts, in near-EBNF notation.

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.

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:

  • orderby is accepted by the parser but currently has no effect in this form: results keep the collection order. To sort, use the fluent OrderBy/OrderByDescending methods instead.
  • All let clauses are evaluated before any where clause, 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:

  1. Primary: literals, identifiers, ( expr )
  2. Member access and method call: a.b, a.b(args)
  3. Unary: !expr, -expr
  4. Multiplicative: *, /, %
  5. Additive: +, -
  6. Comparison: ==, !=, >, >=, <, <=
  7. Logical AND: &&
  8. 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.
  • Line comments start with // and run to the end of line.
  • There are no block comments in .ccr files.

What is intentionally not allowed

The DSL is deliberately small and side-effect-free:

  • No file IO from a query body.
  • No mutable state, every property is a read.
  • No async, evaluation is synchronous over the cached code model.

If a predicate you need is missing, please file an issue with the use case rather than reaching for reflection. The DSL grows on purpose, not by accident.

Where to go next