Skip to content

Best Practices

What separates good custom rules from bad ones.

Writing rules that are productive in a team is a discipline of its own. Here are the most important guidelines.

1. One Rule per File

.codecharter/rules/
├── no-manager-suffix.ccr
├── controller-must-end-controller.ccr
└── domain-must-not-reference-web.ccr

Not:

.codecharter/rules/
└── all-naming-rules.ccr   # 200 lines, 12 rules inside

One @name, one severity, one responsibility per file. This keeps diffs readable and suppressions targeted.

2. Meaningful File Names

The file name appears in the build log when the rule fires. rule-42.ccr helps no one. controller-must-end-controller.ccr immediately tells you what's wrong.

Convention: kebab-case, verb phrase where possible.

3. Start Small with Severity info

Setting a new rule straight to error creates friction. Instead:

  1. Set the rule to info.
  2. Let it run for a week and see which locations it catches.
  3. Fix or suppress existing locations.
  4. Promote to warn.
  5. Once the team has established it as a convention: error.

4. Write @recommendation Honestly

@recommendation "Add 'CancellationToken cancellationToken = default' as the last parameter"

Not:

@recommendation "Fix this"

The recommendation is what the developer sees in the VS Code hover and reads in the build log. If it's useless, the whole rule is useless — devs will disable it rather than follow it.

5. Don't Duplicate Roslyn

If Microsoft.CodeAnalysis.NetAnalyzers already covers a rule (CA1822, CA2007, ...), checking it again with CodeCharter is just noise. Instead:

  • Roslyn analyzers for language-level concerns
  • CodeCharter for team-specific conventions and architecture

6. Keep Architecture Rules Specific

# Good, specific
@name "Domain layer must not reference Web layer"

from t in Types
where t.Namespace.StartsWith("Acme.Domain")
where t.UsedTypes.Any(u => u.Namespace.StartsWith("Acme.Web"))
select t
# Bad, too generic
@name "Layers must not be violated"

from t in Types
where t.UsedTypes.Any(u => true)   # what?
select t

One rule per concrete architectural relationship. If you have five layers, write five rules — don't collapse them into one.

7. Keep an Eye on Performance

Sub-queries on large collections are expensive:

# Expensive when there are 5000 types
from t in Types
where t.UsedTypes.Any(u => u.Methods.Any(m => m.IsAsync))
select t

If you need something like this, check whether you can start from the outer collection directly:

# Direct
from m in Methods
where m.IsAsync
where m.DeclaringType.UsedByTypes.Any(...)
select m

8. Prefer SyntaxIssues When Available

Instead of:

from m in Methods
where m.CalledMethods.Any(c => c.FullName == "System.DateTime.UtcNow.get")
select m

Prefer:

SyntaxIssues.Where(s => s.Kind == "DateTimeDirectUsage")

SyntaxIssues are pre-analyzed and point directly to the location. Faster, more readable, less fragile.

9. No Magic Strings for Namespaces

If you need to hard-code namespace prefixes, at least centralize them:

# In every rule:
where t.Namespace.StartsWith("Acme.Domain")
where u.Namespace.StartsWith("Acme.Web")

If you rename a namespace you'll have to update every file. There's no canonical solution for this today, but we're thinking about DSL constants for v2.

10. Suppressions with Justification

If you set require-justification = true, all suppressions must include a comment. Recommendation: enable it. Suppressions without justification leak.

Anti-Patterns

  • Rules that only have select t without any where. This produces one Finding per element in the collection, which is almost never useful.
  • Rules that match almost every file. Either the severity is wrong, or the rule doesn't fit the team.
  • Rules that only fire on a single location. That might be a suppression pattern in reverse — if you wanted to flag one specific spot, you don't need a rule for that.
  • Hundreds of suppressions on one rule. If 80% of findings are suppressed, the rule is wrong.

Reviews

Treat rule changes like code changes — put them through PR review. A new rule changes the behavior of the build system for everyone; that deserves a discussion.