Skip to main content

Writing a custom sniff

Any class that implements DocbookCS\Sniff\SniffInterface can be registered as a sniff. Extending AbstractSniff is the recommended starting point as it provides property handling and a createViolation() helper.

Minimal example

namespace Acme\Sniff;

use DocbookCS\Sniff\AbstractSniff;
use DocbookCS\Source\File;
use DocbookCS\Violation\SourceRange;

final class DeprecatedUlinkSniff extends AbstractSniff
{
public static function getCode(): string
{
return 'Acme.DeprecatedUlink';
}

public function process(\DOMDocument $document, File $file): array
{
if ($document->getElementsByTagName('ulink')->length === 0) {
return [];
}

$violations = [];

preg_match_all('/<ulink\b/', $file->content, $matches, PREG_OFFSET_CAPTURE);

foreach ($matches[0] as [$match, $offset]) {
$violations[] = $this->createViolation(
$file->path,
'<ulink> is deprecated; use <link xlink:href="..."> instead.',
[SourceRange::fromFile($file, $offset, $offset + strlen($match))],
);
}

return $violations;
}
}

Implementing the interface

getCode()

A static method returning a unique dot-notation string used in reports and configuration:

public static function getCode(): string
{
return 'Vendor.SniffName';
}

process()

Receives the parsed DOMDocument and a DocbookCS\Source\File value object exposing the file path ($file->path) and the raw source ($file->content). Returns a list of Violation objects (or an empty array).

Use the DOM for structural checks, and the raw $file->content string when you need offset information that the DOM does not preserve.

Source ranges

Every violation carries one or more SourceRange objects pointing at the affected source. Build them from byte offsets into the file content:

SourceRange::fromFile($file, $beginOffset, $untilOffset)

The range determines the line reported for the violation and, when the sniff is fixable, the exact source region a fixer may replace.

Entity expansion

DocBook entities are expanded before sniffing, so the DOM can contain nodes that do not exist in the inspected source file. When walking the DOM, skip those with the isSourceBacked() helper so violations are only reported against content that is actually present in the file:

foreach ($document->getElementsByTagName('ulink') as $node) {
if (!$this->isSourceBacked($node)) {
continue;
}
// ...
}

Adding configurable properties

Call $this->getProperty() to read values set via <property> in the config:

$allowedTags = array_filter(
array_map('trim', explode(',', $this->getProperty('allowedTags')))
);

In docbookcs.xml:

<sniff class="Acme\Sniff\DeprecatedUlinkSniff">
<property name="allowedTags" value="ulink,xref"/>
</sniff>

Severity

All sniffs accept a severity property that overrides the default violation level for that sniff:

<sniff class="Acme\Sniff\DeprecatedUlinkSniff">
<property name="severity" value="warning"/>
</sniff>

Accepted values: error, warning.

Making a sniff fixable

To support --fix, implement DocbookCS\Sniff\Fixable (which extends SniffInterface) and point it at a fixer class:

use DocbookCS\Sniff\Fixable;

final class DeprecatedUlinkSniff extends AbstractSniff implements Fixable
{
public static function getFixerClassName(): string
{
return DeprecatedUlinkFixer::class;
}

// ...
}

The fixer implements DocbookCS\Fix\Fixer\Fixer and turns a violation into a replacement for one of its source ranges — return a single Fix, or a FixPlan bundling several fixes that must be applied together:

namespace Acme\Sniff;

use DocbookCS\Fix\Fix;
use DocbookCS\Fix\Fixer\Fixer;
use DocbookCS\Violation\Violation;

final class DeprecatedUlinkFixer implements Fixer
{
public function process(Violation $violation): Fix
{
return Fix::fromViolationAndRange(
$violation,
$violation->rangeOne(),
'<link',
);
}
}

Fixes that conflict with already-accepted fixes, or that would not change the content, are skipped automatically and their violations reported as remaining. Throw a DocbookCS\Fix\FixerException only when the violation's source content does not match what the fixer expects — this aborts the run with a runtime error.

Registering the sniff

Add the fully-qualified class name to docbookcs.xml. The class must be autoloadable (e.g. via Composer):

<sniffs>
<sniff class="Acme\Sniff\DeprecatedUlinkSniff"/>
</sniffs>