Epsitec.Tool.Format
1.9.2.2640
Prefix Reserved
dotnet tool install --global Epsitec.Tool.Format --version 1.9.2.2640
dotnet new tool-manifest
dotnet tool install --local Epsitec.Tool.Format --version 1.9.2.2640
#tool dotnet:?package=Epsitec.Tool.Format&version=1.9.2.2640
nuke :add-package Epsitec.Tool.Format --version 1.9.2.2640
author: Pierre Arnaud maintainer: Pierre Arnaud tags:
- technical
Epsitec Source Format Tool
epsitec-format is a dotnet tool that checks and fixes source text files
according to line-based rules. Its C# rules (comments, indent,
initializers) are a faithful port of the Python scripts in briefcases/_tools
(check_comment_truncation.py, fix_cs_indentation.py,
check_initializer_indent.py), validated against a checked-in golden oracle.
Its other rules check any text file: encoding (byte-order mark, line endings,
final newline, trailing blank lines and whitespace, driven by .editorconfig),
razor (@code blocks in Razor components) and suppressions (misplaced
warning suppressions); header checks the header of C# files (the copyright
and author lines, attribution lines, the copyright year), comment-style
their line comments (two spaces after //, no en dash or em dash, blank lines
around the Arrange, Act and Assert markers, no spec reference), and xmldoc
their XML doc comments (no doc comment on a test method, the opening of a type
summary); layout checks the line length of any text file against the
max_line_length of .editorconfig. xmldoc and layout are report-only.
The tool can also write its findings as JUnit and GitLab Code Quality reports.
Every finding carries a severity (suggestion, warning or error), read
from .editorconfig like the Roslyn ones, and the tool states its exit policy
with --fail-on and --tolerate.
The tool is purely line/text based (regex plus greedy word wrapping). It does not
use Roslyn syntax trees, semantic analysis, or MSBuild, so it starts fast and has
a minimal dependency surface. It is intentionally not part of the
Symbolic.* family.
Installation
The module root carries two scripts that build the tool from the tree and run
it, with nothing to install: epsitec-format.sh (bash) and
epsitec-format.ps1 (PowerShell 7.2 or later). The CI job and the agents use them.
./epsitec-format.sh payroll --msbuild
./epsitec-format.ps1 payroll --msbuild
- Every argument is forwarded to the tool, stdin (or the PowerShell pipeline input) included, and the tool's exit code becomes the script's.
- The scripts build
Tool.Format.csprojinReleasefirst, and send the whole build output to stderr: stdout carries only the tool's report, so--jsonand--msbuildpipes stay clean. A failed build stops the script with the build's exit code. --skip-build(bash) or-SkipBuild(PowerShell), as the first argument, runs the last build as is.- The scripts never change the working directory: the tool's root, the relative
targets and the
--changedpaths are the caller's current directory.
The root scripts stay the way to run the tool in the repository: they run the
rules of the checked-out commit. As an alternative, the tool installs as a
global dotnet tool, the package Epsitec.Tool.Format published on nuget.org,
which exposes the epsitec-format command; update moves a workstation that
installed it earlier to the latest published version:
dotnet tool install --global Epsitec.Tool.Format
dotnet tool update --global Epsitec.Tool.Format
To install a local pack instead, pack the tool, then install it (or update it, on a machine that has it) through a NuGet configuration file of your own:
dotnet pack build-tools/Tool.Format/Tool.Format.csproj -c Release
dotnet tool install --global --configfile <file> Epsitec.Tool.Format
dotnet tool update --global --configfile <file> Epsitec.Tool.Format
<file> is a minimal nuget.config, outside the repository or in a temporary
folder, whose only source is the absolute path of
build-tools/NuGet.Tool.Format/.nupkg, where the pack writes the package:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<clear />
<add key="local-format" value="/absolute/path/to/cresus-2/build-tools/NuGet.Tool.Format/.nupkg" />
</packageSources>
</configuration>
--add-source is not used on purpose: the NuGet.config files of the module
and of the bundle map every package id to nuget.org (packageSourceMapping),
which can keep an added folder from being used for Epsitec.Tool.Format. The
file above clears the sources and declares no mapping, so the ambient
configuration plays no part; the package has no dependencies, so no other
source is needed.
Both commands name the package id, Epsitec.Tool.Format, not the command. To
uninstall:
dotnet tool uninstall --global Epsitec.Tool.Format
Publishing
Run build-tools/NuGet.Tool.Format/zou-nuget.ps1 to pack the tool in Release
and publish it to nuget.org:
$env:NUGET_API_KEY = '<key>'
build-tools/NuGet.Tool.Format/zou-nuget.ps1
It packs Tool.Format.csproj (a PackAsTool project) to
build-tools/NuGet.Tool.Format/.nupkg/, prints the resulting
Epsitec.Tool.Format package (the dotnet tool exposing the epsitec-format
command), and pushes it with dotnet nuget push (API key from
$env:NUGET_API_KEY) without asking. It mirrors the zou-nuget.ps1 scripts
used by the other Epsitec NuGet packages; build-tools/NuGet.Tool.Format/README.md
describes the packaging folder and how to check a package without publishing
it.
After a successful push only, it moves the pending rules of the
Format.Engine release tables (see Diagnostics) from
AnalyzerReleases.Unshipped.md to AnalyzerReleases.Shipped.md, under the
version read from Version.props, and records the move in a maintenance commit
build(format): publish Epsitec.Tool.Format v<version>. A failed push leaves
the tables unchanged. The commit is never pushed: the publisher reviews and
pushes it.
Usage
epsitec-format [<targets>... | --changed <file>|-]
[--fix]
[--rules comments,indent,initializers,encoding,razor,suppressions,header,comment-style,xmldoc,layout]
[--ids|--include-ids FMT0001,...]
[--exclude-ids FMT0001,...]
[--update-year [--year <N>]]
[--extensions <list>]
[--max-columns <N>]
[--json | --msbuild | --summary]
[--fail-on none|unfixable|any]
[--tolerate none|suggestion|warning|error]
[--junit <file>]
[--codequality <file>]
Options
<targets>— zero or more files, directories, or glob patterns (*,**). Defaults to the current directory (.). Directories are scanned recursively for the files at least one active rule accepts (see Rule catalog). A path is skipped when one of its segments isbin,obj,.git,.vs,node_modules,TestResults,artifacts,.nupkgorMicrosoft.CodeAnalysis.Razor.Compiler(the output of the Razor source generator, emitted underGenerated/), or when it lies under a directory excluded by a.fmtignoremarker (see Excluding a directory). Glob expansion usesMicrosoft.Extensions.FileSystemGlobbing.- A file named explicitly (a literal path, a glob match or a
--changedentry) that no active rule accepts is not processed and is reported as ignored, as is a file that is not valid UTF-8 or that contains a NUL byte (see I/O contract). The human,--msbuildand--summarymodes print one line per ignored file on stderr, before the report:Note: ignored {file}: {reason}; stdout is unchanged. The JSON report lists them inignoredFiles. Ignored files never change the exit code. --changed <file>or--changed -— check only the files listed in<file>(relative to the current directory or absolute), or on stdin with-. The list holds one path per line, relative to the current directory or absolute; blank lines are ignored and surrounding whitespace is trimmed. Stdin is decoded as UTF-8, the encoding git writes, whatever the console code page. Listed paths are literal (no glob expansion). A listed path that names no existing file, a file deleted in the diff or a directory, is ignored without error. An empty list checks nothing (no fallback to.) and exits0.--changedreplaces the targets and cannot be combined with them. It also enables the diff-reach kinds (see Diagnostics): a run on a change set reports every kind.--fix— apply fixable rules in place. Absent means check mode (no writes).--rules— comma-separated subset ofcomments,indent,initializers,encoding,razor,suppressions,header,comment-style,xmldoc,layout. Matching is case-insensitive; duplicates are collapsed preserving first-seen order. Absent means all ten in canonical order. An unknown token, or an empty value, is an input error.--ids— comma-separated subset ofFMTidentifiers (see Diagnostics), for example--ids FMT0002,FMT0003. Only issues of these identifiers are reported and, with--fix, applied: the other fixable issues are left in the source untouched. Matching is case-insensitive and duplicates are collapsed. Absent means every identifier within the reach of the run. An identifier named by--idsis reported whatever its reach, even without--changed. An unknown identifier, or an empty value, is an input error.--idscombines with--rules: an issue must pass both.--include-idsis another name of--ids: the two spellings name one option, which takes one value, and the error messages say--ids; giving both is a parse error (see Exit codes). Adotnet_diagnostic.<id>.severity = nonein.editorconfigwins over--ids: the identifier stays off for the files its section matches (see Severities in.editorconfig).--exclude-ids— comma-separatedFMTidentifiers to leave out of the run, for example--exclude-ids FMT0024,FMT0026, parsed like--ids: matching is case-insensitive, duplicates are collapsed, and an unknown identifier or an empty value is an input error. It applies after--ids: to the--idsselection, or, without--ids, to every identifier within the reach of the run. The excluded issues are neither reported nor applied. An exclusion that leaves nothing selected (--ids FMT0003 --exclude-ids FMT0003) is valid: the run reads the files, reports nothing and exits0under every--fail-onpolicy. Excluding an identifier outside the reach of the run (a diff-reach identifier without--changed) changes nothing. The JSONidslists the selection without the excluded identifiers.--exclude-idscombines with--rules,--changed,--fix,--fail-onand the reports like--ids: an issue must pass--rules, the selection, the exclusion and the configuration of its file.--update-year— check the copyright year of the changed files against the current year of the local clock, and bring it up to date with--fix(FMT0019, seeheader). It requires--changedand theheaderrule; with--ids, the selection must holdFMT0019, and--exclude-idsmust not name it:Error: --update-year requires FMT0019, which --exclude-ids removes.Without--update-year,FMT0019is never produced, and naming it in--idsis an input error; naming it in--exclude-idsis accepted and changes nothing.--year <N>— the year--update-yearuses instead of the local clock, for a run that straddles New Year: exactly four ASCII digits. It requires--update-year.--extensions— comma-separated file extensions theencodingandlayoutrules check for this run, replacing their default list (seeencodingandlayout), for example--extensions .md,.txt. Each token is trimmed, given a leading dot when it has none, and lowercased; duplicates are collapsed. An empty value, a token that is.alone or contains/,\,*,?or whitespace, and--extensionswhile the active rules do not includeencoding, are input errors.--max-columns— positive integer wrap width of comment prose, for every file of the run: it wins over theepsitec_comment_line_lengthof.editorconfig. Absent means the width each file's configuration sets, else79(seecomments). Affects comment prose reflow only; separator normalization always targets 79 columns.--json— emit the structured JSON report on stdout. Absent means a human-readable report: one line per issue,{file}:{startLine}-{endLine} {severity} {id} {kind}, or{file} {severity} {id} {kind}for a file-level issue (see JSON report schema), then a summary line.{severity}issuggestion,warningorerror(see Severities in.editorconfig).--msbuild— emit one build-tool diagnostic line per issue on stdout,{absolute path}({startLine},1): {category} {id}: {title}, followed by a space and({detail})when the issue carries a detail; a file-level issue points at line1.{category}isinfofor a suggestion,warningorerrorotherwise, the three categories that editors and build tools read. No summary line. In fix mode, only the issues left unfixed are printed.--summary— emit only the number of issues per identifier and severity, one line perFMTid and severity found, in identifier order, the highest severity first ({id} {severity} {count} {title}, the severity padded to the longest one printed), then the summary line; no file is listed. An identifier that the configuration grades differently from one file to the next gets one line per severity. In fix mode, only the issues left unfixed are counted.--json,--msbuildand--summaryare mutually exclusive.--fail-on none|unfixable|any— which issues left in the source make the run exit1(see Exit codes): none, the report-only ones (unfixable), or any, among those whose severity is above the--toleratelevel. The value is trimmed and matched case-insensitively. Absent meansanyin check mode andnonein fix mode. Any other value, the empty one included, is an input error.--tolerate none|suggestion|warning|error— the highest severity that a successful run tolerates: an issue that--fail-onselects makes the run exit1only when its severity is above this level.nonetolerates no issue,suggestionfails on warnings and errors,warningfails on errors only, anderrornever fails. The value is trimmed and matched case-insensitively. Absent meanswarning: by default, a run fails only when an issue of severityerrorremains. Any other value (info,silent,default, the empty one) is an input error.--junit <file>— also write a JUnit XML report of the issues to this file;--codequality <file>— also write a GitLab Code Quality report of the issues to this file (see Reports for GitLab). Both combine with every stdout mode, which they leave unchanged, and with--fix,--changedand--fail-on. A relative path resolves against the current directory, and a missing parent directory is created. An empty value, and the two options naming the same file (compared case-insensitively on Windows, ordinally elsewhere), are input errors.
Excluding a directory
A file named .fmtignore placed in a directory excludes that directory and all
its subdirectories from every run, whatever brings a file in: directory scan,
glob, literal path or --changed list. Its content is ignored; a comment
saying why the directory is excluded is welcome. The tool looks for the marker
in the file's directory and its ancestors, up to the root of the git working
tree (the first directory holding a .git entry): a marker above the working
tree never applies. build-tools/Format.Engine.Tests/Corpus and
build-tools/Format.Engine.Tests/CommentStyleCorpus carry one, their fixtures
holding formatting issues on purpose; so do the official fixed-width
tariff records of payroll/Payroll.IsTariff/Assets and
payroll/Payroll.IsTariff.Tests/Assets, the three vendored font packages of
ui/DesignSystem/fonts (fontawesome, open-sans, courier-prime; the
house-authored fonts.css stays checked) and the vendored model of
edm/Edm.Processing/All-MiniLM-L6-v2. The Assets folders of the test
projects carry one too, their files being data the tests compare byte for
byte, as do the rename fixtures of build-tools/Symbolic.Engine.Tests/Fixtures
and the sources adapted from the .NET Foundation in
services/Historage.EntityFramework/Fix.
A marker placed under a folder that a project's Content glob copies or
publishes would ship with it: that glob needs an Exclude="...\**\.fmtignore",
as in Payroll.IsTariff.csproj, Edm.Processing.csproj and
Views.Assets.csproj. .gitattributes marks .fmtignore as export-ignore,
which keeps the markers out of git archive (the design-system Pages job
publishes through it).
.fmtignore excludes a directory from every rule. To change one encoding
expectation for some paths, use the standard .editorconfig keys instead: the
root .editorconfig sets trim_trailing_whitespace = false on Naboo.txt and
assets/l10n/*.txt (a translated text may end with a space that belongs to
it), and charset = utf-8-bom on services/DataIO.Tests/Assets/bulletins.csv
(its importer decodes it correctly only through its BOM). To turn one
identifier off for some paths, or to change the severity of its findings, see
Severities in .editorconfig.
Suppression directives
A few lines of a .cs file can stand verbatim, as Roslyn's formatting
analyser (IDE0055, dotnet format) lets them stand: a diagram, an aligned
table.
#pragma warning disable format
// baseSum ------------------o
// o
// cumulated bound ----------x
#pragma warning restore format
#pragma warning disable format, or a bare#pragma warning disable, withdraws every identifier from the lines up to the next#pragma warning restore formator barerestore, except the identifiers of thesuppressionsrule (FMT0011toFMT0013), which keep checking the directives themselves.#pragma warning disable FMT0026(or several identifiers, comma-separated) withdraws only the identifiers it names, up to the nextrestorethat names them or names nothing. Arestore formatdoes not close it. Other identifiers (CA1822,IDE0055...) are ignored by the tool.- A region holds the lines strictly between its two directives; without a
restore, it runs to the end of the file, and thesuppressionsrule reports the unrestoreddisable(FMT0012). Identifiers compare case-insensitively. - On the lines of a region, check mode reports nothing of the identifiers it
withdraws and
--fixrewrites nothing. A finding that spans lines inside and outside the region is dropped as a whole. A directive line is not a comment, so it always ends a comment block. File-level findings (byte-order mark, line endings, final newline) are not affected. - A
#pragmaline in the content of a multi-line raw string is test data, not a directive.#ifnesting is not taken into account.
The C# style guide keeps such a pair for the text that must stand as written, at the narrowest scope that covers it.
Severities in .editorconfig
The Roslyn key dotnet_diagnostic.<id>.severity sets the severity of an FMT
identifier for the files its .editorconfig section matches, or turns the
identifier off for them:
[*.cs]
# The type summary convention is not enforced yet on the existing code.
dotnet_diagnostic.FMT0025.severity = none
# An attribution line is never acceptable.
dotnet_diagnostic.FMT0017.severity = error
dotnet_diagnostic.FMT0018.severity = error
[payroll/Payroll.Calculations.Tests/**.cs]
dotnet_diagnostic.FMT0024.severity = suggestion # doc comments on test methods, catch-up pending
[payroll/Payroll.Calculations.Tests/Legacy/**.cs]
dotnet_diagnostic.FMT0024.severity = none
[payroll/Payroll.Calculations.Tests/Legacy/Kept/**.cs]
dotnet_diagnostic.FMT0024.severity = default # back to the catalog default
| Value | Effect on the identifier, for the files of the section |
|---|---|
none, silent, hidden |
Turned off: its issues are neither reported nor applied. |
suggestion |
Reported with the severity suggestion, info for build tools. |
warning |
Reported with the severity warning. |
error |
Reported with the severity error. |
default, unset, any other value |
The default of the catalog entry, warning for every identifier (see Diagnostics). |
- The sections are the usual
.editorconfigglobs, resolved like every key the tool reads (seeencoding): the value that counts is the one the chain of.editorconfigfiles gives the file. A later section that sets another value on the same key replaces the earlier one for the files it matches, as in the sketch above. silentandhiddenturn the identifier off, asnonedoes: Roslyn keeps asilentdiagnostic out of the build output and out of the error list, and the tool has no other place to show it.- The severity travels with each issue: the human line, the
--msbuildline, the--summaryrows, the JSONseverityfield and the GitLab reports show it (see Reports for GitLab). It decides the exit code with--fail-onand--tolerate(see Exit codes). It never changes what a fix run applies: a fixable issue is applied whatever its severity. - The key, the identifier and the value match case-insensitively
(
dotnet_diagnostic.fmt0003.severity = NONEcounts). A trailing comment (none # reason,error ; reason) does not change the reading. - The file is shared with Roslyn: a non-
FMTidentifier (IDE0005,CA1859) and an unknownFMTidentifier (FMT9999) are ignored without a message. In turn, Roslyn ignores a key whose identifier no analyzer reports: the build anddotnet formatreport nothing for anFMTkey. - An identifier turned off is neither reported nor applied for the file, in
check and fix mode, for line and file-level issues alike. Turning a kind off
does not promise that the file keeps its layout byte for byte, though: the
exceptions of the I/O contract still apply. A file with mixed line endings
that another fix rewrites gets its dominant ending even with
FMT0006turned off, and an appliedFMT0008fix leaves a final newline even withFMT0007turned off. - Turning off wins over
--idsand--include-ids:--ids FMT0024reports noFMT0024in a file whose section turns it off. To audit an identifier turned off, set another value on its key, or run where the key does not apply. With--update-year, a file whose configuration turnsFMT0019off gets no year check and no year fix. - A file whose every identifier is turned off is still matched, read and listed
in
matchedFiles; only.fmtignorekeeps a directory out of a run (see Excluding a directory). - The JSON
idslists the identifiers of the run (--ids,--exclude-ids, the reach); the per-file settings do not change it.
Examples
Check the current directory and print a human summary:
epsitec-format .
Emit the JSON report for one project:
epsitec-format MyProject --json
Apply fixes in place across a glob, then re-check (idempotent):
epsitec-format "MyProject/**/*.cs" --fix
epsitec-format "MyProject/**/*.cs"
Fix only the tab indentation (FMT0003), leaving comment reflows untouched:
./epsitec-format.sh payroll --ids FMT0003 --fix
Count the issues of the whole module per identifier, without listing files:
./epsitec-format.sh build-tools data edm integrations libs payroll services ui welcome --summary
Remove the UTF-8 byte-order marks wherever .editorconfig asks for
charset = utf-8 (this replaces the former remove-bom.ps1 script):
./epsitec-format.sh services/DataIO --rules encoding --ids FMT0005 --fix
Check, for one run, the files the default extension list leaves out:
./epsitec-format.sh . --rules encoding --extensions .gitignore,.gitattributes,.editorconfig --summary
Check only the C# files of the current branch, as build-tool warnings, the diff-reach kinds (the file headers, the comment style, the XML doc comments and the line length) included:
git diff --name-only --diff-filter=ACMR master...HEAD -- '*.cs' | ./epsitec-format.sh --changed - --msbuild
Fix the files about to be committed and bring their copyright year up to date,
as spec-coder does before each commit:
printf '%s\n' payroll/Payroll.Calculations/CalculationEngine.cs | ./epsitec-format.sh --changed - --fix --update-year
Audit the header form of a folder, whatever the reach:
./epsitec-format.sh welcome --ids FMT0014,FMT0015,FMT0016 --summary
Audit the comment style of a folder, whatever the reach:
./epsitec-format.sh payroll --ids FMT0020,FMT0021,FMT0022,FMT0023 --summary
Catch up the comment style of one project mechanically: --ids selects the
diff-reach kinds in a full scan.
./epsitec-format.sh payroll/Payroll.Calculations.Tests --ids FMT0020,FMT0021,FMT0022 --fix
Audit the doc comments and the long lines of a folder, whatever the reach:
./epsitec-format.sh ui --ids FMT0024,FMT0025,FMT0026 --summary
Check the doc comments and the line length of the files of a branch only:
git diff --name-only --diff-filter=ACMR master...HEAD -- '*.cs' | ./epsitec-format.sh --changed - --rules xmldoc,layout --msbuild
Write the JUnit and Code Quality reports of a local run under artifacts/, a
folder the tool never scans and git ignores, so that a zou commit after the
run does not stage them:
./epsitec-format.sh payroll --summary --junit artifacts/epsitec-format.xml --codequality artifacts/epsitec-format.codequality.json
Check every identifier within the reach except the report-only initializer check:
./epsitec-format.sh payroll --exclude-ids FMT0004 --summary
Run the agents' branch check without the line length:
git diff --name-only --diff-filter=ACMR master...HEAD | ./epsitec-format.sh --changed - --exclude-ids FMT0026 --msbuild
--include-ids is --ids, and the exclusion applies after it: this run checks
FMT0024 only.
./epsitec-format.sh ui --include-ids FMT0024,FMT0025 --exclude-ids FMT0025 --summary
The year cannot be both checked and excluded; this run exits 2:
./epsitec-format.sh --changed changed.txt --update-year --exclude-ids FMT0019
Error: --update-year requires FMT0019, which --exclude-ids removes.
Fail a fix run when it leaves a report-only finding, whatever its severity, and inform only in a check run:
./epsitec-format.sh payroll --fix --fail-on unfixable --tolerate none
./epsitec-format.sh payroll --summary --fail-on none
Fail on warnings too, as the CI job does, or on any finding, a suggestion included:
./epsitec-format.sh payroll --summary --tolerate suggestion
./epsitec-format.sh payroll --summary --tolerate none
Fail a fix run only when it leaves a report-only finding graded error:
./epsitec-format.sh payroll --fix --fail-on unfixable
Rule catalog
comments
Reflows contiguous // and /// comment prose runs so each line is as full as
possible without exceeding the configured width, and normalizes
/*****…*****/ separator lines to exactly 79 visual columns. Produces two issue
kinds: comment_prose_reflow (FMT0001) and separator_reflow (FMT0002).
The following comment shapes are deliberately preserved (never reflowed):
- the file's first-line copyright block (the topmost block starting at line 1);
- any block containing an XML
<code> … </code>snippet; - ASCII-table and drawing blocks (aligned columns, rulers, spacious layouts);
- JSON-style comment snippets delimited by
{ … }; - standalone and inline XML-doc element lines (
/// <summary>,/// <param …>…</param>); // var …code-like comment lines;- decorative separators, bare labels, list markers, AAA markers (
// Arrange), headers (Copyright,Author:), and lines containing a URL; - oversized lines whose final token is an XML element (optionally followed by
.or,) or that end with;.
In addition, reflows are suppressed when they are negligible (every break column moves by 4 columns or less) or when every existing line already fits within the configured width plus a 10-column relaxed margin. Generated files are skipped entirely (see Generated code).
The wrap width of the prose is, in this order: the --max-columns of the run;
else the epsitec_comment_line_length that .editorconfig sets for the file,
a house key that no other tool reads; else 79. The root .editorconfig sets
epsitec_comment_line_length = 79 in [*.cs], so that the width is a visible
setting rather than a constant of the tool. Only a value made of ASCII digits,
greater than zero, sets a width; off, 0, unset or any other value sets
none, as an absent key does, and the key matches case-insensitively. The
relaxed margin (10 columns) applies to the width in force. The width of the
separators is not configurable: they stay at 79 columns. max_line_length is
another key, read by layout only: the wrap width of comments and the
longest line of code are two settings.
[*.cs]
max_line_length = 90
epsitec_comment_line_length = 79
comments does not check the two spaces after //, nor the dashes and the
blank lines around the AAA markers: see comment-style.
indent
Replaces tab-based leading indentation with spaces, using a tab width of 4
(next-multiple-of-4 tab stops). Only the leading whitespace run is expanded; tabs
elsewhere on the line are left untouched. Each changed line is one
tab_indentation issue (FMT0003).
initializers
Reports object-initializer braces indented deeper than their owning
var … = new … line. Report-only: these issues are never auto-fixed, even
with --fix. Each issue is one initializer_indent finding (FMT0004)
carrying a brace indent N, expected M detail. Gate CI on these with check
mode.
encoding
Checks the encoding and layout of text files against their .editorconfig
properties, read with the editorconfig package (every .editorconfig from
the file's directory up to the first root = true). Every kind is fixable.
| Kind | Id | Scope | Reported when | Fix |
|---|---|---|---|---|
byte_order_mark |
FMT0005 |
file | charset = utf-8 and the file has a UTF-8 BOM, or charset = utf-8-bom and it has none. |
Removes or adds the BOM. |
end_of_line |
FMT0006 |
file | end_of_line is lf or crlf, the file holds at least one line terminator, and its terminators differ from the expected one or are mixed. |
Writes every line with the expected terminator. |
missing_final_newline |
FMT0007 |
file | insert_final_newline = true and a non-empty file does not end with a newline. |
Adds the final newline; in a file without any line terminator, it takes the expected end_of_line. |
trailing_blank_lines |
FMT0008 |
line | The file ends with blank lines (empty or whitespace-only) after its last non-blank line; one issue over the range. A house rule, independent of .editorconfig. |
Removes the range; the last line kept ends with a newline. |
trailing_whitespace |
FMT0009 |
line | trim_trailing_whitespace = true and a line ends with whitespace; one issue per line. |
Trims the line. |
- The rule accepts the files whose extension is in the default list, unless
--extensionsreplaces it for the run:.cs,.csproj,.props,.targets,.slnx,.razor,.json,.md,.txt,.xml,.xsl,.csv,.yml,.ps1,.sh,.js,.ts,.css,.html. Thelayoutrule accepts the same list. - Only
charset = utf-8andutf-8-bom, andend_of_line = lfandcrlf, are checked; any other value (latin1,utf-16be,utf-16le,cr,unset) means "no check", and so doesfalseforinsert_final_newlineortrim_trailing_whitespace. - Trailing whitespace is what
string.TrimEnd ()removes; a lone\rat the end of a line (not followed by\n) is trailing whitespace. - A whitespace-only line of a trailing blank range raises
FMT0008only, notFMT0009as well, when both kinds are kept. WhenFMT0008is left out (--ids FMT0009,--exclude-ids FMT0008, or.editorconfig), such a line raisesFMT0009. - In Markdown, a line ending with two spaces is a hard line break.
trim_trailing_whitespace = truereports it asFMT0009, consistently with the house Markdown style, and--fixremoves the two spaces, so the two lines then render as one paragraph line. A hard break that must stay is written with another construct: a separate paragraph or list item.
razor
Reports, one razor_code_block issue (FMT0010) per line, the @code and
@functions directives of .razor files (^\s*@(code|functions)\b: alone on
their line, followed by spaces, or directly by {). Members belong in a
.razor.cs code-behind partial; @{ … } render blocks are not reported.
Report-only. Known limitation: a directive inside a multi-line Razor
comment @* … *@ is reported.
suppressions
Reports misplaced warning suppressions. Report-only. It accepts .cs files
and files named exactly .editorconfig.
editorconfig_location(FMT0011, file-level): an.editorconfigfile whose directory is not the module root, the directory holding.git. Analyser severities are decided in the root file only. Outside a working tree, nothing is reported.pragma_without_restore(FMT0012): a#pragma warning disablethat a laterrestorenever matches, with the detailunrestored: X, Y(orunrestored: allfor a baredisable). Adisableof a warning is matched by any laterrestorethat names it or that names nothing; a baredisableis matched only by a later barerestore. Identifiers compare case-insensitively, and a trailing//comment is not an identifier.pragma_without_ids(FMT0013): a#pragma warning disablethat names no warning.
Known limitations: the rule ignores #if nesting, and a line of a multi-line
verbatim string that starts with #pragma is read as a directive. The content
of a multi-line raw string is never read as a directive. The regions the
directives delimit are described in Suppression directives.
header
Checks the header of .cs files: the two lines the C# style guide requires
(.github/instructions/csharp-style.instructions.md), the ban on attribution
lines, and, on request, the copyright year.
Header block. The run of lines, from line 1, that start with // and not
with ///. It ends at the first other line: a blank line, code, a /// line
or an indented comment. It is the topmost block that comments never reflows.
The attribution lines of the block (see FMT0017) are set aside before the
form is checked, so that deleting them never changes the verdict of the form
kinds; the other lines, in order, are the effective header.
Copyright line. The first line of the effective header, right-trimmed, must match, ordinally:
^// Copyright © (?<start>[0-9]{4})(?:-(?<end>[0-9]{4}))?, EPSITEC SA, CH-1400 Yverdon-les-Bains, Switzerland$
// is followed by exactly two spaces, never a tab or a single space.
Author line. The second line of the effective header, right-trimmed, is checked in this order; the first failed check gives the detail:
- it starts with
// Author:or// Authors:, followed by one space. Both labels are accepted, whatever the number of names, in every folder; - after the authors, it holds
, Maintainer:or, Maintainers:, followed by one space. The first occurrence of either separates the authors from the maintainers; the two labels are chosen independently; - each list is split into names on a comma followed by one space, or on an
ampersand between two single spaces (
A, B,A & BandA, B & Calike), and each name on single spaces into words. Every name then goes through checks 4 to 7, the authors first, each list in order; - no word of the name is
&, and no word isetorand, ignoring case; - the name is at least two words separated by single spaces, each word made of letters with inner hyphens or apostrophes: an empty name, initials, a single word or any other character fails here;
- the name ends with a non-empty run of words entirely in upper case (the surname, accents and hyphens included);
- no word before that run is entirely in upper case.
| Author line | Verdict |
|---|---|
// Author: Pierre ARNAUD, Maintainer: Pierre ARNAUD |
Accepted. |
// Authors: Pierre ARNAUD, Catia GUIDI, Maintainers: Pierre ARNAUD |
Accepted. |
// Authors: Jonny QUARTA & Gillian FRIES, Maintainers: Jonny QUARTA |
Accepted. |
// Author: Jean-Marc DE LA FONTAINE, Maintainer: Ludwig van BEETHOVEN |
Accepted: DE LA FONTAINE and BEETHOVEN are the surnames. |
// Authors: Zoë MÜLLER-ÉTIENNE, Maintainer: Jeanne D'ARC |
Accepted. |
// By: Pierre ARNAUD, Maintainer: Pierre ARNAUD |
Check 1. |
// Author: Pascal ZWEILIN, Catia GUIDI Maintainer: Pascal ZWEILIN |
Check 2: no comma before the label. |
// Author: Pierre ARNAUD et Catia GUIDI, Maintainer: Pierre ARNAUD |
Check 4: et. |
// Author: GF, Maintainer: GF |
Check 5: initials. |
// Author: COPILOT, Maintainer: Pascal ZWEILIN |
Check 5: a single word. |
// Author: Jonny Quarta, Maintainer: Jonny QUARTA |
Check 6: mixed-case surname. |
// Author: Claude Code, Maintainer: Pierre ARNAUD |
Check 6. |
// Author: Pierre ARNAUD Catia GUIDI, Maintainer: Pierre ARNAUD |
Check 7: two names without a separator. |
The rule keeps no list of names: a tool name in mixed case fails check 6 and a single-word one check 5, while an upper-case tool name of two words passes and is left to human review.
| Id | Kind | Reach | Scope | Reported when | Fix |
|---|---|---|---|---|---|
FMT0014 |
header_missing |
diff | file | The effective header is empty, its first line is not a copyright line, or it holds a copyright line and nothing after it. Detail: expected the copyright line first or expected the author line after the copyright line. |
None. |
FMT0015 |
header_author |
diff | line | FMT0014 is not raised and the second line of the effective header breaks the author grammar. Detail: the first failed check. |
None. |
FMT0016 |
header_extra_lines |
diff | line | FMT0014 is not raised and the effective header has more than two lines, an empty // included; one issue per run of consecutive lines, detail {n} extra line(s). |
None. |
FMT0017 |
header_attribution |
tree | line | A line of the header block matches, ignoring case, ^//\s*(?:assisted-by\|co-authored-by\|signed-off-by)\s*:. |
Deletes the line. |
FMT0018 |
source_attribution |
tree | line | Any other line matches, ignoring case, ^\s*(?://\|/\*\|\*).*\b(?:assisted-by\|co-authored-by\|signed-off-by)\s*:: a whole-line comment (//, ///, the first line of a /* */ comment or a continuation line starting with *) that carries one of the tokens followed by a colon. |
None. |
FMT0019 |
copyright_year |
diff | line | A year is given (--update-year) and the upper bound of the copyright line (its second year, else its only one) is older. Detail: 2024 -> 2024-2026 or 2020-2024 -> 2020-2026. |
Rewrites the years as <start>-<year>. |
- A file with no recognizable header gets
FMT0014alone among the form kinds, neverFMT0015orFMT0016on top of it; a tab or a single space after//is such a file.FMT0019reads the copyright line only: a canonical copyright line with nothing after it raisesFMT0014and, when the year is older,FMT0019. - The year is updated only on a canonical copyright line and never goes
backwards: a file whose upper bound is the given year, or a later one, is
unchanged. The rewritten line drops its trailing whitespace. The year is
checked only with
--update-year, which requires--changed(see Options). - A mention of a token without the colon (
never add Assisted-By trailers) is not reported. - Generated files are exempt from every kind of the rule. A file named
GlobalSuppressions.cscarries no header: it gets neitherFMT0014,FMT0015,FMT0016norFMT0019, and still getsFMT0017andFMT0018. Any other file, a file holding only[assembly: …]attributes included, carries the header. - Fix order:
headerapplies its fixes afterencodingandindent, on trimmed and re-indented lines, and beforecomments, which then works on the header as fixed. Both fixable kinds edit lines of the header block only, the blockcommentsnever reflows, so no line is proposed by both rules.
Known limitations:
- Outside the header block, only whole-line comments are read: a trailing
comment after code and a string literal are not, while a line of a multi-line
raw or verbatim string that starts with
//or*is read as a comment, and a continuation line of a/* … */comment that does not start with*is not read. - Only
.csfiles are read: an attribution line in a.razor,.ps1or.shfile is not reported. No option exempts a legitimate mention of a token followed by a colon (a comment of a commit-message checker, for example); sinceFMT0018has the tree reach, such a comment is a finding of every run, and is to be reworded. - Names are not normalized: a name saved with a decomposed accent (
Efollowed by U+0301) or written with the typographic apostrophe U+2019 fails check 5. - An upper-case tool name of two words passes the grammar.
- In fix mode, the report-only issues of the rule (
FMT0015,FMT0016,FMT0018) carry the line numbers read before the rule's own deletions: when attribution lines of the header block are deleted above such an issue, the written file holds it that many lines higher. The check-mode rerun gives the final positions. - When the header block holds only attribution lines and a comment block
follows it directly (a
///block, or an indented//block), that block starts on line 2 in check mode, wherecommentsmay report it, and on line 1 once the deletion is applied in fix mode, wherecommentsskips it as the topmost block. Check mode and fix mode then disagree for that run; the file has no header either, and a diff-reach run reportsFMT0014on it.
comment-style
Checks the whole-line comments of .cs files against the C# style guide
(.github/instructions/csharp-style.instructions.md): two spaces after //,
no en dash or em dash in a comment, a blank line around each Arrange, Act or
Assert marker, and no spec reference anywhere in the file. The rule has no
Python counterpart; it is separate from comments, whose parity with the
Python reference stays exact.
Scanned lines. The three fixable kinds never read nor rewrite two sets of lines:
- the header block (see
header), which theheaderrule owns; - the content of a multi-line raw string literal, which holds test data or
generator output. A raw string opens on a line that is not a comment, whose
right-trimmed text ends with a run of three or more
", and that holds no other such run. The lines that follow are its content, up to the first line whose left-trimmed text starts with as many", the closing line. The quotes of a closing line never open a literal, but the rest of the line, read the same way, can open the next one (""" + x + """). A single-line raw string ("""a""") opens nothing.
FMT0023 reads every line, those two sets included.
| Id | Kind | Reach | Scope | Reported when | Fix |
|---|---|---|---|---|---|
FMT0020 |
comment_gap |
diff | line | A // line whose // is followed by exactly one space, then text, outside the exclusions below. |
Writes two spaces after //. |
FMT0021 |
comment_dash |
diff | line | A line whose first non-blank characters are // (//, /// and //// alike) holds U+2013, U+2014 or one of the entities –, —, –, —, –, — (the x in either case); one issue per line. |
Replaces each occurrence with - and keeps the characters around it: a — b becomes a - b, a—b becomes a-b. |
FMT0022 |
aaa_blank_line |
diff | line | A blank line is missing before a marker or after the comment block of a marker; one issue per missing blank line, on the line it must precede. Detail: expected a blank line before the marker or expected a blank line after the comment block of the marker on line <m>. |
Inserts the blank line. |
FMT0023 |
spec_reference |
tree | line | The line matches \bSPEC-[0-9]{3}-[0-9]{3}\b, case-sensitive, in a comment, a string literal or anywhere else; one issue per line, detail the distinct references of the line. |
None. |
FMT0020. A /// line, a //// line, a bare //, a // followed by two
or more spaces, by a tab or by no space, and a trailing comment after code are
never candidates. A candidate is not reported when its text:
- starts with three characters of the separator set:
-,=,*,_,~,+,#,/and the box-drawing characters U+2500–U+257F (// ------,// ────,// ====== Title);// --locale …stays a candidate. The test reads the line as read, so// ——— Titlegets the two spaces, then its dashes, in the same run; - starts with
<auto-generatedor</auto-generated, ignoring case; - reads as commented-out code: it starts with
varand a space, ends with;or{, starts with}, starts with a call-shaped keyword (if (,else if (,foreach (,for (,while (,switch (,using (,catch (,lock (, spaces allowed before the parenthesis), or starts withthis..
An AAA marker written // Act is a candidate like any prose line.
FMT0021. Every //, /// and //// line outside the header block and
raw-string content is read, XML doc <code> samples and commented-out code
included: the exclusions of commented-out code and separators belong to
FMT0020 only. The layout of a /// block, the one comments produces, does
not change. Replacing a dash character keeps every column, so ASCII tables stay
aligned, while replacing an entity shortens the line. &mdash; holds no
entity and is left alone; the other dashes (U+2012 figure dash, U+2015
horizontal bar, U+2212 minus sign) and -- are not reported.
FMT0022. A marker is a // line outside the header block and
raw-string content, that is line 1 or follows a line that is not a comment
(a line whose first non-blank characters are not //), and whose text starts
with Arrange, Act or Assert followed by the end of the line, a space, a
tab, :, &, /, , or -, case-sensitive. This covers the bare markers,
the combined forms (// Act & Assert, // Arrange / Act) and the annotated
forms (// Act: re-extract., // Arrange (see …)); it rejects
// Assert.AreEqual (…), ActivitySource and every /// line. The comment
block of a marker is the marker and the comment lines that directly follow it.
A blank line (empty or whitespace-only) is required:
- before a marker, unless it is on line 1 or the line before it ends with
{; - after the comment block of a marker, unless the block ends the file or the
line after it starts with
}. A comment line after the marker never stands in for the blank line: the blank line goes after the last line of the block, never between the marker and its explanation.
Several issues on one line. One line can carry a gap issue, a dash issue
and a blank-line insertion. The rule emits them in that order and chains them:
each issue starts from the line as the earlier selected issues of that line
leave it, and the marker test reads that same text. // Act—call after a code
line therefore gets its gap, its dash and its blank lines in one run
(// Act-call). Fix mode applies the issues of one line in emission order. A
kind that the run does not select (see --ids) is not emitted, so it never
enters the chain.
Fix order. comment-style applies its fixes after encoding, indent and
header, and before comments. It never rewrites nor inserts in the header
block, keeps the indentation of every line, inserts only empty lines, never at
the end of the file, and writes no trailing whitespace, so it creates no
finding for a rule that ran before it. It runs before comments because a gap
fix lengthens a line by one column: a run whose longest line had 89 columns
then has one of 90, beyond the relaxed margin, and comments reflows it in the
same fix run, with the two spaces of the run's first line. comments only
moves words: it adds no dash and no blank line, and a line it starts with
Act inside a run follows a comment line, so it is never a marker.
Generated files are exempt from every kind of the rule.
Known limitations:
- Multi-line verbatim strings (
@"…"over several lines) and/* */comments are not tracked: a//line inside a multi-line verbatim string is read as a comment, and can be rewritten. - A line that ends with a run of three quotes without opening a raw string
literal (a verbatim string ending with an escaped quote,
@"A ""text""", or a trailing comment ending with""") is read as a raw-string opening: the following lines go unreported until a line starts with the same run of quotes, and from a real closing line met that way on, the content of that real literal is read as code. - A raw-string opening line with no closing line hides the rest of the file from the fixable kinds.
- Trailing comments after code are not read: no gap fix, no dash fix.
- A
//with no space or with a tab is not reported. FMT0023reads whole lines, in.csfiles only: a spec reference in a.razoror.mdfile or in a script is not reported.- A prose line that starts with
Arrange,ActorAssertfollowed by a space is a marker, unless it directly follows another comment line. - A marker that directly follows another comment line is read as part of that comment: the rule neither recognizes it nor requires a blank line before it, although the style guide still asks for one.
- In check mode, the
originalLinesof a chained issue are the line as the earlier issues of that line leave it, not the line of the file. - A fix run can report an
FMT0001that the check run before it did not: the gap fix pushed a run past the relaxed margin (see Fix order above). - When a run of
commentsstarts with a one-space line thatcomment-styleleaves alone (commented-out code, a separator of box-drawing characters thatcommentsdoes not treat as decorative, an<auto-generatedline in a file that is not generated) and continues with prose, and one of its lines exceeds 89 columns,commentsreflows the prose with one space after//. The next run then reportsFMT0020on those lines.
xmldoc
Checks the XML doc comments of .cs files against the C# style guide
(.github/instructions/csharp-style.instructions.md): no /// on a test
method, and a type summary that opens with The <c>TypeName</c> <kind>. Both
kinds are report-only: the rule never deletes nor rewords a /// line, and the
layout of a doc block stays the one comments produces. The rule has no Python
counterpart.
Doc blocks. A doc line is a line whose first non-blank characters are
/// and whose fourth non-blank character, if any, is not /: a //// line
is an ordinary comment, as for the compiler. A doc block is a maximal run of
consecutive doc lines outside the content of a multi-line raw string literal,
found from line 1 as comment-style finds it. A trailing /// after code is
not a doc line.
What follows a block. From the line after the block, the rule skips, in any
order and any number, blank lines and attribute sections. A section opens on a
line whose trimmed text starts with [, and extends over the following lines
until the counts of [ and ] since its opening are equal, the characters of
string literals included. The first line that is neither is the declaration
line, whatever it holds: a declaration, a // comment, a directive or another
doc line, which opens its own block.
| Id | Kind | Reach | Scope | Reported when | Fix |
|---|---|---|---|---|---|
FMT0024 |
test_method_doc |
diff | line | A skipped attribute section holds a test attribute, whatever the declaration line holds, also when the file ends after the sections. The issue covers the doc block. | None. |
FMT0025 |
type_summary_opening |
diff | line | The declaration line declares a type, and the summary text of the block does not open with the expected opening. The issue covers the summary lines; detail expected "<opening>". |
None. |
Test attributes. The inner text of a section is the text between its first
[ and its last ], its lines joined by one space. The section holds a test
attribute when that text matches, ordinal and case-sensitive:
(?:^|[,\[])\s*(?:method:\s*)?(?:global::)?(?:Microsoft\.VisualStudio\.TestTools\.UnitTesting\.)?(?:Data)?TestMethod(?:Attribute)?\s*(?:\(|,|\]|$)
This covers [TestMethod], [DataTestMethod], [TestMethod ()],
[TestMethod ("name")], the Microsoft.VisualStudio.TestTools.UnitTesting. and
global:: qualifications, the Attribute suffix, the method: target, a test
attribute listed after another one ([DataRow (1), TestMethod]) and two
sections on one line in either order ([TestMethod][Timeout (100)]). It
rejects [TestMethodCustom], [MyTestMethod], [TestInitialize] and
[TestClass].
Type declarations. The declaration line declares a type when it matches, case-sensitive:
^\s*(?:(?:public|internal|private|protected|sealed|static|abstract|partial|readonly|file|ref|unsafe|new)\s+)*(?<kind>record\s+struct|record\s+class|record|class|struct|interface|enum)\s+(?<name>@?[A-Za-z_][A-Za-z0-9_]*)\s*(?:<(?<params>[^>]*)>)?
When the name is followed by a < that the declaration line does not close
(public abstract class Foo<), the type parameter list is read on the
following lines, each trimmed and joined by one space, up to the first >;
with no > before the end of the file, the block raises nothing. Apart from
that list, what follows the name is not read: a base list, a primary
constructor, where constraints, { or ;.
Expected opening. The <c>, the expected name, </c>, one space and the
kind. The kind is the keyword as declared: record struct for a
record struct, whatever its modifiers, record for a record or a
record class, and class, struct, interface or enum for themselves (a
ref struct or a readonly struct gives struct), with one exception: in the
code-behind file X.razor.cs of a Razor component (file name compared
case-insensitively), the class named X is the component, and its kind is
component (The <c>X</c> component); the other types of the file keep their
kind, and a class declared in any other file is a class. The expected name is
the declared name without @, followed, for a generic type, by its type
parameter names in braces, separated by a comma and a space, without their
attribute sections and their in or out variance, as in a cref: for example
The <c>Repository{TEntity}</c> class. The <T> form does not conform.
Summary text. The lines of the block from the one holding <summary> to the
one holding </summary>, each stripped of its leading whitespace and its ///,
joined by one space; the text between the two tags, whitespace runs collapsed
to one space, trimmed. A block with no <summary>, or with no </summary>
after it (an <inheritdoc/> block, a missing summary), raises nothing. The text
conforms when, ordinal and case-sensitive:
- it starts with
The <c>; - the text up to the next
</c>equals the expected name once every whitespace character is removed from both ({TSource,TTarget}and{TSource, TTarget}both conform); </c>is followed by one space and the kind, then by the end of the text or by a character that is not a letter, a digit or_;- for the kind
record, the kind is not followed by a space andstruct.
Nested types are checked like any other, and a partial type wherever a doc
block precedes one of its declarations. Generated files are exempt from both
kinds.
Known limitations:
- A
//comment or a preprocessor directive between a block and its element stops the search: the block is not read as a type summary, and it givesFMT0024only when a test attribute stands between the block and that line. - An attribute section left open to the end of the file makes the block raise nothing, whatever the sections before it hold.
- A declaration written on an attribute line, after its
], or split over lines before its name, is not read; a type parameter list continued on the following lines is read. - Brackets inside the string literals of an attribute count.
- Delegates are not checked, and neither is a missing summary.
- Only the opening of a summary is read: what follows the kind, and the other sentences, are not.
layout
Reports the lines wider than the max_line_length that .editorconfig sets
for the file. The rule is report-only and has no Python counterpart.
| Id | Kind | Reach | Scope | Reported when | Fix |
|---|---|---|---|---|---|
FMT0026 |
line_too_long |
diff | line | The visual width of the right-trimmed line exceeds max_line_length; one issue per line, detail <width> columns, max_line_length = <limit>. |
None. |
- The rule accepts the files the
encodingrule accepts: the default list ofencoding, or the--extensionslist of the run, which still requires theencodingrule.--extensionstherefore names the text files of the run for both rules: a narrowed run is not widened, and--extensions .sqllets amax_line_lengthset on.sqlfiles apply. - Which accepted files are checked is the configuration's choice. Only a value
made of ASCII digits, greater than zero, enables the check;
off,0,-1,unset, any other value or no value means no check. The root.editorconfigsetsmax_line_length = 90in[*.cs]only: today, only.csfiles are checked. - Every line is measured, comments, raw-string content and the header block
included. A tab advances to the next multiple of 4, and trailing whitespace,
the business of
FMT0009, does not count. - The width counts UTF-16 code units: an accented Latin letter counts one column, a character outside the Basic Multilingual Plane two, and a combining mark one.
- The tool reads whole files: with
--changed, it reports every long line of a file that a change touches, not only the lines the change writes.
Generated files are exempt.
Generated code
A file is generated code when its first line contains <auto-generated
(case-insensitive) or when .editorconfig sets generated_code = true on it
(the root file does so for data/Data.Migrations/Migrations/*.cs). Generated
files are exempt from every kind except FMT0005 (a generator has no reason to
write a BOM) and FMT0011.
Diagnostics
Every issue kind has a stable identifier with the FMT prefix, registered in
the diagnostic prefix registry of build-tools/README.md. The catalog is
FormatDiagnostics in Format.Engine; the reports look the identifier up by
kind.
| Id | Kind | Rule | Reach | Title |
|---|---|---|---|---|
FMT0001 |
comment_prose_reflow |
comments |
tree | Comment prose is not wrapped to the target width |
FMT0002 |
separator_reflow |
comments |
tree | Separator comment is not exactly 79 columns wide |
FMT0003 |
tab_indentation |
indent |
tree | Leading indentation uses tabs |
FMT0004 |
initializer_indent |
initializers |
tree | Object-initializer brace is indented deeper than its declaration |
FMT0005 |
byte_order_mark |
encoding |
tree | File byte-order mark does not match the charset |
FMT0006 |
end_of_line |
encoding |
tree | Line endings do not match end_of_line |
FMT0007 |
missing_final_newline |
encoding |
tree | File does not end with a newline |
FMT0008 |
trailing_blank_lines |
encoding |
tree | File ends with blank lines |
FMT0009 |
trailing_whitespace |
encoding |
tree | Line ends with whitespace |
FMT0010 |
razor_code_block |
razor |
tree | Razor component declares an @code or @functions block |
FMT0011 |
editorconfig_location |
suppressions |
tree | .editorconfig file outside the module root |
FMT0012 |
pragma_without_restore |
suppressions |
tree | #pragma warning disable without a matching restore |
FMT0013 |
pragma_without_ids |
suppressions |
tree | #pragma warning disable without an identifier |
FMT0014 |
header_missing |
header |
diff | File does not start with the copyright and author header |
FMT0015 |
header_author |
header |
diff | Header author line does not follow the Author/Maintainer grammar |
FMT0016 |
header_extra_lines |
header |
diff | File header has more than two lines |
FMT0017 |
header_attribution |
header |
tree | Attribution line in the file header |
FMT0018 |
source_attribution |
header |
tree | Attribution line in a comment |
FMT0019 |
copyright_year |
header |
diff | Copyright year is not up to date |
FMT0020 |
comment_gap |
comment-style |
diff | Line comment is not followed by two spaces |
FMT0021 |
comment_dash |
comment-style |
diff | Comment contains an en dash or an em dash |
FMT0022 |
aaa_blank_line |
comment-style |
diff | Arrange, Act or Assert marker is not set off by blank lines |
FMT0023 |
spec_reference |
comment-style |
tree | Source file cites a spec |
FMT0024 |
test_method_doc |
xmldoc |
diff | Test method carries an XML doc comment |
FMT0025 |
type_summary_opening |
xmldoc |
diff | Type summary does not open with the type name and kind |
FMT0026 |
line_too_long |
layout |
diff | Line is longer than max_line_length |
Every identifier has a reach, which says which runs check it:
tree— every run: a full scan (the CI job) reports the kind;diff— only the runs restricted to a change set, that is the runs with--changed(the agents' runs), or any run whose--idsnames the kind. These kinds are too numerous in the existing tree to be reported by every full scan; they are reported on the files a change touches.
Positional targets never give the diff reach, even when they name a single
file. The reach is a property of the catalog entry (FormatDiagnostic.Reach);
the JSON report gives the reach of the run (see JSON report schema). The
release tables do not carry it: their Roslyn format holds the title in Notes,
and the catalog is the one source of the reach.
- An identifier is an identity, not a slot: it is never renumbered nor reused.
build-tools/Format.Engine/AnalyzerReleases.Shipped.mdandAnalyzerReleases.Unshipped.mdlist every identifier, in the Roslyn release tracking format.Format.Engineis not an analyzer, so Roslyn does not check them: the testFormatDiagnosticsTests.ReleaseTables_ListEveryCatalogIdrequires every catalog identifier in exactly one of the two tables, and no identifier absent from the catalog.- A new rule appends its identifier to the catalog and to
AnalyzerReleases.Unshipped.md; the publish script moves it to the Shipped table (see Publishing). - Every identifier has the default severity
warning(FormatDiagnostic.DefaultSeverity), which theSeveritycolumn of the release tables states and the testFormatDiagnosticsTests.All_DefaultSeverityIsWarning_AsInReleaseTableschecks. The configuration changes it per file (see Severities in.editorconfig).
Exit codes
| Exit code | Meaning |
|---|---|
0 |
No issue that the --fail-on policy selects with a severity above the --tolerate level (by default: a check run that leaves no issue of severity error, or a fix run that completed, including the no-change case). |
1 |
At least one issue that the --fail-on policy selects with a severity above the --tolerate level (by default: an issue of severity error in check mode, none in fix mode). The CI lint-gate signal, with --tolerate suggestion in the CI job. |
2 |
Bad usage, unknown or empty --rules, unknown or empty --ids, an unknown or empty --exclude-ids (Error: unknown diagnostic '<id>'. Valid identifiers: ..., Error: --exclude-ids must not be empty.), an empty --extensions, an invalid extension token, --extensions without the encoding rule, non-positive --max-columns, more than one of --json, --msbuild and --summary, --changed combined with targets, a --changed list file that does not exist, a literal target path that does not exist, a --year that is not four ASCII digits (Error: --year must be a four-digit year.), --year without --update-year (Error: --year requires --update-year.), --update-year without --changed (Error: --update-year requires --changed.), --update-year while the active rules do not include header (Error: --update-year requires the header rule.), an --ids selection holding FMT0019 without --update-year (Error: FMT0019 requires --update-year.), --update-year with an --ids selection that does not hold FMT0019 (Error: --update-year requires FMT0019 in --ids.), --update-year with an --exclude-ids selection that holds FMT0019 (Error: --update-year requires FMT0019, which --exclude-ids removes.), a --fail-on value other than none, unfixable and any (Error: --fail-on must be none, unfixable or any.), a --tolerate value other than none, suggestion, warning and error (Error: --tolerate must be none, suggestion, warning or error.), an empty --junit or --codequality value (Error: --junit requires a file path., Error: --codequality requires a file path.), or --junit and --codequality naming the same file (Error: --junit and --codequality must name different files.). A glob or directory matching zero accepted files is not an error (exits 0), and neither is a --changed path that does not exist (it is ignored). Ignored files never change the exit code. |
3 |
A fix-time safety check failed (file changed under the tool), a read/write error occurred (including on the --changed list file), or a --junit or --codequality report could not be written (Error: cannot write report: <path>: <message>). |
Row 2 covers the errors the tool itself reports, after the command line is
parsed. An error that System.CommandLine reports while parsing is the
parser's: it writes the error to stderr and the help to stdout, runs nothing,
and exits 1, which is not the exit code of an input error of the tool. This
is the case of --ids and --include-ids given together, one option given
twice: Option '--ids' expects a single argument but 2 were provided.
An issue left in the source makes the run exit 1 when two conditions hold:
--fail-on selects it, and its severity is above the --tolerate level.
--fail-on states which issues left in the source are selected:
| Policy | Selected: the issues left in the source that are | In check mode | In fix mode |
|---|---|---|---|
none |
none | always 0 |
always 0 |
unfixable |
report-only (not fixable) | fixable issues are not selected | the report-only issues are selected |
any |
of any kind | every issue is selected | the issues left unfixed are selected, that is the report-only ones |
| absent | as any in check mode, as none in fix mode |
every issue is selected | always 0 |
--tolerate states the highest severity that does not fail the run:
| Level | A selected issue fails the run when its severity is |
|---|---|
none |
any: suggestion, warning or error |
suggestion |
warning or error |
warning (absent) |
error |
error |
never |
Together, for a check run (--fail-on any, the default of check mode), with
issues of the severities of the first column left in the source:
| Severities left | --tolerate none |
suggestion |
warning (default) |
error |
|---|---|---|---|---|
| none | 0 |
0 |
0 |
0 |
suggestion only |
1 |
0 |
0 |
0 |
up to warning |
1 |
1 |
0 |
0 |
up to error |
1 |
1 |
1 |
0 |
Every identifier has the default severity warning: without a severity set in
.editorconfig, a check run without options exits 0 whatever it finds, and
fails only with --tolerate suggestion or none. This is a change of the
default behaviour, which exited 1 on any issue in check mode: the CI job
passes --tolerate suggestion to keep failing on warnings. The agents read the
findings the tool prints, not its exit code.
Exit 2 and exit 3 keep their precedence: they are decided before, whatever
the policy. In fix mode, every selected fixable issue is applied or the run
fails with exit 3, so any and unfixable give the same exit code there.
Without --fail-on, fix mode exits 0 whatever it leaves, report-only issues
(initializers, razor, suppressions, FMT0014–FMT0016 and
FMT0018 of header, FMT0023 of comment-style, xmldoc and layout, that
is FMT0024–FMT0026) included; --fail-on unfixable or any makes
them count, when their severity is above the --tolerate level.
JSON report schema
--json emits a single UTF-8 JSON object on stdout with stable camelCase fields:
{
"root": "<absolute run root>",
"targets": ["..."],
"maxColumns": null,
"fixMode": false,
"rules": ["comments", "indent", "initializers", "encoding", "razor", "suppressions", "header", "comment-style", "xmldoc", "layout"],
"ids": ["FMT0001", "FMT0002", "...", "FMT0013", "FMT0017", "FMT0018", "FMT0023"],
"reach": "tree",
"copyrightYear": null,
"matchedFileCount": 0,
"matchedFiles": ["..."],
"ignoredFiles": [
{ "file": "Path/To/utf16.txt", "reason": "not valid UTF-8" }
],
"issueCount": 0,
"fixedIssueCount": 0,
"modifiedFiles": ["..."],
"issues": [
{
"rule": "comments",
"kind": "comment_prose_reflow",
"id": "FMT0001",
"severity": "warning",
"file": "Path/To/File.cs",
"startLine": 12,
"endLine": 14,
"scope": "line",
"originalLines": ["..."],
"proposedLines": ["..."],
"fixable": true,
"fixed": false,
"detail": null
},
{
"rule": "encoding",
"kind": "byte_order_mark",
"id": "FMT0005",
"severity": "suggestion",
"file": "Path/To/File.cs",
"startLine": 0,
"endLine": 0,
"scope": "file",
"originalLines": [],
"proposedLines": [],
"fixable": true,
"fixed": false,
"detail": "expected no BOM, charset = utf-8"
}
]
}
scope is "line" for an issue over a range of lines, "file" for a
file-level issue, which concerns the file as a whole (its BOM, its line
endings, its final newline, its location): a file-level issue has startLine
and endLine equal to 0 and empty originalLines and proposedLines, and it
sorts before the line issues of its file. ignoredFiles lists the files the
run did not process, with the reason (no active rule accepts this file,
not valid UTF-8 or contains a NUL byte); an ignored file is not in
matchedFiles.
rules lists the rules the run applies, in run order: without --rules, the
ten names, xmldoc and layout last. ids lists the identifiers the run checks: the --ids selection,
else the identifiers within the reach of the run, without FMT0019 when no
year is checked, and in both cases without the --exclude-ids identifiers
([] when the exclusion leaves nothing); the per-file settings of
.editorconfig do not change it. A run on positional targets checks the sixteen tree-reach
identifiers shown above, a --changed run the twenty-five identifiers
FMT0001–FMT0018 and FMT0020–FMT0026, and a
--changed --update-year run all twenty-six. ids does not depend on --rules.
reach is "tree" or "diff", the reach of the run ("diff" with
--changed). maxColumns is the --max-columns value, a number, or null
without the option: each file then takes the width its configuration sets,
else 79 (see comments). copyrightYear is the year the copyright lines
must reach, a number with --update-year, else null.
id is the FMT identifier of the issue's kind (see Diagnostics). severity
is the severity of the issue in its file: "suggestion", "warning" or
"error" (see Severities in .editorconfig). detail is always present
(serialized as JSON null when there is no note). With
--changed, targets lists the paths kept from the list, in the shape of
file (relative to the root when under it), possibly none ([]).
originalLines and proposedLines are always arrays (single-element for
separator_reflow). They are equal for every report-only issue and for every
trailing_whitespace issue: both are right-trimmed by contract, so the
equality does not mean that the issue is a no-op. Integer fields are
culture-invariant.
Reports for GitLab
--junit <file> and --codequality <file> write, beside the stdout report, a
JUnit XML report and a GitLab Code Quality report of the issues. A GitLab job
publishes them with artifacts:reports:junit and
artifacts:reports:codequality, as the epsitec-format job of
.gitlab-ci.yml does: the pipeline's Tests tab lists the FMT suites, and
the merge request shows the Code Quality findings (what it shows depends on
the GitLab tier).
- Both reports hold the issues left in the source (
fixedisfalse), in the report order of the run (file, thenstartLine): in fix mode, the fixed issues are left out, as--msbuilddoes. - Every issue points at a line: its
startLine, or1for a file-level issue.<path>below is the report path of the issue: relative to the run root with forward slashes, absolute outside it. Run from the module root, as the CI job runs, the paths are relative to the repository root, as GitLab expects. - Both are written whatever the stdout mode (human,
--json,--msbuild,--summary), which they leave unchanged, even when there is no issue, and before the exit code, so that a run that exits1leaves them on disk. The JUnit report is written first. A report that cannot be written printsError: cannot write report: <path>: <message>on stderr and exits3; a report not yet written is then not attempted. An invalid input (exit2) writes no report, and neither does an operational failure of the run itself (exit3). - Bytes: UTF-8 without BOM, LF line endings, a final newline.
- Locally, write them under
artifacts/: the scan skips everyartifactssegment, and the module.gitignoreignores the folder, so that azou commitnever stages the reports.
JUnit
One test suite per identifier, one failed test case per issue:
- the root
<testsuites name="epsitec-format" tests="N" failures="N" errors="0">,Nthe number of issues; - one
<testsuite name="<id>: <title>" tests="k" failures="k" errors="0" skipped="0">per identifier with at least one issue, in identifier order; - one
<testcase classname="<id>" name="<path>:<line>" file="<path>">per issue, in report order. When two test cases of one suite would get the same name, the second gets a space and#2appended to its name, the third a space and#3, and so on, so that GitLab keeps every case; - inside it,
<failure type="<id>" message="<title>[ (<detail>)]">, whose text is<path>:<line>: <category> <id>: <title>[ (<detail>)],<category>being the one of the--msbuildline (info,warningorerror). Every issue is a failed test case, whatever its severity.
With no issue, the root element stands alone, tests="0" failures="0". A
character of a path or a detail that XML 1.0 cannot hold (a control character
other than tab, line feed and carriage return) is written as U+FFFD, so that
the report always parses.
<?xml version="1.0" encoding="utf-8"?>
<testsuites name="epsitec-format" tests="1" failures="1" errors="0">
<testsuite name="FMT0003: Leading indentation uses tabs" tests="1" failures="1" errors="0" skipped="0">
<testcase classname="FMT0003" name="payroll/Payroll.Import/Foo.cs:12" file="payroll/Payroll.Import/Foo.cs">
<failure type="FMT0003" message="Leading indentation uses tabs">payroll/Payroll.Import/Foo.cs:12: warning FMT0003: Leading indentation uses tabs</failure>
</testcase>
</testsuite>
</testsuites>
Code Quality
A JSON array, [] when there is no issue, one object per issue (the GitLab
subset of the CodeClimate format):
type:"issue";check_name: the identifier;description: the title, followed by a space and(<detail>)when the issue has a detail;categories:["Style"];severity: from the severity of the issue:"info"for a suggestion,"critical"for an error; for a warning,"major"forFMT0017andFMT0018(attribution lines) andFMT0023(spec reference), which break rules ofAGENTS.mditself rather than the style guide, and"minor"for every other identifier;location:{ "path": "<path>", "lines": { "begin": <line> } };fingerprint: the lowercase hexadecimal SHA-256 of the UTF-8 bytes of<id>,\n,<path>,\n,<content>,\n,<occurrence>.<content>is the issue'soriginalLines, each stripped of its leading whitespace, joined by\n(empty for a file-level issue);<occurrence>is the zero-based rank, in report order, of the issue among the issues of the report with the same identifier, path and content. The line number is not hashed: the fingerprint survives lines inserted or removed above the issue, and two identical findings of one file stay distinct.
The writer escapes <, >, &, the quotes and the non-ASCII characters of
the strings as \u sequences (\u003Cc\u003E for <c>), which a JSON
reader decodes.
[
{
"type": "issue",
"check_name": "FMT0003",
"description": "Leading indentation uses tabs",
"categories": ["Style"],
"severity": "minor",
"location": { "path": "payroll/Payroll.Import/Foo.cs", "lines": { "begin": 12 } },
"fingerprint": "9f2c6d0a4e3b8f71c5d2a6e0b4f8c1d3e7a9b2c4d6e8f0a1b3c5d7e9f1a2b4c6"
}
]
The values are illustrative, and so is the line layout: the writer puts each array element and each property on its own line.
I/O contract
- Read: files are decoded as strict UTF-8 (a leading BOM is tolerated and
remembered). A file that is not valid UTF-8 (a UTF-16 file with a BOM, a
Latin-1 file), or whose bytes contain a NUL byte (a BOM-less UTF-16 file, a
binary file), is never analyzed nor written: it is reported as ignored.
Lines are split on
\n, with a\rright before it treated as part of the terminator, so both CRLF and LF split cleanly; a\rthat ends the file with no\nafter it stays in the last line. - Layout: the tool observes, on read, the BOM, the dominant line ending, mixed endings, the final newline and the trailing empty lines of each file.
- Write (only files the tool rewrites): the observed layout is preserved —
BOM or no BOM, line ending, final newline or none, trailing empty lines —
unless an
encodingissue changes it. One exception: a file with mixed line endings is written with its dominant ending throughout (LF on a tie). This replaces the former contract (UTF-8 without BOM, lines joined with\n, exactly one trailing\n): a fix ofcommentsno longer strips a BOM or turns CRLF into LF as a side effect. - Expectations:
.editorconfigis read on the CLI side, through theeditorconfigpackage; the engine (Format.Engine) receives the resolved expectations and depends on the BCL only. - Each issue's
originalLinesare the source lines of its range with trailing whitespace removed; fix mode compares them with the right-trimmed file lines before it replaces the range. One exception: when a rule emits several fixable issues on one line (comment-style), each issue after the first carries the line as the earlier ones leave it, and fix mode applies the issues of one line in emission order. - Replacement lines of
commentsnever carry trailing whitespace. Replacement lines ofindentkeep the line's trailing whitespace: the rule rewrites the leading indentation only. The lines thatFMT0020orFMT0021ofcomment-stylerewrites are right-trimmed; the line that anFMT0022insertion moves down keeps its bytes (the proposed lines are an empty line, then that line as read), unless a gap or dash issue of the same line rewrote it first, in which case it is right-trimmed like any rewritten line. The tool does not trim trailing whitespace on lines it does not otherwise rewrite, and files with no applied fix are left byte-identical.
Relationship to the Python reference
The comments, indent and initializers rules are a faithful, line-based
port of the briefcases/_tools Python scripts. The comments rule is validated
by a golden-oracle parity test that compares the C# output against reference
output captured from check_comment_truncation.py over a checked-in corpus.
Where this document and the scripts disagree on a fine-grained detail of these
three rules, the scripts (and the golden oracle) are authoritative. The
encoding, razor, suppressions, header, comment-style, xmldoc and
layout rules have no Python counterpart. comment-style is a rule of its own, rather than new kinds
of comments, so that comments keeps its parity and its golden oracle is
unchanged; it has a golden corpus of its own,
build-tools/Format.Engine.Tests/CommentStyleCorpus, whose expected files are
written by hand.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
This package has no dependencies.
| Version | Downloads | Last Updated |
|---|---|---|
| 1.9.2.2640 | 69 | 9/30/2026 |
| 1.9.0.2640 | 75 | 9/28/2026 |
| 1.8.0.2639 | 80 | 9/26/2026 |
| 1.7.0.2639 | 77 | 9/26/2026 |
| 1.6.1.2639 | 75 | 9/24/2026 |
| 0.9.3.2624 | 233 | 6/21/2026 |