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
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.
5 changes: 3 additions & 2 deletions docs/available-rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,8 @@ Namespace: `Boundwize\StructArmed\Rule\Rules\File`.
|---|---|---|
| `Psr1PhpTagsRule` | `new Psr1PhpTagsRule(sourcePaths: ['src/'])` | PHP files use only `<?php` and `<?=` tags. Supports `--fix`. |
| `Psr1SymbolsOrSideEffectsRule` | `new Psr1SymbolsOrSideEffectsRule(sourcePaths: ['src/'])` | A file declares symbols or causes side effects, but does not do both. |
| `Psr1Utf8WithoutBomRule` | `new Psr1Utf8WithoutBomRule(sourcePaths: ['src/'])` | PHP files use valid UTF-8 without a byte order mark. |
| `Psr1ValidUtf8Rule` | `new Psr1ValidUtf8Rule(sourcePaths: ['src/'])` | PHP files use valid UTF-8 encoding. |
| `Psr1Utf8WithoutBomRule` | `new Psr1Utf8WithoutBomRule(sourcePaths: ['src/'])` | PHP files do not start with a byte order mark. Supports `--fix`. |
{: .rule-table }

Pass `sourcePaths: null` or omit it to let the rule read PSR-4 paths from `composer.json`.
Expand Down Expand Up @@ -90,7 +91,7 @@ Namespace: `Boundwize\StructArmed\Rule\Rules\Class_`.

`classNamePattern` and `excludePattern` are regular expressions matched against the fully-qualified class name.

`Psr1PhpTagsRule`, `MustBeFinalRule`, `MustDeclareConstantVisibilityRule`, `MustDeclareMethodVisibilityRule`, and `MustDeclarePropertyVisibilityRule` implement `Boundwize\StructArmed\Rule\FixableInterface`, so StructArmed can automatically normalize invalid PHP opening tags, add the `final` class modifier, and add missing constant, method, or property visibility modifiers when you run `vendor/bin/structarmed analyse --fix`.
`Psr1PhpTagsRule`, `Psr1Utf8WithoutBomRule`, `MustBeFinalRule`, `MustDeclareConstantVisibilityRule`, `MustDeclareMethodVisibilityRule`, and `MustDeclarePropertyVisibilityRule` implement `Boundwize\StructArmed\Rule\FixableInterface`, so StructArmed can automatically normalize invalid PHP opening tags, remove UTF-8 byte order marks, add the `final` class modifier, and add missing constant, method, or property visibility modifiers when you run `vendor/bin/structarmed analyse --fix`.

## Layer Rules

Expand Down
2 changes: 1 addition & 1 deletion docs/presets.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ StructArmed ships with presets for common PHP standards and architecture styles.

| Preset | Rules |
|---|---|
| `Preset::PSR1()` | Basic Coding Standard checks: PHP tags, UTF-8 without BOM, symbols vs side effects, PSR-4 class placement, StudlyCaps class names, upper-case class constants, camelCase methods |
| `Preset::PSR1()` | Basic Coding Standard checks: PHP tags, valid UTF-8, UTF-8 without BOM, symbols vs side effects, PSR-4 class placement, StudlyCaps class names, upper-case class constants, camelCase methods |
| `Preset::PSR12()` | Extends PSR-1: all methods, constants, and properties must declare explicit visibility |
| `Preset::PSR15()` | `*Middleware` classes must implement PSR-15 `MiddlewareInterface`; `*Handler` classes must implement PSR-15 `RequestHandlerInterface`; StructArmed also enforces matching `Middleware`/`Handler` suffixes for implementations of those interfaces |
| `Preset::PSR4()` | Verifies configured source paths exist in composer.json `autoload` or `autoload-dev` PSR-4 mappings |
Expand Down
4 changes: 4 additions & 0 deletions src/Preset/Presets/Psr1Preset.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use Boundwize\StructArmed\Rule\Rules\File\Psr1PhpTagsRule;
use Boundwize\StructArmed\Rule\Rules\File\Psr1SymbolsOrSideEffectsRule;
use Boundwize\StructArmed\Rule\Rules\File\Psr1Utf8WithoutBomRule;
use Boundwize\StructArmed\Rule\Rules\File\Psr1ValidUtf8Rule;
use Boundwize\StructArmed\Rule\Rules\Method\MethodNameMustBeCamelCaseRule;

final readonly class Psr1Preset implements PresetInterface
Expand All @@ -19,6 +20,8 @@

public const FILES_MUST_USE_VALID_TAGS = 'psr1.files.must_use_valid_tags';

public const FILES_MUST_USE_VALID_UTF8 = 'psr1.files.must_use_valid_utf8';

public const FILES_MUST_USE_UTF8_WITHOUT_BOM = 'psr1.files.must_use_utf8_without_bom';

public const FILES_SHOULD_DECLARE_SYMBOLS_OR_SIDE_EFFECTS = 'psr1.files.should_declare_symbols_or_side_effects';
Expand Down Expand Up @@ -46,6 +49,7 @@ public function apply(Architecture $architecture): void
$architecture->layer($layerName, $this->sourcePaths ?? []);

$architecture->rule(self::FILES_MUST_USE_VALID_TAGS, new Psr1PhpTagsRule($this->sourcePaths));
$architecture->rule(self::FILES_MUST_USE_VALID_UTF8, new Psr1ValidUtf8Rule($this->sourcePaths));
$architecture->rule(self::FILES_MUST_USE_UTF8_WITHOUT_BOM, new Psr1Utf8WithoutBomRule($this->sourcePaths));
$architecture->rule(
self::FILES_SHOULD_DECLARE_SYMBOLS_OR_SIDE_EFFECTS,
Expand Down
33 changes: 22 additions & 11 deletions src/Rule/Rules/File/Psr1Utf8WithoutBomRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,17 @@
use Boundwize\StructArmed\Analyser\FileAnalysisProvider;
use Boundwize\StructArmed\Architecture;
use Boundwize\StructArmed\Rule\FileAnalysisRuleInterface;
use Boundwize\StructArmed\Rule\FixableInterface;
use Boundwize\StructArmed\Rule\RuleViolation;

use function file_get_contents;
use function file_put_contents;
use function is_file;
use function sprintf;
use function str_starts_with;
use function substr;

final readonly class Psr1Utf8WithoutBomRule implements FileAnalysisRuleInterface
final readonly class Psr1Utf8WithoutBomRule implements FileAnalysisRuleInterface, FixableInterface
{
/**
* @param list<string>|null $sourcePaths
Expand Down Expand Up @@ -62,19 +68,24 @@ public function evaluateProjectAllWithProvider(
line: 1,
className: '',
);
continue;
}

if (! $fileAnalysisProvider->hasValidUtf8($file)) {
$violations[] = new RuleViolation(
message: sprintf('File [%s] must use valid UTF-8 encoding', $file),
file: $file,
line: 1,
className: '',
);
}
}

return $violations;
}

public function fix(RuleViolation $ruleViolation): bool
{
if (! is_file($ruleViolation->file)) {
return false;
}

$code = (string) file_get_contents($ruleViolation->file);

if (! str_starts_with($code, "\xEF\xBB\xBF")) {
return false;
}

return file_put_contents($ruleViolation->file, substr($code, 3)) !== false;
}
}
70 changes: 70 additions & 0 deletions src/Rule/Rules/File/Psr1ValidUtf8Rule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<?php

declare(strict_types=1);

namespace Boundwize\StructArmed\Rule\Rules\File;

use Boundwize\StructArmed\Analyser\FileAnalysisProvider;
use Boundwize\StructArmed\Architecture;
use Boundwize\StructArmed\Rule\FileAnalysisRuleInterface;
use Boundwize\StructArmed\Rule\RuleViolation;

use function sprintf;

final readonly class Psr1ValidUtf8Rule implements FileAnalysisRuleInterface
{
/**
* @param list<string>|null $sourcePaths
*/
public function __construct(
private ?array $sourcePaths = null,
private ?PhpFileFinder $phpFileFinder = null,
) {
}

public function evaluateProject(string $basePath, Architecture $architecture, array $skipPaths = []): ?RuleViolation
{
return $this->evaluateProjectAll($basePath, $architecture, $skipPaths)[0] ?? null;
}

/**
* @param list<string> $skipPaths
* @return RuleViolation[]
*/
public function evaluateProjectAll(string $basePath, Architecture $architecture, array $skipPaths = []): array
{
return $this->evaluateProjectAllWithProvider(
$basePath,
$architecture,
new FileAnalysisProvider(),
$skipPaths,
);
}

/**
* @param list<string> $skipPaths
* @return RuleViolation[]
*/
public function evaluateProjectAllWithProvider(
string $basePath,
Architecture $architecture,
FileAnalysisProvider $fileAnalysisProvider,
array $skipPaths = [],
): array {
$phpFileFinder = $this->phpFileFinder ?? new PhpFileFinder($this->sourcePaths);
$violations = [];

foreach ($phpFileFinder->files($basePath, $skipPaths) as $file) {
if (! $fileAnalysisProvider->hasValidUtf8($file)) {
$violations[] = new RuleViolation(
message: sprintf('File [%s] must use valid UTF-8 encoding', $file),
file: $file,
line: 1,
className: '',
);
}
}

return $violations;
}
}
5 changes: 4 additions & 1 deletion tests/Preset/PresetTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ public function testPsr1PresetRegistersSourceLayerAndRules(): void

$rules = $architecture->getRules();
$this->assertArrayHasKey(Psr1Preset::FILES_MUST_USE_VALID_TAGS, $rules);
$this->assertArrayHasKey(Psr1Preset::FILES_MUST_USE_VALID_UTF8, $rules);
$this->assertArrayHasKey(Psr1Preset::FILES_MUST_USE_UTF8_WITHOUT_BOM, $rules);
$this->assertArrayHasKey(Psr1Preset::FILES_SHOULD_DECLARE_SYMBOLS_OR_SIDE_EFFECTS, $rules);
$this->assertArrayHasKey(Psr4Preset::CLASSES_MUST_MATCH_COMPOSER, $rules);
Expand All @@ -60,6 +61,7 @@ public function testPsr12PresetAppliesPsr1RulesAndAddsVisibilityRules(): void

$rules = $architecture->getRules();
$this->assertArrayHasKey(Psr1Preset::FILES_MUST_USE_VALID_TAGS, $rules);
$this->assertArrayHasKey(Psr1Preset::FILES_MUST_USE_VALID_UTF8, $rules);
$this->assertArrayHasKey(Psr1Preset::FILES_MUST_USE_UTF8_WITHOUT_BOM, $rules);
$this->assertArrayHasKey(Psr1Preset::FILES_SHOULD_DECLARE_SYMBOLS_OR_SIDE_EFFECTS, $rules);
$this->assertArrayHasKey(Psr4Preset::CLASSES_MUST_MATCH_COMPOSER, $rules);
Expand Down Expand Up @@ -220,9 +222,10 @@ public function testPsr1AndPsr12BothEnabledDoNotDuplicatePsr1Rules(): void

$rules = $architecture->getRules();

$this->assertCount(14, $rules);
$this->assertCount(15, $rules);

$this->assertArrayHasKey(Psr1Preset::FILES_MUST_USE_VALID_TAGS, $rules);
$this->assertArrayHasKey(Psr1Preset::FILES_MUST_USE_VALID_UTF8, $rules);
$this->assertArrayHasKey(Psr1Preset::FILES_MUST_USE_UTF8_WITHOUT_BOM, $rules);
$this->assertArrayHasKey(Psr1Preset::FILES_SHOULD_DECLARE_SYMBOLS_OR_SIDE_EFFECTS, $rules);
$this->assertArrayHasKey(Psr4Preset::CLASSES_MUST_MATCH_COMPOSER, $rules);
Expand Down
69 changes: 65 additions & 4 deletions tests/Rule/File/Psr1Utf8WithoutBomRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@
namespace Boundwize\StructArmed\Tests\Rule\File;

use Boundwize\StructArmed\Architecture;
use Boundwize\StructArmed\Rule\FixableInterface;
use Boundwize\StructArmed\Rule\Rules\File\PhpFileFinder;
use Boundwize\StructArmed\Rule\Rules\File\Psr1Utf8WithoutBomRule;
use Boundwize\StructArmed\Rule\RuleViolation;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\TestCase;

use function bin2hex;
use function file_get_contents;
use function file_put_contents;
use function mkdir;
use function random_bytes;
Expand Down Expand Up @@ -41,7 +43,7 @@ public function testViolatesUtf8Bom(): void
}
}

public function testViolatesInvalidUtf8(): void
public function testPassesInvalidUtf8WithoutBom(): void
{
$basePath = $this->makeTempDir();

Expand All @@ -51,16 +53,15 @@ public function testViolatesInvalidUtf8(): void

$violations = (new Psr1Utf8WithoutBomRule(['src/']))->evaluateProjectAll($basePath, Architecture::define());

$this->assertCount(1, $violations);
$this->assertStringContainsString('valid UTF-8', $violations[0]->message);
$this->assertSame([], $violations);
} finally {
unlink($basePath . '/src/Foo.php');
rmdir($basePath . '/src');
rmdir($basePath);
}
}

public function testPassesValidUtf8AndMissingPaths(): void
public function testPassesFilesWithoutBomAndMissingPaths(): void
{
$basePath = $this->makeTempDir();

Expand Down Expand Up @@ -102,6 +103,66 @@ public function testEvaluateProjectReturnsFirstViolation(): void
}
}

public function testIsFixable(): void
{
$this->assertInstanceOf(FixableInterface::class, new Psr1Utf8WithoutBomRule(['src/']));
}

public function testFixesUtf8Bom(): void
{
$basePath = $this->makeTempDir();

try {
mkdir($basePath . '/src');
file_put_contents($basePath . '/src/Foo.php', "\xEF\xBB\xBF<?php class Foo {}");

$psr1Utf8WithoutBomRule = new Psr1Utf8WithoutBomRule(['src/']);
$violation = $psr1Utf8WithoutBomRule->evaluateProject($basePath, Architecture::define());

$this->assertInstanceOf(RuleViolation::class, $violation);
$this->assertTrue($psr1Utf8WithoutBomRule->fix($violation));
$this->assertSame('<?php class Foo {}', file_get_contents($basePath . '/src/Foo.php'));
} finally {
unlink($basePath . '/src/Foo.php');
rmdir($basePath . '/src');
rmdir($basePath);
}
}

public function testDoesNotFixFileWithoutBom(): void
{
$basePath = $this->makeTempDir();

try {
mkdir($basePath . '/src');
file_put_contents($basePath . '/src/Foo.php', '<?php class Foo {}');

$psr1Utf8WithoutBomRule = new Psr1Utf8WithoutBomRule(['src/']);

$this->assertFalse($psr1Utf8WithoutBomRule->fix(new RuleViolation(
message: 'File must use UTF-8 without BOM',
file: $basePath . '/src/Foo.php',
line: 1,
className: '',
)));
$this->assertSame('<?php class Foo {}', file_get_contents($basePath . '/src/Foo.php'));
} finally {
unlink($basePath . '/src/Foo.php');
rmdir($basePath . '/src');
rmdir($basePath);
}
}

public function testDoesNotFixMissingFile(): void
{
$this->assertFalse((new Psr1Utf8WithoutBomRule(['src/']))->fix(new RuleViolation(
message: 'File must use UTF-8 without BOM',
file: sys_get_temp_dir() . '/structarmed-missing-file-' . bin2hex(random_bytes(6)) . '.php',
line: 1,
className: '',
)));
}

public function testReturnsAllViolationsWhenMultipleFilesViolate(): void
{
$basePath = $this->makeTempDir();
Expand Down
Loading
Loading