Skip to content
Open
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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
2 changes: 1 addition & 1 deletion bin/phpstan
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ use Symfony\Component\Console\Helper\ProgressBar;
require_once __DIR__ . '/../src/Process/Restart/IniComposer.php';
require_once __DIR__ . '/../src/Process/Restart/RestartExecutor.php';
require_once __DIR__ . '/../src/Process/Restart/RestartManager.php';
RestartManager::run($_SERVER['argv']);
RestartManager::run($_SERVER['argv'], null, null, 'phpstan');
TurboExtensionEnabler::trustOwnTypesIfSuitable($_SERVER['argv']);

$analysisStartTime = microtime(true);
Expand Down
2 changes: 1 addition & 1 deletion build/PHPStan/Build/InlineCallCollector.php
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ final class InlineCallCollector implements Collector
/**
* @param list<string>|null $directories the closed world to scan for overrides; null = directories()
*/
public function __construct(private Parser $parser, private ReflectionProvider $reflectionProvider, private ?array $directories = null)
public function __construct(private Parser $parser, private ReflectionProvider $reflectionProvider, private ?array $directories)
{
}

Expand Down
149 changes: 149 additions & 0 deletions build/PHPStan/Build/NoOptionalParameterRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
<?php declare(strict_types = 1);

namespace PHPStan\Build;

use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;
use function count;
use function is_string;
use function preg_match;
use function sprintf;
use function strtolower;

/**
* Optional parameters are allowed only in @api methods and functions, where
* adding a parameter must not break existing callers. Internal code should
* pass every argument explicitly.
*
* A class-level @api tag covers public methods (and protected methods of non-final classes),
* but not the constructor, which is covered only by its own @api tag.
*
* @implements Rule<Node\FunctionLike>
*/
final class NoOptionalParameterRule implements Rule
{

public function getNodeType(): string
{
return Node\FunctionLike::class;
}

public function processNode(Node $node, Scope $scope): array
{
$positionsOptionalInAncestors = [];
if ($node instanceof Node\Stmt\ClassMethod) {
$methodName = $node->name->toString();
$classReflection = $scope->getClassReflection();
$declaringReflection = $scope->isInTrait() ? $scope->getTraitReflection() : $classReflection;
if ($classReflection === null || $declaringReflection === null || $this->isApiMethod($node, $declaringReflection)) {
return [];
}
$description = sprintf('method %s::%s()', $declaringReflection->getDisplayName(), $methodName);
$positionsOptionalInAncestors = $this->getPositionsOptionalInAncestors($classReflection, $methodName);
} elseif ($node instanceof Node\Stmt\Function_) {
if ($this->hasApiTag($node)) {
return [];
}
$description = sprintf('function %s()', $node->namespacedName !== null ? $node->namespacedName->toString() : $node->name->toString());
} else {
return [];
}

$errors = [];
foreach ($node->getParams() as $position => $param) {
if ($param->default === null) {
continue;
}
if (!$param->var instanceof Node\Expr\Variable || !is_string($param->var->name)) {
continue;
}
if (isset($positionsOptionalInAncestors[$position])) {
continue;
}

$errors[] = RuleErrorBuilder::message(sprintf(
'Parameter $%s of %s must not be optional. Optional parameters are allowed only in @api code.',
$param->var->name,
$description,
))
->identifier('phpstan.optionalParameter')
->line($param->getStartLine())
->build();
}

return $errors;
}

private function isApiMethod(Node\Stmt\ClassMethod $node, ClassReflection $classReflection): bool
{
if ($this->hasApiTag($node)) {
return true;
}

if (strtolower($node->name->toString()) === '__construct') {
return false;
}

if ($node->isPrivate()) {
return false;
}

if ($node->isProtected() && $classReflection->isFinal()) {
return false;
}

$resolvedPhpDoc = $classReflection->getResolvedPhpDoc();
if ($resolvedPhpDoc === null) {
return false;
}

foreach ($resolvedPhpDoc->getPhpDocNodes() as $phpDocNode) {
if (count($phpDocNode->getTagsByName('@api')) > 0) {
return true;
}
}

return false;
}

private function hasApiTag(Node $node): bool
{
$docComment = $node->getDocComment();
if ($docComment === null) {
return false;
}

return preg_match('~@api(?![\w-])~', $docComment->getText()) === 1;
}

/**
* PHP requires an overriding method to keep these parameters optional.
*
* @return array<int, true>
*/
private function getPositionsOptionalInAncestors(ClassReflection $classReflection, string $methodName): array
{
$positions = [];
foreach ($classReflection->getAncestors() as $ancestor) {
if ($ancestor->getName() === $classReflection->getName() || $ancestor->isTrait() || !$ancestor->hasNativeMethod($methodName)) {
continue;
}

foreach ($ancestor->getNativeMethod($methodName)->getVariants() as $variant) {
foreach ($variant->getParameters() as $position => $parameter) {
if (!$parameter->isOptional()) {
continue;
}

$positions[$position] = true;
}
}
}

return $positions;
}

}
Loading
Loading