• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

sanmai / phpstan-rules / 16610059202

29 Jul 2025 11:59PM UTC coverage: 99.583% (-0.4%) from 100.0%
16610059202

Pull #31

github

web-flow
Merge 380389517 into 20b3f6015
Pull Request #31: Add NoPublicStaticMethods rule

30 of 31 new or added lines in 1 file covered. (96.77%)

239 of 240 relevant lines covered (99.58%)

33.4 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.77
/src/Rules/NoStaticMethodsRule.php
1
<?php
2

3
/**
4
 * Copyright 2025 Alexey Kopytko <alexey@kopytko.com>
5
 *
6
 * Licensed under the Apache License, Version 2.0 (the "License");
7
 * you may not use this file except in compliance with the License.
8
 * You may obtain a copy of the License at
9
 *
10
 * http://www.apache.org/licenses/LICENSE-2.0
11
 *
12
 * Unless required by applicable law or agreed to in writing, software
13
 * distributed under the License is distributed on an "AS IS" BASIS,
14
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
 * See the License for the specific language governing permissions and
16
 * limitations under the License.
17
 */
18

19
declare(strict_types=1);
20

21
namespace Sanmai\PHPStanRules\Rules;
22

23
use Override;
24
use PhpParser\Node;
25
use PhpParser\Node\Stmt\Class_;
26
use PhpParser\Node\Stmt\ClassMethod;
27
use PHPStan\Analyser\Scope;
28
use PHPStan\Reflection\ReflectionProvider;
29
use PHPStan\Rules\IdentifierRuleError;
30
use PHPStan\Rules\Rule;
31
use PHPStan\Rules\RuleErrorBuilder;
32
use PHPStan\Reflection\ClassReflection;
33

34
use function array_slice;
35
use function str_ends_with;
36

37
/**
38
 * @implements Rule<Class_>
39
 */
40
final class NoStaticMethodsRule implements Rule
41
{
42
    public const ERROR_MESSAGE = 'Only one public static method is allowed per class. Static methods are impossible to mock in tests.';
43
    public const IDENTIFIER = 'sanmai.noStaticMethods';
44

45
    public function __construct(
46
        private ReflectionProvider $reflectionProvider
47
    ) {}
48✔
48

49
    #[Override]
50
    public function getNodeType(): string
51
    {
52
        return Class_::class;
48✔
53
    }
54

55
    /**
56
     * @return list<IdentifierRuleError>
57
     * @param Class_ $node
58
     */
59
    #[Override]
60
    public function processNode(Node $node, Scope $scope): array
61
    {
62
        // Skip test files - they commonly need multiple static methods for data providers
63
        if ($this->isTestFile($scope)) {
48✔
64
            return [];
24✔
65
        }
66

67
        // Skip classes with private constructors using reflection
68
        if ($this->hasPrivateConstructor($node)) {
24✔
69
            return [];
12✔
70
        }
71

72
        return $this->processPublicStaticMethods($node);
24✔
73
    }
74

75
    private function isTestFile(Scope $scope): bool
76
    {
77
        // Check if file ends with "Test.php" or "TestCase.php"
78
        return str_ends_with($scope->getFile(), 'Test.php')
48✔
79
            || str_ends_with($scope->getFile(), 'TestCase.php');
48✔
80
    }
81

82
    private function getReflection(Class_ $node): ?ClassReflection
83
    {
84
        if (null === $node->namespacedName) {
24✔
85
            return null;
12✔
86
        }
87

88
        $name = $node->namespacedName->toString();
24✔
89

90
        if (!$this->reflectionProvider->hasClass($name)) {
24✔
NEW
91
            return null;
×
92
        }
93

94
        return $this->reflectionProvider->getClass($name);
24✔
95
    }
96

97
    /**
98
     * Check if class has a private constructor
99
     */
100
    private function hasPrivateConstructor(Class_ $node): bool
101
    {
102
        $classReflection = $this->getReflection($node);
24✔
103
        return $classReflection?->hasConstructor() && $classReflection->getConstructor()->isPrivate();
24✔
104
    }
105

106
    /**
107
     * Process public static methods and return errors for violations
108
     * @return list<IdentifierRuleError>
109
     */
110
    private function processPublicStaticMethods(Class_ $node): array
111
    {
112
        $publicStaticMethods = $this->getPublicStaticMethods($node);
24✔
113
        $additionalMethods = array_slice($publicStaticMethods, 1);
24✔
114

115
        // Create error for each additional public static method (beyond the first one)
116
        $errors = [];
24✔
117
        foreach ($additionalMethods as $method) {
24✔
118
            $errors[] = RuleErrorBuilder::message(self::ERROR_MESSAGE)
24✔
119
                ->line($method->getLine())
24✔
120
                ->identifier(self::IDENTIFIER)
24✔
121
                ->build();
24✔
122
        }
123

124
        return $errors;
24✔
125
    }
126

127

128
    /**
129
     * @return ClassMethod[]
130
     */
131
    private function getPublicStaticMethods(Class_ $class): array
132
    {
133
        $methods = [];
24✔
134

135
        foreach ($class->stmts as $stmt) {
24✔
136
            if ($stmt instanceof ClassMethod && $stmt->isStatic() && $stmt->isPublic()) {
24✔
137
                $methods[] = $stmt;
24✔
138
            }
139
        }
140

141
        return $methods;
24✔
142
    }
143
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc