Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft: hide class-level template types from static members #2232

Open
wants to merge 5 commits into
base: 1.10.x
Choose a base branch
from
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions conf/bleedingEdge.neon
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,4 @@ parameters:
closureDefaultParameterTypeRule: true
newRuleLevelHelper: true
instanceofType: true
disallowClassLevelTemplatesInStaticMethods: true
3 changes: 3 additions & 0 deletions conf/config.neon
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ parameters:
closureDefaultParameterTypeRule: false
newRuleLevelHelper: false
instanceofType: false
disallowClassLevelTemplatesInStaticMethods: false
fileExtensions:
- php
checkAdvancedIsset: false
Expand Down Expand Up @@ -288,6 +289,7 @@ parametersSchema:
closureDefaultParameterTypeRule: bool()
newRuleLevelHelper: bool()
instanceofType: bool()
disallowClassLevelTemplatesInStaticMethods: bool()
])
fileExtensions: listOf(string())
checkAdvancedIsset: bool()
Expand Down Expand Up @@ -1125,6 +1127,7 @@ services:
class: PHPStan\Type\FileTypeMapper
arguments:
phpParser: @defaultAnalysisParser
disallowClassLevelTemplatesInStaticMethods: %featureToggles.disallowClassLevelTemplatesInStaticMethods%

-
class: PHPStan\Type\TypeAliasResolver
Expand Down
13 changes: 12 additions & 1 deletion src/Type/FileTypeMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ public function __construct(
private PhpDocNodeResolver $phpDocNodeResolver,
private AnonymousClassNameHelper $anonymousClassNameHelper,
private FileHelper $fileHelper,
private bool $disallowClassLevelTemplatesInStaticMethods,
)
{
}
Expand Down Expand Up @@ -304,9 +305,19 @@ function (Node $node) use ($fileName, $lookForTrait, &$traitFound, $traitMethodA
if ($node instanceof Node\Stmt\ClassLike || $node instanceof Node\Stmt\ClassMethod || $node instanceof Node\Stmt\Function_) {
$phpDocString = GetLastDocComment::forNode($node);
if ($phpDocString !== null) {
$typeMapStack[] = function () use ($namespace, $uses, $className, $functionName, $phpDocString, $typeMapStack, $constUses): TemplateTypeMap {
$typeMapStack[] = function () use ($node, $namespace, $uses, $className, $functionName, $phpDocString, $typeMapStack, $constUses): TemplateTypeMap {
$phpDocNode = $this->resolvePhpDocStringToDocNode($phpDocString);
$typeMapCb = $typeMapStack[count($typeMapStack) - 1] ?? null;

// static methods exist in their own scope
if (
$this->disallowClassLevelTemplatesInStaticMethods
&& $node instanceof Node\Stmt\ClassMethod
&& $node->isStatic()
) {
$typeMapCb = null;
}

$currentTypeMap = $typeMapCb !== null ? $typeMapCb() : null;
$nameScope = new NameScope($namespace, $uses, $className, $functionName, $currentTypeMap, [], false, $constUses);
$templateTags = $this->phpDocNodeResolver->resolveTemplateTags($phpDocNode, $nameScope);
Expand Down
2 changes: 2 additions & 0 deletions tests/PHPStan/Analyser/NodeScopeResolverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1200,6 +1200,8 @@ public function dataFileAsserts(): iterable
yield from $this->gatherAssertTypes(__DIR__ . '/data/trait-instance-of.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/mysql-stmt.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/list-shapes.php');

yield from $this->gatherAssertTypes(__DIR__ . '/data/static-methods-template-types.php');
}

/**
Expand Down
2 changes: 1 addition & 1 deletion tests/PHPStan/Analyser/data/generics.php
Original file line number Diff line number Diff line change
Expand Up @@ -1395,7 +1395,7 @@ class CarFactoryProcessor {
*/
public function process($class): void {
$car = $class::create();
assertType(Car::class, $car);
assertType(T::class, $car); // @template T is unknown in static method
}
}

Expand Down
49 changes: 49 additions & 0 deletions tests/PHPStan/Analyser/data/static-methods-template-types.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php

namespace StaticMethodsTemplateTypes;

use function PHPStan\Testing\assertType;

/**
* @template T
*/
class Foo
{

/**
* @template U
* @param T $t
* @return T
*/
public static function bar($t)
{
}

/**
* @template U
* @param U $u
* @return U
*/
public static function baz($u)
{
}

/**
* @return T
*/
public static function qux()
{
}

public static function doFoo()
{
assertType('StaticMethodsTemplateTypes\T', self::bar(42));
assertType('int', self::baz(42));
assertType('StaticMethodsTemplateTypes\T', self::qux());
}

}

assertType('StaticMethodsTemplateTypes\T', Foo::bar(42));
assertType('int', Foo::baz(42));
assertType('StaticMethodsTemplateTypes\T', Foo::qux());
7 changes: 6 additions & 1 deletion tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -470,7 +470,12 @@ public function testBug6249(): void
// discussion https://github.com/phpstan/phpstan/discussions/6249
$this->checkThisOnly = false;
$this->checkExplicitMixed = true;
$this->analyse([__DIR__ . '/data/bug-6249.php'], []);
$this->analyse([__DIR__ . '/data/bug-6249.php'], [
[
'Parameter #1 $iterable of static method Bug6249N3\Cw<(int|string),mixed>::fromIterable() expects iterable<Bug6249N3\TKey, Bug6249N3\T>, Bug6249N2\Eii given.',
145,
],
]);
}

public function testBug5749(): void
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,10 @@ public function testExistingClassInTypehint(): void
'Template type U of method TestMethodTypehints\TemplateTypeMissingInParameter::doFoo() is not referenced in a parameter.',
130,
],
[
'Method TestMethodTypehints\TemplateInStaticMethod::doBar() has invalid return type TestMethodTypehints\T.',
163,
],
]);
}

Expand Down
13 changes: 13 additions & 0 deletions tests/PHPStan/Rules/Methods/data/bug-6249.php
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,18 @@ public static function fromIterable(iterable $iterable): self
return new self($iterable);
}

/**
* @template UKey of array-key
* @template U
* @param iterable<UKey, U> $iterable
*
* @return self<UKey, U>
*/
public static function fromIterableCorrect(iterable $iterable): self
{
return new self($iterable);
}

public function count(): int
{
if (is_array($this->iterable) || $this->iterable instanceof Countable) {
Expand All @@ -131,6 +143,7 @@ class Foo
public function doFoo()
{
\Bug6249N3\Cw::fromIterable(new \Bug6249N2\Eii([]));
\Bug6249N3\Cw::fromIterableCorrect(new \Bug6249N2\Eii([]));
}

}
24 changes: 24 additions & 0 deletions tests/PHPStan/Rules/Methods/data/typehints.php
Original file line number Diff line number Diff line change
Expand Up @@ -142,3 +142,27 @@ public function doBar(string $class): void
}

}

/**
* @template T
*/
class TemplateInStaticMethod
{

/**
* @return T
*/
public function doFoo()
{

}

/**
* @return T
*/
public static function doBar()
{

}

}