Extending DressCode
Extend the PHP code style checker with your own rule and its decisions, a preset, a plugin, an analysis or upgrade data for your library: when each one is the right choice, and which parts of DressCode you can rely on as public API.
Five Places You Can Extend
A rule is the answer to “I also want to check this”. It declares the decisions it enforces. Then, as a NodeRule, it says
which nodes it cares about, asks its question and fixes the code if needed; or, as a GapRule, it makes a claim on the
whitespace between tokens. Users then set its decisions just like those of the engine: the rules of a project in the section
project, the rules of a plugin in a section named after the plugin. You can write one in an afternoon: How to Write Your Own Rule.
A preset is the answer to “I want this style on all my projects”. It is a NEON file in the shape of a configuration that carries decisions. It can build on another preset, and you can use it for a whole project or just for a part of it. See Your Own Preset and Plugin.
A plugin is the answer to “I want to share this as a package”. It is a class implementing
DressCode\Plugin, whose only method getManifest() returns a PluginManifest: the rules of
the package, the section of their decisions, decisions shared by several rules, named presets, the plugins it builds on, analyses,
excluded paths, a file filter and the address of the rule pages. A project names it in the key use next to the
presets, either by its class or by the name of a package that announces it in extra.dresscode.plugin of its
composer.json. A plugin that is merely installed is not loaded, because it brings code that runs on every check.
A plugin does not decide how the code looks: the project or a preset writes the decisions. See Your Own Preset and Plugin.
An analysis is the answer to “several rules need this information about a file”. It is a plain class built on top of
the tree, which rules ask for. The engine creates it once and throws it away whenever the tree changes; only an analysis
implementing PassAnalysis lasts for the whole pass. The built-in ones are NameResolver and Scope, PhpDoc, PhpSymbols with
PhpSignatures (what PHP itself declares) and Types (the types of the code from the PHPStan of the
project). See A Rule in Detail.
Upgrade data is the answer to “the new version of my library renamed its API, and users should fix their code”.
These are not rules but NEON files in a directory that a package announces in extra.dresscode.upgrading-dir. They
fill the maps of the engine, such as upgrading.api.replacedClasses, or the maps of plugin rules. See Replacement Maps.
What you cannot extend is the parser. It is a separate library, PhpSyntax; its
grammar is PHP and nothing else, and the tree it produces is the same for all rules. That is exactly why rules can be combined
without knowing about each other. You also cannot add your own output format: the output is a contract of its own, and if you want
to process the results somewhere else, read the JSON from dresscode check --format json.
What Is Public API
A plugin can rely on these classes between minor versions:
- writing a rule:
Rulewith its subclassesNodeRuleandGapRule,RuleInfo,RuleContext,Claim,Space,Line,Gap,StylewithImportStyle(the shape of imports),Violationand the enumsStage,Severity,TristateandRisk; - decisions:
Decision,Domainwith the domains inDomains\(Words,Shapes,Count,Flag,Names,Text,GrammarEntry,Map), the valuesValuesandValuewith the layer that set the value (Config\Layer,Config\LayerKind), and the catalogueConfig\Catalogue; - configuration:
Config,Profile,Override,PluginandPluginManifest; presets are NEON files and their names; - rule helpers:
GlobalCalls(which global function a call calls),CodeWriter(code written in the style the file uses),QualifiedNames(the two halves of a fully qualified name) and the grammar of the maps inUpgrading\(MemberMap,MemberMapGrammar,MemberPattern,MemberMapMatch,ArgumentPattern,ArgumentPatternItem,ArgumentBindings,FunctionPattern); the built-in rules inRules\only by the name and namespace of their class, so that you can name them in the configuration; their methods are not public; - analyses and testing: everything in
Analyses\exceptRegistry,PhpStan,PhpSymbolsDataandPhpSignaturesData, plusTesting\RuleTesterandTesting\UpgradingTestertogether with their exceptionTesting\TestFailure, and the file resultFileResultthatUpgradingTester::runSample()returns; - command line:
Console\ApplicationandConsole\UsageExceptionfor a program that wants to embed a run; - exceptions:
ConfigurationException,RuleException,ConvergenceException; - from the library PhpSyntax, everything not marked
@internal:Parser,Lexer,Printer,Node,Token,Trivia, the nodes, the analyses,Style,Indentation, the enums andParseException.
Anything marked @internal can change at any time: the engine, loading and resolving the configuration, the output
formats, and the helpers that the built-in rules share only among themselves. So a program that wants to run DressCode calls it
through the command line. A test guards both lists, so a class that is in neither of them fails the test suite instead of
becoming public API by accident. And because a minor version can add a new tree node for new PHP syntax, every match
over node classes needs a default branch.
Some public classes are created only by the engine: a rule reads FileResult, ImportStyle,
LineSplits or MemberMapMatch but does not create them, so their constructor is @internal.
The page Version Compatibility sums up what may and may not change between
versions.
Naming
The decision path is what users write and read, so its name matters most. The path names a thing in the code, not the step the
fix takes: acme.exceptionMessagePeriod: required, not acme.addPeriod: true. The value then says how the
thing is written. The built-in catalogue uses settled words for that: forbidden and required for what
must not, or must, appear in the code (in the section upgrading also for a construct of newer PHP and for replacing
something deprecated); sameLine and nextLine for the position of a token; a code sample in quotes for a
shape; and a number for blank lines. Where a thing has several places, each one gets its own key
(braces.position.class, braces.position.function), and a parameter that only refines something carries
the name of what it refines (mistakes.debugOutputFunctions). Paths are written in camelCase.
By convention, a rule class is named after the state it enforces, with the suffix Rule, so
ExceptionMessagePeriodRule enforces acme.exceptionMessagePeriod. ruleUrl builds the address
of the rule's page from this name, without the suffix and with a lowercase first letter. A preset is named
vendor/name, and a plugin's section is named after the plugin. This is the convention of the built-in catalogue, and
a plugin that follows it fits right in.