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

Unused schema properties #42

Open
wants to merge 10 commits into
base: main
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/composer.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,12 @@ jobs:

- name: Composer outdated
if: matrix.php == '8.1'
run: composer outdated -D --strict --ignore phpunit/phpunit --ignore nikic/php-parser
run: composer outdated -D --strict --ignore phpunit/phpunit --ignore nikic/php-parser --ignore phpstan/phpstan

- name: Composer outdated
if: matrix.php == '8.2' || matrix.php == '8.3'
run: composer outdated -D --strict --ignore phpunit/phpunit --ignore tomaj/nette-api --ignore nikic/php-parser
run: composer outdated -D --strict --ignore phpunit/phpunit --ignore tomaj/nette-api --ignore nikic/php-parser --ignore phpstan/phpstan

- name: Composer outdated
if: matrix.php != '8.1' && matrix.php != '8.2' && matrix.php != '8.3'
run: composer outdated -D --strict --ignore nikic/php-parser
run: composer outdated -D --strict --ignore nikic/php-parser --ignore phpstan/phpstan
4 changes: 3 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
"license": ["MIT"],
"require": {
"php": ">=7.4 <8.4",
"phpstan/phpstan": "^1.8"
"ext-json": "*",
"phpstan/phpstan": "^1.8",
"ext-random": "*"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pouzivas tu ext-random na nieco? plus pls prehod ext pod "php" a za to balicek phpstan

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tu random mi tam kázalo dat toto tu. Upravím to bez toho.

},
"require-dev": {
"nikic/php-parser": "^4.14",
Expand Down
17 changes: 17 additions & 0 deletions rules.neon
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,21 @@ services:
factory: Efabrica\PHPStanRules\Rule\Tomaj\NetteApi\InputParamNameRule
tags:
- phpstan.rules.rule
-
class: Efabrica\PHPStanRules\Rule\Schema\UnusedProperties
-
class: Efabrica\PHPStanRules\Rule\Schema\NeverUsedProperties
-
class: Efabrica\PHPStanRules\Collector\Schema\SchemaUsage
-
class: Efabrica\PHPStanRules\Collector\Schema\SchemaDefinitions

conditionalTags:
Efabrica\PHPStanRules\Rule\Schema\UnusedProperties:
phpstan.rules.rule: false
Efabrica\PHPStanRules\Rule\Schema\NeverUsedProperties:
phpstan.rules.rule: false
Efabrica\PHPStanRules\Collector\Schema\SchemaUsage:
phpstan.collector: false
Efabrica\PHPStanRules\Collector\Schema\SchemaDefinitions:
phpstan.collector: false
55 changes: 55 additions & 0 deletions src/Collector/Schema/SchemaDefinitions.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php

declare(strict_types=1);

namespace Efabrica\PHPStanRules\Collector\Schema;

use PhpParser\Node;
use PhpParser\Node\Stmt\Class_;
use PHPStan\Analyser\Scope;
use PHPStan\Collectors\Collector;
use ReflectionClass;
use ReflectionMethod;

/**
* @implements Collector<Class_, array{string, bool, string, int}>
*/
final class SchemaDefinitions implements Collector
{
public function getNodeType(): string
{
return Class_::class;
}

public function processNode(Node $node, Scope $scope): ?array
{
if (!$node instanceof Class_) {
return null;
}

/** @var class-string */
$className = !is_null($node->namespacedName) ? $node->namespacedName->toString() : '';
if (strpos($className, '\\Schema\\') !== false) {
return null;
}
$reflectionClass = new ReflectionClass($className);
$reflectionConstructor = $reflectionClass->hasMethod('__construct') ? $reflectionClass->getMethod('__construct') : null;
$reflectionConstructorParameters = ($reflectionConstructor instanceof ReflectionMethod) ? $reflectionConstructor->getParameters() : null;

$params = [];
if (is_array($reflectionConstructorParameters) && count($reflectionConstructorParameters) > 0) {
foreach ($reflectionConstructorParameters as $arg) {
$tmp = [];
$tmp['name'] = $arg->getName();
$tmp['type'] = (string) $arg->getType();
$tmp['key'] = $arg->getPosition();

$params[] = $tmp;
}
}
if (empty($params)) {
return null;
}
return [$className, $reflectionClass->isAbstract(), (string)json_encode($params), $node->getLine()];
}
}
56 changes: 56 additions & 0 deletions src/Collector/Schema/SchemaUsage.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?php

declare(strict_types=1);

namespace Efabrica\PHPStanRules\Collector\Schema;

use PhpParser\Node;
use PhpParser\Node\Expr\Array_;
use PhpParser\Node\Expr\New_;
use PhpParser\Node\Name;
use PHPStan\Analyser\Scope;
use PHPStan\Collectors\Collector;

/**
* @implements Collector<New_, array{string, string, int}>
*/
final class SchemaUsage implements Collector
{
public function getNodeType(): string
{
return New_::class;
}

public function processNode(Node $node, Scope $scope)
{
if (!$node->class instanceof Name) {
return null;
}
$className = $node->class->toCodeString();
if (strpos($className, '\\Schema\\') === false) {
return null;
}

$params = [];
foreach ($node->getArgs() as $key => $arg) {
$tmp = [];
if (!empty($arg->name)) {
$tmp['name'] = $arg->name->name;
}
$tmp['key'] = $key;
$tmp['type'] = get_class($arg->value);
if ($arg->value instanceof Array_) {
$tmp['aditional'] = count($arg->value->items);
}
if (strpos($tmp['type'], 'Scalar') !== false && property_exists($arg->value, 'value') && (is_numeric($arg->value->value) || is_string($arg->value->value))) {
$tmp['aditional'] = $arg->value->value;
}

$params[] = $tmp;
}
if (empty($params)) {
return null;
}
return [$node->class->toCodeString(), (string) json_encode($params), $node->getLine()];
}
}
2 changes: 1 addition & 1 deletion src/PhpStormMetaParser/NodeVisitor/OverrideVisitor.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ public function enterNode(Node $node)
$classNameParts = $call->class->parts;
$alias = $classNameParts[0] ?? null;

if ($alias && isset($this->uses[$alias])) {
if (!is_null($alias) && isset($this->uses[$alias])) {
unset($classNameParts[0]);
$classNameParts = array_merge($this->uses[$alias], $classNameParts);
}
Expand Down
2 changes: 1 addition & 1 deletion src/Rule/General/TraitContextRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ public function processNode(Node $classLike, Scope $scope): array
$usedTrait = $usedTraitName->toString();
$reflectionClass = new ReflectionClass($usedTrait);
$comment = $reflectionClass->getDocComment();
if (!$comment) {
if ($comment === false) {
continue;
}

Expand Down
154 changes: 154 additions & 0 deletions src/Rule/Schema/NeverUsedProperties.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
<?php

declare(strict_types=1);

namespace Efabrica\PHPStanRules\Rule\Schema;

use Efabrica\PHPStanRules\Collector\Schema\SchemaDefinitions;
use Efabrica\PHPStanRules\Collector\Schema\SchemaUsage;
use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\Node\CollectedDataNode;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;

/**
* @implements Rule<CollectedDataNode>
*/
final class NeverUsedProperties implements Rule
{
/**
* @var array<string, array{
* 0: string,
* 1: bool,
* 2: string,
* 3: int,
* attributes: array<int, string>,
* file: string
* }>
*/
private array $schemaDefinitions = [];

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

public function processNode(Node $node, Scope $scope): array
{
if (!$node instanceof CollectedDataNode) {
return [];
}
$schemaUsage = $node->get(SchemaUsage::class);
$this->schemaDefinitions = $this->convertSchemaDefinitions($node->get(SchemaDefinitions::class));

$warnings = [];
foreach ($this->schemaDefinitions as $schemaName => $schemaData) {
$schemaUse = $this->getSchemasUse($schemaUsage, $schemaName);
$unusedProperties = $this->getUnusedProperties($schemaUse, $schemaName);
if (count($unusedProperties) > 0) {
$warnings[] = RuleErrorBuilder::message(sprintf(
'Class "%s" contains never used properties "%s".',
$schemaName,
implode(',', $unusedProperties),
))
->file($this->schemaDefinitions[trim($schemaName, '\\')]['file'])->line($this->schemaDefinitions[trim($schemaName, '\\')][3])->build();
}
}

return $warnings;
}

/**
* @param array<string, array<int, array{0: string, 1: bool, 2: string, 3: int}>> $schemaDefinitions
*
* @return array<string, array{
* 0: string,
* 1: bool,
* 2: string,
* 3: int,
* attributes: array<int, string>,
* file: string
* }>
*/
private function convertSchemaDefinitions(array $schemaDefinitions): array
{
$result = [];
foreach ($schemaDefinitions as $key => $value) {
$tmp = $value[0];
$attributes = json_decode($tmp[2], true);
$tmp['attributes'] = [];
if (is_array($attributes)) {
foreach ($attributes as $attribute) {
$tmp['attributes'][(int) $attribute['key']] = (string) $attribute['name'];
}
}
$tmp['file'] = $key;
$result[$tmp[0]] = $tmp;
}
return $result;
}

/**
* @param array<string, array<int, array{
* 0: string,
* 1: string,
* 2: int
* }>> $schemaUsage
*
* @return array<int, array{
* 0: string,
* 1: string,
* 2: int
* }>
*/
private function getSchemasUse(array $schemaUsage, string $schemaName): array
{
$tmp = [];
foreach ($schemaUsage as $schemaArray) {
foreach ($schemaArray as $schema) {
if (trim($schema[0], '\\') === $schemaName) {
$tmp[] = $schema;
}
}
}
return $tmp;
}

/**
* @param array<int, array{
* 0: string,
* 1: string,
* 2: int
* }> $schemas
*
* @return array<int, string>
*/
private function getUnusedProperties(array $schemas, string $schemaName): array
{
if (empty($schemas)) {
return [];
}
$attributesArray = [];
$result = [];
foreach ($schemas as $schema) {
$attributesArray[] = json_decode($schema[1], true);
}

foreach ($attributesArray as $attributes) {
if (is_array($attributes)) {
foreach ($attributes as $attribute) {
$result[$attribute['key']] = true;
}
}
}

$return = [];
foreach ($this->schemaDefinitions[trim($schemaName, '\\')]['attributes'] as $k => $r) {
if (!isset($result[$k])) {
$return[] = $r;
}
}
return $return;
}
}
Loading