Skip to content
This repository has been archived by the owner on Oct 8, 2024. It is now read-only.

Commit

Permalink
Merge branch 'ntr/custom-phpstan-rules' into '6.1'
Browse files Browse the repository at this point in the history
NTR - Add custom phpstan rules for Decoratables

See merge request shopware/6/product/development!115
  • Loading branch information
pweyck committed Jan 8, 2020
2 parents 30bba89 + 239d273 commit 4a1f9bd
Show file tree
Hide file tree
Showing 9 changed files with 288 additions and 5 deletions.
5 changes: 5 additions & 0 deletions dev-ops/analyze/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,10 @@
"scripts": {
"post-install-cmd": ["@composer bin all install --ansi"],
"post-update-cmd": ["@composer bin all update --ansi"]
},
"autoload": {
"psr-4": {
"Shopware\\Development\\Analyze\\": "src/"
}
}
}
9 changes: 7 additions & 2 deletions dev-ops/analyze/phpstan-config-generator.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
use Shopware\Development\Kernel;
use Symfony\Component\Dotenv\Dotenv;

$classLoader = require __DIR__ . '/../../vendor/autoload.php';
$autoLoadFile = __DIR__ . '/../../vendor/autoload.php';
$classLoader = require $autoLoadFile;
(new Dotenv(true))->load(__DIR__ . '/../../.env');

$shopwareVersion = Versions::getVersion('shopware/platform');
Expand All @@ -30,11 +31,15 @@
$phpStanConfig = str_replace(
[
"\n # the placeholder \"%ShopwareHashedCacheDir%\" will be replaced on execution by dev-ops/analyze/phpstan-config-generator.php script",
"\n # the placeholder \"%ShopwareAutoloadFile%\" will be replaced on execution by dev-ops/analyze/phpstan-config-generator.php script",
'%ShopwareHashedCacheDir%',
'%ShopwareAutoloadFile%'
],
[
'',
$relativeCacheDir
'',
$relativeCacheDir,
$autoLoadFile
],
$phpStanConfigDist
);
Expand Down
17 changes: 17 additions & 0 deletions dev-ops/analyze/src/PHPStan/Rules/AnnotationBasedRuleHelper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php declare(strict_types=1);

namespace Shopware\Development\Analyze\PHPStan\Rules;

use PHPStan\Reflection\ClassReflection;

class AnnotationBasedRuleHelper
{
public const DECORATABLE_ANNOTATION = 'Decoratable';

public static function isClassTaggedWithAnnotation(ClassReflection $class, string $annotation): bool
{
$reflection = $class->getNativeReflection();

return $reflection->getDocComment() && strpos($reflection->getDocComment(), '@' . $annotation) !== false;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php declare(strict_types=1);

namespace Shopware\Development\Analyze\PHPStan\Rules\Decoratable;

use PhpParser\Node;
use PhpParser\Node\Stmt\ClassMethod;
use PHPStan\Analyser\Scope;
use PHPStan\Rules\Rule;
use Shopware\Development\Analyze\PHPStan\Rules\AnnotationBasedRuleHelper;

class DecoratableDoesNotAddPublicMethodRule implements Rule
{
public function getNodeType(): string
{
return ClassMethod::class;
}

/**
* @param ClassMethod $node
*/
public function processNode(Node $node, Scope $scope): array
{
if (!$scope->isInClass()) {
// skip
return [];
}

$class = $scope->getClassReflection();
if (!AnnotationBasedRuleHelper::isClassTaggedWithAnnotation($class, AnnotationBasedRuleHelper::DECORATABLE_ANNOTATION)) {
return [];
}

if (!$node->isPublic() || $node->isMagic()) {
return [];
}

$method = $class->getMethod($node->name->name, $scope);

if ($method->getPrototype()->getDeclaringClass()->isInterface()) {
return [];
}
return [
sprintf(
'The service "%s" is marked as "@Decoratable", but adds public method "%s", that is not defined by any Interface.',
$class->getName(),
$method->getName()
)
];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php declare(strict_types=1);

namespace Shopware\Development\Analyze\PHPStan\Rules\Decoratable;

use PhpParser\Node;
use PhpParser\Node\Expr\MethodCall;
use PHPStan\Analyser\Scope;
use PHPStan\Rules\Rule;
use Shopware\Development\Analyze\PHPStan\Rules\AnnotationBasedRuleHelper;

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

/**
* @param MethodCall $node
*/
public function processNode(Node $node, Scope $scope): array
{
if (!$scope->isInClass()) {
// skip
return [];
}

$class = $scope->getClassReflection();
if (!AnnotationBasedRuleHelper::isClassTaggedWithAnnotation($class, AnnotationBasedRuleHelper::DECORATABLE_ANNOTATION)) {
return [];
}

$method = $scope->getType($node->var)->getMethod($node->name->name, $scope);
if (!$method->isPublic() || $method->getDeclaringClass()->getName() !== $class->getName()) {
return [];
}

return [
sprintf(
'The service "%s" is marked as "@Decoratable", but calls it\'s own public method "%s", which breaks decoration.',
$class->getName(),
$method->getName()
)
];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<?php declare(strict_types=1);

namespace Shopware\Development\Analyze\PHPStan\Rules\Decoratable;

use PhpParser\Node;
use PhpParser\Node\Stmt\Class_;
use PHPStan\Analyser\Scope;
use PHPStan\Broker\Broker;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Rules\Rule;
use Shopware\Development\Analyze\PHPStan\Rules\AnnotationBasedRuleHelper;

class DecoratableImplementsInterfaceRule implements Rule
{
/**
* @var Broker
*/
private $broker;

public function __construct(Broker $broker)
{
$this->broker = $broker;
}

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

/**
* @param Class_ $node
*/
public function processNode(Node $node, Scope $scope): array
{
if (!$node->namespacedName) {
// skip anonymous classes
return [];
}

$class = $this->broker->getClass($scope->resolveName($node->namespacedName));

if (!AnnotationBasedRuleHelper::isClassTaggedWithAnnotation($class, AnnotationBasedRuleHelper::DECORATABLE_ANNOTATION)) {
return [];
}

if ($this->implementsInterface($class)) {
return [];
}

return [
sprintf(
'The service "%s" is marked as "@Decoratable", but does not implement an interface.',
$class->getName()
)
];
}

private function implementsInterface(ClassReflection $class): bool
{
if (!empty($class->getInterfaces())) {
return true;
}

if ($class->getParentClass()) {
return $this->implementsInterface($class->getParentClass());
}

return false;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<?php declare(strict_types=1);

namespace Shopware\Development\Analyze\PHPStan\Rules\Decoratable;

use PhpParser\Node;
use PhpParser\Node\Expr;
use PhpParser\Node\Stmt\Class_;
use PHPStan\Analyser\Scope;
use PHPStan\Broker\Broker;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Reflection\ParameterReflection;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleError;
use PHPStan\Rules\RuleErrorBuilder;
use PHPStan\Type\Type;
use Shopware\Development\Analyze\PHPStan\Rules\AnnotationBasedRuleHelper;

class DecoratableNotDirectlyDependetRule implements Rule
{
/**
* @var Broker
*/
private $broker;

public function __construct(Broker $broker)
{
$this->broker = $broker;
}

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

/**
* @param Class_ $node
*/
public function processNode(Node $node, Scope $scope): array
{
if (!$node->namespacedName) {
// skip anonymous classes
return [];
}

$class = $this->broker->getClass($scope->resolveName($node->namespacedName));
$errors = [];

foreach ($node->getProperties() as $property) {
foreach ($property->props as $prop) {
$propReflections = $class->getProperty($prop->name->name, $scope);
$this->containsDecoratableTypeDependence($propReflections->getReadableType(), $errors, $class->getName(), $property->getStartLine());
$this->containsDecoratableTypeDependence($propReflections->getWritableType(), $errors, $class->getName(), $property->getStartLine());
}
}

foreach ($node->getMethods() as $method) {
$methodReflection = $class->getMethod($method->name->name, $scope);
foreach ($methodReflection->getVariants() as $variant) {
$this->containsDecoratableTypeDependence($variant->getReturnType(), $errors, $class->getName(), $method->getStartLine());

/** @var ParameterReflection $param */
foreach ($variant->getParameters() as $param) {
$this->containsDecoratableTypeDependence($param->getType(), $errors, $class->getName(), $method->getStartLine());
}
}
}

return $errors;
}

/**
* @param string[]|RuleError[] $errors
*/
private function containsDecoratableTypeDependence(Type $type, array &$errors, string $originalClassname, int $startLine): void
{
foreach ($type->getReferencedClasses() as $className) {
$class = $this->broker->getClass($className);
if (!$class->isInterface() && AnnotationBasedRuleHelper::isClassTaggedWithAnnotation($class, AnnotationBasedRuleHelper::DECORATABLE_ANNOTATION)) {
$errors[] = RuleErrorBuilder::message(
sprintf(
'The service "%s" has a direct dependency on decoratable service "%s", but must only depend on it\'s interface.',
$originalClassname,
$class->getName()
)
)->line($startLine)
->build();
}
}
}
}
2 changes: 1 addition & 1 deletion dev-ops/common/actions/static-analyze.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@
#DESCRIPTION: Run static code analysis on core

php dev-ops/analyze/phpstan-config-generator.php
php dev-ops/analyze/vendor/bin/phpstan analyze --configuration vendor/shopware/platform/phpstan.neon
php dev-ops/analyze/vendor/bin/phpstan analyze --autoload-file=dev-ops/analyze/vendor/autoload.php --configuration vendor/shopware/platform/phpstan.neon
php dev-ops/analyze/vendor/bin/psalm --config=vendor/shopware/platform/psalm.xml --threads=4 --show-info=false
4 changes: 2 additions & 2 deletions dev-ops/pre-commit
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

PLATFORM_ROOT="$(git rev-parse --show-toplevel)"
PROJECT_ROOT="${PROJECT_ROOT:-"$(cd "$PLATFORM_ROOT"/.. && git rev-parse --show-toplevel)"}"
AUTOLOAD_FILE="$PROJECT_ROOT/vendor/autoload.php"
AUTOLOAD_FILE="$PROJECT_ROOT/dev-ops/analyze/vendor/autoload.php"

function onExit {
if [[ $? != 0 ]]
Expand Down Expand Up @@ -64,7 +64,7 @@ then
php ${PROJECT_ROOT}/dev-ops/analyze/vendor/bin/phpstan analyze --no-progress --configuration ${PROJECT_ROOT}/platform/phpstan.neon --autoload-file="$AUTOLOAD_FILE" ${PHP_FILES}
else
docker-compose exec -u $(id -u) -T -w /app/platform app_server php /app/dev-ops/analyze/phpstan-config-generator.php
docker-compose exec -u $(id -u) -T -w /app/platform app_server php /app/dev-ops/analyze/vendor/bin/phpstan analyze --no-progress --configuration /app/platform/phpstan.neon --autoload-file="/app/vendor/autoload.php" ${PHP_FILES}
docker-compose exec -u $(id -u) -T -w /app/platform app_server php /app/dev-ops/analyze/vendor/bin/phpstan analyze --no-progress --configuration /app/platform/phpstan.neon --autoload-file="/app/dev-ops/analyze/vendor/autoload.php" ${PHP_FILES}
fi

php ${PROJECT_ROOT}/dev-ops/analyze/vendor/bin/psalm --config=${PROJECT_ROOT}/vendor/shopware/platform/psalm.xml --threads=2 --show-info=false --root=${PROJECT_ROOT} ${PHP_FILES}
Expand Down

0 comments on commit 4a1f9bd

Please sign in to comment.