From e5f39f3f8a111dff52ccffdeb8427cc08a631f9d Mon Sep 17 00:00:00 2001 From: Nik Spyratos <17888779+nikspyratos@users.noreply.github.com> Date: Thu, 16 Apr 2026 07:49:33 +0200 Subject: [PATCH 1/8] Feat: Update local scope analyzing rules to check the scope is not a relationship/other method --- src/NodeAnalyzer/ScopeAnalyzer.php | 101 ++++++++++++++++++ ...odelAttributesAndScopesProtectedRector.php | 12 +-- ...thodToScopeAttributedClassMethodRector.php | 7 +- 3 files changed, 108 insertions(+), 12 deletions(-) create mode 100644 src/NodeAnalyzer/ScopeAnalyzer.php diff --git a/src/NodeAnalyzer/ScopeAnalyzer.php b/src/NodeAnalyzer/ScopeAnalyzer.php new file mode 100644 index 00000000..042164a1 --- /dev/null +++ b/src/NodeAnalyzer/ScopeAnalyzer.php @@ -0,0 +1,101 @@ +nodeNameResolver->getName($classMethod); + + if ($name === null) { + return false; + } + + if (! str_starts_with($name, 'scope') || strlen($name) <= 5 || ! ctype_upper($name[5])) { + return false; + } + + return $this->hasBuilderFirstParameter($classMethod) && $this->hasScopeReturnType($classMethod); + } + + /** + * Determine if a class method is a scope (either by name convention or #[Scope] attribute). + */ + public function isScopeMethod(ClassMethod $classMethod): bool + { + if ($this->isNamedScope($classMethod)) { + return true; + } + + return $this->phpAttributeAnalyzer->hasPhpAttribute($classMethod, self::SCOPE_ATTRIBUTE); + } + + /** + * Check that the first parameter is either untyped or typed as a query Builder. + */ + private function hasBuilderFirstParameter(ClassMethod $classMethod): bool + { + if ($classMethod->params === []) { + return false; + } + + $firstParam = $classMethod->params[0]; + + // Untyped parameter — common pattern: scopeActive($query) + if ($firstParam->type === null) { + return true; + } + + // Typed parameter — check if it's an Eloquent Builder type + return $this->nodeTypeResolver->isObjectType( + $firstParam->type, + new ObjectType('Illuminate\Database\Eloquent\Builder') + ); + } + + /** + * Check that the method has a return type compatible with a query scope. + * Query scopes typically have no return type, void, or return a Builder. + */ + private function hasScopeReturnType(ClassMethod $classMethod): bool + { + if ($classMethod->returnType === null) { + return true; + } + + if ($classMethod->returnType instanceof Identifier && $classMethod->returnType->toString() === 'void') { + return true; + } + + return $this->nodeTypeResolver->isObjectType( + $classMethod->returnType, + new ObjectType('Illuminate\Database\Eloquent\Builder') + ); + } +} diff --git a/src/Rector/ClassMethod/MakeModelAttributesAndScopesProtectedRector.php b/src/Rector/ClassMethod/MakeModelAttributesAndScopesProtectedRector.php index 0005999e..177f3cb3 100644 --- a/src/Rector/ClassMethod/MakeModelAttributesAndScopesProtectedRector.php +++ b/src/Rector/ClassMethod/MakeModelAttributesAndScopesProtectedRector.php @@ -9,10 +9,10 @@ use PHPStan\Analyser\Scope; use PHPStan\Reflection\ClassReflection; use PHPStan\Type\ObjectType; -use Rector\Php80\NodeAnalyzer\PhpAttributeAnalyzer; use Rector\PHPStan\ScopeFetcher; use Rector\Privatization\NodeManipulator\VisibilityManipulator; use RectorLaravel\AbstractRector; +use RectorLaravel\NodeAnalyzer\ScopeAnalyzer; use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample; use Symplify\RuleDocGenerator\ValueObject\RuleDefinition; @@ -23,7 +23,7 @@ class MakeModelAttributesAndScopesProtectedRector extends AbstractRector { public function __construct( private readonly VisibilityManipulator $visibilityManipulator, - private readonly PhpAttributeAnalyzer $phpAttributeAnalyzer + private readonly ScopeAnalyzer $scopeAnalyzer, ) {} public function getRuleDefinition(): RuleDefinition @@ -136,12 +136,6 @@ private function isAttributeMethod(ClassMethod $classMethod): bool private function isScopeMethod(ClassMethod $classMethod): bool { - $name = $this->getName($classMethod); - - if ((bool) preg_match('/^scope.+$/', $name)) { - return true; - } - - return $this->phpAttributeAnalyzer->hasPhpAttribute($classMethod, 'Illuminate\Database\Eloquent\Attributes\Scope'); + return $this->scopeAnalyzer->isScopeMethod($classMethod); } } diff --git a/src/Rector/ClassMethod/ScopeNamedClassMethodToScopeAttributedClassMethodRector.php b/src/Rector/ClassMethod/ScopeNamedClassMethodToScopeAttributedClassMethodRector.php index f0cefc07..57fc9aa7 100644 --- a/src/Rector/ClassMethod/ScopeNamedClassMethodToScopeAttributedClassMethodRector.php +++ b/src/Rector/ClassMethod/ScopeNamedClassMethodToScopeAttributedClassMethodRector.php @@ -15,6 +15,7 @@ use PHPStan\Type\ObjectType; use Rector\Php80\NodeAnalyzer\PhpAttributeAnalyzer; use RectorLaravel\AbstractRector; +use RectorLaravel\NodeAnalyzer\ScopeAnalyzer; use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample; use Symplify\RuleDocGenerator\ValueObject\RuleDefinition; @@ -28,6 +29,7 @@ final class ScopeNamedClassMethodToScopeAttributedClassMethodRector extends Abst public function __construct( private readonly PhpAttributeAnalyzer $phpAttributeAnalyzer, private readonly ReflectionProvider $reflectionProvider, + private readonly ScopeAnalyzer $scopeAnalyzer, ) {} public function getRuleDefinition(): RuleDefinition @@ -87,12 +89,11 @@ public function refactor(Node $node): ?Node continue; } - $name = $this->getName($classMethod); - // make sure it starts with scope and the next character is upper case - if (! str_starts_with($name, 'scope') || ! ctype_upper(substr($name, 5, 1))) { + if (! $this->scopeAnalyzer->isNamedScope($classMethod)) { continue; } + $name = $this->getName($classMethod); $newName = lcfirst(str_replace('scope', '', $name)); if ($classReflection->hasMethod($newName)) { From 910bea6a0e08384d0b3325e6828860eaa8384db9 Mon Sep 17 00:00:00 2001 From: Nik Spyratos <17888779+nikspyratos@users.noreply.github.com> Date: Thu, 16 Apr 2026 07:49:47 +0200 Subject: [PATCH 2/8] Update fixtures --- .../Fixture/skip_relation_methods.php.inc | 16 +++++++++ .../Fixture/fixture.php.inc | 4 +-- .../Fixture/non_duplicate_nodes.php.inc | 2 +- .../Fixture/non_method_duplicate.php.inc | 2 +- .../scope_with_extra_parameters.php.inc | 34 +++++++++++++++++++ .../Fixture/skip_relation_methods.php.inc | 16 +++++++++ .../Fixture/typed_builder_parameter.php.inc | 34 +++++++++++++++++++ 7 files changed, 104 insertions(+), 4 deletions(-) create mode 100644 tests/Rector/ClassMethod/MakeModelAttributesAndScopesProtectedRector/Fixture/skip_relation_methods.php.inc create mode 100644 tests/Rector/ClassMethod/ScopeNamedClassMethodToScopeAttributedClassMethodRector/Fixture/scope_with_extra_parameters.php.inc create mode 100644 tests/Rector/ClassMethod/ScopeNamedClassMethodToScopeAttributedClassMethodRector/Fixture/skip_relation_methods.php.inc create mode 100644 tests/Rector/ClassMethod/ScopeNamedClassMethodToScopeAttributedClassMethodRector/Fixture/typed_builder_parameter.php.inc diff --git a/tests/Rector/ClassMethod/MakeModelAttributesAndScopesProtectedRector/Fixture/skip_relation_methods.php.inc b/tests/Rector/ClassMethod/MakeModelAttributesAndScopesProtectedRector/Fixture/skip_relation_methods.php.inc new file mode 100644 index 00000000..84050356 --- /dev/null +++ b/tests/Rector/ClassMethod/MakeModelAttributesAndScopesProtectedRector/Fixture/skip_relation_methods.php.inc @@ -0,0 +1,16 @@ +hasMany(ScopeType::class); + } +} + +?> diff --git a/tests/Rector/ClassMethod/ScopeNamedClassMethodToScopeAttributedClassMethodRector/Fixture/fixture.php.inc b/tests/Rector/ClassMethod/ScopeNamedClassMethodToScopeAttributedClassMethodRector/Fixture/fixture.php.inc index 038f9403..7e83ba65 100644 --- a/tests/Rector/ClassMethod/ScopeNamedClassMethodToScopeAttributedClassMethodRector/Fixture/fixture.php.inc +++ b/tests/Rector/ClassMethod/ScopeNamedClassMethodToScopeAttributedClassMethodRector/Fixture/fixture.php.inc @@ -6,7 +6,7 @@ use Illuminate\Database\Eloquent\Model; class SomeClass extends Model { - public function scopeSomeMethod() + public function scopeSomeMethod($query) { } @@ -23,7 +23,7 @@ use Illuminate\Database\Eloquent\Model; class SomeClass extends Model { #[\Illuminate\Database\Eloquent\Attributes\Scope] - protected function someMethod() + protected function someMethod($query) { } diff --git a/tests/Rector/ClassMethod/ScopeNamedClassMethodToScopeAttributedClassMethodRector/Fixture/non_duplicate_nodes.php.inc b/tests/Rector/ClassMethod/ScopeNamedClassMethodToScopeAttributedClassMethodRector/Fixture/non_duplicate_nodes.php.inc index 671fff60..833349a0 100644 --- a/tests/Rector/ClassMethod/ScopeNamedClassMethodToScopeAttributedClassMethodRector/Fixture/non_duplicate_nodes.php.inc +++ b/tests/Rector/ClassMethod/ScopeNamedClassMethodToScopeAttributedClassMethodRector/Fixture/non_duplicate_nodes.php.inc @@ -7,7 +7,7 @@ use Illuminate\Database\Eloquent\Model; class NonDuplicateAttributeNodes extends Model { #[\Illuminate\Database\Eloquent\Attributes\Scope] - protected function scopeSomeMethod() + protected function scopeSomeMethod($query) { } diff --git a/tests/Rector/ClassMethod/ScopeNamedClassMethodToScopeAttributedClassMethodRector/Fixture/non_method_duplicate.php.inc b/tests/Rector/ClassMethod/ScopeNamedClassMethodToScopeAttributedClassMethodRector/Fixture/non_method_duplicate.php.inc index 4bfd156d..5be287eb 100644 --- a/tests/Rector/ClassMethod/ScopeNamedClassMethodToScopeAttributedClassMethodRector/Fixture/non_method_duplicate.php.inc +++ b/tests/Rector/ClassMethod/ScopeNamedClassMethodToScopeAttributedClassMethodRector/Fixture/non_method_duplicate.php.inc @@ -6,7 +6,7 @@ use Illuminate\Database\Eloquent\Model; class NonDuplicateMethod extends Model { - public function scopeSomeMethod() + public function scopeSomeMethod($query) { } diff --git a/tests/Rector/ClassMethod/ScopeNamedClassMethodToScopeAttributedClassMethodRector/Fixture/scope_with_extra_parameters.php.inc b/tests/Rector/ClassMethod/ScopeNamedClassMethodToScopeAttributedClassMethodRector/Fixture/scope_with_extra_parameters.php.inc new file mode 100644 index 00000000..3249c313 --- /dev/null +++ b/tests/Rector/ClassMethod/ScopeNamedClassMethodToScopeAttributedClassMethodRector/Fixture/scope_with_extra_parameters.php.inc @@ -0,0 +1,34 @@ +where('type', $type); + } +} + +?> +----- +where('type', $type); + } +} + +?> diff --git a/tests/Rector/ClassMethod/ScopeNamedClassMethodToScopeAttributedClassMethodRector/Fixture/skip_relation_methods.php.inc b/tests/Rector/ClassMethod/ScopeNamedClassMethodToScopeAttributedClassMethodRector/Fixture/skip_relation_methods.php.inc new file mode 100644 index 00000000..dc94876f --- /dev/null +++ b/tests/Rector/ClassMethod/ScopeNamedClassMethodToScopeAttributedClassMethodRector/Fixture/skip_relation_methods.php.inc @@ -0,0 +1,16 @@ +hasMany(ScopeType::class); + } +} + +?> diff --git a/tests/Rector/ClassMethod/ScopeNamedClassMethodToScopeAttributedClassMethodRector/Fixture/typed_builder_parameter.php.inc b/tests/Rector/ClassMethod/ScopeNamedClassMethodToScopeAttributedClassMethodRector/Fixture/typed_builder_parameter.php.inc new file mode 100644 index 00000000..acc7cccb --- /dev/null +++ b/tests/Rector/ClassMethod/ScopeNamedClassMethodToScopeAttributedClassMethodRector/Fixture/typed_builder_parameter.php.inc @@ -0,0 +1,34 @@ +where('active', 1); + } +} + +?> +----- +where('active', 1); + } +} + +?> From 3d64e2b5d0ceb850ee02096900a73cefe9e05fc4 Mon Sep 17 00:00:00 2001 From: Nik Spyratos <17888779+nikspyratos@users.noreply.github.com> Date: Thu, 16 Apr 2026 07:54:50 +0200 Subject: [PATCH 3/8] Simplify ScopeAnalyzer --- src/NodeAnalyzer/ScopeAnalyzer.php | 44 +++++++----------------------- 1 file changed, 10 insertions(+), 34 deletions(-) diff --git a/src/NodeAnalyzer/ScopeAnalyzer.php b/src/NodeAnalyzer/ScopeAnalyzer.php index 042164a1..14459705 100644 --- a/src/NodeAnalyzer/ScopeAnalyzer.php +++ b/src/NodeAnalyzer/ScopeAnalyzer.php @@ -15,6 +15,8 @@ { private const string SCOPE_ATTRIBUTE = 'Illuminate\Database\Eloquent\Attributes\Scope'; + private const string ELOQUENT_BUILDER = 'Illuminate\Database\Eloquent\Builder'; + public function __construct( private NodeNameResolver $nodeNameResolver, private NodeTypeResolver $nodeTypeResolver, @@ -22,22 +24,14 @@ public function __construct( ) {} /** - * Determine if a class method is a named query scope (convention-based, with "scope" prefix). - * - * A named scope must: - * 1. Have a name matching "scope" + uppercase char + rest - * 2. Have at least one parameter where the first parameter is either untyped or typed as a Builder - * 3. Have no return type, a void return type, or a Builder return type + * Checks for the "scope" + uppercase char naming convention, a Builder-typed + * first parameter, and a void/Builder/untyped return. */ public function isNamedScope(ClassMethod $classMethod): bool { $name = $this->nodeNameResolver->getName($classMethod); - if ($name === null) { - return false; - } - - if (! str_starts_with($name, 'scope') || strlen($name) <= 5 || ! ctype_upper($name[5])) { + if ($name === null || ! str_starts_with($name, 'scope') || strlen($name) <= 5 || ! ctype_upper($name[5])) { return false; } @@ -45,20 +39,14 @@ public function isNamedScope(ClassMethod $classMethod): bool } /** - * Determine if a class method is a scope (either by name convention or #[Scope] attribute). + * Named scope OR #[Scope] attribute. */ public function isScopeMethod(ClassMethod $classMethod): bool { - if ($this->isNamedScope($classMethod)) { - return true; - } - - return $this->phpAttributeAnalyzer->hasPhpAttribute($classMethod, self::SCOPE_ATTRIBUTE); + return $this->isNamedScope($classMethod) + || $this->phpAttributeAnalyzer->hasPhpAttribute($classMethod, self::SCOPE_ATTRIBUTE); } - /** - * Check that the first parameter is either untyped or typed as a query Builder. - */ private function hasBuilderFirstParameter(ClassMethod $classMethod): bool { if ($classMethod->params === []) { @@ -67,22 +55,13 @@ private function hasBuilderFirstParameter(ClassMethod $classMethod): bool $firstParam = $classMethod->params[0]; - // Untyped parameter — common pattern: scopeActive($query) if ($firstParam->type === null) { return true; } - // Typed parameter — check if it's an Eloquent Builder type - return $this->nodeTypeResolver->isObjectType( - $firstParam->type, - new ObjectType('Illuminate\Database\Eloquent\Builder') - ); + return $this->nodeTypeResolver->isObjectType($firstParam->type, new ObjectType(self::ELOQUENT_BUILDER)); } - /** - * Check that the method has a return type compatible with a query scope. - * Query scopes typically have no return type, void, or return a Builder. - */ private function hasScopeReturnType(ClassMethod $classMethod): bool { if ($classMethod->returnType === null) { @@ -93,9 +72,6 @@ private function hasScopeReturnType(ClassMethod $classMethod): bool return true; } - return $this->nodeTypeResolver->isObjectType( - $classMethod->returnType, - new ObjectType('Illuminate\Database\Eloquent\Builder') - ); + return $this->nodeTypeResolver->isObjectType($classMethod->returnType, new ObjectType(self::ELOQUENT_BUILDER)); } } From 2a972750750d9f312798bb1f32d1d94bf566473b Mon Sep 17 00:00:00 2001 From: Nik Spyratos <17888779+nikspyratos@users.noreply.github.com> Date: Thu, 16 Apr 2026 08:30:33 +0200 Subject: [PATCH 4/8] Cover edge cases from #409 --- ...thodToScopeAttributedClassMethodRector.php | 2 +- .../Fixture/skip_non_scope_signatures.php.inc | 29 +++++++++++++++++++ .../skip_non_builder_first_param.php.inc | 14 +++++++++ .../skip_non_scope_return_type.php.inc | 16 ++++++++++ 4 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 tests/Rector/ClassMethod/MakeModelAttributesAndScopesProtectedRector/Fixture/skip_non_scope_signatures.php.inc create mode 100644 tests/Rector/ClassMethod/ScopeNamedClassMethodToScopeAttributedClassMethodRector/Fixture/skip_non_builder_first_param.php.inc create mode 100644 tests/Rector/ClassMethod/ScopeNamedClassMethodToScopeAttributedClassMethodRector/Fixture/skip_non_scope_return_type.php.inc diff --git a/src/Rector/ClassMethod/ScopeNamedClassMethodToScopeAttributedClassMethodRector.php b/src/Rector/ClassMethod/ScopeNamedClassMethodToScopeAttributedClassMethodRector.php index 57fc9aa7..8dff25d1 100644 --- a/src/Rector/ClassMethod/ScopeNamedClassMethodToScopeAttributedClassMethodRector.php +++ b/src/Rector/ClassMethod/ScopeNamedClassMethodToScopeAttributedClassMethodRector.php @@ -94,7 +94,7 @@ public function refactor(Node $node): ?Node } $name = $this->getName($classMethod); - $newName = lcfirst(str_replace('scope', '', $name)); + $newName = lcfirst(substr($name, 5)); if ($classReflection->hasMethod($newName)) { continue; diff --git a/tests/Rector/ClassMethod/MakeModelAttributesAndScopesProtectedRector/Fixture/skip_non_scope_signatures.php.inc b/tests/Rector/ClassMethod/MakeModelAttributesAndScopesProtectedRector/Fixture/skip_non_scope_signatures.php.inc new file mode 100644 index 00000000..b187dc53 --- /dev/null +++ b/tests/Rector/ClassMethod/MakeModelAttributesAndScopesProtectedRector/Fixture/skip_non_scope_signatures.php.inc @@ -0,0 +1,29 @@ +where('foo', true); + } + + public function scopeVoid(): void {} + + public function scopeItem(): BelongsTo + { + return $this->belongsTo(ScopeItem::class); + } +} + +?> diff --git a/tests/Rector/ClassMethod/ScopeNamedClassMethodToScopeAttributedClassMethodRector/Fixture/skip_non_builder_first_param.php.inc b/tests/Rector/ClassMethod/ScopeNamedClassMethodToScopeAttributedClassMethodRector/Fixture/skip_non_builder_first_param.php.inc new file mode 100644 index 00000000..64432963 --- /dev/null +++ b/tests/Rector/ClassMethod/ScopeNamedClassMethodToScopeAttributedClassMethodRector/Fixture/skip_non_builder_first_param.php.inc @@ -0,0 +1,14 @@ + diff --git a/tests/Rector/ClassMethod/ScopeNamedClassMethodToScopeAttributedClassMethodRector/Fixture/skip_non_scope_return_type.php.inc b/tests/Rector/ClassMethod/ScopeNamedClassMethodToScopeAttributedClassMethodRector/Fixture/skip_non_scope_return_type.php.inc new file mode 100644 index 00000000..3a851225 --- /dev/null +++ b/tests/Rector/ClassMethod/ScopeNamedClassMethodToScopeAttributedClassMethodRector/Fixture/skip_non_scope_return_type.php.inc @@ -0,0 +1,16 @@ + From f1807b71111ff3487ffe8276f43df35d3076780e Mon Sep 17 00:00:00 2001 From: Nik Spyratos <17888779+nikspyratos@users.noreply.github.com> Date: Sun, 19 Apr 2026 10:33:10 +0200 Subject: [PATCH 5/8] Feedback: directly call scopeAnalyzer service from MakeModelAttributesAndScopesProtectedRector --- .../MakeModelAttributesAndScopesProtectedRector.php | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/Rector/ClassMethod/MakeModelAttributesAndScopesProtectedRector.php b/src/Rector/ClassMethod/MakeModelAttributesAndScopesProtectedRector.php index 177f3cb3..811017e0 100644 --- a/src/Rector/ClassMethod/MakeModelAttributesAndScopesProtectedRector.php +++ b/src/Rector/ClassMethod/MakeModelAttributesAndScopesProtectedRector.php @@ -97,7 +97,7 @@ private function shouldSkipNode(ClassMethod $classMethod, Scope $scope): bool return true; } - if (! $this->isAttributeMethod($classMethod) && ! $this->isScopeMethod($classMethod)) { + if (! $this->isAttributeMethod($classMethod) && ! $this->scopeAnalyzer->isScopeMethod($classMethod)) { return true; } @@ -133,9 +133,4 @@ private function isAttributeMethod(ClassMethod $classMethod): bool return $this->isObjectType($classMethod->returnType, new ObjectType('Illuminate\Database\Eloquent\Casts\Attribute')); } - - private function isScopeMethod(ClassMethod $classMethod): bool - { - return $this->scopeAnalyzer->isScopeMethod($classMethod); - } } From d0e83cee86866c56e37f3f78d98887cb84a4dde6 Mon Sep 17 00:00:00 2001 From: Nik Spyratos <17888779+nikspyratos@users.noreply.github.com> Date: Sun, 19 Apr 2026 10:40:40 +0200 Subject: [PATCH 6/8] Feedback: Simplify named scope check to suggested regex, ditch return type checks --- src/NodeAnalyzer/ScopeAnalyzer.php | 24 +++++------------------- 1 file changed, 5 insertions(+), 19 deletions(-) diff --git a/src/NodeAnalyzer/ScopeAnalyzer.php b/src/NodeAnalyzer/ScopeAnalyzer.php index 14459705..a8e3eb87 100644 --- a/src/NodeAnalyzer/ScopeAnalyzer.php +++ b/src/NodeAnalyzer/ScopeAnalyzer.php @@ -4,7 +4,6 @@ namespace RectorLaravel\NodeAnalyzer; -use PhpParser\Node\Identifier; use PhpParser\Node\Stmt\ClassMethod; use PHPStan\Type\ObjectType; use Rector\NodeNameResolver\NodeNameResolver; @@ -24,18 +23,18 @@ public function __construct( ) {} /** - * Checks for the "scope" + uppercase char naming convention, a Builder-typed - * first parameter, and a void/Builder/untyped return. + * Checks for the "scope" + uppercase char naming convention and a + * Builder-typed (or untyped) first parameter. */ public function isNamedScope(ClassMethod $classMethod): bool { - $name = $this->nodeNameResolver->getName($classMethod); + $name = (string) $this->nodeNameResolver->getName($classMethod); - if ($name === null || ! str_starts_with($name, 'scope') || strlen($name) <= 5 || ! ctype_upper($name[5])) { + if (! preg_match('/^scope[A-Z].+$/', $name)) { return false; } - return $this->hasBuilderFirstParameter($classMethod) && $this->hasScopeReturnType($classMethod); + return $this->hasBuilderFirstParameter($classMethod); } /** @@ -61,17 +60,4 @@ private function hasBuilderFirstParameter(ClassMethod $classMethod): bool return $this->nodeTypeResolver->isObjectType($firstParam->type, new ObjectType(self::ELOQUENT_BUILDER)); } - - private function hasScopeReturnType(ClassMethod $classMethod): bool - { - if ($classMethod->returnType === null) { - return true; - } - - if ($classMethod->returnType instanceof Identifier && $classMethod->returnType->toString() === 'void') { - return true; - } - - return $this->nodeTypeResolver->isObjectType($classMethod->returnType, new ObjectType(self::ELOQUENT_BUILDER)); - } } From 19a1523d67b34827ac6978db5f191e8842e671a9 Mon Sep 17 00:00:00 2001 From: Nik Spyratos <17888779+nikspyratos@users.noreply.github.com> Date: Sun, 19 Apr 2026 10:40:49 +0200 Subject: [PATCH 7/8] Update tests per feedback cases --- ...ects_scope_with_non_builder_return.php.inc | 33 ++++++++++++++++++ .../Fixture/skip_non_scope_signatures.php.inc | 5 --- .../Fixture/non_builder_return_type.php.inc | 34 +++++++++++++++++++ .../skip_non_scope_return_type.php.inc | 16 --------- 4 files changed, 67 insertions(+), 21 deletions(-) create mode 100644 tests/Rector/ClassMethod/MakeModelAttributesAndScopesProtectedRector/Fixture/protects_scope_with_non_builder_return.php.inc create mode 100644 tests/Rector/ClassMethod/ScopeNamedClassMethodToScopeAttributedClassMethodRector/Fixture/non_builder_return_type.php.inc delete mode 100644 tests/Rector/ClassMethod/ScopeNamedClassMethodToScopeAttributedClassMethodRector/Fixture/skip_non_scope_return_type.php.inc diff --git a/tests/Rector/ClassMethod/MakeModelAttributesAndScopesProtectedRector/Fixture/protects_scope_with_non_builder_return.php.inc b/tests/Rector/ClassMethod/MakeModelAttributesAndScopesProtectedRector/Fixture/protects_scope_with_non_builder_return.php.inc new file mode 100644 index 00000000..bfdfab31 --- /dev/null +++ b/tests/Rector/ClassMethod/MakeModelAttributesAndScopesProtectedRector/Fixture/protects_scope_with_non_builder_return.php.inc @@ -0,0 +1,33 @@ + +----- + diff --git a/tests/Rector/ClassMethod/MakeModelAttributesAndScopesProtectedRector/Fixture/skip_non_scope_signatures.php.inc b/tests/Rector/ClassMethod/MakeModelAttributesAndScopesProtectedRector/Fixture/skip_non_scope_signatures.php.inc index b187dc53..2dea9756 100644 --- a/tests/Rector/ClassMethod/MakeModelAttributesAndScopesProtectedRector/Fixture/skip_non_scope_signatures.php.inc +++ b/tests/Rector/ClassMethod/MakeModelAttributesAndScopesProtectedRector/Fixture/skip_non_scope_signatures.php.inc @@ -8,11 +8,6 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo; class SkipNonScopeSignatures extends Model { - public function scopeString(Builder $query): string - { - return 'foo'; - } - public function scopeParam(string $param): Builder { return $this->where('foo', true); diff --git a/tests/Rector/ClassMethod/ScopeNamedClassMethodToScopeAttributedClassMethodRector/Fixture/non_builder_return_type.php.inc b/tests/Rector/ClassMethod/ScopeNamedClassMethodToScopeAttributedClassMethodRector/Fixture/non_builder_return_type.php.inc new file mode 100644 index 00000000..312767bd --- /dev/null +++ b/tests/Rector/ClassMethod/ScopeNamedClassMethodToScopeAttributedClassMethodRector/Fixture/non_builder_return_type.php.inc @@ -0,0 +1,34 @@ + +----- + diff --git a/tests/Rector/ClassMethod/ScopeNamedClassMethodToScopeAttributedClassMethodRector/Fixture/skip_non_scope_return_type.php.inc b/tests/Rector/ClassMethod/ScopeNamedClassMethodToScopeAttributedClassMethodRector/Fixture/skip_non_scope_return_type.php.inc deleted file mode 100644 index 3a851225..00000000 --- a/tests/Rector/ClassMethod/ScopeNamedClassMethodToScopeAttributedClassMethodRector/Fixture/skip_non_scope_return_type.php.inc +++ /dev/null @@ -1,16 +0,0 @@ - From afb33fa5c7f8bd4b562a6cf08bf4d4ac11838b21 Mon Sep 17 00:00:00 2001 From: Nik Spyratos <17888779+nikspyratos@users.noreply.github.com> Date: Mon, 20 Apr 2026 07:02:49 +0200 Subject: [PATCH 8/8] Fix PHPStan notice --- src/NodeAnalyzer/ScopeAnalyzer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NodeAnalyzer/ScopeAnalyzer.php b/src/NodeAnalyzer/ScopeAnalyzer.php index a8e3eb87..d2c6bf79 100644 --- a/src/NodeAnalyzer/ScopeAnalyzer.php +++ b/src/NodeAnalyzer/ScopeAnalyzer.php @@ -30,7 +30,7 @@ public function isNamedScope(ClassMethod $classMethod): bool { $name = (string) $this->nodeNameResolver->getName($classMethod); - if (! preg_match('/^scope[A-Z].+$/', $name)) { + if (! (bool) preg_match('/^scope[A-Z].+$/', $name)) { return false; }