Suppressions and the Baseline

How to silence a decision on one line, in a block or in a whole file, why a suppression stops the fix as well, and how a baseline lets you turn on a decision that your code does not meet yet.

Four Levels

Personally, I don't like exceptions to rules and I don't write them into code; if I must, then in the configuration, for a whole path. But there are places where a decision is simply wrong, and there you want a precise tool, not a hammer. Suppression has four levels, from the narrowest to the widest:

level how where
one line or statement // dresscode:ignore in the code
block dresscode:disable and dresscode:enable in the code
whole file dresscode:ignoreFile in the code
path keep in an override, excludePaths in the configuration

Alongside them there is the baseline: an exception not to a decision but to time. It records the violations you cannot fix today and reports only the new ones.

Whichever level you choose, one thing holds: a suppressed violation is not fixed. A rule may change the code only after it has reported the violation and the report was not suppressed, and it is the engine of the tool that enforces this, not the author of the rule. So a suppression is not just silence in the output; it really turns the decision off. The baseline is a different case and never prevents a fix, as its section describes.

Suppressing on a Line

A dresscode:ignore comment at the end of a line suppresses the violations on that line. Without a name, it suppresses everything; with a decision path, only that decision; several paths are separated by commas:

$isEmpty = $value == null; // dresscode:ignore expressions.comparison.loose

Instead of the whole path, you can also give a section, which covers all decisions under it: // dresscode:ignore spacing silences all spacing on the line.

A comment on a line of its own applies to what starts on the line below it, even if it spans several lines. It can be a statement or a class member, but also a single array item or an argument: a comment above the first item of an array covers only that item, not the whole array. Here it covers the whole statement:

// dresscode:ignore multiline.edges.array, multiline.trailingComma.array
$matrix = [[1, 0, 0],
	[0, 1, 0],
	[0, 0, 1]];

After two dashes, write why the exception applies. DressCode does not read that text; it is for whoever opens the code after you. ESLint and PHP_CodeSniffer use the same form:

// dresscode:ignore upgrading.functions.arraySearchFunctions -- a loop is faster than array_any() on this hot path
foreach ($kinds as $kind) {
	if ($this->kind === $kind) {
		return true;
	}
}

//, # and /* */ all work. You copy the path from the output, where it stands next to every finding. Instead of the path, DressCode also accepts the name of a PHP CS Fixer or PHP_CodeSniffer rule that its decision covers, and it understands the comments phpcs:ignore, phpcs:disable, phpcs:enable, phpcs:ignoreFile and the annotation @phpcsSuppress, so if you are migrating from another tool, you do not have to touch the code at all. dresscode migrate-suppressions then rewrites them to DressCode paths; it also rewrites a comment with a rule name from an older version of DressCode.

A name that matches no decision suppresses nothing, and the run warns about it, so that the code does not keep a comment that only looks as if it does something:

Comment `dresscode:ignore noIsNull` names `noIsNull`, which is no decision, section or rule, so it silences nothing.

Where the project already has its own comment to mark intent, for example // intentionally == at a comparison that should not be made strict, you do not have to rewrite it. The suppressionComments key maps a comment pattern to the decisions that stay silent on its line, just as if dresscode:ignore were there:

suppressionComments:
	'~\bintentionally\s+[!=]=~': expressions.comparison.loose

The pattern is a regular expression including its delimiters, and it is matched against the text of one comment, so ^ means the start of the comment. The value is a path, a section or a list of them. A comment at the end of a line applies to that line, a comment on a line of its own to the statement below it. The nette standard carries such a map: // intentionally == silences expressions.comparison.loose and // @ silences mistakes.errorSuppression.

An item can also carry an example of a comment the pattern accepts, and then it is written as a map:

suppressionComments:
	'~^//\s*@\s+\S~':
		decisions: mistakes.errorSuppression
		example: '// @ <why>'

DressCode verifies the example: it puts a word in place of <why>, and the comment must match the pattern. For a violation of that decision, the agent format offers it to the agent as a way to keep a justified exception, with the real reason in place of <why>; dresscode config prints it next to the pattern. A higher layer that writes the same pattern replaces the whole item, including the example.

Suppressing in a Block

// dresscode:disable file.longLines
$expired = 'Your subscription has expired. Renew it in the account settings to keep access to every premium feature of the shop.';
$declined = 'Your card was declined by the bank. Check the card details or choose another payment method to complete the order.';
// dresscode:enable

disable without a name turns off everything until enable; enable without a name turns on everything that disable turned off. Without enable, disable applies until the end of the file.

Suppressing in a Whole File

<?php // dresscode:ignoreFile

This comment, anywhere in the file, turns off everything for the whole file. It is useful for generated code that lives among hand-written code. When there are several such files, it is cleaner to exclude them by path or by content, see skipWhen in the configuration.

Turning Off for a Path

An exception that applies to a whole directory belongs in the configuration, not in a hundred files. An override with the value keep turns off a decision for such a path:

overrides:
	-
		paths: [legacy]
		expressions:
			comparison: keep
	-
		paths: [tests/fixtures]
		file:
			longLines: keep

The rest of the configuration checks such a file normally. excludePaths skips whole paths; both are described on the page Configuration.

Often, though, the decision should not disappear for that path, only apply with a different value: tests use different spacing between methods than the source code, not no spacing at all. An override can do this too, giving its own values to the files that match the path:

overrides:
	-
		paths: [tests/**.phpt]
		blankLines:
			afterImports: 2

Baseline

You introduce a new standard in one commit: dresscode fix fixes what can be fixed, and you commit the result separately, as described in Getting Started. But after such a commit, something may remain that no tool can do for you. The decision types.declaration.parameter: required wants a type on every parameter, and you have three hundred functions without one. For each of them you need to think about which type belongs there, and that is weeks of work. Until you turn the decision on, more of them appear with every commit.

This is exactly what a baseline is for: a list of the violations you cannot fix today. With it, you can turn the decision on right away, the old violations wait until you get to them, and every new one is reported. The same goes for a risky fix that the project has not accepted yet, or for a line that is longer than file.lineLength allows and that DressCode cannot break for you.

In static analysis, a baseline is a matter of course; PHPStan has had one for years. But neither PHP CS Fixer nor PHP_CodeSniffer has one, and in both projects the request for one got stuck on the same question: what should automatic fixing do with it? DressCode answers simply: the baseline contains only what the fix leaves. What can be fixed is fixed, and the baseline keeps track of the rest.

That is why the baseline command writes it for code where fix has nothing left to do. Both commands take the paths from the paths key in the configuration; you can also list them after the command:

dresscode fix
dresscode baseline
Baseline with 312 violations written to `dresscode-baseline.neon`.
Name it under `baseline` in the configuration to make it apply.

The order matters. If fix would still change something, baseline writes nothing, lists the files that would change, and ends with exit code 1; nothing that can be fixed belongs in the baseline:

Error: The baseline is generated over code a fix leaves alone: a fix would change 3 files. Run `fix` first.
  src/Cart.php
  src/Order.php
  tests/CartTest.php
See https://dresscode.run/suppressing#baseline

Risky fixes that change the behavior of the code and that the project has not accepted in the fixRisky key belong in the baseline, because the tool does not make them without your permission. Risky fixes that types or nameResolution would decide do not belong in it: they are only warnings, and they disappear as soon as you give DressCode what it is missing. The baseline command has no --only option, because a baseline comes from the whole configuration, not from a few decisions.

The file is created next to the configuration and in the same format (.neon next to dresscode.neon, .php next to dresscode.php). It starts to apply once the configuration names it; after that, the next dresscode baseline writes into the file it names:

baseline: dresscode-baseline.neon

Inside, there is the version of the format and, for each file, a list of violations with the path of the decision, the message and a fingerprint:

version: 2
files:
	src/Order.php:
		-
			decision: naming.method
			message: The method `get_total` must be written in camelCase.
			fingerprint: ea4c48c2e21d2b35

The fingerprint is computed from the decision, the content of the line and the order of the violations of that decision on the line, not from the line number, so the baseline survives edits elsewhere in the file. The message is not part of it: it is in the entry for people to read, so they can see what it is about, and when a new version of DressCode words it differently, the baseline still recognizes the violation. Violations the baseline knows are not reported, and the summary of the run admits them, so that nobody ends up praising the emperor's new clothes:

OK  312 violations in the baseline in 214 files

A baseline never prevents a fix. A dresscode:ignore comment says “the decision is wrong here”, and that is why it stops the fix too. A baseline says “we can't do this yet”, and there is no reason to leave alone what we already can fix. When a violation from the baseline becomes fixable, for example because the project has accepted the risky fixes of that decision, fix fixes it like any other.

When a violation listed in the baseline disappears from the code, because someone fixed it or fix fixed it as soon as it could, the run points out that the entry no longer matches anything (1 entry of the baseline no longer matches a violation; regenerate it with `dresscode baseline`.). Run fix and generate the baseline again. The run points out only the entries of the files it processed, and with --only only the entries of the decisions that ran, so a check of part of the project does not report as stale what it did not ask about. A baseline should shrink, and once it is empty, delete the line from the configuration and the file with it.

The new version cannot read a baseline from the time when DressCode reported findings per rule: the fingerprints are computed differently, so it rejects the file and advises you to generate it again. Just run fix and dresscode baseline.