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 5979579 commit 7240be3
Show file tree
Hide file tree
Showing 12 changed files with 218 additions and 44 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 --no-cache -m",
"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-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
4 changes: 4 additions & 0 deletions phpunit.xml.dist
Expand Up @@ -48,4 +48,8 @@
<directory>tests</directory>
</testsuite>
</testsuites>

<php>
<const name="__TESTING__" value="1" />
</php>
</phpunit>
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('__TESTING__')) {
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
12 changes: 4 additions & 8 deletions src/Psalm/Internal/LanguageServer/LanguageServer.php
Expand Up @@ -188,18 +188,14 @@ function (): void {
/**
* The initialize request is sent as the first request from the client to the server.
*
* @param ClientCapabilities $capabilities The capabilities provided by the client (editor)
* @param string|null $rootPath The rootPath of the workspace. Is null if no folder is open.
* @param int|null $processId The process Id of the parent process that started the server.
* Is null if the process has not been started by another process. If the parent process is
* not alive then the server should exit (see exit notification) its process.
* @psalm-return Promise<InitializeResult>
* @psalm-suppress PossiblyUnusedMethod
* TODO: Remove args in Psalm 6
*/
public function initialize(
ClientCapabilities $capabilities,
?string $rootPath = null,
?int $processId = null
ClientCapabilities $_,
?string $__ = null,
?int $___ = null
): Promise {
return call(
/** @return Generator<int, true, mixed, InitializeResult> */
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 7240be3

Please sign in to comment.