Epsitec.Tool.Format 1.9.2.2640

Prefix Reserved
dotnet tool install --global Epsitec.Tool.Format --version 1.9.2.2640
                    
This package contains a .NET tool you can call from the shell/command line.
dotnet new tool-manifest
                    
if you are setting up this repo
dotnet tool install --local Epsitec.Tool.Format --version 1.9.2.2640
                    
This package contains a .NET tool you can call from the shell/command line.
#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.csproj in Release first, and send the whole build output to stderr: stdout carries only the tool's report, so --json and --msbuild pipes 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 --changed paths 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 is bin, obj, .git, .vs, node_modules, TestResults, artifacts, .nupkg or Microsoft.CodeAnalysis.Razor.Compiler (the output of the Razor source generator, emitted under Generated/), or when it lies under a directory excluded by a .fmtignore marker (see Excluding a directory). Glob expansion uses Microsoft.Extensions.FileSystemGlobbing.
  • A file named explicitly (a literal path, a glob match or a --changed entry) 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, --msbuild and --summary modes print one line per ignored file on stderr, before the report: Note: ignored {file}: {reason}; stdout is unchanged. The JSON report lists them in ignoredFiles. 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 exits 0. --changed replaces 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 of comments, 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 of FMT identifiers (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 --ids is reported whatever its reach, even without --changed. An unknown identifier, or an empty value, is an input error. --ids combines with --rules: an issue must pass both. --include-ids is 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). A dotnet_diagnostic.<id>.severity = none in .editorconfig wins over --ids: the identifier stays off for the files its section matches (see Severities in .editorconfig).
  • --exclude-ids — comma-separated FMT identifiers 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 --ids selection, 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 exits 0 under every --fail-on policy. Excluding an identifier outside the reach of the run (a diff-reach identifier without --changed) changes nothing. The JSON ids lists the selection without the excluded identifiers. --exclude-ids combines with --rules, --changed, --fix, --fail-on and 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, see header). It requires --changed and the header rule; with --ids, the selection must hold FMT0019, and --exclude-ids must not name it: Error: --update-year requires FMT0019, which --exclude-ids removes. Without --update-year, FMT0019 is never produced, and naming it in --ids is an input error; naming it in --exclude-ids is accepted and changes nothing.
  • --year <N> — the year --update-year uses 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 the encoding and layout rules check for this run, replacing their default list (see encoding and layout), 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 --extensions while the active rules do not include encoding, are input errors.
  • --max-columns — positive integer wrap width of comment prose, for every file of the run: it wins over the epsitec_comment_line_length of .editorconfig. Absent means the width each file's configuration sets, else 79 (see comments). 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} is suggestion, warning or error (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 line 1. {category} is info for a suggestion, warning or error otherwise, 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 per FMT id 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, --msbuild and --summary are mutually exclusive.
  • --fail-on none|unfixable|any — which issues left in the source make the run exit 1 (see Exit codes): none, the report-only ones (unfixable), or any, among those whose severity is above the --tolerate level. The value is trimmed and matched case-insensitively. Absent means any in check mode and none in 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-on selects makes the run exit 1 only when its severity is above this level. none tolerates no issue, suggestion fails on warnings and errors, warning fails on errors only, and error never fails. The value is trimmed and matched case-insensitively. Absent means warning: by default, a run fails only when an issue of severity error remains. 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, --changed and --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 format or bare restore, except the identifiers of the suppressions rule (FMT0011 to FMT0013), which keep checking the directives themselves.
  • #pragma warning disable FMT0026 (or several identifiers, comma-separated) withdraws only the identifiers it names, up to the next restore that names them or names nothing. A restore format does 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 the suppressions rule reports the unrestored disable (FMT0012). Identifiers compare case-insensitively.
  • On the lines of a region, check mode reports nothing of the identifiers it withdraws and --fix rewrites 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 #pragma line in the content of a multi-line raw string is test data, not a directive. #if nesting 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 .editorconfig globs, resolved like every key the tool reads (see encoding): the value that counts is the one the chain of .editorconfig files 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.
  • silent and hidden turn the identifier off, as none does: Roslyn keeps a silent diagnostic 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 --msbuild line, the --summary rows, the JSON severity field and the GitLab reports show it (see Reports for GitLab). It decides the exit code with --fail-on and --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 = NONE counts). A trailing comment (none # reason, error ; reason) does not change the reading.
  • The file is shared with Roslyn: a non-FMT identifier (IDE0005, CA1859) and an unknown FMT identifier (FMT9999) are ignored without a message. In turn, Roslyn ignores a key whose identifier no analyzer reports: the build and dotnet format report nothing for an FMT key.
  • 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 FMT0006 turned off, and an applied FMT0008 fix leaves a final newline even with FMT0007 turned off.
  • Turning off wins over --ids and --include-ids: --ids FMT0024 reports no FMT0024 in 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 turns FMT0019 off 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 .fmtignore keeps a directory out of a run (see Excluding a directory).
  • The JSON ids lists 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 --extensions replaces it for the run: .cs, .csproj, .props, .targets, .slnx, .razor, .json, .md, .txt, .xml, .xsl, .csv, .yml, .ps1, .sh, .js, .ts, .css, .html. The layout rule accepts the same list.
  • Only charset = utf-8 and utf-8-bom, and end_of_line = lf and crlf, are checked; any other value (latin1, utf-16be, utf-16le, cr, unset) means "no check", and so does false for insert_final_newline or trim_trailing_whitespace.
  • Trailing whitespace is what string.TrimEnd () removes; a lone \r at the end of a line (not followed by \n) is trailing whitespace.
  • A whitespace-only line of a trailing blank range raises FMT0008 only, not FMT0009 as well, when both kinds are kept. When FMT0008 is left out (--ids FMT0009, --exclude-ids FMT0008, or .editorconfig), such a line raises FMT0009.
  • In Markdown, a line ending with two spaces is a hard line break. trim_trailing_whitespace = true reports it as FMT0009, consistently with the house Markdown style, and --fix removes 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 .editorconfig file 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 disable that a later restore never matches, with the detail unrestored: X, Y (or unrestored: all for a bare disable). A disable of a warning is matched by any later restore that names it or that names nothing; a bare disable is matched only by a later bare restore. Identifiers compare case-insensitively, and a trailing // comment is not an identifier.
  • pragma_without_ids (FMT0013): a #pragma warning disable that 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.

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:

  1. it starts with // Author: or // Authors:, followed by one space. Both labels are accepted, whatever the number of names, in every folder;
  2. 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;
  3. each list is split into names on a comma followed by one space, or on an ampersand between two single spaces (A, B, A & B and A, B & C alike), and each name on single spaces into words. Every name then goes through checks 4 to 7, the authors first, each list in order;
  4. no word of the name is &, and no word is et or and, ignoring case;
  5. 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;
  6. the name ends with a non-empty run of words entirely in upper case (the surname, accents and hyphens included);
  7. 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 FMT0014 alone among the form kinds, never FMT0015 or FMT0016 on top of it; a tab or a single space after // is such a file. FMT0019 reads the copyright line only: a canonical copyright line with nothing after it raises FMT0014 and, 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.cs carries no header: it gets neither FMT0014, FMT0015, FMT0016 nor FMT0019, and still gets FMT0017 and FMT0018. Any other file, a file holding only [assembly: …] attributes included, carries the header.
  • Fix order: header applies its fixes after encoding and indent, on trimmed and re-indented lines, and before comments, which then works on the header as fixed. Both fixable kinds edit lines of the header block only, the block comments never 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 .cs files are read: an attribution line in a .razor, .ps1 or .sh file 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); since FMT0018 has 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 (E followed 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, where comments may report it, and on line 1 once the deletion is applied in fix mode, where comments skips 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 reports FMT0014 on 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 the header rule 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 &ndash;, &mdash;, &#8211;, &#8212;, &#x2013;, &#x2014; (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 // ——— Title gets the two spaces, then its dashes, in the same run;
  • starts with <auto-generated or </auto-generated, ignoring case;
  • reads as commented-out code: it starts with var and 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 with this..

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. &amp;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.
  • FMT0023 reads whole lines, in .cs files only: a spec reference in a .razor or .md file or in a script is not reported.
  • A prose line that starts with Arrange, Act or Assert followed 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 originalLines of 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 FMT0001 that 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 comments starts with a one-space line that comment-style leaves alone (commented-out code, a separator of box-drawing characters that comments does not treat as decorative, an <auto-generated line in a file that is not generated) and continues with prose, and one of its lines exceeds 89 columns, comments reflows the prose with one space after //. The next run then reports FMT0020 on 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 &lt;T&gt; 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 and struct.

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 gives FMT0024 only 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 encoding rule accepts: the default list of encoding, or the --extensions list of the run, which still requires the encoding rule. --extensions therefore names the text files of the run for both rules: a narrowed run is not widened, and --extensions .sql lets a max_line_length set on .sql files 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 .editorconfig sets max_line_length = 90 in [*.cs] only: today, only .cs files 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 --ids names 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.md and AnalyzerReleases.Unshipped.md list every identifier, in the Roslyn release tracking format. Format.Engine is not an analyzer, so Roslyn does not check them: the test FormatDiagnosticsTests.ReleaseTables_ListEveryCatalogId requires 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 the Severity column of the release tables states and the test FormatDiagnosticsTests.All_DefaultSeverityIsWarning_AsInReleaseTables checks. 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 (fixed is false), in the report order of the run (file, then startLine): in fix mode, the fixed issues are left out, as --msbuild does.
  • Every issue points at a line: its startLine, or 1 for 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 exits 1 leaves them on disk. The JUnit report is written first. A report that cannot be written prints Error: cannot write report: <path>: <message> on stderr and exits 3; a report not yet written is then not attempted. An invalid input (exit 2) writes no report, and neither does an operational failure of the run itself (exit 3).
  • Bytes: UTF-8 without BOM, LF line endings, a final newline.
  • Locally, write them under artifacts/: the scan skips every artifacts segment, and the module .gitignore ignores the folder, so that a zou commit never 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">, N the 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 #2 appended 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 --msbuild line (info, warning or error). 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" for FMT0017 and FMT0018 (attribution lines) and FMT0023 (spec reference), which break rules of AGENTS.md itself 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's originalLines, 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 \r right before it treated as part of the terminator, so both CRLF and LF split cleanly; a \r that ends the file with no \n after 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 encoding issue 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 of comments no longer strips a BOM or turns CRLF into LF as a side effect.
  • Expectations: .editorconfig is read on the CLI side, through the editorconfig package; the engine (Format.Engine) receives the resolved expectations and depends on the BCL only.
  • Each issue's originalLines are 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 comments never carry trailing whitespace. Replacement lines of indent keep the line's trailing whitespace: the rule rewrites the leading indentation only. The lines that FMT0020 or FMT0021 of comment-style rewrites are right-trimmed; the line that an FMT0022 insertion 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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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