Skip to content
Merged
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
36 changes: 36 additions & 0 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,42 @@ vendor/bin/structarmed analyse --config=path/to/structarmed.php
vendor/bin/structarmed analyze --config=path/to/structarmed.php
```

## Base Path

The project root defaults to the directory the command runs in. Pass `--basepath` when StructArmed is installed somewhere else, for example in a `tools/structarmed` directory with its own `composer.json`:

```text
composer.json
src/
tests/
tools/
└── structarmed/
├── composer.json
├── structarmed.php
└── vendor/
```

```bash
cd tools/structarmed
vendor/bin/structarmed analyse --basepath=../../
```

`-d` is a short alias for `--basepath`:

```bash
vendor/bin/structarmed analyse -d ../../
```

Everything relative to the project root now resolves against the base path: layer paths such as `->layer('Config', 'src/ConfigProvider.php')`, scan paths given on the command line, the `composer.json` read by the composer rules and PSR-4 layers, the cache directory, and baseline paths. The config file is discovered in the current directory first, then in the base path; `--config` keeps pointing to a path relative to the current directory.

`--clear-cache` accepts the same option, so the cache of a project analysed through `--basepath` is cleared with:

```bash
vendor/bin/structarmed --basepath=../../ --clear-cache
```

Options may be given before or after the command.

## Auto-Fix Violations

Use `--fix` to automatically apply fixes for violations produced by rules that implement `Boundwize\StructArmed\Rule\FixableInterface`.
Expand Down
17 changes: 16 additions & 1 deletion src/Cli/AnalyseCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
* @phpstan-type CommandOptions array{
* report?: string,
* config?: string,
* basepath?: string,
* generate-baseline?: string,
* no-progress?: true,
* clear-cache?: true,
Expand All @@ -54,6 +55,8 @@
private const VALUE_OPTIONS = [
'--report' => 'report',
'--config' => 'config',
'--basepath' => 'basepath',
'-d' => 'basepath',
'--generate-baseline' => 'generate-baseline',
];

Expand Down Expand Up @@ -98,6 +101,18 @@ public function run(array $arguments, string $basePath): int
return 1;
}

$workingDirectory = $basePath;

if (isset($options['basepath'])) {
$basePath = Path::normalise(Path::resolve($options['basepath'], $workingDirectory), canonicalise: true);

if (! is_dir($basePath)) {
echo sprintf("Error: base path [%s] not found.\n", $options['basepath']);

return 1;
}
}

foreach ($scanPaths as $scanPath) {
$fullScanPath = Path::resolve($scanPath, $basePath);

Expand All @@ -123,7 +138,7 @@ public function run(array $arguments, string $basePath): int
}

try {
$configFile = $options['config'] ?? ConfigLoader::discover($basePath);
$configFile = $options['config'] ?? ConfigLoader::discover($workingDirectory, $basePath);
$architecture = ConfigLoader::load($configFile);
} catch (RuntimeException $runtimeException) {
return $this->reportError($runtimeException);
Expand Down
39 changes: 27 additions & 12 deletions src/Cli/ClearCacheCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,24 @@
use Boundwize\StructArmed\Cache\AnalysisResultCache;
use Boundwize\StructArmed\Cache\FileHashProvider;
use Boundwize\StructArmed\Config\ConfigLoader;
use Boundwize\StructArmed\Util\Path;
use RuntimeException;

use function count;
use function explode;
use function is_dir;
use function sprintf;
use function str_starts_with;
use function strlen;
use function substr;

use const PHP_EOL;

final readonly class ClearCacheCommand
{
private const VALUE_OPTIONS = [
'--config' => 'config',
'--basepath' => 'basepath',
'-d' => 'basepath',
];

/**
* @param list<string> $arguments
*/
Expand All @@ -28,15 +34,12 @@ public function run(array $arguments, string $basePath): int
$counter = count($arguments);

for ($i = 0; $i < $counter; $i++) {
$argument = $arguments[$i];

if (str_starts_with($argument, '--config=')) {
$options['config'] = substr($argument, strlen('--config='));
continue;
}
$argument = $arguments[$i];
$optionAndValue = explode('=', $argument, 2);
$option = $optionAndValue[0];

if ($argument === '--config') {
$options['config'] = $arguments[++$i] ?? '';
if (isset(self::VALUE_OPTIONS[$option])) {
$options[self::VALUE_OPTIONS[$option]] = $optionAndValue[1] ?? $arguments[++$i] ?? '';
continue;
}

Expand All @@ -46,10 +49,22 @@ public function run(array $arguments, string $basePath): int
return 1;
}

$workingDirectory = $basePath;

if (isset($options['basepath'])) {
$basePath = Path::normalise(Path::resolve($options['basepath'], $workingDirectory), canonicalise: true);

if (! is_dir($basePath)) {
echo sprintf("Error: base path [%s] not found.\n", $options['basepath']);

return 1;
}
}

$cacheDirectory = null;

try {
$configFile = $options['config'] ?? ConfigLoader::discover($basePath);
$configFile = $options['config'] ?? ConfigLoader::discover($workingDirectory, $basePath);
$cacheDirectory = ConfigLoader::load($configFile)->getCacheDirectory();
} catch (RuntimeException $runtimeException) {
if (isset($options['config'])) {
Expand Down
23 changes: 19 additions & 4 deletions src/Cli/StructArmedApplication.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,39 @@
use Boundwize\StructArmed\Version;

use function array_slice;
use function array_values;
use function getcwd;
use function in_array;
use function sprintf;

final readonly class StructArmedApplication
{
private const COMMANDS = ['init', 'analyse', 'analyze', '--clear-cache', '--version', '-V', '--help', '-h'];

/**
* @param list<string> $argv
*/
public function run(array $argv, ?string $basePath = null): int
{
$basePath ??= (string) getcwd();
$command = $argv[1] ?? null;
$arguments = array_slice($argv, 1);
$command = $arguments[0] ?? null;

if ($command === '--internal-worker') {
return AnalysisNodeWorker::run($argv[2] ?? '', $argv[3] ?? '');
}

// Options may precede the command: `structarmed --basepath=../../ --clear-cache`.
foreach ($arguments as $index => $argument) {
if (in_array($argument, self::COMMANDS, true)) {
$command = $argument;
unset($arguments[$index]);
break;
}
}

$arguments = array_values($arguments);

if (in_array($command, ['--version', '-V'], true)) {
echo sprintf("StructArmed %s\n", Version::current());

Expand All @@ -39,15 +54,15 @@ public function run(array $argv, ?string $basePath = null): int
}

if ($command === 'init') {
return (new InitCommand())->run(array_slice($argv, 2), $basePath);
return (new InitCommand())->run($arguments, $basePath);
}

if ($command === '--clear-cache') {
return (new ClearCacheCommand())->run(array_slice($argv, 2), $basePath);
return (new ClearCacheCommand())->run($arguments, $basePath);
}

if (in_array($command, ['analyse', 'analyze'], true)) {
return (new AnalyseCommand())->run(array_slice($argv, 2), $basePath);
return (new AnalyseCommand())->run($arguments, $basePath);
}

echo sprintf("Unknown command: %s\n\n", $command);
Expand Down
6 changes: 3 additions & 3 deletions src/Cli/Usage.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ public static function render(): string
structarmed --version
structarmed init [--preset=ddd|mvc|psr4|psr1|psr12|per|psr15|yagni|codequality|all]
structarmed analyse|analyze [path ...] [--config=path/to/structarmed.php]
[--report=console|json|github] [--no-progress] [--clear-cache] [--disable-parallel]
[--fix] [--generate-baseline=structarmed-baseline.php]
structarmed --clear-cache [--config=path/to/structarmed.php]
[-d|--basepath=path/to/project] [--report=console|json|github] [--no-progress]
[--clear-cache] [--disable-parallel] [--fix] [--generate-baseline=structarmed-baseline.php]
structarmed --clear-cache [--config=path/to/structarmed.php] [-d|--basepath=path/to/project]

TXT;
}
Expand Down
24 changes: 15 additions & 9 deletions src/Config/ConfigLoader.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,16 +35,22 @@ public static function load(string $configPath): Architecture
return $architecture;
}

public static function discover(string $basePath): string
/**
* Searches the base paths in order, so the CLI's working directory wins over
* a --basepath project root that also holds a config file.
*/
public static function discover(string ...$basePaths): string
{
$candidates = [
$basePath . '/structarmed.php',
$basePath . '/structarmed.dist.php',
];

foreach ($candidates as $candidate) {
if (file_exists($candidate)) {
return $candidate;
foreach ($basePaths as $basePath) {
$candidates = [
$basePath . '/structarmed.php',
$basePath . '/structarmed.dist.php',
];

foreach ($candidates as $candidate) {
if (file_exists($candidate)) {
return $candidate;
}
}
}

Expand Down
55 changes: 55 additions & 0 deletions tests/Cli/StructArmedApplicationCommandRoutingTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,39 @@ public function testApplicationRejectsUnknownClearCacheOption(): void
$this->assertStringContainsString('Unknown option: --bad-option', $output);
}

public function testApplicationRejectsMissingClearCacheBasePath(): void
{
[$exitCode, $output] = $this->runApplication(
['structarmed', '--clear-cache', '--basepath', 'missing'],
self::BASE_PATH
);

$this->assertSame(1, $exitCode);
$this->assertStringContainsString('Error: base path [missing] not found.', $output);
}

public function testApplicationAcceptsOptionsBeforeClearCacheCommand(): void
{
[$exitCode, $output] = $this->runApplication(
['structarmed', '--basepath=missing', '--clear-cache'],
self::BASE_PATH
);

$this->assertSame(1, $exitCode);
$this->assertStringContainsString('Error: base path [missing] not found.', $output);
}

public function testApplicationAcceptsShortClearCacheBasePathOption(): void
{
[$exitCode, $output] = $this->runApplication(
['structarmed', '--clear-cache', '-d=missing'],
self::BASE_PATH
);

$this->assertSame(1, $exitCode);
$this->assertStringContainsString('Error: base path [missing] not found.', $output);
}

public function testInitCommandRejectsUnknownOption(): void
{
[$exitCode, $output] = $this->runApplication(['structarmed', 'init', '--bad-option'], self::BASE_PATH);
Expand Down Expand Up @@ -119,6 +152,28 @@ public function testAnalyseCommandRejectsMissingScanPath(): void
$this->assertStringContainsString('Error: path [missing] not found.', $output);
}

public function testAnalyseCommandRejectsMissingBasePath(): void
{
[$exitCode, $output] = $this->runApplication(
['structarmed', 'analyse', '--basepath=missing'],
self::BASE_PATH
);

$this->assertSame(1, $exitCode);
$this->assertStringContainsString('Error: base path [missing] not found.', $output);
}

public function testAnalyseCommandAcceptsShortBasePathOption(): void
{
[$exitCode, $output] = $this->runApplication(
['structarmed', 'analyse', '-d', 'missing'],
self::BASE_PATH
);

$this->assertSame(1, $exitCode);
$this->assertStringContainsString('Error: base path [missing] not found.', $output);
}

public function testAnalyseCommandAcceptsAbsoluteScanPath(): void
{
[$exitCode, $output] = $this->runApplication(
Expand Down
Loading
Loading