Skip to content

Commit

Permalink
@psalm-public-api annotation
Browse files Browse the repository at this point in the history
  • Loading branch information
jack-worman committed Dec 22, 2022
1 parent 1cde7e4 commit 703723a
Show file tree
Hide file tree
Showing 8 changed files with 129 additions and 6 deletions.
17 changes: 16 additions & 1 deletion docs/annotating_code/supported_annotations.md
Original file line number Diff line number Diff line change
Expand Up @@ -630,7 +630,7 @@ class Success implements Promise {
* @return Promise<string>
*/
function fetch(): Promise {
return new Success('{"data":[]}');
return new Success('{"data":[]}');
}

function (): Generator {
Expand All @@ -642,6 +642,21 @@ function (): Generator {
```
This annotation supports only generic types, meaning that e.g. `@psalm-yield string` would be ignored.

### `@psalm-public-api`

Used to tell Psalm that a class is used, even if no references to it can be
found. Unused issues will be suppressed.

For example, in frameworks, controllers are often invoked "magically" without
any explicit references to them in your code. You should mark these classes with
`@psalm-public-api`.
```php
/**
* @psalm-public-api
*/
class UnreferencedClass {}
```

## Type Syntax

Psalm supports PHPDoc’s [type syntax](https://docs.phpdoc.org/latest/guide/guides/types.html), and also the [proposed PHPDoc PSR type syntax](https://github.com/php-fig/fig-standards/blob/master/proposed/phpdoc.md#appendix-a-types).
Expand Down
1 change: 1 addition & 0 deletions src/Psalm/DocComment.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ final class DocComment
'taint-unescape', 'self-out', 'consistent-constructor', 'stub-override',
'require-extends', 'require-implements', 'param-out', 'ignore-var',
'consistent-templates', 'if-this-is', 'this-out', 'check-type', 'check-type-exact',
'public-api',
];

/**
Expand Down
33 changes: 28 additions & 5 deletions src/Psalm/Internal/Codebase/ClassLikes.php
Original file line number Diff line number Diff line change
Expand Up @@ -851,7 +851,12 @@ public function consolidateAnalyzedData(Methods $methods, ?Progress $progress, b
&& !$classlike_storage->is_trait
) {
if ($find_unused_code) {
if (!$this->file_reference_provider->isClassReferenced($fq_class_name_lc)) {
if ($classlike_storage->public_api
|| $this->file_reference_provider->isClassReferenced($fq_class_name_lc)
) {
$this->checkMethodReferences($classlike_storage, $methods);
$this->checkPropertyReferences($classlike_storage);
} else {
IssueBuffer::maybeAdd(
new UnusedClass(
'Class ' . $classlike_storage->name . ' is never used',
Expand All @@ -860,9 +865,6 @@ public function consolidateAnalyzedData(Methods $methods, ?Progress $progress, b
),
$classlike_storage->suppressed_issues,
);
} else {
$this->checkMethodReferences($classlike_storage, $methods);
$this->checkPropertyReferences($classlike_storage);
}
}

Expand Down Expand Up @@ -1690,6 +1692,17 @@ private function checkMethodReferences(ClassLikeStorage $classlike_storage, Meth
$method_id = $declaring_method_id;
}

// TODO: UnusedParam should always be checked.
if ($classlike_storage->public_api
&& ($method_storage->visibility === ClassLikeAnalyzer::VISIBILITY_PUBLIC
|| ($method_storage->visibility === ClassLikeAnalyzer::VISIBILITY_PROTECTED
&& !$classlike_storage->final
)
)
) {
continue;
}

if ($method_storage->location
&& !$project_analyzer->canReportIssues($method_storage->location->file_path)
&& !$codebase->analyzer->canReportIssues($method_storage->location->file_path)
Expand All @@ -1715,7 +1728,7 @@ private function checkMethodReferences(ClassLikeStorage $classlike_storage, Meth
&& $method_name !== '__unserialize'
&& $method_name !== '__set_state'
&& $method_name !== '__debuginfo'
&& $method_name !== '__tostring' // can be called in array_unique)
&& $method_name !== '__tostring' // can be called in array_unique
) {
$method_location = $method_storage->location;

Expand Down Expand Up @@ -2064,6 +2077,16 @@ private function checkPropertyReferences(ClassLikeStorage $classlike_storage): v
$codebase = $project_analyzer->getCodebase();

foreach ($classlike_storage->properties as $property_name => $property_storage) {
if ($classlike_storage->public_api
&& ($property_storage->visibility === ClassLikeAnalyzer::VISIBILITY_PUBLIC
|| ($property_storage->visibility === ClassLikeAnalyzer::VISIBILITY_PROTECTED
&& !$classlike_storage->final
)
)
) {
continue;
}

$referenced_property_name = strtolower($classlike_storage->name) . '::$' . $property_name;
$property_referenced = $this->file_reference_provider->isClassPropertyReferenced(
$referenced_property_name,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -474,6 +474,8 @@ public static function parse(
$info->description = $parsed_docblock->description;
}

$info->public_api = isset($parsed_docblock->tags['psalm-public-api']);

self::addMagicPropertyToInfo($comment, $info, $parsed_docblock->tags, 'property');
self::addMagicPropertyToInfo($comment, $info, $parsed_docblock->tags, 'psalm-property');
self::addMagicPropertyToInfo($comment, $info, $parsed_docblock->tags, 'property-read');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -681,6 +681,8 @@ public function start(PhpParser\Node\Stmt\ClassLike $node): ?bool
if ($docblock_info->description) {
$storage->description = $docblock_info->description;
}

$storage->public_api = $docblock_info->public_api;
}

foreach ($node->stmts as $node_stmt) {
Expand Down
2 changes: 2 additions & 0 deletions src/Psalm/Internal/Scanner/ClassLikeDocblockComment.php
Original file line number Diff line number Diff line change
Expand Up @@ -101,4 +101,6 @@ class ClassLikeDocblockComment
public array $implementation_requirements = [];

public ?string $description = null;

public bool $public_api = false;
}
2 changes: 2 additions & 0 deletions src/Psalm/Storage/ClassLikeStorage.php
Original file line number Diff line number Diff line change
Expand Up @@ -462,6 +462,8 @@ final class ClassLikeStorage implements HasAttributesInterface
*/
public $description;

public bool $public_api = false;

public function __construct(string $name)
{
$this->name = $name;
Expand Down
76 changes: 76 additions & 0 deletions tests/UnusedCodeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1229,6 +1229,42 @@ class A {
$a->bar[$a->foo] = "bar";
print_r($a->bar);',
],
'psalm-public-api with unused class' => [
'code' => <<<'PHP'
<?php
/** @psalm-public-api */
class A {}
PHP,
],
'psalm-public-api with unused public and protected property' => [
'code' => <<<'PHP'
<?php
/** @psalm-public-api */
class A {
public int $b = 0;
protected int $c = 0;
}
PHP,
],
'psalm-public-api with unused public and protected method' => [
'code' => <<<'PHP'
<?php
/** @psalm-public-api */
class A {
public function b(): void {}
protected function c(): void {}
}
PHP,
],
'psalm-public-api with unused method arg' => [
'code' => <<<'PHP'
<?php
/** @psalm-public-api */
class A {
public function b(int $c): void {}
}
PHP,
],
];
}

Expand Down Expand Up @@ -1749,6 +1785,46 @@ public static function f()
',
'error_message' => 'InaccessibleProperty',
],
'psalm-public-api with unused private property' => [
'code' => <<<'PHP'
<?php
/** @psalm-public-api */
class A {
private int $b = 0;
}
PHP,
'error_message' => 'UnusedProperty',
],
'psalm-public-api with final class and unused protected property' => [
'code' => <<<'PHP'
<?php
/** @psalm-public-api */
final class A {
protected int $b = 0;
}
PHP,
'error_message' => 'PossiblyUnusedProperty',
],
'psalm-public-api with unused private method' => [
'code' => <<<'PHP'
<?php
/** @psalm-public-api */
class A {
private function b(): void {}
}
PHP,
'error_message' => 'UnusedMethod',
],
'psalm-public-api with final class and unused protected method' => [
'code' => <<<'PHP'
<?php
/** @psalm-public-api */
final class A {
protected function b(): void {}
}
PHP,
'error_message' => 'PossiblyUnusedMethod',
],
];
}
}

0 comments on commit 703723a

Please sign in to comment.