Skip to content

Commit

Permalink
@psalm-api
Browse files Browse the repository at this point in the history
  • Loading branch information
jack-worman committed Dec 23, 2022
1 parent 8b05f2e commit 703a1e1
Show file tree
Hide file tree
Showing 17 changed files with 246 additions and 42 deletions.
2 changes: 1 addition & 1 deletion composer.json
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
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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
Expand Up @@ -48,4 +48,8 @@
<directory>tests</directory>
</testsuite>
</testsuites>

<php>
<const name="__IS_TEST_ENV__" value="1" />
</php>
</phpunit>
9 changes: 8 additions & 1 deletion psalm-baseline.xml
@@ -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
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
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
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
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
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
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
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

0 comments on commit 703a1e1

Please sign in to comment.