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

Add @psalm-api annotation #8987

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@
"phpunit"
],
"verify-callmap": "phpunit tests/Internal/Codebase/InternalCallMapHandlerTest.php",
"psalm": "@php ./psalm --find-dead-code",
"psalm": "@php ./psalm",
"tests": [
"@lint",
"@cs",
Expand Down
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-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-api`.
```php
/**
* @psalm-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
6 changes: 5 additions & 1 deletion docs/running_psalm/issues/PossiblyUnusedMethod.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
# PossiblyUnusedMethod

Emitted when `--find-dead-code` is turned on and Psalm cannot find any calls to a public or protected method.
Emitted when `--find-dead-code` is turned on and Psalm cannot find any calls to
a public or protected method.

If this method is used and part of the public API, annotate the containing class
with `@psalm-api`.

```php
<?php
Expand Down
6 changes: 5 additions & 1 deletion docs/running_psalm/issues/PossiblyUnusedProperty.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
# PossiblyUnusedProperty

Emitted when `--find-dead-code` is turned on and Psalm cannot find any uses of a particular public/protected property
Emitted when `--find-dead-code` is turned on and Psalm cannot find any uses of a
particular public/protected property.

If this property is used and part of the public API, annotate the containing
class with `@psalm-api`.

```php
<?php
Expand Down
5 changes: 4 additions & 1 deletion docs/running_psalm/issues/UnusedClass.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# UnusedClass

Emitted when `--find-dead-code` is turned on and Psalm cannot find any uses of a given class
Emitted when `--find-dead-code` is turned on and Psalm cannot find any uses of a
given class.

If this class is used and part of the public API, annotate it with `@psalm-api`.

```php
<?php
Expand Down
6 changes: 5 additions & 1 deletion docs/running_psalm/issues/UnusedMethod.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
# UnusedMethod

Emitted when `--find-dead-code` is turned on and Psalm cannot find any uses of a given private method or function
Emitted when `--find-dead-code` is turned on and Psalm cannot find any uses of a
given private method or function.

If this method is used and part of the public API, annotate the containing class
with `@psalm-api`.

```php
<?php
Expand Down
6 changes: 5 additions & 1 deletion docs/running_psalm/issues/UnusedProperty.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
# UnusedProperty

Emitted when `--find-dead-code` is turned on and Psalm cannot find any uses of a private property
Emitted when `--find-dead-code` is turned on and Psalm cannot find any uses of a
private property.

If this property is used and part of the public API, annotate the containing
class with `@psalm-api`.

```php
<?php
Expand Down
4 changes: 4 additions & 0 deletions phpunit.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,8 @@
<directory>tests</directory>
</testsuite>
</testsuites>

Copy link
Collaborator

Choose a reason for hiding this comment

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

I never used this config, does it makes sense to keep it?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This config is used by default. It should be used if no other config file is given.

<php>
<const name="__IS_TEST_ENV__" value="1" />
</php>
</phpunit>
9 changes: 8 additions & 1 deletion psalm-baseline.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<files psalm-version="dev-master@91081f77fdd47a35d12bd87b31291c95f98be8ae">
<files psalm-version="dev-master@6fc9e50b9765d573db796e81522af759bc6987a5">
<file src="examples/TemplateChecker.php">
<PossiblyUndefinedIntArrayOffset>
<code>$comment_block-&gt;tags['variablesfrom'][0]</code>
Expand Down Expand Up @@ -308,6 +308,13 @@
<code>$type &gt; 4</code>
</DocblockTypeContradiction>
</file>
<file src="src/Psalm/Internal/LanguageServer/LanguageServer.php">
<PossiblyUnusedParam>
<code>$capabilities</code>
<code>$processId</code>
<code>$rootPath</code>
</PossiblyUnusedParam>
</file>
<file src="src/Psalm/Internal/LanguageServer/Message.php">
<PossiblyUndefinedIntArrayOffset>
<code>$pair[1]</code>
Expand Down
8 changes: 8 additions & 0 deletions src/Psalm/Config.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
use function class_exists;
use function clearstatcache;
use function count;
use function defined;
use function dirname;
use function explode;
use function extension_loaded;
Expand All @@ -69,6 +70,7 @@
use function filetype;
use function flock;
use function fopen;
use function fwrite;
use function get_class;
use function get_defined_constants;
use function get_defined_functions;
Expand Down Expand Up @@ -122,6 +124,7 @@
use const PHP_VERSION_ID;
use const PSALM_VERSION;
use const SCANDIR_SORT_NONE;
use const STDERR;

/**
* @psalm-suppress PropertyNotSetInConstructor
Expand Down Expand Up @@ -441,6 +444,8 @@ class Config
public $forbidden_functions = [];

/**
* TODO: Psalm 6: Update default to be true and remove warning.
*
* @var bool
*/
public $find_unused_code = false;
Expand Down Expand Up @@ -1090,6 +1095,9 @@ private static function fromXmlAndPaths(
$attribute_text = (string) $config_xml['findUnusedCode'];
$config->find_unused_code = $attribute_text === 'true' || $attribute_text === '1';
$config->find_unused_variables = $config->find_unused_code;
} elseif (!defined('__IS_TEST_ENV__')) {
fwrite(STDERR, 'Warning: "findUnusedCode" will be defaulted to "true" in Psalm 6. You should explicitly'
. ' enable or disable this setting.' . PHP_EOL);
}

if (isset($config_xml['findUnusedVariablesAndParams'])) {
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',
'api',
];

/**
Expand Down
123 changes: 89 additions & 34 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,10 +865,8 @@ public function consolidateAnalyzedData(Methods $methods, ?Progress $progress, b
),
$classlike_storage->suppressed_issues,
);
} else {
$this->checkMethodReferences($classlike_storage, $methods);
$this->checkPropertyReferences($classlike_storage);
}
$this->checkMethodParamReferences($classlike_storage);
}

$this->findPossibleMethodParamTypes($classlike_storage);
Expand Down Expand Up @@ -1690,6 +1693,16 @@ private function checkMethodReferences(ClassLikeStorage $classlike_storage, Meth
$method_id = $declaring_method_id;
}

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 @@ -1900,36 +1913,68 @@ private function checkMethodReferences(ClassLikeStorage $classlike_storage, Meth
}
}
}
}
}
}

if ($method_storage->visibility !== ClassLikeAnalyzer::VISIBILITY_PRIVATE
&& !$classlike_storage->is_interface
) {
foreach ($method_storage->params as $offset => $param_storage) {
if (empty($classlike_storage->overridden_method_ids[$method_name])
&& $param_storage->location
&& !$param_storage->promoted_property
&& !$this->file_reference_provider->isMethodParamUsed(
strtolower((string) $method_id),
$offset,
)
) {
if ($method_storage->final) {
IssueBuffer::maybeAdd(
new UnusedParam(
'Param #' . ($offset + 1) . ' is never referenced in this method',
$param_storage->location,
),
$method_storage->suppressed_issues,
);
} else {
IssueBuffer::maybeAdd(
new PossiblyUnusedParam(
'Param #' . ($offset + 1) . ' is never referenced in this method',
$param_storage->location,
),
$method_storage->suppressed_issues,
);
}

private function checkMethodParamReferences(ClassLikeStorage $classlike_storage): void
{
foreach ($classlike_storage->appearing_method_ids as $method_name => $appearing_method_id) {
$appearing_fq_classlike_name = $appearing_method_id->fq_class_name;

if ($appearing_fq_classlike_name !== $classlike_storage->name) {
continue;
}

$method_id = $appearing_method_id;

if (isset($classlike_storage->methods[$method_name])) {
$method_storage = $classlike_storage->methods[$method_name];
} else {
$declaring_method_id = $classlike_storage->declaring_method_ids[$method_name];

$declaring_fq_classlike_name = $declaring_method_id->fq_class_name;
$declaring_method_name = $declaring_method_id->method_name;

try {
$declaring_classlike_storage = $this->classlike_storage_provider->get($declaring_fq_classlike_name);
} catch (InvalidArgumentException $e) {
continue;
}

$method_storage = $declaring_classlike_storage->methods[$declaring_method_name];
$method_id = $declaring_method_id;
}

if ($method_storage->visibility !== ClassLikeAnalyzer::VISIBILITY_PRIVATE
&& !$classlike_storage->is_interface
) {
foreach ($method_storage->params as $offset => $param_storage) {
if (empty($classlike_storage->overridden_method_ids[$method_name])
&& $param_storage->location
&& !$param_storage->promoted_property
&& !$this->file_reference_provider->isMethodParamUsed(
strtolower((string) $method_id),
$offset,
)
) {
if ($method_storage->final) {
IssueBuffer::maybeAdd(
new UnusedParam(
'Param #' . ($offset + 1) . ' is never referenced in this method',
$param_storage->location,
),
$method_storage->suppressed_issues,
);
} else {
IssueBuffer::maybeAdd(
new PossiblyUnusedParam(
'Param #' . ($offset + 1) . ' is never referenced in this method',
$param_storage->location,
),
$method_storage->suppressed_issues,
);
}
}
}
Expand Down Expand Up @@ -2064,6 +2109,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-api']) || isset($parsed_docblock->tags['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