mirror of
https://github.com/danog/psalm.git
synced 2024-11-30 04:39:00 +01:00
Validate docs by running code through them
This commit is contained in:
parent
6bfb27165d
commit
c4be89bf37
@ -79,15 +79,10 @@ class A {}
|
||||
|
||||
### DuplicateParam
|
||||
|
||||
Emitted when a class param is defined twice
|
||||
Emitted when a function has a param defined twice
|
||||
|
||||
```php
|
||||
class A {
|
||||
/** @var ?string */
|
||||
public $foo;
|
||||
/** @var ?string */
|
||||
public $foo;
|
||||
}
|
||||
function foo(int $b, string $b) {}
|
||||
```
|
||||
|
||||
### EmptyArrayAccess
|
||||
@ -583,6 +578,16 @@ Emitted when a required param is before a param that is not required. Included i
|
||||
function foo(int $i = 5, string $j) : void {}
|
||||
```
|
||||
|
||||
### MissingClosureParamType
|
||||
|
||||
Emitted when a closure paramter has no type information associated with it
|
||||
|
||||
```php
|
||||
$a = function($a): string {
|
||||
return "foo";
|
||||
};
|
||||
```
|
||||
|
||||
### MissingClosureReturnType
|
||||
|
||||
Emitted when a closure lacks a return type
|
||||
@ -621,6 +626,14 @@ Emitted when using `include` or `require` on a file that does not exist
|
||||
require("nonexistent.php");
|
||||
```
|
||||
|
||||
### MissingParamType
|
||||
|
||||
Emitted when a function paramter has no type information associated with it
|
||||
|
||||
```php
|
||||
function foo($a) : void {}
|
||||
```
|
||||
|
||||
### MissingPropertyType
|
||||
|
||||
Emitted when a property is defined on a class without a type
|
||||
@ -748,7 +761,7 @@ function foo() : int {
|
||||
Emitted when assigning a value on a string using a value for which Psalm cannot infer a type
|
||||
|
||||
```php
|
||||
"hello"[$_GET['foo']] = "h";
|
||||
"hello"[0] = $_GET['foo'];
|
||||
```
|
||||
|
||||
### MixedTypeCoercion
|
||||
@ -813,10 +826,16 @@ function bar(I $i) : void {
|
||||
|
||||
### NonStaticSelfCall
|
||||
|
||||
Emitted when
|
||||
Emitted when calling a non-static function statically
|
||||
|
||||
```php
|
||||
class A {
|
||||
public function foo(): void {}
|
||||
|
||||
public function bar(): void {
|
||||
self::foo();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### NullableReturnStatement
|
||||
@ -981,9 +1000,9 @@ foo()->bar();
|
||||
Emitted when
|
||||
|
||||
```php
|
||||
/** @return string|int */
|
||||
/** @return int|stdClass */
|
||||
function foo() {
|
||||
return rand(0, 1) ? 5 : "i";
|
||||
return rand(0, 1) ? 5 : new stdClass;
|
||||
}
|
||||
function bar(int $i) : void {}
|
||||
bar(foo());
|
||||
@ -1104,9 +1123,8 @@ function foo(?array $a) : void {
|
||||
Emitted when trying to set a value on a possibly null array
|
||||
|
||||
```php
|
||||
function foo(?array $a) : void {
|
||||
$a[0] = "5";
|
||||
}
|
||||
$a = null;
|
||||
$a[0][] = 1;
|
||||
```
|
||||
|
||||
### PossiblyNullArrayOffset
|
||||
@ -1250,6 +1268,9 @@ class A {
|
||||
return $a + 4;
|
||||
}
|
||||
}
|
||||
|
||||
$a = new A();
|
||||
$a->foo(1, 2);
|
||||
```
|
||||
|
||||
### PropertyNotSetInConstructor
|
||||
@ -1271,10 +1292,10 @@ Emitted when iterating over an object’s properties. This issue exists because
|
||||
|
||||
```php
|
||||
class A {
|
||||
/** @var string */
|
||||
/** @var string|null */
|
||||
public $foo;
|
||||
|
||||
/** @var string */
|
||||
/** @var string|null */
|
||||
public $bar;
|
||||
}
|
||||
|
||||
@ -1519,14 +1540,6 @@ function requireFile(string $s) : void {
|
||||
}
|
||||
```
|
||||
|
||||
### UntypedParam
|
||||
|
||||
Emitted when a function paramter has no type information associated with it
|
||||
|
||||
```php
|
||||
function foo($a) : void {}
|
||||
```
|
||||
|
||||
### UnusedClass
|
||||
|
||||
Emitted when `--find-dead-code` is turned on and Psalm cannot find any uses of a given class
|
||||
@ -1549,7 +1562,7 @@ class A {
|
||||
private function foo() : void {}
|
||||
private function bar() : void {}
|
||||
}
|
||||
new A();
|
||||
$a = new A();
|
||||
```
|
||||
|
||||
### UnusedParam
|
||||
|
166
tests/DocumentationTest.php
Normal file
166
tests/DocumentationTest.php
Normal file
@ -0,0 +1,166 @@
|
||||
<?php
|
||||
namespace Psalm\Tests;
|
||||
|
||||
use Psalm\Checker\FileChecker;
|
||||
use Psalm\Config;
|
||||
use Psalm\Context;
|
||||
|
||||
class DocumentationTest extends TestCase
|
||||
{
|
||||
/** @var \Psalm\Checker\ProjectChecker */
|
||||
protected $project_checker;
|
||||
|
||||
/**
|
||||
* @return array<string, array<int, string>>
|
||||
*/
|
||||
private static function getCodeBlocksFromDocs()
|
||||
{
|
||||
$issue_file = dirname(dirname(__FILE__)) . DIRECTORY_SEPARATOR . 'docs' . DIRECTORY_SEPARATOR . 'issues.md';
|
||||
|
||||
if (!file_exists($issue_file)) {
|
||||
throw new \UnexpectedValueException('docs not found');
|
||||
}
|
||||
|
||||
$file_contents = file_get_contents($issue_file);
|
||||
|
||||
if (!$file_contents) {
|
||||
throw new \UnexpectedValueException('Docs are empty');
|
||||
}
|
||||
|
||||
$file_lines = explode(PHP_EOL, $file_contents);
|
||||
|
||||
$issue_code = [];
|
||||
|
||||
$current_issue = null;
|
||||
|
||||
for ($i = 0, $j = count($file_lines); $i < $j; ++$i) {
|
||||
$current_line = $file_lines[$i];
|
||||
|
||||
if (substr($current_line, 0, 4) === '### ') {
|
||||
$current_issue = trim(substr($current_line, 4));
|
||||
++$i;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (substr($current_line, 0, 6) === '```php' && $current_issue) {
|
||||
$current_block = '';
|
||||
++$i;
|
||||
|
||||
do {
|
||||
$current_block .= $file_lines[$i] . PHP_EOL;
|
||||
++$i;
|
||||
} while (substr($file_lines[$i], 0, 3) !== '```' && $i < $j);
|
||||
|
||||
$issue_code[$current_issue][] = trim($current_block);
|
||||
}
|
||||
}
|
||||
|
||||
return $issue_code;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function setUp()
|
||||
{
|
||||
FileChecker::clearCache();
|
||||
\Psalm\FileManipulation\FunctionDocblockManipulator::clearCache();
|
||||
|
||||
$this->file_provider = new Provider\FakeFileProvider();
|
||||
|
||||
$this->project_checker = new \Psalm\Checker\ProjectChecker(
|
||||
new TestConfig(),
|
||||
$this->file_provider,
|
||||
new Provider\FakeParserCacheProvider()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider providerFileCheckerInvalidCodeParse
|
||||
* @small
|
||||
*
|
||||
* @param string $code
|
||||
* @param string $error_message
|
||||
* @param array<string> $error_levels
|
||||
* @param bool $check_references
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function testInvalidCode($code, $error_message, $error_levels = [], $check_references = false)
|
||||
{
|
||||
if (strpos($this->getName(), 'SKIPPED-') !== false) {
|
||||
$this->markTestSkipped();
|
||||
}
|
||||
|
||||
$this->project_checker->getCodebase()->collect_references = $check_references;
|
||||
|
||||
foreach ($error_levels as $error_level) {
|
||||
$this->project_checker->config->setCustomErrorLevel($error_level, Config::REPORT_SUPPRESS);
|
||||
}
|
||||
|
||||
$this->expectException('\Psalm\Exception\CodeException');
|
||||
$this->expectExceptionMessageRegexp('/\b' . preg_quote($error_message, '/') . '\b/');
|
||||
|
||||
$file_path = self::$src_dir_path . 'somefile.php';
|
||||
|
||||
$this->addFile($file_path, $code);
|
||||
|
||||
$context = new Context();
|
||||
$context->collect_references = $check_references;
|
||||
|
||||
$this->analyzeFile($file_path, $context);
|
||||
|
||||
if ($check_references) {
|
||||
$this->project_checker->getCodebase()->checkClassReferences();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function providerFileCheckerInvalidCodeParse()
|
||||
{
|
||||
$invalid_code_data = [];
|
||||
|
||||
foreach (self::getCodeBlocksFromDocs() as $issue_name => $blocks) {
|
||||
switch ($issue_name) {
|
||||
case 'InvalidFalsableReturnType':
|
||||
$ignored_issues = ['FalsableReturnStatement'];
|
||||
break;
|
||||
|
||||
case 'InvalidNullableReturnType':
|
||||
$ignored_issues = ['NullableReturnStatement'];
|
||||
break;
|
||||
|
||||
case 'InvalidReturnType':
|
||||
$ignored_issues = ['InvalidReturnStatement'];
|
||||
break;
|
||||
|
||||
case 'MixedInferredReturnType':
|
||||
$ignored_issues = ['MixedReturnStatement'];
|
||||
break;
|
||||
|
||||
case 'MixedStringOffsetAssignment':
|
||||
$ignored_issues = ['MixedAssignment'];
|
||||
break;
|
||||
|
||||
case 'UnusedClass':
|
||||
case 'UnusedMethod':
|
||||
$ignored_issues = ['UnusedVariable'];
|
||||
break;
|
||||
|
||||
default:
|
||||
$ignored_issues = [];
|
||||
}
|
||||
|
||||
$invalid_code_data[$issue_name] = [
|
||||
'<?php' . PHP_EOL . $blocks[0],
|
||||
$issue_name,
|
||||
$ignored_issues,
|
||||
strpos($issue_name, 'Unused') !== false || strpos($issue_name, 'Unevaluated') !== false,
|
||||
];
|
||||
}
|
||||
|
||||
return $invalid_code_data;
|
||||
}
|
||||
}
|
@ -2,6 +2,7 @@
|
||||
namespace Psalm\Tests;
|
||||
|
||||
use Psalm\Checker\FileChecker;
|
||||
use Psalm\Config;
|
||||
use Psalm\Context;
|
||||
|
||||
class UnusedCodeTest extends TestCase
|
||||
@ -56,7 +57,10 @@ class UnusedCodeTest extends TestCase
|
||||
$code
|
||||
);
|
||||
|
||||
$this->analyzeFile($file_path, new Context());
|
||||
$context = new Context();
|
||||
$context->collect_references = true;
|
||||
|
||||
$this->analyzeFile($file_path, $context);
|
||||
$this->project_checker->getCodebase()->checkClassReferences();
|
||||
}
|
||||
|
||||
@ -65,10 +69,11 @@ class UnusedCodeTest extends TestCase
|
||||
*
|
||||
* @param string $code
|
||||
* @param string $error_message
|
||||
* @param array<string> $error_levels
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function testInvalidCode($code, $error_message)
|
||||
public function testInvalidCode($code, $error_message, $error_levels = [])
|
||||
{
|
||||
if (strpos($this->getName(), 'SKIPPED-') !== false) {
|
||||
$this->markTestSkipped();
|
||||
@ -79,12 +84,19 @@ class UnusedCodeTest extends TestCase
|
||||
|
||||
$file_path = self::$src_dir_path . 'somefile.php';
|
||||
|
||||
foreach ($error_levels as $error_level) {
|
||||
$this->project_checker->config->setCustomErrorLevel($error_level, Config::REPORT_SUPPRESS);
|
||||
}
|
||||
|
||||
$this->addFile(
|
||||
$file_path,
|
||||
$code
|
||||
);
|
||||
|
||||
$this->analyzeFile($file_path, new Context());
|
||||
$context = new Context();
|
||||
$context->collect_references = true;
|
||||
|
||||
$this->analyzeFile($file_path, $context);
|
||||
$this->project_checker->getCodebase()->checkClassReferences();
|
||||
}
|
||||
|
||||
@ -540,6 +552,17 @@ class UnusedCodeTest extends TestCase
|
||||
$a->passedByRef($b);
|
||||
}',
|
||||
],
|
||||
'constructorIsUsed' => [
|
||||
'<?php
|
||||
class A {
|
||||
public function __construct() {
|
||||
$this->foo();
|
||||
}
|
||||
private function foo() : void {}
|
||||
}
|
||||
$a = new A();
|
||||
echo (bool) $a;',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
@ -859,6 +882,7 @@ class UnusedCodeTest extends TestCase
|
||||
|
||||
$a = new A();',
|
||||
'error_message' => 'PossiblyUnusedProperty',
|
||||
'error_levels' => ['UnusedVariable'],
|
||||
],
|
||||
'unusedProperty' => [
|
||||
'<?php
|
||||
@ -869,6 +893,7 @@ class UnusedCodeTest extends TestCase
|
||||
|
||||
$a = new A();',
|
||||
'error_message' => 'UnusedProperty',
|
||||
'error_levels' => ['UnusedVariable'],
|
||||
],
|
||||
'privateUnusedMethod' => [
|
||||
'<?php
|
||||
|
Loading…
Reference in New Issue
Block a user