1
0
mirror of https://github.com/danog/psalm.git synced 2024-11-30 04:39:00 +01:00

Added test to enforce that all supported annotations are documented (#4723)

* Added test to enforce that all supported annotations are documented

Well, at least mentioned.

Refs vimeo/psalm#3816

* Type things

* Make things pretty

* Only check @psalm- annotations, group

* Add documentation for `@psalm-require-extends` and `@psalm-require-implements`

* Dropped logicalOr that has become redundant

* Add explicit tag

* Document @psalm-template

* Add @psalm-template-covariant

* Document `@psalm-method`

* Add list of undocumented docblock annotations

Co-authored-by: Matthew Brown <github@muglug.com>
This commit is contained in:
Bruce Weirdan 2020-11-28 04:48:16 +02:00 committed by Daniil Gentili
parent 1c48258fe2
commit d13f0b6a7c
Signed by: danog
GPG Key ID: 8C1BE3B34B230CA7
4 changed files with 161 additions and 20 deletions

View File

@ -7,7 +7,7 @@ Psalm supports a wide range of docblock annotations.
Psalm uses the following PHPDoc tags to understand your code: Psalm uses the following PHPDoc tags to understand your code:
- [`@var`](https://docs.phpdoc.org/latest/references/phpdoc/tags/var.html) - [`@var`](https://docs.phpdoc.org/latest/references/phpdoc/tags/var.html)
Used for specifying the types of properties and variables Used for specifying the types of properties and variables@
- [`@return`](https://docs.phpdoc.org/latest/references/phpdoc/tags/return.html) - [`@return`](https://docs.phpdoc.org/latest/references/phpdoc/tags/return.html)
Used for specifying the return types of functions, methods and closures Used for specifying the return types of functions, methods and closures
- [`@param`](https://docs.phpdoc.org/latest/references/phpdoc/tags/param.html) - [`@param`](https://docs.phpdoc.org/latest/references/phpdoc/tags/param.html)
@ -68,7 +68,7 @@ function addFoo(?string &$s) : void {
} }
``` ```
### `@psalm-var`, `@psalm-param`, `@psalm-return`, `@psalm-property`, `@psalm-property-read`, `@psalm-property-write` ### `@psalm-var`, `@psalm-param`, `@psalm-return`, `@psalm-property`, `@psalm-property-read`, `@psalm-property-write`, `@psalm-method`
When specifying types in a format not supported by phpDocumentor ([but supported by Psalm](#type-syntax)) you may wish to prepend `@psalm-` to the PHPDoc tag, so as to avoid confusing your IDE. If a `@psalm`-prefixed tag is given, Psalm will use it in place of its non-prefixed counterpart. When specifying types in a format not supported by phpDocumentor ([but supported by Psalm](#type-syntax)) you may wish to prepend `@psalm-` to the PHPDoc tag, so as to avoid confusing your IDE. If a `@psalm`-prefixed tag is given, Psalm will use it in place of its non-prefixed counterpart.
@ -475,6 +475,38 @@ class User {
} }
``` ```
### `@psalm-require-extends`
The @psalm-require-extends-annotation allows you to define a requirements that a trait imposes on the using class.
```php
abstract class DatabaseModel {
// methods, properties, etc.
}
/**
* @psalm-require-extends DatabaseModel
*/
trait SoftDeletingTrait {
// useful but scoped functionality, that depends on methods/properties from DatabaseModel
}
class MyModel extends DatabaseModel {
// valid
use SoftDeletingTrait;
}
class NormalClass {
// triggers an error
use SoftDeletingTrait;
}
```
### `@psalm-require-implements`
Behaves the same way as `@psalm-require-extends`, but for interfaces.
## Type Syntax ## Type Syntax
Psalm supports PHPDocs [type syntax](https://docs.phpdoc.org/latest/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). Psalm supports PHPDocs [type syntax](https://docs.phpdoc.org/latest/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).

View File

@ -68,9 +68,9 @@ class One_off_instance_of_MyContainer {
This pattern can be used in large number of different situations like mocking, collections, iterators and loading arbitrary objects. Psalm has a large number of annotations to make it easy to use templated types in your codebase. This pattern can be used in large number of different situations like mocking, collections, iterators and loading arbitrary objects. Psalm has a large number of annotations to make it easy to use templated types in your codebase.
## `@template` ## `@template`, `@psalm-template`
The `@template` tag allows classes and functions to declare a generic type parameter. The `@template`/`@psalm-template` tag allows classes and functions to declare a generic type parameter.
As a very simple example, this function returns whatever is passed in: As a very simple example, this function returns whatever is passed in:
@ -380,7 +380,7 @@ function takesDogList(Collection $dog_collection) : void {
Here we're not doing anything bad we're just iterating over an array of objects. But Psalm still gives that same basic error "getNoises expects a `Collection<Animal>`, but `Collection<Dog>` was passed". Here we're not doing anything bad we're just iterating over an array of objects. But Psalm still gives that same basic error "getNoises expects a `Collection<Animal>`, but `Collection<Dog>` was passed".
We can tell Psalm that it's safe to pass subtypes for the templated param `T` by using the annotation `@template-covariant T`: We can tell Psalm that it's safe to pass subtypes for the templated param `T` by using the annotation `@template-covariant T` (or `@psalm-template-covariant T`):
```php ```php
<?php <?php

View File

@ -22,7 +22,7 @@ use function strspn;
class DocComment class DocComment
{ {
private const PSALM_ANNOTATIONS = [ public const PSALM_ANNOTATIONS = [
'return', 'param', 'template', 'var', 'type', 'return', 'param', 'template', 'var', 'type',
'template-covariant', 'property', 'property-read', 'property-write', 'method', 'template-covariant', 'property', 'property-read', 'property-write', 'method',
'assert', 'assert-if-true', 'assert-if-false', 'suppress', 'assert', 'assert-if-true', 'assert-if-false', 'suppress',

View File

@ -1,39 +1,84 @@
<?php <?php
namespace Psalm\Tests; namespace Psalm\Tests;
use DOMAttr;
use DOMDocument;
use DOMXPath;
use PHPUnit\Framework\Constraint\Constraint;
use Psalm\Config;
use Psalm\Context;
use Psalm\DocComment;
use Psalm\Internal\RuntimeCaches;
use Psalm\Tests\Internal\Provider;
use function array_filter;
use function array_keys; use function array_keys;
use function array_shift;
use function count; use function count;
use const DIRECTORY_SEPARATOR;
use const LIBXML_NONET;
use function dirname; use function dirname;
use function explode; use function explode;
use function file_exists; use function file_exists;
use function file_get_contents; use function file_get_contents;
use function glob;
use function implode; use function implode;
use function in_array;
use function preg_quote; use function preg_quote;
use Psalm\Config;
use Psalm\Context;
use Psalm\Tests\Internal\Provider;
use function sort; use function sort;
use function strpos; use function strpos;
use function str_replace;
use function substr; use function substr;
use function trim; use function trim;
use function glob;
use function str_replace;
use function array_shift;
use DOMDocument;
use DOMXPath;
use DOMAttr;
use Psalm\Internal\RuntimeCaches;
use function array_filter;
use function var_export; use function var_export;
use const DIRECTORY_SEPARATOR;
use const LIBXML_NONET;
class DocumentationTest extends TestCase class DocumentationTest extends TestCase
{ {
/**
* a list of all files containing annotation documentation
*/
private const ANNOTATION_DOCS = [
'docs/annotating_code/supported_annotations.md',
'docs/annotating_code/templated_annotations.md',
'docs/annotating_code/adding_assertions.md',
'docs/security_analysis/annotations.md',
];
/**
* annotations that we dont want documented
*/
private const INTENTIONALLY_UNDOCUMENTED_ANNOTATIONS = [
'@psalm-self-out', // I'm fairly sure it's intentionally undocumented, but can't find the reference
'@psalm-variadic',
];
/**
* These should be documented
*/
private const WALL_OF_SHAME = [
'@psalm-assert-untainted',
'@psalm-consistent-constructor',
'@psalm-flow',
'@psalm-generator-return',
'@psalm-ignore-variable-method',
'@psalm-ignore-variable-property',
'@psalm-override-method-visibility',
'@psalm-override-property-visibility',
'@psalm-scope-this',
'@psalm-seal-methods',
'@psalm-stub-override',
'@psalm-taint-unescape',
'@psalm-yield',
];
/** @var \Psalm\Internal\Analyzer\ProjectAnalyzer */ /** @var \Psalm\Internal\Analyzer\ProjectAnalyzer */
protected $project_analyzer; protected $project_analyzer;
/** @var string */
private static $docContents = '';
/** /**
* @return array<string, array<int, string>> * @return array<string, array<int, string>>
*/ */
@ -295,4 +340,68 @@ class DocumentationTest extends TestCase
"Duplicate shortcodes found: \n" . var_export($duplicate_shortcodes, true) "Duplicate shortcodes found: \n" . var_export($duplicate_shortcodes, true)
); );
} }
/** @dataProvider knownAnnotations */
public function testAllAnnotationsAreDocumented(string $annotation): void
{
if ('' === self::$docContents) {
foreach (self::ANNOTATION_DOCS as $file) {
self::$docContents .= file_get_contents(__DIR__ . '/../' . $file);
}
}
$this->assertThat(
self::$docContents,
$this->conciseExpected($this->stringContains('@psalm-' . $annotation)),
"'@psalm-$annotation' is not present in the docs"
);
}
/** @return iterable<string, array{string}> */
public function knownAnnotations(): iterable
{
foreach (DocComment::PSALM_ANNOTATIONS as $annotation) {
if (in_array('@psalm-' . $annotation, self::INTENTIONALLY_UNDOCUMENTED_ANNOTATIONS, true)) {
continue;
}
if (in_array('@psalm-' . $annotation, self::WALL_OF_SHAME, true)) {
continue;
}
yield $annotation => [$annotation];
}
}
/**
* Creates a constraint wrapper that displays the expected value in a concise form
*/
public function conciseExpected(Constraint $inner): Constraint
{
return new class ($inner) extends Constraint
{
/** @var Constraint */
private $inner;
public function __construct(Constraint $inner)
{
$this->inner = $inner;
}
public function toString(): string
{
return $this->inner->toString();
}
protected function matches($other): bool
{
return $this->inner->matches($other);
}
protected function failureDescription($other): string
{
return $this->exporter()->shortenedExport($other) . ' ' . $this->toString();
}
};
}
} }