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

sanmai / phpstan-rules / 16611207729

30 Jul 2025 01:26AM UTC coverage: 99.587% (-0.4%) from 100.0%
16611207729

Pull #31

github

web-flow
Merge f7d082ff6 into ead5abd39
Pull Request #31: Add NoPublicStaticMethods rule

32 of 33 new or added lines in 1 file covered. (96.97%)

241 of 242 relevant lines covered (99.59%)

32.33 hits per line

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

96.97
/src/Rules/NoPublicStaticMethodsRule.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 NoPublicStaticMethodsRule 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.noPublicStaticMethods';
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 abstract classes - they often have multiple static utility methods
68
        if ($node->isAbstract()) {
24✔
69
            return [];
12✔
70
        }
71

72
        // Skip classes with private constructors using reflection
73
        if ($this->hasPrivateConstructor($node)) {
24✔
74
            return [];
12✔
75
        }
76

77
        return $this->processPublicStaticMethods($node);
24✔
78
    }
79

80
    private function isTestFile(Scope $scope): bool
81
    {
82
        // Check if file ends with "Test.php" or "TestCase.php"
83
        return str_ends_with($scope->getFile(), 'Test.php')
48✔
84
            || str_ends_with($scope->getFile(), 'TestCase.php');
48✔
85
    }
86

87
    private function getReflection(Class_ $node): ?ClassReflection
88
    {
89
        if (null === $node->namespacedName) {
24✔
90
            return null;
12✔
91
        }
92

93
        $name = $node->namespacedName->toString();
24✔
94

95
        if (!$this->reflectionProvider->hasClass($name)) {
24✔
NEW
96
            return null;
×
97
        }
98

99
        return $this->reflectionProvider->getClass($name);
24✔
100
    }
101

102
    /**
103
     * Check if class has a private constructor
104
     */
105
    private function hasPrivateConstructor(Class_ $node): bool
106
    {
107
        $classReflection = $this->getReflection($node);
24✔
108
        return $classReflection?->hasConstructor() && $classReflection->getConstructor()->isPrivate();
24✔
109
    }
110

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

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

129
        return $errors;
24✔
130
    }
131

132

133
    /**
134
     * @return ClassMethod[]
135
     */
136
    private function getPublicStaticMethods(Class_ $class): array
137
    {
138
        $methods = [];
24✔
139

140
        foreach ($class->stmts as $stmt) {
24✔
141
            if ($stmt instanceof ClassMethod && $stmt->isStatic() && $stmt->isPublic()) {
24✔
142
                $methods[] = $stmt;
24✔
143
            }
144
        }
145

146
        return $methods;
24✔
147
    }
148
}
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