Configuration
The configuration is a single file in the root of your project: dresscode.neon, or
dresscode.php if you prefer PHP. You don't assemble rules in it. You write down how your code looks: each key names
one thing in the code, and its value says how that thing is written. A standard provides the base, sets add intents on top of it,
and anything you want different you write key by key, for the whole project or just a part of it.
The Configuration File
This is the whole configuration of a project that builds on PER Coding Style, wants to clean up needless code and does a few things its own way:
use:
- perCs
- cleanup
paths:
- src
- tests
excludePaths:
- tests/fixtures
braces:
position:
class: nextLine
blankLines:
aroundMethods: 1
beforeStatement:
return: 1
spacing:
concatenation: '$a.$b'
The same in PHP, where each NEON key is a named argument of the DressCode\Config constructor, and the sections of
decisions go together into the decisions argument:
<?php declare(strict_types=1);
use DressCode\Config;
return new Config(
use: ['perCs', 'cleanup'],
paths: ['src', 'tests'],
excludePaths: ['tests/fixtures'],
decisions: [
'braces' => ['position' => ['class' => 'nextLine']],
'blankLines' => ['aroundMethods' => 1, 'beforeStatement' => ['return' => 1]],
'spacing' => ['concatenation' => '$a.$b'],
],
);
The rest of this page uses NEON, because it is easier to read. All names in the configuration (keys, values and preset names)
are written in camelCase. In NEON, a list with a single item may also be written as a plain value, so paths: src is
the same as paths: [src]. This works for use, paths, excludePaths,
fileExtensions, fixRisky, reportOnly and for the paths in overrides.
DressCode looks for the file from the current directory upward, and the directory where it finds it becomes the root of the
project; all paths in the configuration are relative to it. When there is no configuration and the run gets a standard with the
--use option, the root is the directory of the nearest composer.json, or the current directory if there
is none, so a run from any subdirectory of the project has the same relative paths and the same cache. Next to the configuration
there may be a template dresscode.neon.dist (or .php.dist) that you commit; a file without
.dist then replaces it completely, for example for local changes. Having both NEON and PHP side by side is an error;
neither of them takes precedence. You can force another file with the --config option.
A typo is not silently skipped; it is an error when the configuration loads, and the message offers what you most likely
meant: Key `spacing.cal` is unknown; write `spacing.call`. The same happens with a value the key does not take, and
the message lists the values it does take. Without a configuration file, DressCode refuses to run and recommends dresscode init, because it has no style of its own; it runs only
with a preset from the --use option: dresscode check src --use perCs.
Decisions: What a Key Names and How It Is Written
A key of the configuration names a thing in the code, not the rule that checks it. The space between a function name and its
parenthesis is spacing.call, the brace of a class is braces.position.class, the blank lines around
methods are blankLines.aroundMethods. The value then says how the thing is written: that it must or must not be in
the code (required, forbidden), or a word (nextLine), a code sample ('$a.$b'),
a count (1), a list or a map. We call such a key with its value a decision, and you will see its whole path
next to every finding in the output:
src/Cart.php error 9:52 Expected a line break before the opening brace. braces.position.class
You write the path into the configuration when you want a different value, into a dresscode:ignore comment when
one place should stay as it is, and after dresscode explain when you want to know what the key is for and what it
takes. There are about twenty sections, from file to upgrading, and each has a page with all its keys;
the map of the whole tree is on the page Configuration Reference.
Why this matters is easy to see with blank lines. These are six lines of one section:
blankLines:
afterNamespace: 1
afterImports: 1
aroundMethods: 1
bodyStart: 0
bodyEnd: 0
beforeStatement:
return: 1
In PHP CS Fixer, the same takes six rules: blank_line_after_namespace, single_line_after_imports,
no_blank_lines_after_class_opening, class_attributes_separation, no_extra_blank_lines with
the token list curly_brace_block, and blank_line_before_statement. In PHP_CodeSniffer with Slevomat, it
takes nine sniffs, one of them with five properties. All three configurations do the same thing to the same file; they differ in
how quickly you can see what they say.
A key that neither you nor any preset writes requires nothing. And where even what a preset says should not apply, you write
keep: “nothing is enforced here, the code stays as it is”. keep is the one word for “don't touch
this”. Every decision that requires something accepts it, and on a whole section it applies to everything under it:
blankLines: keep # nothing decides about blank lines
expressions:
comparison:
yoda: keep # Yoda conditions may stay as they are
Configuration Reference describes which values each key takes, from a word through a range to a map.
Standards and Sets
You don't have to write hundreds of keys, because the configuration rests on two questions you can answer right away.
How should the code look? That is the standard, a preset that decides how the code looks, and you choose one:
| standard | what it is |
|---|---|
psr12 |
PSR-12, section by section |
perCs |
PER Coding Style 3.1, the successor of PSR-12 |
nette |
Nette Coding Standard, PER Coding Style with tabs and the layout of the Nette libraries |
symfony |
Symfony Coding Standards, following the @Symfony set of PHP CS Fixer |
laravel |
the style Laravel Pint writes by default, its laravel preset |
What else do you want from the code besides its looks? Those are the sets, presets of one intent that decide nothing about the looks, and so they never conflict with the standard:
| set | what it decides |
|---|---|
modernizations |
constructs of newer PHP where an older form says the same thing |
deprecations |
what a newer PHP or a library has deprecated or removed |
cleanup |
code that serves no purpose |
correctness |
what is most likely a bug |
types |
types written where PHP reads them |
compilerOptimizations |
global names in the form PHP optimizes already at compile time |
Both go into the use key, the standard first and the sets after it; a single preset can be written without a list
(use: perCs):
use:
- perCs
- cleanup
- modernizations
A set names an intent, not a list of keys. Instead of twenty decisions you write modernizations, and when
DressCode learns a new modernization in the next version, the set brings it without a single line in your configuration.
A decision that writes newer PHP than the project allows simply waits until the target version allows it. The nette
standard also decides about code hygiene, because it is part of Nette: it uses the cleanup, types and
correctness sets itself and sets a few of their decisions back to keep, for example native types of
parameters, properties and return values. No standard carries the modernizations and deprecations sets,
because upgrading to a newer PHP is something you run when you want it.
A preset is a file of the same shape as your configuration. The page Standards and Sets shows exactly what each one decides, with comments
explaining why. Built-in presets are written short, as in the examples: the full name has the form vendor/name, and
the vendor of the built-in ones is dresscode, so perCs is short for dresscode/perCs.
dresscode config and dresscode explain print names in the full form. A package of the vendor
dresscode is written short too, for example upgrade-nette instead of
dresscode/upgrade-nette; a preset from a plugin of another vendor is always written in full. And the use
key also takes a path to a file, for example use: [perCs, ../shared/house.neon], relative to the file that
contains it.
Layers
For each file, the values are composed from layers, from the lowest to the highest:
- the presets from the
usekey in the order they are written, each on top of the presets it builds on, - the decisions of your configuration,
- the overrides for a part of the project that the file matches, in the order they are written,
- the command line (
--use,--set).
A higher layer changes only what it says itself, and how it changes it depends on the kind of value:
- a word, a code sample, a count and yes or no replace the value from below;
- a list replaces the whole list from below, even the default one:
hygiene.aliasFunctions: [core, mysqli]means only these two groups of aliases, whatever the preset said. If you want to add to it, write...into the list, which stands for the list from below:hygiene.aliasFunctions: [..., mysqli]; - a map of names merges item by item, and an item with the value
keepremoves that name from all layers below it, even from package data; an exact item takes precedence over a pattern with an asterisk, whichever layer wrote it, and an exclamation mark after the map's key (replacedClasses!:) replaces the maps from below completely; - a section and a key with subkeys merge key by key, so changing one number from a preset means writing one number;
keepwritten on a section or on a key with subkeys cancels everything under it, and a later layer brings back only the keys it names itself.
Each preset is applied only once, at the place where something first names it. When you list it again, it changes nothing, and the run says so:
Warning: The configuration uses preset `dresscode/perCs`, which preset `dresscode/nette` already brings; the entry does nothing.
The same holds for the sets a standard uses. nette builds on cleanup, types and
correctness and turns some of their decisions off, for example types.declaration.parameter. When you
write use: [nette, types], the types set is applied only once, under nette, so you don't
get the turned-off decisions back, and the run prints the same warning. If you want them, write them yourself:
use: nette
types:
declaration:
parameter: required
property: required
return: required
The order of keys in the file has no effect on the run; DressCode itself decides the order in which rules run. dresscode config prints where each
value came from, in the shape of the file, with the layer in a comment next to each key.
PHP and Package Versions
The PHP version the code is written for is a property of the project, and it is read from composer.json, from the
constraint in require.php. A decision that writes syntax of a newer PHP applies only when every version the
constraint allows knows that syntax, so with ^8.1 PHP 8.1 decides, even if the project runs on a newer one. The
lower bound is raised by config.platform.php in composer.json, a value Composer never changes by itself:
with ^8.1 and the platform 8.3, PHP 8.3 decides. A constraint without a lower bound, such as
<8.4 or *, does not give a lowest version, so the default PHP 8.0 applies. DressCode does not
support versions older than PHP 8.0: it checks the code as PHP 8.0 and warns that a fix may write syntax the older PHP does not
know. The Target line in the header of every command's output shows where the target version came from.
You write a different version into the targets key when it differs from composer.json or when
DressCode cannot read it from there, and into an override when a part of the project runs on a different PHP:
targets:
php: '8.2'
Just like a PHP version, a decision may also require a package in some version, when it checks the API of that package.
The version of the package is read from composer.json and from the installed packages next to it, and what counts is
the version the project builds on: for a package the project requires itself, that is the lowest version its constraint allows,
not the installed one. A package that the project requires without a lower bound (*), or does not require at all and
gets only as a dependency of another package, may be in any version: a decision that wants a certain version of such a package
therefore does not apply, and its upgrade data is only reported; nothing is rewritten according to it. You can also give the
version the code should be written for in the targets key, for example when upgrading libraries you convert
the code to a new version before you install it. The version is written in quotes, because NEON would read 3.10 as
the number 3.1:
targets:
php: '8.2'
nette/forms: '3.3'
Different Values for Part of the Project
A project rarely looks the same everywhere. You want library code as fast as possible, tests have their own helper functions,
and generator templates use a different indentation. An override in the overrides key applies to the files that match
its paths, and it may contain decisions, presets in use, the PHP version in targets,
namespaces, nameResolution, fixRisky, reportOnly, severity and
suppressionComments:
use: nette
nameResolution: certain
overrides:
# the library writes global names the way PHP can optimize them
-
paths: [src]
use: [compilerOptimizations]
# tests declare helper functions in their own namespace
-
paths: [tests]
nameResolution: uncertain
blankLines:
aroundMethods: 1
severity:
phpdoc.property.plainComment: warning
# generator templates are indented with two spaces
-
paths: [resources/stubs]
indentation:
unit: '2 spaces'
The result:
- Files in
srcget thecompilerOptimizationsset on top of thenettestandard. The set writes global functions and constants so that PHP can optimize them. - In
tests,nameResolution: uncertainsays that the lists of functions from the configuration are not complete there, and fixes that depend on a function being global are only reported there as risky; the details are on the page Functions and Constants in Namespaces. There is one blank line between methods there instead of two, and a plain comment above a property, where a doc comment belongs, only warns. - Templates in
resources/stubsare indented with two spaces, and everything else is the same as in the rest of the project.
Paths have the same syntax as excludePaths and are relative to the root of the project. A file that matches
several overrides gets their layers in the order they are written. A decision that should not apply to a part of the project gets
keep in the override, and a later override can turn it on again for the files that match both. DressCode goes through
every override right at the start of the run, even when no file matches it, so it reports a typo right away, together with the
override it is in:
Error: The override for `tests`: Key `expresions` is unknown; write `expressions`.
dresscode config --file tests/CartTest.php shows what finally applies to a particular file.
Warnings and Risky Fixes
Every violation is an error and fails the build, unless the configuration softens it. The severity key does that:
a map of names to warning or error. A name is a decision, a group, a section or a preset, and it
applies to everything under it; within one layer, the most specific name wins. A warning enforces the same as an error, and
fix fixes it the same way, but the output counts it separately and the exit code stays 0; the
--max-warnings <n> option sets a threshold. This is useful for what fix does not fix, mainly
long lines:
severity:
file.longLines: warning
phpdoc: warning
The severity is not part of the value of a decision, so a new value does not carry it along: a severity applies to the value
written in the same layer or in a layer below it. The psr12 and perCs standards soften long lines this
way. When a project writes file.longLines: forbidden itself and gives no severity with it, long lines are an error
again, because now the project has decided about them.
A risky fix is one that can change what the code does, and it is always a property of a particular place. Turning
in_array($role, $roles) into in_array($role, $roles, true) is safe where values of a single type are
searched; elsewhere the code may stop finding a value it found before. So DressCode fixes what it can fix safely and reports a
risky occurrence, together with what would decide it:
- The type of a value. The types DressCode takes from your project's PHPStan would decide it.
- The name of a function. An unqualified
is_null()in a namespace may be your own functionApp\is_null(). The linenameResolution: certaindecides that. - Your judgment.
file.strictTypes.declarationturns on strict types, which by its nature changes how the code behaves;functions.staticClosurecannot see whether you bind the closure to an object somewhere else. No extra information helps here; the choice is yours.
In the output, a risky place has the status risky, and under the message there is a sentence about what can go
wrong at that place:
src/Users.php
risky 17:16 The `in_array()` call must pass `$strict = true`. mistakes.strictArgument
Risky because a strict search no longer finds a value of another type.
Writing a decision into the configuration, yourself or through a preset, does not yet mean agreeing to its risky fixes. The
project gives its consent with the fixRisky key, a list of decisions whose risky fixes it accepts; instead of a
decision, there may be a section or a preset, which applies to everything under it. The only section with the same name as a set
is types: you write the section as types.* and the set by its full name dresscode/types,
because the bare name types could mean either, and DressCode refuses it. A preset cannot give this consent. That is
why a Git hook or an editor needs no option: it fixes what the project has accepted and reports the rest. The
--fix-risky option allows the risky fixes of all decisions for a single run,
--fix-risky=mistakes.strictArgument only those of that one decision:
fixRisky:
- mistakes.strictArgument
- hygiene.classNameNotation
The fixRisky list adds up across layers, so an override can accept, for its part of the project only, risky fixes
you don't want to accept elsewhere. Without consent, the risky place is reported, and its severity follows what would decide it.
Where types or nameResolution would be enough, it is a warning, because forcing such a place through the exit code
would push you to consent blindly. Where the fix changes behavior by its nature, it is an error like any other violation. The
summary counts risky fixes by their cause and says for each what to do about it:
2 risky fixes wait, the type is unknown: set `typeAnalysis: phpstan`. See https://dresscode.run/types#enable 1 risky fix waits, a name may reach a function of the namespace: run `dresscode init`, which lists what the namespaces declare, or set `nameResolution: certain` if they declare nothing. See https://dresscode.run/namespaces#name-resolution
The opposite case is handled by the reportOnly key: decisions, sections or presets whose violations are reported
but never fixed. This is useful when you introduce the tool and want to see first what would change, or for a decision whose fix
the project does by hand. The names are read the same way as in fixRisky, the list adds up across layers, and an
override may have one for its part of the project; a preset cannot write it. A decision in reportOnly does not wait
for consent to a risky fix either, because it offers no fix:
reportOnly:
- multiline
- mistakes.strictArgument
Where your judgment decides, run dresscode fix --ask-risky: it makes the safe fixes as usual, then asks about each
remaining risky fix separately, shows its diff and makes only those you accept. Accept in fixRisky what is fine for
the whole decision, and mark a place that should stay as it is with a dresscode:ignore comment.
Paths
paths says what is checked: files and directories relative to the root of the project. Paths given on the command
line take precedence; but a directory that contains some of the paths from paths is narrowed to just those, see Command Line.
excludePaths leaves paths out, and it only adds: it adds yours to the default list (vendor,
node_modules, temp, tmp, log, and all directories and files starting with a
dot), and every plugin does the same. Nobody can bring back what another one excluded.
paths:
- src
- tests
excludePaths:
- tests/fixtures
- '*.generated.php'
A pattern with a slash is anchored to the root, so tests/fixtures is exactly that one directory. A pattern
without a slash matches the name of a file or directory at any depth. An asterisk stands for anything except a slash, two
asterisks also for slashes, so tests/**.phpt matches all tests at any depth. Two asterisks with a slash mean zero or
any number of directories, the same as in .gitignore, so src/**/Test.php also finds
src/Test.php. A question mark stands for one character except a slash, [abc] for one of the listed
characters and [!abc] for any other one. DressCode refuses an invalid pattern, for example one with an unclosed
square bracket, right when it reads the configuration.
fileExtensions says which extensions are taken as PHP (the default is only php); a project with Nette
Tester tests adds phpt. To skip a file by its content, for example generated code by its header, use
skipWhen and pass it a callback as an entity; it gets the content of the
file and its path, and returns true when the file should be skipped:
skipWhen: App\CodeStyle\GeneratedFilter::isGenerated(...)
Types and Namespaces
There are two things about a project DressCode cannot learn from a single file, so you tell it with keys.
typeAnalysis: phpstan says that the types of the code are taken from the PHPStan installed in the project, with
its phpstan.neon and extensions. Only with types do the decisions that make no sense without them apply, for example
rewriting the deprecated API of libraries. It is a decision of the whole project; neither a preset nor an override can set it. The
page Types from PHPStan describes what types make possible and what they cost.
namespaces lists the functions and constants that the namespaces of the project declare, and
nameResolution: certain says that there are no others. DressCode then knows whether an unqualified
strlen() in a namespace means the global function, and the fixes that depend on it stop being risky. The page Functions and Constants in Namespaces explains why this matters so much.
typeAnalysis: phpstan
nameResolution: certain
namespaces:
constants:
- 'App\{VERSION, DEBUG}'
Plugins
A plugin is a class implementing the DressCode\Plugin interface. Through it, a package brings a project its own
rules and presets, analyses, excluded paths and a file filter. A package announces its plugin in composer.json under
the key extra.dresscode.plugin, and after installation DressCode finds it in
vendor/composer/installed.json. But DressCode loads it only when the project lets it in: by writing the name of the
package or the class of the plugin into the use key. A plugin brings code that runs on every check, and the project
lets it in the same way Composer lets in its own plugins. The plugin of an installed package that use does not name
is not loaded, and the run warns about it. Only two kinds of plugins do not need to be named: a plugin announced by the
composer.json of the project itself, and the plugins of upgrade packages, which dresscode upgrade loads
by itself. If the plugin of a package had the same name as some preset, use could not tell which one you mean, and
DressCode refuses it with an error. A plugin that lives right in the project is listed in the use key next to the
presets, by its class, or by an entity with arguments:
use:
- acme/coding-rules
- Acme\CodeStyle\Plugin
- perCs
- acme/house
The first line lets in the plugin of the package acme/coding-rules; the package name itself is not a preset.
A plugin does not decide how the code is written. Its rules have their decisions in their own section, named after the plugin,
and its presets only become available; the project itself writes what it wants from them into use and into the
plugin's section. That is why the style of a project can always be read in its own file. A plugin belongs to the whole project,
so only the configuration lists it. DressCode refuses a plugin in the use of an override or a preset with the error
Plugin `Acme\CodeStyle\Plugin` is used by the configuration of the project, never by a preset or an override. How to
write a plugin is described in Your Own Preset and Plugin.
A package may also carry upgrade data: files describing what versions of libraries renamed or removed. The package announces
their directory under extra.dresscode.upgrading-dir. The data turns on no decision by itself: it applies only when
the project says upgrading.packages: yes, which both the deprecations and modernizations
sets do. Only then does it also turn on the rewrites of the package's plugin that its files name; the plugin itself is loaded
during check and fix only where use names it. DressCode itself carries data of the same
form about what PHP has deprecated, and the requirement upgrading.php.deprecatedApi: forbidden applies it. More in How Upgrading Libraries Works.
Objects and Callbacks
Where the PHP configuration passes an object or a callback, in NEON you write an entity. It looks like a call in PHP, and you may know it from the configuration of Nette DI:
App\CodeStyle\Filter(8.1, strict: true)creates an object; the arguments may be named,App\CodeStyle\Filter::create(8.1)calls a static method and uses what it returns,App\CodeStyle\Filter::isGenerated(...)passes the method itself as a callback,App\CodeStyle\Filter(8.1)::withLimit(100)calls a method on what the previous link of the chain returned.
Only three places take an entity: a plugin in the use key, the skipWhen key and an analysis factory
in the analyses key, written as a pair of a class and an entity:
use:
- App\CodeStyle\Plugin(strict: true)
- perCs
skipWhen: App\CodeStyle\GeneratedFilter::isGenerated(...)
rules:
- App\CodeStyle\ExceptionMessagePeriodRule
analyses:
- App\CodeStyle\Analyses\Imports
App\CodeStyle\Analyses\Calls: App\CodeStyle\Analyses\Calls::create(...)
skipWhen and the factory of an analysis are evaluated right when the configuration loads, while a plugin from
use is created only once it is certain that its class really is a plugin. So the configuration of a repository that
you only download and check cannot run any other code through a class name. In NEON, a project rule is named only by its class.
A rule with its own dependencies is built by a factory every time DressCode needs it, for example separately for the files of
each override, and such a factory is a plain closure in dresscode.php:
return new Config(
use: ['perCs'],
rules: [
App\CodeStyle\ExceptionMessagePeriodRule::class,
App\CodeStyle\CacheKeyRule::class => fn() => new App\CodeStyle\CacheKeyRule(App\CodeStyle\Keys::load()),
],
);
Command Line
The command line is the highest layer and lies above the configuration and above all overrides. --use adds a
preset or a plugin, --set gives one decision a value in NEON, and --only narrows the run to the
decisions, sections or presets it names, without changing values:
dresscode fix --set hygiene.is_null=forbidden # the configuration plus this decision
dresscode fix --only cleanup # only the decisions of the cleanup set
dresscode check src --use psr12 # someone else's project without a configuration
These options are not keys of the configuration file: they decide one run, not the project. The page Command Line describes how to use them to try a decision, introduce it in one commit or clean up code step by step.
Other Keys
baseline: a file listing violations that cannot be fixed yet and should not be reported; the commanddresscode baselinewrites it, see Suppressions and the Baseline.suppressionComments: the project's own comments that silence a decision on their line, see Suppressions and the Baseline.cacheDir: where DressCode stores which files passed clean, and withtypeAnalysis: phpstanalso PHPStan's cache; the default is the directorydresscodein the system temporary directory.rulesandruleUrl: the project's own rules, named by class, and the address of their pages with the placeholder{slug}, which every finding of theirs links to in the terminal.analyses: registers your own analysis for your own rules; an analysis of the same class from the configuration takes precedence over the one from a plugin.
dresscode config shows what finally applies from all of this: the resolved configuration in the shape of the file,
with the layer that set each value, and for a decision that does not apply, the reason. With --file, the same for one
file, including overrides, and with --format json the same as data.