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
2 changes: 1 addition & 1 deletion docs/assets/no-violation.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion docs/assets/structarmed-showoff.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
37 changes: 34 additions & 3 deletions src/Analyser/Analyser.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@
use function is_dir;
use function is_file;
use function sprintf;
use function str_ends_with;
use function str_starts_with;
use function strpbrk;
use function substr;
Expand Down Expand Up @@ -871,12 +870,41 @@ private function collectClassNodes(
public function filesForAnalysis(Architecture $architecture, array $scanPaths = [], ?array $layers = null): array
{
return $this->collectPhpFiles(
$layers ?? $this->resolveLayers($architecture),
$this->withRootComposerFileInSourceLayer($layers ?? $this->resolveLayers($architecture)),
$scanPaths,
$architecture->getSkipPaths()
);
}

/**
* @param array<string, string|list<string>> $layers
* @return array<string, string|list<string>>
*/
private function withRootComposerFileInSourceLayer(array $layers): array
{
foreach ($layers as $layerPaths) {
if (in_array('composer.json', (array) $layerPaths, true)) {
return $layers;
}
}

foreach ($layers as $layerName => $layerPaths) {
if (! $this->isSourceLayerName($layerName)) {
continue;
}

$layers[$layerName] = [...(array) $layerPaths, 'composer.json'];
break;
}

return $layers;
}

private function isSourceLayerName(string $layerName): bool
{
return $layerName === 'Source' || str_starts_with($layerName, 'Source[');
}

/**
* @param array<string, string|list<string>> $layers
* @param list<string> $scanPaths
Expand All @@ -895,7 +923,10 @@ private function collectPhpFiles(array $layers, array $scanPaths, array $skipPat
);

if (is_file($fullPath)) {
if (str_ends_with($fullPath, '.php') && ! $this->isSkipped($fullPath, $skipMatchers)) {
if (
Path::isAnalysableFile($fullPath, $this->basePath)
&& ! $this->isSkipped($fullPath, $skipMatchers)
) {
$files[] = $fullPath;
}

Expand Down
3 changes: 1 addition & 2 deletions src/Cli/AnalyseCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@
use function is_file;
use function microtime;
use function sprintf;
use function str_ends_with;
use function str_starts_with;
use function strlen;
use function substr;
Expand Down Expand Up @@ -128,7 +127,7 @@ public function run(array $arguments, string $basePath): int
continue;
}

if (is_file($fullScanPath) && str_ends_with($fullScanPath, '.php')) {
if (is_file($fullScanPath) && Path::isAnalysableFile($fullScanPath, $basePath)) {
continue;
}

Expand Down
6 changes: 6 additions & 0 deletions src/Rule/Rules/Composer/Psr4DirectoryExistsRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@

final readonly class Psr4DirectoryExistsRule extends AbstractJsonRecastFixableRule implements ProjectRuleInterface
{
use SkipsComposerFileTrait;

public function __construct(
private Psr4PathResolver $psr4PathResolver = new Psr4PathResolver(),
) {
Expand All @@ -29,6 +31,10 @@ public function evaluateProject(string $basePath, Architecture $architecture, ar
{
$composerFile = rtrim($basePath, '/') . '/composer.json';

if ($this->isComposerFileSkipped($basePath, $composerFile, $skipPaths)) {
return null;
}

if (! file_exists($composerFile)) {
return $this->violation(
'composer.json was not found',
Expand Down
8 changes: 7 additions & 1 deletion src/Rule/Rules/Composer/Psr4EmptyNamespacePrefixRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@

final readonly class Psr4EmptyNamespacePrefixRule implements MultipleProjectRuleViolationInterface
{
use SkipsComposerFileTrait;

public function __construct(
private Psr4PathResolver $psr4PathResolver = new Psr4PathResolver(),
) {
Expand All @@ -31,12 +33,16 @@ public function evaluateProject(string $basePath, Architecture $architecture, ar

/**
* @return list<RuleViolation>
* @param string[] $skipPaths
* @param list<string> $skipPaths
*/
public function evaluateProjectAll(string $basePath, Architecture $architecture, array $skipPaths = []): array
{
$composerFile = rtrim($basePath, '/') . '/composer.json';

if ($this->isComposerFileSkipped($basePath, $composerFile, $skipPaths)) {
return [];
}

if (! file_exists($composerFile)) {
return [];
}
Expand Down
8 changes: 7 additions & 1 deletion src/Rule/Rules/Composer/Psr4RootPathRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@

final readonly class Psr4RootPathRule implements MultipleProjectRuleViolationInterface
{
use SkipsComposerFileTrait;

public function __construct(
private Psr4PathResolver $psr4PathResolver = new Psr4PathResolver(),
) {
Expand All @@ -30,12 +32,16 @@ public function evaluateProject(string $basePath, Architecture $architecture, ar

/**
* @return list<RuleViolation>
* @param string[] $skipPaths
* @param list<string> $skipPaths
*/
public function evaluateProjectAll(string $basePath, Architecture $architecture, array $skipPaths = []): array
{
$composerFile = rtrim($basePath, '/') . '/composer.json';

if ($this->isComposerFileSkipped($basePath, $composerFile, $skipPaths)) {
return [];
}

if (! file_exists($composerFile)) {
return [];
}
Expand Down
6 changes: 6 additions & 0 deletions src/Rule/Rules/Composer/Psr4SourcePathsRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@

final readonly class Psr4SourcePathsRule implements ProjectRuleInterface
{
use SkipsComposerFileTrait;

/**
* @param list<string> $sourcePaths
*/
Expand All @@ -48,6 +50,10 @@ public function evaluateProject(string $basePath, Architecture $architecture, ar
{
$composerFile = rtrim($basePath, '/') . '/composer.json';

if ($this->isComposerFileSkipped($basePath, $composerFile, $skipPaths)) {
return null;
}

if (! file_exists($composerFile)) {
return $this->violation(
'composer.json was not found',
Expand Down
50 changes: 50 additions & 0 deletions src/Rule/Rules/Composer/SkipsComposerFileTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php

declare(strict_types=1);

namespace Boundwize\StructArmed\Rule\Rules\Composer;

use Boundwize\StructArmed\Util\Path;

use function fnmatch;
use function str_starts_with;
use function strpbrk;

trait SkipsComposerFileTrait
{
/**
* @param list<string> $skipPaths
*/
private function isComposerFileSkipped(string $basePath, string $composerFile, array $skipPaths): bool
{
if ($skipPaths === []) {
return false;
}

$normalisedBasePath = Path::normalise($basePath, canonicalise: true);
$normalisedComposerFile = Path::normalise($composerFile, canonicalise: true);

foreach ($skipPaths as $skipPath) {
$absoluteSkipPath = Path::resolve(Path::normalise($skipPath), $normalisedBasePath);

if (strpbrk($absoluteSkipPath, '*?[') !== false) {
if (fnmatch($absoluteSkipPath, $normalisedComposerFile)) {
return true;
}

continue;
}

$normalisedSkipPath = Path::normalise($absoluteSkipPath, canonicalise: true);

if (
$normalisedComposerFile === $normalisedSkipPath
|| str_starts_with($normalisedComposerFile, $normalisedSkipPath . '/')
) {
return true;
}
}

return false;
}
}
19 changes: 19 additions & 0 deletions src/Util/Path.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use function preg_replace;
use function realpath;
use function rtrim;
use function str_ends_with;
use function str_replace;
use function str_starts_with;
use function strlen;
Expand Down Expand Up @@ -64,6 +65,24 @@ public static function resolve(string $path, string $basePath): string
: rtrim($basePath, '/\\') . '/' . $path;
}

public static function isAnalysableFile(string $path, string $basePath): bool
{
if (str_ends_with($path, '.php')) {
return true;
}

if (! str_ends_with($path, 'composer.json')) {
return false;
}

$resolvedPath = self::resolve($path, $basePath);

return self::normalise($resolvedPath, canonicalise: true) === self::normalise(
self::resolve('composer.json', $basePath),
canonicalise: true,
);
}

private static function isAbsolute(string $path): bool
{
if (str_starts_with($path, '/') || str_starts_with($path, '\\\\')) {
Expand Down
Loading
Loading