Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 24 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,15 +40,16 @@ Implement `DocbookCS\Sniff\SniffInterface` (or extend `AbstractSniff`):
namespace Acme\DocbookSniffs;

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

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

public function process(\DOMDocument $document, string $content, string $filePath): array
public function process(\DOMDocument $document, File $file): array
{
$violations = [];
// ... inspect $document, add violations via $this->createViolation(...)
Expand All @@ -63,6 +64,27 @@ Register it in your config:
<sniff class="Acme\DocbookSniffs\MySniff" />
```

## CLI Scope

By default, DocbookCS checks the current Git diff from its upstream branch point
through the working tree. Alternatively, a unified diff can be piped or file and
directory paths passed. The inspection scope is limited to the given diff or the
full contents of the given file paths.

XML references are expanded by default, but violations and fixes remain limited
to the given scope. With `--wide`, every file, inferred from paths or diff, as
a whole will be checked and referenced `SYSTEM` XML files recursively included
in violation reports and fixing.

| Input | `--wide` | Full File(s) | References |
|------------|---------:|-------------:|-----------:|
| none | no | no | no |
| none | yes | yes | yes |
| path | no | yes | no |
| path | yes | yes | yes |
| piped diff | no | no | no |
| piped diff | yes | yes | yes |

## License

Apache 2.0
11 changes: 9 additions & 2 deletions bin/docbook-cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use DocbookCS\Application;

foreach ($candidates as $file) {
if (!is_file($file)) {
continue;
continue;
}

require $file;
Expand All @@ -25,4 +25,11 @@ use DocbookCS\Application;
exit(2);
})();

new Application($argv ?? [])->run();
try {
$application = Application::fromGlobals($argv ?? []);
} catch (\RuntimeException $e) {
fwrite(STDERR, $e->getMessage() . PHP_EOL);
exit(2);
}

exit($application->run());
3 changes: 3 additions & 0 deletions phpcs.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
<ruleset name="PHP_CodeSniffer" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://raw.githubusercontent.com/squizlabs/PHP_CodeSniffer/refs/heads/master/phpcs.xsd">
<description>DocbookCS Coding Standard</description>
<rule ref="PSR12"/>
<rule ref="PSR2.Methods.FunctionCallSignature.SpaceBeforeCloseBracket">
<exclude-pattern type="relative">tests/*</exclude-pattern>
</rule>

<arg name="cache" value="var/.phpcs-cache.json"/>
<arg name="basepath" value="./"/>
Expand Down
3 changes: 3 additions & 0 deletions phpunit.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@
<testsuite name="integration">
<directory>tests/Integration</directory>
</testsuite>
<testsuite name="feature">
<directory>tests/Feature</directory>
</testsuite>
</testsuites>

<source restrictNotices="true" restrictWarnings="true">
Expand Down
168 changes: 81 additions & 87 deletions src/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,16 @@
use DocbookCS\Config\ConfigData;
use DocbookCS\Config\ConfigParser;
use DocbookCS\Config\ConfigParserException;
use DocbookCS\Diff\DiffParser;
use DocbookCS\Progress\ConsoleProgress;
use DocbookCS\Progress\NullProgress;
use DocbookCS\Progress\ProgressInterface;
use DocbookCS\Report\Reporter\CheckstyleReporter;
use DocbookCS\Report\Reporter\ConsoleReporter;
use DocbookCS\Report\Reporter\JsonReporter;
use DocbookCS\Report\Reporter\ReporterInterface;
use DocbookCS\Runner\SniffRunner;
use DocbookCS\Runner\RunCoordinator;
use DocbookCS\Runner\RunMode;
use DocbookCS\Runner\RunPlanner;

final class Application
{
Expand All @@ -32,29 +33,64 @@ final class Application
/** @var resource */
private $stderr;

/** @var resource */
private $stdin;
private ?string $stdin;

/**
* @param list<string> $argv
* @throws \RuntimeException if redirected stdin cannot be read.
* @api
*/
public static function fromGlobals(array $argv): self
{
$stdin = null;
$stat = fstat(STDIN);

if ($stat === false) {
return new self($argv, stdin: $stdin);
}

$type = $stat['mode'] & 0170000;

if ($type === 0010000 || $type === 0100000) {
$stdin = stream_get_contents(STDIN);

if ($stdin === false) {
throw new \RuntimeException('Could not read diff from stdin.');
}
}

return new self($argv, stdin: $stdin);
}

/**
* @param list<string> $argv
* @param ?resource $stdout
* @param ?resource $stderr
* @param ?resource $stdin
*/
public function __construct(array $argv, mixed $stdout = null, mixed $stderr = null, mixed $stdin = null)
{
public function __construct(
array $argv,
mixed $stdout = null,
mixed $stderr = null,
?string $stdin = null,
) {
$this->argv = $argv;
$this->stdout = $stdout ?? STDOUT;
$this->stderr = $stderr ?? STDERR;
$this->stdin = $stdin ?? STDIN;
$this->stdin = $stdin;
}

/**
* @return int Exit code (0 = success, 1 = violations found, 2 = runtime error).
*/
public function run(): int
{
$options = $this->parseArgv();
try {
$options = $this->parseArgv();
} catch (\InvalidArgumentException $e) {
$this->writeError('Error: ' . $e->getMessage() . PHP_EOL);

return 2;
}

if ($options['help']) {
$this->printHelp();
Expand All @@ -76,31 +112,22 @@ public function run(): int
return 2;
}

$overridePaths = $options['paths'] !== [] ? $options['paths'] : null;

// If override paths are relative, resolve them against cwd.
if ($overridePaths !== null) {
$overridePaths = $this->resolveOverridePaths($overridePaths);
}

$diff = null;

if ($options['diff'] !== null) {
try {
$diffContent = $this->readDiff($options['diff']);
$diff = (new DiffParser())->parse($diffContent);
} catch (\Throwable $e) {
$this->writeError('Error reading diff: ' . $e->getMessage() . PHP_EOL);
try {
$runPlan = new RunPlanner(
config: $config,
mode: RunMode::fromFixFlag($options['fix']),
wide: $options['wide'],
)->plan($options['paths'], $this->stdin);
} catch (\Throwable $e) {
$this->writeError('Error resolving input: ' . $e->getMessage() . PHP_EOL);

return 2;
}
return 2;
}

$progress = $this->createProgress($options);

try {
$runner = new SniffRunner($progress);
$report = $runner->run($config, $overridePaths, $diff);
$report = new RunCoordinator($progress)->run($runPlan);
} catch (\Throwable $e) {
$this->writeError('Runtime error: ' . $e->getMessage() . PHP_EOL);

Expand All @@ -118,39 +145,20 @@ public function run(): int
return (int) $report->hasViolations();
}

/**
* @param list<string> $paths
* @return list<string>
*/
private function resolveOverridePaths(array $paths): array
{
$cwd = getcwd() ?: '.';
$resolved = [];

foreach ($paths as $path) {
if (str_starts_with($path, '/') || preg_match('#^[a-zA-Z]:[/\\\\]#', $path)) {
$resolved[] = $path;
continue;
}

$resolved[] = $cwd . '/' . $path;
}

return $resolved;
}

/**
* @return array{
* help: bool,
* version: bool,
* config: string,
* report: string,
* colors: bool,
* fix: bool,
* wide: bool,
* quiet: bool,
* paths: list<string>,
* diff: string|null,
* perf: bool,
* }
* @throws \InvalidArgumentException for unsupported or removed options.
*/
private function parseArgv(): array
{
Expand All @@ -160,9 +168,10 @@ private function parseArgv(): array
'config' => self::DEFAULT_CONFIG,
'report' => 'console',
'colors' => $this->detectColorSupport(),
'fix' => false,
'wide' => false,
'quiet' => false,
'paths' => [],
'diff' => null,
'perf' => false,
];

Expand Down Expand Up @@ -227,57 +236,37 @@ private function parseArgv(): array
continue;
}

// --diff = read from stdin
// --diff=FILE = read from file
// --diff=- = read from stdin (explicit)
if ($arg === '--diff') {
$result['diff'] = '';
if ($arg === '--perf') {
$result['perf'] = true;
$i++;
continue;
}

if (str_starts_with($arg, '--diff=')) {
$result['diff'] = substr($arg, 7);
if ($arg === '--fix') {
$result['fix'] = true;
$i++;
continue;
}

if ($arg === '--perf') {
$result['perf'] = true;
if ($arg === '--wide') {
$result['wide'] = true;
$i++;
continue;
}

// Anything else is a path to scan.
if (!str_starts_with($arg, '-')) {
$result['paths'][] = $arg;
$i++;
continue;
}

$i++;
throw new \InvalidArgumentException(sprintf('Unknown option: %s', $arg));
}

return $result;
}

/** @throws \RuntimeException if the source cannot be read. */
private function readDiff(string $source): string
{
if ($source === '' || $source === '-') {
$content = stream_get_contents($this->stdin);
if ($content === false) {
throw new \RuntimeException('Could not read diff from stdin.'); // @codeCoverageIgnore
}
return $content;
}

$content = @file_get_contents($source);
if ($content === false) {
throw new \RuntimeException(sprintf('Could not read diff file: %s', $source));
}

return $content;
}

/** @param array{report: string, quiet: bool, colors: bool} $options */
private function createProgress(array $options): ProgressInterface
{
Expand Down Expand Up @@ -382,22 +371,27 @@ private function printHelp(): void
--report=<format> Output format: console (default), checkstyle, json.
--colors Force ANSI color output.
--no-colors Disable ANSI color output.
--diff[=<file>] Restrict analysis to files changed in a unified diff.
Omit the value or pass "-" to read the diff from stdin.
Violations are only reported when the violating element
is on or contains a changed line (parent-context aware).
--fix Automatically fix violations when fixers exist
(experimental).
--wide Check whole selected files and recursively include
referenced XML files.

Arguments:
<file-or-directory> One or more files or directories to scan.
If omitted, the paths from the config file are used.
Paths cannot be combined with diff input.

Examples:
docbook-cs
docbook-cs --config=myconfig.xml reference/
docbook-cs --report=checkstyle --no-colors > report.xml
docbook-cs . --fix
docbook-cs reference/
docbook-cs reference/strings/functions/strlen.xml
git diff HEAD | docbook-cs --diff --report=checkstyle
docbook-cs --diff=changes.patch --report=json
docbook-cs reference/strings/functions/strlen.xml --wide
docbook-cs reference/strings/functions/strlen.xml --wide --fix
git diff HEAD | docbook-cs
git diff HEAD | docbook-cs --wide
git diff HEAD | docbook-cs --wide --fix --report=checkstyle

HELP;

Expand Down
Loading