Skip to content

First custom rule

Expressing a team-specific convention as a CodeCharter rule.

A typical case: your team has agreed that repository classes must end with Repository. So far this gets challenged in PR reviews. With CodeCharter it is enforced by the build.

Create the rule set in the repo

In the repo root:

mkdir -p rules

All .ccr files go into this directory. The CLI auto-discovers rules in a rules/ directory in the current working directory — no additional configuration is needed. If your rules live somewhere else, point the CLI at them with --rules <dir>.

Write the rule

rules/repository-naming.ccr:

@name "Repository class must end in 'Repository'"
@severity error
@category "Naming"
@description "Implementations of the Repository pattern must have a 'Repository' suffix for discoverability"
@recommendation "Rename the class to end with 'Repository', e.g. 'UserData' -> 'UserRepository'"

from t in Types
where t.Kind == "Class"
where t.Namespace.FullName.StartsWith("Acme.Infrastructure.Repositories")
where !t.IsAbstract
where !t.Name.EndsWith("Repository")
select t

Anatomy:

  • @name is the human-readable title shown in tooling. The rule slug (used in findings output and suppressions) is the filename without extension, so this rule's slug is repository-naming.
  • @severity is info, warn, or error. Defaults to warn if omitted.
  • @category groups the rule in the output (optional).
  • @description explains what is being checked (optional but recommended).
  • @recommendation says how to fix it (optional but recommended).
  • Body: a LINQ query against the schema. Here we look for classes in the repository namespace that are not abstract and whose name does not end with "Repository".

Try it locally

First check that the rule parses and type-checks:

codecharter validate rules/repository-naming.ccr

Then run the analysis. The analyze argument is the path to a .sln, .slnx, or .csproj file:

codecharter analyze MySolution.sln

If your solution contains a class named UserData in the repository namespace, it now appears as an error.

Note: Running with only a local rules/ directory and no platform profiles prints a [DEPRECATION] notice on stderr pointing to platform profiles. The analysis still runs normally.

To verify the rule actually fires, you can give it a spec with hit and miss cases: codecharter test rules/repository-naming.ccr --scaffold generates a repository-naming.spec.md sidecar next to the rule, and codecharter test rules/ runs all specs.

What else you can express

The DSL is a subset of LINQ. You can access all type, method, property, field, event, namespace, and assembly properties. Examples:

// Find methods without CancellationToken
from m in Methods
where m.IsAsync
where !m.Parameters.Any(p => p.TypeShortName == "CancellationToken")
where m.AccessModifier == "Public"
select m
// Layer violation: Domain references Web
from t in Types
where t.Namespace.FullName.StartsWith("Acme.Domain")
where t.UsedTypes.Any(u => u.Namespace.FullName.StartsWith("Acme.Web"))
select t
// Classes with too many methods
from t in Types
where t.Kind == "Class"
where t.NumberOfMethods > 25
select t

Schema overview and further details under Syntax reference.

Best practices for custom rules

  • One rule per .ccr file — this is a property of the format: each .ccr file contains exactly one query, and the file name becomes the rule ID.
  • Descriptive file names. They appear in build logs.
  • Honest @recommendation. If the recommendation is unhelpful, developers will disable the rule rather than fix the code.
  • Start small. A rule that produces 50 findings cannot be rolled out. Downgrade it to warn or write it more specifically.

More under Best practices.