Your Own Preset and Plugin
Package the style you want on all your projects as a preset and your own rules as a plugin with a section of
decisions, then share both through Composer. A project turns them on with a single line in the key use, for all of
its code or just a part of it.
A Preset in NEON
A preset is a NEON file shaped like a configuration. It carries decisions, the use key listing what it builds on,
and possibly suppressionComments; nothing more. Here is the house style of a company called Acme:
# Acme house style
#
# PER Coding Style with tabs, lines of up to 100 characters, and the messages of exceptions as sentences.
use: perCs
file:
lineLength: 100
indentation:
unit: tab
expressions:
comparison:
# a comparison reads in the order it is spoken
yoda: forbidden
acme:
exceptionMessagePeriod: required
- A preset starts with a comment: the name on the first line, and below it what the preset is. A comment above a key says why the decision has its value; the standards and sets of DressCode are written this way too.
usenames the presets yours builds on, whether standards, sets or other presets. They lie below your preset in the order they are written, each one only once, so what your preset writes wins over what it builds on, and the project above it wins over the preset. Values are merged the same way as in the configuration: a map is merged by names, a word or a list replaces the whole value, andkeepon a section removes everything under it.- The decisions of a preset can belong to the engine as well as to a plugin, like
acme.exceptionMessagePeriodin the example; the project must then have the plugin; otherwise the catalogue does not know the key. - A preset cannot make the choices that belong to the project. The facts about the project (
targets,typeAnalysis,namespaces,nameResolution), the scope of the run (paths,excludePathsand others),fixRisky, plugins andrulesare left to the project. DressCode rejects a preset that sets them, with the errorPreset `acme/house` sets `fixRisky`, which the project decides, not a preset.But a preset may use theseveritykey to make violations of a decision only warnings, likefile.longLines: warningin thepsr12standard. - A preset meant to be added to any standard is a set: it does not decide the layout of the code, does not build on a standard, and does not touch spaces, indentation, line breaking or order. Say so in its opening comment.
A preset does not have to worry about the PHP version or about what the project has installed: a decision whose rule asks for
a PHP version or a package in requires is silently skipped where the project does not have them. In the same way, a
decision whose rule needs types is silently skipped in a project without typeAnalysis: phpstan.
A project uses a preset by putting the path to its file in use, for example use: tools/house.neon,
or its name when a plugin brings it. Because it is a layer like any other, you
can also use it just for a part of the project, in an override:
use:
- Acme\CodeStyle\Plugin
- perCs
overrides:
-
paths: [src]
use: [acme/house]
A Plugin with Rules and Presets
A plugin is what a package brings to a project: a class implementing DressCode\Plugin, whose
getManifest() method returns a PluginManifest. The manifest lists the rules, the presets with their
names and files, the section that holds the decisions of the plugin's rules, decisions shared by several of its rules, the
plugins it builds on, analyses, excluded paths, the file filter skipWhen and the address of the rule pages
ruleUrl. Here is the plugin of the Acme package:
namespace Acme\CodeStyle;
use DressCode\PluginManifest;
final class Plugin implements \DressCode\Plugin
{
public function getManifest(): PluginManifest
{
return new PluginManifest(
rules: [ExceptionMessagePeriodRule::class, NoDebugCallsRule::class],
presets: ['acme/house' => __DIR__ . '/house.neon'],
section: 'acme',
excludePaths: ['generated'],
ruleUrl: 'https://acme.dev/rules/{slug}',
);
}
}
The section is the name of the plugin in the configuration. All the decisions of its rules lie under it,
acme.exceptionMessagePeriod and acme.debugCalls, so they do not clash with the engine, with another
plugin or with the project's rules in the project section. A plugin that brings rules must have a section, and it
must not be a reserved key or a section of the engine. Users then write the decisions of the plugin just like any others, see them
next to findings, suppress them with dresscode:ignore acme.debugCalls, accept their risky fixes in
fixRisky and have them explained by dresscode explain acme.
Shared decision. A decision usually belongs to one rule, which returns it from getDecisions(). When two
rules with independent logic enforce one thing, for example debugging output in a call and in a comment, the manifest declares the
decision in decisions, and both rules name it in #[RuleInfo(decisions: [...])]. The user writes one key
that turns on both rules, and the findings of both are reported under it:
return new PluginManifest(
rules: [DebugCallsRule::class, DebugCommentsRule::class],
section: 'acme',
decisions: [new Decision('acme.debugOutput', Domain::state('forbidden'), 'Debugging output left in the code')],
);
#[RuleInfo(Stage::Structure, decisions: ['acme.debugOutput'])]
final class DebugCommentsRule extends NodeRule
A rule may also name a decision of the engine, such as file.lineLength, and follow it just like the rules of the
engine do. A rule that only reads a decision without enforcing it names it in reads instead of
decisions: the decision does not turn it on and findings are not reported under it, but the rule finds the value in
its Values, even when the run leaves out the rules that enforce it. Tests find shared decisions automatically:
RuleTester reads the plugin from the key extra.dresscode.plugin in the composer.json of the
package where the rule lives.
Presets from the manifest get their names in the form vendor/name, so the project uses them by name in
use (acme/house) or for a single run with --use. ruleUrl holds the address of
the rule pages with the placeholder {slug}, which stands for the class name of the rule without the Rule
suffix and with a lowercase first letter; dresscode explain prints it for every decision of the rule. The excluded
paths are added to those of the project, the skipWhen filter skips a file even when the project has its own filter,
and when the project lists an analysis of the same class as the plugin, the project's analysis applies. A plugin that another
one builds on lies below it, and each class is used only once.
Notice what is not in the manifest: a plugin gives no decision a value and does not set a style, because
PluginManifest has no such key at all; decisions only declares decisions. The rules of the plugin turn
on only when the project, or a preset the project uses, writes their decisions. Which style applies is up to the project, so you
can always read it in the project's own file:
use:
- Acme\CodeStyle\Plugin
- acme/house
acme:
debugCalls: forbidden
mistakes:
debugOutputFunctions: [dump]
The rules of a plugin may also read the keys of the engine. acme.debugCalls in the example takes the list of
debugging functions from the engine parameter mistakes.debugOutputFunctions, so the project writes it once for all
rules that ask about debugging functions; A Rule in
Detail describes how the rule does it.
This is exactly how the dresscode/upgrade-nette package works: its plugin has the nette section with
decisions such as nette.linkArguments or the map nette.flagsAsNamedArguments, and the package carries
its upgrade data next to it, in the upgrading/ directory.
A plugin goes into the use key of the configuration, next to the presets, by its class or as an entity with arguments. Neither an override nor a
preset can list it, because it makes rules and presets available to the whole project; DressCode rejects a preset that would list
it, with the error
Preset `acme/house`: Plugin `Acme\CodeStyle\Plugin` is used by the configuration of the project, never by a preset or an override.
The skipWhen filter and the factory of an analysis, however, can also be written in dresscode.neon
itself, as an entity, and the factory of a rule
with dependencies in dresscode.php. A plugin is the right fit where a package carries the settings and the project
only turns them on.
A Composer Package
A package with a preset or with rules is an ordinary Composer package that requires dresscode/dresscode and
shares the autoloader with the project. It announces its plugin in composer.json under the key
extra.dresscode.plugin, so after installation the project does not need to know its class and lets it in by the name
of the package in use:
{
"name": "acme/code-style",
"require": {
"dresscode/dresscode": "^1.0"
},
"autoload": {
"psr-4": {"Acme\\CodeStyle\\": "src/"}
},
"extra": {
"dresscode": {
"plugin": "Acme\\CodeStyle\\Plugin"
}
}
}
Besides plugin, DressCode recognizes two more keys under extra.dresscode, upgrading-dir
and packages, with data for upgrading
libraries; it reports any other key as a warning and skips it, because the package may be newer than the DressCode that reads
it. Inside the package are the rule classes, the preset files, the plugin, and the fixtures with tests. The names of presets carry
the vendor of the package (acme/…) and the decisions carry the section of the plugin, so that they do not clash
with others. The user then installs the package, writes its name into use and chooses everything else in the
configuration:
use:
- perCs
- acme/code-style
- acme/house
The name of a package is not a preset; it only lets its plugin in. A plugin brings code that runs on every check, so it is
loaded only when the project names it, just as Composer runs only the plugins the project allows. For an installed package that
use does not name, the run reports the warning
Package `acme/code-style` brings the plugin `Acme\CodeStyle\Plugin`, which loads only where `use` names the package or the plugin; skipped.
Upgrade data under the key upgrading is loaded even when use does not name the package, because it
carries no code.