diff --git a/src/Analyser/FileAnalysisProvider.php b/src/Analyser/FileAnalysisProvider.php index 2e18bbb..53c2277 100644 --- a/src/Analyser/FileAnalysisProvider.php +++ b/src/Analyser/FileAnalysisProvider.php @@ -8,11 +8,14 @@ use Boundwize\StructArmed\Util\Path; use PhpParser\Error; use PhpParser\Node; +use PhpParser\Node\Expr\FuncCall; +use PhpParser\Node\Name; use PhpParser\Node\Stmt; use PhpParser\Node\Stmt\ClassLike; use PhpParser\Node\Stmt\Const_; use PhpParser\Node\Stmt\Declare_; use PhpParser\Node\Stmt\Else_; +use PhpParser\Node\Stmt\Expression; use PhpParser\Node\Stmt\Function_; use PhpParser\Node\Stmt\GroupUse; use PhpParser\Node\Stmt\If_; @@ -285,7 +288,21 @@ private function isSymbolDeclaration(Stmt $stmt): bool { return $stmt instanceof ClassLike || $stmt instanceof Function_ - || $stmt instanceof Const_; + || $stmt instanceof Const_ + || $this->isDefineCall($stmt); + } + + /** + * A top-level `define('CONST', ...)` call declares a constant symbol under PSR-1, + * mirroring PHP_CodeSniffer's PSR1.Files.SideEffects sniff. Method calls such as + * `$obj->define(...)` or `Foo::define(...)` are not FuncCall nodes, so they never match. + */ + private function isDefineCall(Stmt $stmt): bool + { + return $stmt instanceof Expression + && $stmt->expr instanceof FuncCall + && $stmt->expr->name instanceof Name + && $stmt->expr->name->toLowerString() === 'define'; } private function isNeutralStatement(Stmt $stmt): bool diff --git a/tests/Rule/File/Psr1SymbolsOrSideEffectsRuleTest.php b/tests/Rule/File/Psr1SymbolsOrSideEffectsRuleTest.php index dd3048b..ac907b9 100644 --- a/tests/Rule/File/Psr1SymbolsOrSideEffectsRuleTest.php +++ b/tests/Rule/File/Psr1SymbolsOrSideEffectsRuleTest.php @@ -174,6 +174,104 @@ final class Foo {} } } + public function testPassesDefineConstantNextToClass(): void + { + $basePath = $this->makeTempDir(); + + try { + mkdir($basePath . '/src'); + file_put_contents( + $basePath . '/src/Foo.php', + "evaluateProjectAll( + $basePath, + Architecture::define() + ); + + $this->assertSame([], $violations); + } finally { + unlink($basePath . '/src/Foo.php'); + rmdir($basePath . '/src'); + rmdir($basePath); + } + } + + public function testPassesDefinedGuardedDefineNextToClass(): void + { + $basePath = $this->makeTempDir(); + + try { + mkdir($basePath . '/src'); + file_put_contents( + $basePath . '/src/Foo.php', + "evaluateProjectAll( + $basePath, + Architecture::define() + ); + + $this->assertSame([], $violations); + } finally { + unlink($basePath . '/src/Foo.php'); + rmdir($basePath . '/src'); + rmdir($basePath); + } + } + + public function testViolatesEchoNextToDefineAndClass(): void + { + $basePath = $this->makeTempDir(); + + try { + mkdir($basePath . '/src'); + file_put_contents( + $basePath . '/src/Foo.php', + "evaluateProjectAll( + $basePath, + Architecture::define() + ); + + $this->assertCount(1, $violations); + $this->assertSame(3, $violations[0]->line); + } finally { + unlink($basePath . '/src/Foo.php'); + rmdir($basePath . '/src'); + rmdir($basePath); + } + } + + public function testViolatesDefineMixedWithSideEffectCall(): void + { + $basePath = $this->makeTempDir(); + + try { + mkdir($basePath . '/src'); + file_put_contents( + $basePath . '/src/Foo.php', + "evaluateProjectAll( + $basePath, + Architecture::define() + ); + + $this->assertCount(1, $violations); + $this->assertSame(3, $violations[0]->line); + } finally { + unlink($basePath . '/src/Foo.php'); + rmdir($basePath . '/src'); + rmdir($basePath); + } + } + public function testViolatesConditionalSideEffectNextToSymbol(): void { $basePath = $this->makeTempDir();