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 src/Analyser/ClassNode.php
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ public function accessesSuperglobals(): bool
public function constructorParamCount(): int
{
foreach ($this->methods as $method) {
if ($method->name === '__construct') {
if ($method->isConstructor()) {
return $method->paramCount;
}
}
Expand Down
6 changes: 4 additions & 2 deletions src/Analyser/MethodNode.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

namespace Boundwize\StructArmed\Analyser;

use function strcasecmp;

final readonly class MethodNode
{
public function __construct(
Expand All @@ -27,11 +29,11 @@ public function isPublic(): bool

public function isConstructor(): bool
{
return $this->name === '__construct';
return strcasecmp($this->name, '__construct') === 0;
}

public function isDestructor(): bool
{
return $this->name === '__destruct';
return strcasecmp($this->name, '__destruct') === 0;
}
}
20 changes: 20 additions & 0 deletions tests/Analyser/ClassNodeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -400,4 +400,24 @@ className: 'App\\Domain\\Order',
$this->assertSame(2, $withConstructor->constructorParamCount());
$this->assertSame(0, $withoutConstructor->constructorParamCount());
}

public function testConstructorParamCountMatchesConstructorCaseInsensitively(): void
{
$classNode = new ClassNode(
className: 'App\\Domain\\Order',
file: '/src/Order.php',
line: 1,
layer: 'Domain',
extends: null,
isAbstract: false,
isFinal: true,
isInterface: false,
isReadonly: false,
methods: [
new MethodNode('__CONSTRUCT', 'public', false, false, 3, 1, 3),
],
);

$this->assertSame(3, $classNode->constructorParamCount());
}
}
15 changes: 15 additions & 0 deletions tests/Analyser/MethodNodeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,19 @@ public function testMethodHelpers(): void
$this->assertFalse($protectedMethod->isConstructor());
$this->assertFalse($protectedMethod->isDestructor());
}

public function testConstructorAndDestructorDetectionIsCaseInsensitive(): void
{
$upperConstructor = new MethodNode('__CONSTRUCT', 'public', false, false, 0, 1, 3, isMagic: true);
$mixedConstructor = new MethodNode('__Construct', 'public', false, false, 0, 1, 3, isMagic: true);
$upperDestructor = new MethodNode('__DESTRUCT', 'public', false, false, 0, 1, 3, isMagic: true);
$mixedDestructor = new MethodNode('__Destruct', 'public', false, false, 0, 1, 3, isMagic: true);

$this->assertTrue($upperConstructor->isConstructor());
$this->assertTrue($mixedConstructor->isConstructor());
$this->assertFalse($upperConstructor->isDestructor());
$this->assertTrue($upperDestructor->isDestructor());
$this->assertTrue($mixedDestructor->isDestructor());
$this->assertFalse($upperDestructor->isConstructor());
}
}
117 changes: 117 additions & 0 deletions tests/Rule/Method/MustHaveReturnTypeRuleFunctionalTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
<?php

declare(strict_types=1);

namespace Boundwize\StructArmed\Tests\Rule\Method;

use Boundwize\StructArmed\Analyser\Analyser;
use Boundwize\StructArmed\Analyser\AnalyserOptions;
use Boundwize\StructArmed\Architecture;
use Boundwize\StructArmed\Rule\Rules\Method\MustHaveReturnTypeRule;
use Boundwize\StructArmed\Rule\RuleViolationCollection;
use Boundwize\StructArmed\Tests\Support\TemporaryDirectoryCleanupTrait;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\TestCase;

use function dirname;
use function file_put_contents;
use function is_dir;
use function mkdir;

#[CoversClass(MustHaveReturnTypeRule::class)]
final class MustHaveReturnTypeRuleFunctionalTest extends TestCase
{
use TemporaryDirectoryCleanupTrait;

public function testFlagsOnlyPublicMethodsMissingReturnTypeOnRealSources(): void
{
$basePath = $this->makeTempProject([
'src/Domain/OrderService.php' => <<<'PHP'
<?php

namespace App\Domain;

final class OrderService
{
public function __construct(private string $name)
{
}

public function process()
{
}

public function total(): int
{
return 0;
}

private function helper()
{
}
}
PHP,
]);

$violations = $this->analyse($basePath)->forRule('domain.must_have_return_type');

$this->assertCount(1, $violations);
$this->assertStringContainsString('App\Domain\OrderService::process()', $violations[0]->message);
}

public function testIgnoresConstructorAndDestructorDeclaredWithDifferentCase(): void
{
// PHP method names are case-insensitive: __CONSTRUCT()/__Destruct() are
// still ctor/dtor and may not declare a return type, so no violation.
$basePath = $this->makeTempProject([
'src/Domain/PaymentService.php' => <<<'PHP'
<?php

namespace App\Domain;

final class PaymentService
{
public function __CONSTRUCT()
{
}

public function __Destruct()
{
}
}
PHP,
]);

$violations = $this->analyse($basePath)->forRule('domain.must_have_return_type');

$this->assertCount(0, $violations);
}

private function analyse(string $basePath): RuleViolationCollection
{
$architecture = Architecture::define()
->layer('Domain', 'src/Domain/')
->rule('domain.must_have_return_type', new MustHaveReturnTypeRule('Domain'));

return (new Analyser($basePath))
->analyse($architecture, [], null, AnalyserOptions::sequential());
}

/** @param array<string, string> $files */
private function makeTempProject(array $files): string
{
$basePath = $this->makeTemporaryDirectory('structarmed-must-have-return-type');

foreach ($files as $file => $contents) {
$path = $basePath . '/' . $file;

if (! is_dir(dirname($path))) {
mkdir(dirname($path), 0777, true);
}

file_put_contents($path, $contents);
}

return $basePath;
}
}
12 changes: 12 additions & 0 deletions tests/Rule/Method/MustHaveReturnTypeRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,18 @@ public function testIgnoresDestructor(): void
$this->assertNotInstanceOf(RuleViolation::class, $mustHaveReturnTypeRule->evaluate($classNode));
}

public function testIgnoresConstructorAndDestructorRegardlessOfCase(): void
{
$mustHaveReturnTypeRule = new MustHaveReturnTypeRule(layer: 'Domain');
$classNode = $this->makeNode([
$this->method('__CONSTRUCT', hasReturnType: false),
$this->method('__Destruct', hasReturnType: false),
]);

// PHP method names are case-insensitive, so these are still ctor/dtor
$this->assertNotInstanceOf(RuleViolation::class, $mustHaveReturnTypeRule->evaluate($classNode));
}

public function testIgnoresPrivateMethods(): void
{
$mustHaveReturnTypeRule = new MustHaveReturnTypeRule(layer: 'Domain');
Expand Down
Loading