Skip to content

Commit

Permalink
Merge pull request #152 from sascha-egerer/task/add-container-private…
Browse files Browse the repository at this point in the history
…-rule

[FEATURE] Add rule to check if service is private
  • Loading branch information
sabbelasichon authored Feb 28, 2024
2 parents 947b026 + 6188f32 commit b56a301
Show file tree
Hide file tree
Showing 34 changed files with 1,054 additions and 6 deletions.
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,3 +143,14 @@ $myIntAttribute = $site->getAttribute('myIntAttribute');
// PHPStan will now know that $myStringAttribute is of type string
$myStringAttribute = $site->getAttribute('myStringAttribute');
```

### Check for private Services
You have to provide a path to App_KernelDevelopmentDebugContainer.xml or similar XML file describing your container.
This is generated by [ssch/typo3-debug-dump-pass](https://github.com/sabbelasichon/typo3-debug-dump-pass) in your /var/cache/{TYPO3_CONTEXT}/ folder.

```NEON
parameters:
typo3:
containerXmlPath: var/cache/development/App_KernelDevelopmentDebugContainer.xml
```

1 change: 1 addition & 0 deletions build.xml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
passthru="true"
checkreturn="true"
>
<arg line="--memory-limit=-1"/>
</exec>
</target>

Expand Down
7 changes: 5 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@
"typo3/cms-core": "^11.5 || ^12.4 || ^13.0",
"typo3/cms-extbase": "^11.5 || ^12.4 || ^13.0",
"bnf/phpstan-psr-container": "^1.0",
"composer/semver": "^3.3"
"composer/semver": "^3.3",
"ssch/typo3-debug-dump-pass": "^0.0.2",
"ext-simplexml": "*"
},
"require-dev": {
"consistence-community/coding-standard": "^3.11.1",
Expand All @@ -27,7 +29,8 @@
"phing/phing": "^2.17.4",
"phpstan/phpstan-strict-rules": "^1.5.1",
"phpunit/phpunit": "^9.6.16",
"symfony/polyfill-php80": "^1.28.0"
"symfony/polyfill-php80": "^1.28.0",
"phpstan/phpstan-phpunit": "^1.3"
},
"autoload": {
"psr-4": {
Expand Down
21 changes: 17 additions & 4 deletions extension.neon
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
includes:
- rules.neon

services:
-
class: SaschaEgerer\PhpstanTypo3\Reflection\RepositoryFindMethodsClassReflectionExtension
Expand Down Expand Up @@ -51,10 +54,6 @@ services:
siteGetAttributeMapping: %typo3.siteGetAttributeMapping%
tags:
- phpstan.rules.rule
-
class: SaschaEgerer\PhpstanTypo3\Rule\ValidatorResolverOptionsRule
tags:
- phpstan.rules.rule
-
class: SaschaEgerer\PhpstanTypo3\Type\RepositoryQueryDynamicReturnTypeExtension
tags:
Expand Down Expand Up @@ -105,10 +104,23 @@ services:
class: SaschaEgerer\PhpstanTypo3\Type\DateTimeAspectGetDynamicReturnTypeExtension
tags:
- phpstan.broker.dynamicMethodReturnTypeExtension
-
class: SaschaEgerer\PhpstanTypo3\Service\PrivateServiceAnalyzer
-
class: SaschaEgerer\PhpstanTypo3\Service\PrototypeServiceDefinitionChecker
# service map
typo3.serviceMapFactory:
class: SaschaEgerer\PhpstanTypo3\Contract\ServiceMapFactory
factory: SaschaEgerer\PhpstanTypo3\Service\XmlServiceMapFactory
arguments:
containerXmlPath: %typo3.containerXmlPath%
-
factory: @typo3.serviceMapFactory::create()
parameters:
bootstrapFiles:
- phpstan.bootstrap.php
typo3:
containerXmlPath: null
contextApiGetAspectMapping:
date: TYPO3\CMS\Core\Context\DateTimeAspect
visibility: TYPO3\CMS\Core\Context\VisibilityAspect
Expand Down Expand Up @@ -194,6 +206,7 @@ parameters:
- pageErrorHandler
parametersSchema:
typo3: structure([
containerXmlPath: schema(string(), nullable())
contextApiGetAspectMapping: arrayOf(string())
requestGetAttributeMapping: arrayOf(string())
siteGetAttributeMapping: arrayOf(string())
Expand Down
2 changes: 2 additions & 0 deletions phpstan.neon
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
includes:
- vendor/phpstan/phpstan-strict-rules/rules.neon
- vendor/phpstan/phpstan-phpunit/extension.neon
- extension.neon
- tests/Unit/Type/data/context-get-aspect-return-types.neon
- tests/Unit/Type/data/request-get-attribute-return-types.neon
Expand All @@ -12,6 +13,7 @@ parameters:
- tests
reportUnmatchedIgnoredErrors: false
excludePaths:
- '*tests/*/Fixtures/*'
- '*tests/*/Fixture/*'
- '*tests/*/Source/*'
- '*tests/*/data/*'
Expand Down
4 changes: 4 additions & 0 deletions rules.neon
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
rules:
- SaschaEgerer\PhpstanTypo3\Rule\ValidatorResolverOptionsRule
- SaschaEgerer\PhpstanTypo3\Rule\ContainerInterfacePrivateServiceRule
- SaschaEgerer\PhpstanTypo3\Rule\GeneralUtilityMakeInstancePrivateServiceRule
17 changes: 17 additions & 0 deletions src/Contract/ServiceDefinitionChecker.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php declare(strict_types = 1);

namespace SaschaEgerer\PhpstanTypo3\Contract;

use PhpParser\Node;
use PhpParser\Node\Expr\StaticCall;
use SaschaEgerer\PhpstanTypo3\Service\ServiceDefinition;

interface ServiceDefinitionChecker
{

/**
* @param Node\Expr\MethodCall|StaticCall $node
*/
public function isPrototype(ServiceDefinition $serviceDefinition, Node $node): bool;

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

namespace SaschaEgerer\PhpstanTypo3\Contract;

use PhpParser\Node\Expr;
use PHPStan\Analyser\Scope;
use SaschaEgerer\PhpstanTypo3\Service\ServiceDefinition;

interface ServiceMap
{

/**
* @return ServiceDefinition[]
*/
public function getServiceDefinitions(): array;

public function getServiceDefinitionById(string $id): ?ServiceDefinition;

public function getServiceIdFromNode(Expr $node, Scope $scope): ?string;

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

namespace SaschaEgerer\PhpstanTypo3\Contract;

interface ServiceMapFactory
{

public function create(): ServiceMap;

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

namespace SaschaEgerer\PhpstanTypo3\Rule;

use PhpParser\Node;
use PhpParser\Node\Expr\MethodCall;
use PHPStan\Analyser\Scope;
use PHPStan\Rules\Rule;
use PHPStan\Type\ObjectType;
use SaschaEgerer\PhpstanTypo3\Service\NullServiceDefinitionChecker;
use SaschaEgerer\PhpstanTypo3\Service\PrivateServiceAnalyzer;

/**
* @implements Rule<MethodCall>
*/
final class ContainerInterfacePrivateServiceRule implements Rule
{

private PrivateServiceAnalyzer $privateServiceAnalyzer;

public function __construct(PrivateServiceAnalyzer $privateServiceAnalyzer)
{
$this->privateServiceAnalyzer = $privateServiceAnalyzer;
}

public function getNodeType(): string
{
return MethodCall::class;
}

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

return $this->privateServiceAnalyzer->analyze($node, $scope, new NullServiceDefinitionChecker());
}

private function shouldSkip(MethodCall $node, Scope $scope): bool
{
if (!$node->name instanceof Node\Identifier) {
return true;
}

$methodCallArguments = $node->getArgs();

if (!isset($methodCallArguments[0])) {
return true;
}

$methodCallName = $node->name->name;

if ($methodCallName !== 'get') {
return true;
}

$argType = $scope->getType($node->var);

$isPsrContainerType = (new ObjectType('Psr\Container\ContainerInterface'))->isSuperTypeOf($argType);
$isTestCaseType = (new ObjectType('TYPO3\TestingFramework\Core\Functional\FunctionalTestCase'))->isSuperTypeOf($argType);

if ($isTestCaseType->yes()) {
return true;
}

return !$isPsrContainerType->yes();
}

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

namespace SaschaEgerer\PhpstanTypo3\Rule;

use PhpParser\Node;
use PhpParser\Node\Expr\StaticCall;
use PHPStan\Analyser\Scope;
use PHPStan\Rules\Rule;
use SaschaEgerer\PhpstanTypo3\Service\PrivateServiceAnalyzer;
use SaschaEgerer\PhpstanTypo3\Service\PrototypeServiceDefinitionChecker;
use TYPO3\CMS\Core\Utility\GeneralUtility;

/**
* @implements Rule<StaticCall>
*/
final class GeneralUtilityMakeInstancePrivateServiceRule implements Rule
{

private PrivateServiceAnalyzer $privateServiceAnalyzer;

private PrototypeServiceDefinitionChecker $prototypeServiceDefinitionChecker;

public function __construct(PrivateServiceAnalyzer $privateServiceAnalyzer, PrototypeServiceDefinitionChecker $prototypeServiceDefinitionChecker)
{
$this->privateServiceAnalyzer = $privateServiceAnalyzer;
$this->prototypeServiceDefinitionChecker = $prototypeServiceDefinitionChecker;
}

public function getNodeType(): string
{
return StaticCall::class;
}

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

return $this->privateServiceAnalyzer->analyze($node, $scope, $this->prototypeServiceDefinitionChecker);
}

private function shouldSkip(StaticCall $node): bool
{
if (!$node->name instanceof Node\Identifier) {
return true;
}

$methodCallArguments = $node->getArgs();

if (!isset($methodCallArguments[0])) {
return true;
}

$methodCallName = $node->name->name;

if ($methodCallName !== 'makeInstance') {
return true;
}

if (count($methodCallArguments) > 1) {
return true;
}

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

return $node->class->toString() !== GeneralUtility::class;
}

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

namespace SaschaEgerer\PhpstanTypo3\Service;

use PhpParser\Node\Expr;
use PHPStan\Analyser\Scope;
use SaschaEgerer\PhpstanTypo3\Contract\ServiceMap;

final class DefaultServiceMap implements ServiceMap
{

/** @var ServiceDefinition[] */
private array $serviceDefinitions;

/**
* @param ServiceDefinition[] $serviceDefinitions
*/
public function __construct(array $serviceDefinitions)
{
$this->serviceDefinitions = $serviceDefinitions;
}

public function getServiceDefinitions(): array
{
return $this->serviceDefinitions;
}

public function getServiceDefinitionById(string $id): ?ServiceDefinition
{
return $this->serviceDefinitions[$id] ?? null;
}

public function getServiceIdFromNode(Expr $node, Scope $scope): ?string
{
$strings = $scope->getType($node)->getConstantStrings();
return count($strings) === 1 ? $strings[0]->getValue() : null;
}

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

namespace SaschaEgerer\PhpstanTypo3\Service;

use PhpParser\Node\Expr;
use PHPStan\Analyser\Scope;
use SaschaEgerer\PhpstanTypo3\Contract\ServiceMap;

final class FakeServiceMap implements ServiceMap
{

public function getServiceDefinitions(): array
{
return [];
}

public function getServiceDefinitionById(string $id): ?ServiceDefinition
{
return null;
}

public function getServiceIdFromNode(Expr $node, Scope $scope): ?string
{
return null;
}

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

namespace SaschaEgerer\PhpstanTypo3\Service;

use PhpParser\Node;
use SaschaEgerer\PhpstanTypo3\Contract\ServiceDefinitionChecker;

final class NullServiceDefinitionChecker implements ServiceDefinitionChecker
{

public function isPrototype(ServiceDefinition $serviceDefinition, Node $node): bool
{
return false;
}

}
Loading

0 comments on commit b56a301

Please sign in to comment.