Skip to content

Commit

Permalink
Bleeding edge - Backward Compatible PHPStan API rules
Browse files Browse the repository at this point in the history
  • Loading branch information
ondrejmirtes committed May 20, 2021
1 parent 184bf9e commit 8a05e0d
Show file tree
Hide file tree
Showing 13 changed files with 279 additions and 1 deletion.
1 change: 1 addition & 0 deletions conf/bleedingEdge.neon
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@ parameters:
skipCheckGenericClasses: []
rememberFunctionValues: true
preciseExceptionTracking: true
apiRules: true
5 changes: 5 additions & 0 deletions conf/config.level0.neon
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ parameters:
missingClosureNativeReturnCheckObjectTypehint: false

conditionalTags:
PHPStan\Rules\Api\ApiInstantiationRule:
phpstan.rules.rule: %featureToggles.apiRules%
PHPStan\Rules\Functions\ClosureUsesThisRule:
phpstan.rules.rule: %featureToggles.closureUsesThis%
PHPStan\Rules\Missing\MissingClosureNativeReturnTypehintRule:
Expand Down Expand Up @@ -58,6 +60,9 @@ rules:
- PHPStan\Rules\Variables\UnsetRule

services:
-
class: PHPStan\Rules\Api\ApiInstantiationRule

-
class: PHPStan\Rules\Classes\ExistingClassInInstanceOfRule
tags:
Expand Down
7 changes: 6 additions & 1 deletion conf/config.neon
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ parameters:
- RecursiveIterator
rememberFunctionValues: false
preciseExceptionTracking: false
apiRules: false
fileExtensions:
- php
checkAlwaysTrueCheckTypeFunctionCall: false
Expand Down Expand Up @@ -208,7 +209,8 @@ parametersSchema:
objectFromNewClass: bool(),
skipCheckGenericClasses: listOf(string()),
rememberFunctionValues: bool(),
preciseExceptionTracking: bool()
preciseExceptionTracking: bool(),
apiRules: bool()
])
fileExtensions: listOf(string())
checkAlwaysTrueCheckTypeFunctionCall: bool()
Expand Down Expand Up @@ -725,6 +727,9 @@ services:
class: PHPStan\Reflection\SignatureMap\SignatureMapProvider
factory: @PHPStan\Reflection\SignatureMap\SignatureMapProviderFactory::create()

-
class: PHPStan\Rules\Api\ApiRuleHelper

-
class: PHPStan\Rules\AttributesCheck

Expand Down
80 changes: 80 additions & 0 deletions src/Rules/Api/ApiInstantiationRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\Api;

use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;

/**
* @implements Rule<Node\Expr\New_>
*/
class ApiInstantiationRule implements Rule
{

private ApiRuleHelper $apiRuleHelper;

private ReflectionProvider $reflectionProvider;

public function __construct(
ApiRuleHelper $apiRuleHelper,
ReflectionProvider $reflectionProvider
)
{
$this->apiRuleHelper = $apiRuleHelper;
$this->reflectionProvider = $reflectionProvider;
}

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

public function processNode(Node $node, Scope $scope): array
{
if ($this->apiRuleHelper->isInPhpStanNamespace($scope->getNamespace())) {
return [];
}

if (!$node->class instanceof Node\Name) {
return [];
}

$className = $scope->resolveName($node->class);
if (!$this->reflectionProvider->hasClass($className)) {
return [];
}

$classReflection = $this->reflectionProvider->getClass($className);
if (!$this->apiRuleHelper->isInPhpStanNamespace($classReflection->getName())) {
return [];
}

$ruleError = RuleErrorBuilder::message(sprintf(
'Creating new %s is not covered by backward compatibility promise. The class might change in a minor PHPStan version.',
$classReflection->getDisplayName()
))->tip(sprintf(
"If you think it should be covered by backward compatibility promise, open a discussion:\n %s\n\n See also:\n https://phpstan.org/developing-extensions/backward-compatibility-promise",
'https://github.com/phpstan/phpstan/discussions'
))->build();

if (!$classReflection->hasConstructor()) {
return [$ruleError];
}

$constructor = $classReflection->getConstructor();
$docComment = $constructor->getDocComment();
if ($docComment === null) {
return [$ruleError];
}

if (strpos($docComment, '@api') === false) {
return [$ruleError];
}

return [];
}

}
21 changes: 21 additions & 0 deletions src/Rules/Api/ApiRuleHelper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\Api;

class ApiRuleHelper
{

public function isInPhpStanNamespace(?string $namespace): bool
{
if ($namespace === null) {
return false;
}

if (strtolower($namespace) === 'phpstan') {
return true;
}

return stripos($namespace, 'PHPStan\\') === 0;
}

}
2 changes: 2 additions & 0 deletions src/Type/Constant/ConstantIntegerType.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,10 @@ class ConstantIntegerType extends IntegerType implements ConstantScalarType

private int $value;

/** @api */
public function __construct(int $value)
{
parent::__construct();
$this->value = $value;
}

Expand Down
1 change: 1 addition & 0 deletions src/Type/Generic/TemplateIntegerType.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ public function __construct(
IntegerType $bound
)
{
parent::__construct();
$this->scope = $scope;
$this->strategy = $templateTypeStrategy;
$this->variance = $templateTypeVariance;
Expand Down
1 change: 1 addition & 0 deletions src/Type/IntegerRangeType.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ class IntegerRangeType extends IntegerType implements CompoundType

private function __construct(?int $min, ?int $max)
{
parent::__construct();
assert($min === null || $max === null || $min <= $max);
assert($min !== null || $max !== null);

Expand Down
5 changes: 5 additions & 0 deletions src/Type/IntegerType.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ class IntegerType implements Type
use UndecidedComparisonTypeTrait;
use NonGenericTypeTrait;

/** @api */
public function __construct()
{
}

public function describe(VerbosityLevel $level): string
{
return 'int';
Expand Down
46 changes: 46 additions & 0 deletions tests/PHPStan/Rules/Api/ApiInstantiationRuleTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\Api;

use PHPStan\Testing\RuleTestCase;

/**
* @extends RuleTestCase<ApiInstantiationRule>
*/
class ApiInstantiationRuleTest extends RuleTestCase
{

protected function getRule(): \PHPStan\Rules\Rule
{
return new ApiInstantiationRule(
new ApiRuleHelper(),
$this->createReflectionProvider()
);
}

public function testRuleInPhpStan(): void
{
$this->analyse([__DIR__ . '/data/new-in-phpstan.php'], []);
}

public function testRuleOutOfPhpStan(): void
{
$tip = sprintf(
"If you think it should be covered by backward compatibility promise, open a discussion:\n %s\n\n See also:\n https://phpstan.org/developing-extensions/backward-compatibility-promise",
'https://github.com/phpstan/phpstan/discussions'
);
$this->analyse([__DIR__ . '/data/new-out-of-phpstan.php'], [
[
'Creating new PHPStan\Type\FileTypeMapper is not covered by backward compatibility promise. The class might change in a minor PHPStan version.',
17,
$tip,
],
[
'Creating new PHPStan\DependencyInjection\NeonAdapter is not covered by backward compatibility promise. The class might change in a minor PHPStan version.',
18,
$tip,
],
]);
}

}
59 changes: 59 additions & 0 deletions tests/PHPStan/Rules/Api/ApiRuleHelperTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\Api;

use PHPUnit\Framework\TestCase;

class ApiRuleHelperTest extends TestCase
{

public function dataIsInPhpStanNamespace(): array
{
return [
[
null,
false,
],
[
'PHPStan',
true,
],
[
'PhpStan',
true,
],
[
'PHPStan\\Foo',
true,
],
[
'PhpStan\\Foo',
true,
],
[
'App\\Foo',
false,
],
[
'PHPStanWorkshop',
false,
],
[
'PHPStanWorkshop\\',
false,
],
];
}

/**
* @dataProvider dataIsInPhpStanNamespace
* @param string|null $namespace
* @param bool $expected
*/
public function testIsInPhpStanNamespace(?string $namespace, bool $expected): void
{
$rule = new ApiRuleHelper();
$this->assertSame($expected, $rule->isInPhpStanNamespace($namespace));
}

}
26 changes: 26 additions & 0 deletions tests/PHPStan/Rules/Api/data/new-in-phpstan.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

namespace PHPStan\Foo;

use PHPStan\DependencyInjection\NeonAdapter;
use PHPStan\Type\FileTypeMapper;
use PHPStan\Type\IntegerType;

class Foo
{

public function doFoo(): void
{
new Nonexistent();
new Bar();
new IntegerType();
new FileTypeMapper();
new NeonAdapter();
}

}

class Bar
{

}
26 changes: 26 additions & 0 deletions tests/PHPStan/Rules/Api/data/new-out-of-phpstan.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

namespace App;

use PHPStan\DependencyInjection\NeonAdapter;
use PHPStan\Type\FileTypeMapper;
use PHPStan\Type\IntegerType;

class Foo
{

public function doFoo(): void
{
new Nonexistent();
new Bar();
new IntegerType();
new FileTypeMapper(); // error - has constructor
new NeonAdapter(); // error - does not have a constructor
}

}

class Bar
{

}

0 comments on commit 8a05e0d

Please sign in to comment.