Skip to content

Commit d8ff487

Browse files
committed
Initial commit
0 parents  commit d8ff487

16 files changed

+374
-0
lines changed

.coveralls.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
service_name: travis-ci
2+
coverage_clover: coverage.xml
3+
json_path: coverage.json

.gitattributes

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
tests export-ignore
2+
.coveralls.yml export-ignore
3+
.gitattributes export-ignore
4+
.gitignore export-ignore
5+
.travis.yml export-ignore
6+
phpstan.neon export-ignore
7+
phpunit.xml.dist export-ignore
8+
ruleset.xml export-ignore

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
/vendor
2+
composer.lock

.travis.yml

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
language: php
2+
3+
sudo: false
4+
5+
cache:
6+
directories:
7+
- $HOME/.composer/cache
8+
9+
env: # intentionally blank
10+
11+
php:
12+
- 7.0
13+
- 7.1
14+
15+
matrix:
16+
fast_finish: true
17+
include:
18+
- php: 7.1
19+
env: COVERAGE="1"
20+
allow_failures:
21+
- php: 7.1
22+
env: COVERAGE="1"
23+
24+
install:
25+
- travis_retry composer install --no-interaction --prefer-dist
26+
27+
script:
28+
- if [ "$COVERAGE" != "1" ]; then composer check; fi
29+
- if [ "$COVERAGE" == "1" ]; then ./vendor/bin/phpunit --coverage-clover=./coverage.xml; fi
30+
31+
after_success:
32+
- >
33+
if [ $COVERAGE == "1" ]; then
34+
wget https://github.com/satooshi/php-coveralls/releases/download/v1.0.1/coveralls.phar
35+
&& php ./coveralls.phar --verbose
36+
|| true; fi

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
The MIT License (MIT)
2+
3+
Copyright (c) 2017 Lukáš Unger
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Symfony extension for PHPStan
2+
3+
## What does it do?
4+
5+
* Provides correct return type for `ContainerInterface::get()` method,
6+
* screams at you when you try to get unknown or private service from the container.
7+
8+
## Installation
9+
10+
```sh
11+
composer require --dev lookyman/phpstan-symfony
12+
```
13+
14+
## Configuration
15+
16+
Put this into your `phpstan.neon` config:
17+
18+
```neon
19+
includes:
20+
- vendor/lookyman/phpstan-symfony/extension.neon
21+
services:
22+
- Lookyman\PHPStan\Symfony\ServiceMap(/path/to/appDevDebugProjectContainer.xml)
23+
```

composer.json

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
{
2+
"name": "lookyman/phpstan-symfony",
3+
"license": "MIT",
4+
"description": "Symfony extension for PHPStan",
5+
"keywords": ["PHPStan", "Symfony"],
6+
"authors": [
7+
{
8+
"name": "Lukáš Unger",
9+
"email": "[email protected]",
10+
"homepage": "https://lookyman.net"
11+
}
12+
],
13+
"require": {
14+
"php": "^7.0",
15+
"symfony/dependency-injection": "^3.2",
16+
"roave/security-advisories": "dev-master"
17+
},
18+
"require-dev": {
19+
"jakub-onderka/php-parallel-lint": "^0.9.2",
20+
"phpstan/phpstan": "^0.6.4",
21+
"lookyman/coding-standard": "^0.0.4",
22+
"phpunit/phpunit": "^6.0"
23+
},
24+
"autoload": {
25+
"psr-4": {
26+
"Lookyman\\PHPStan\\Symfony\\": "src/"
27+
}
28+
},
29+
"autoload-dev": {
30+
"psr-4": {
31+
"Lookyman\\PHPStan\\Symfony\\": "tests/"
32+
}
33+
},
34+
"scripts": {
35+
"lint": "parallel-lint ./src ./tests",
36+
"cs": "phpcs --colors --extensions=php --encoding=utf-8 --standard=./ruleset.xml -sp ./src ./tests",
37+
"tests": "phpunit --coverage-text",
38+
"stan": "phpstan analyse -l 5 -c ./phpstan.neon ./src ./tests",
39+
"check": [
40+
"@lint",
41+
"@cs",
42+
"@tests",
43+
"@stan"
44+
]
45+
}
46+
}

extension.neon

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
services:
2+
-
3+
class: Lookyman\PHPStan\Symfony\Type\ContainerInterfaceDynamicReturnTypeExtension
4+
tags: [phpstan.broker.dynamicMethodReturnTypeExtension]
5+
-
6+
class: Lookyman\PHPStan\Symfony\Rules\ContainerInterfacePrivateServiceRule
7+
tags: [phpstan.rules.rule]
8+
-
9+
class: Lookyman\PHPStan\Symfony\Rules\ContainerInterfaceUnknownServiceRule
10+
tags: [phpstan.rules.rule]

phpstan.neon

Whitespace-only changes.

phpunit.xml.dist

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<phpunit forceCoversAnnotation="true" mapTestClassNameToCoveredClassName="true" verbose="true" colors="true">
3+
<testsuites>
4+
<testsuite name="Symfony extension for PHPStan">
5+
<directory>./tests</directory>
6+
</testsuite>
7+
</testsuites>
8+
<filter>
9+
<whitelist processUncoveredFilesFromWhitelist="true">
10+
<directory>./src</directory>
11+
</whitelist>
12+
</filter>
13+
</phpunit>

ruleset.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<ruleset name="PHPStan Symfony">
3+
<rule ref="vendor/lookyman/coding-standard/LookymanCodingStandard/ruleset-7.0.xml"/>
4+
</ruleset>
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<?php
2+
3+
declare(strict_types = 1);
4+
5+
namespace Lookyman\PHPStan\Symfony\Rules;
6+
7+
use Lookyman\PHPStan\Symfony\ServiceMap;
8+
use PhpParser\Node;
9+
use PhpParser\Node\Arg;
10+
use PhpParser\Node\Expr\MethodCall;
11+
use PhpParser\Node\Scalar\String_;
12+
use PHPStan\Analyser\Scope;
13+
use PHPStan\Rules\Rule;
14+
use Symfony\Component\DependencyInjection\ContainerInterface;
15+
16+
final class ContainerInterfacePrivateServiceRule implements Rule
17+
{
18+
19+
/**
20+
* @var ServiceMap
21+
*/
22+
private $serviceMap;
23+
24+
public function __construct(ServiceMap $symfonyServiceMap)
25+
{
26+
$this->serviceMap = $symfonyServiceMap;
27+
}
28+
29+
public function getNodeType(): string
30+
{
31+
return MethodCall::class;
32+
}
33+
34+
/**
35+
* @param MethodCall $node
36+
* @param Scope $scope
37+
* @return array
38+
*/
39+
public function processNode(Node $node, Scope $scope): array
40+
{
41+
$services = $this->serviceMap->getServices();
42+
return $node->name === 'get'
43+
&& $scope->getType($node->var)->getClass() === ContainerInterface::class
44+
&& isset($node->args[0])
45+
&& $node->args[0] instanceof Arg
46+
&& $node->args[0]->value instanceof String_
47+
&& \array_key_exists($node->args[0]->value->value, $services)
48+
&& !$services[$node->args[0]->value->value]['public']
49+
? [\sprintf('Service "%s" is private.', $node->args[0]->value->value)]
50+
: [];
51+
}
52+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?php
2+
3+
declare(strict_types = 1);
4+
5+
namespace Lookyman\PHPStan\Symfony\Rules;
6+
7+
use Lookyman\PHPStan\Symfony\ServiceMap;
8+
use PhpParser\Node;
9+
use PhpParser\Node\Arg;
10+
use PhpParser\Node\Expr\MethodCall;
11+
use PhpParser\Node\Scalar\String_;
12+
use PHPStan\Analyser\Scope;
13+
use PHPStan\Rules\Rule;
14+
use Symfony\Component\DependencyInjection\ContainerInterface;
15+
16+
final class ContainerInterfaceUnknownServiceRule implements Rule
17+
{
18+
19+
/**
20+
* @var ServiceMap
21+
*/
22+
private $serviceMap;
23+
24+
public function __construct(ServiceMap $symfonyServiceMap)
25+
{
26+
$this->serviceMap = $symfonyServiceMap;
27+
}
28+
29+
public function getNodeType(): string
30+
{
31+
return MethodCall::class;
32+
}
33+
34+
/**
35+
* @param MethodCall $node
36+
* @param Scope $scope
37+
* @return array
38+
*/
39+
public function processNode(Node $node, Scope $scope): array
40+
{
41+
$services = $this->serviceMap->getServices();
42+
return $node->name === 'get'
43+
&& $scope->getType($node->var)->getClass() === ContainerInterface::class
44+
&& isset($node->args[0])
45+
&& $node->args[0] instanceof Arg
46+
&& $node->args[0]->value instanceof String_
47+
&& !\array_key_exists($node->args[0]->value->value, $services)
48+
? [\sprintf('Service "%s" is not registered in the container.', $node->args[0]->value->value)]
49+
: [];
50+
}
51+
}

src/ServiceMap.php

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<?php
2+
3+
declare(strict_types = 1);
4+
5+
namespace Lookyman\PHPStan\Symfony;
6+
7+
final class ServiceMap
8+
{
9+
10+
/**
11+
* @var array
12+
*/
13+
private $services;
14+
15+
public function __construct(string $containerXml)
16+
{
17+
$this->services = $aliases = [];
18+
/** @var \SimpleXMLElement $def */
19+
foreach (\simplexml_load_file($containerXml)->services->service as $def) {
20+
$attrs = $def->attributes();
21+
if (!isset($attrs->id)) {
22+
continue;
23+
}
24+
$service = [
25+
'class' => isset($attrs->class) ? (string) $attrs->class : \null,
26+
'public' => !isset($attrs->public) || (string) $attrs->public !== 'false',
27+
'synthetic' => isset($attrs->synthetic) && (string) $attrs->synthetic === 'true',
28+
];
29+
if (isset($attrs->alias)) {
30+
$aliases[(string) $attrs->id] = \array_merge($service, ['alias' => (string) $attrs->alias]);
31+
} else {
32+
$this->services[(string) $attrs->id] = $service;
33+
}
34+
}
35+
foreach ($aliases as $id => $alias) {
36+
if (\array_key_exists($alias['alias'], $this->services)) {
37+
$this->services[$id] = [
38+
'class' => $this->services[$alias['alias']]['class'],
39+
'public' => $alias['public'],
40+
'synthetic' => $alias['synthetic'],
41+
];
42+
}
43+
}
44+
}
45+
46+
public function getServices(): array
47+
{
48+
return $this->services;
49+
}
50+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<?php
2+
3+
declare(strict_types = 1);
4+
5+
namespace Lookyman\PHPStan\Symfony\Type;
6+
7+
use Lookyman\PHPStan\Symfony\ServiceMap;
8+
use PhpParser\Node\Arg;
9+
use PhpParser\Node\Expr\MethodCall;
10+
use PhpParser\Node\Scalar\String_;
11+
use PHPStan\Analyser\Scope;
12+
use PHPStan\Reflection\MethodReflection;
13+
use PHPStan\Type\DynamicMethodReturnTypeExtension;
14+
use PHPStan\Type\ObjectType;
15+
use PHPStan\Type\Type;
16+
use Symfony\Component\DependencyInjection\ContainerInterface;
17+
18+
final class ContainerInterfaceDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension
19+
{
20+
21+
/**
22+
* @var ServiceMap
23+
*/
24+
private $serviceMap;
25+
26+
public function __construct(ServiceMap $symfonyServiceMap)
27+
{
28+
$this->serviceMap = $symfonyServiceMap;
29+
}
30+
31+
public static function getClass(): string
32+
{
33+
return ContainerInterface::class;
34+
}
35+
36+
public function isMethodSupported(MethodReflection $methodReflection): bool
37+
{
38+
return $methodReflection->getName() === 'get';
39+
}
40+
41+
public function getTypeFromMethodCall(
42+
MethodReflection $methodReflection,
43+
MethodCall $methodCall,
44+
Scope $scope
45+
): Type {
46+
$services = $this->serviceMap->getServices();
47+
return isset($methodCall->args[0])
48+
&& $methodCall->args[0] instanceof Arg
49+
&& $methodCall->args[0]->value instanceof String_
50+
&& \array_key_exists($methodCall->args[0]->value->value, $services)
51+
&& !$services[$methodCall->args[0]->value->value]['synthetic']
52+
? new ObjectType($services[$methodCall->args[0]->value->value]['class'], \false)
53+
: $methodReflection->getReturnType();
54+
}
55+
}

tests/.gitkeep

Whitespace-only changes.

0 commit comments

Comments
 (0)