1
0
mirror of https://github.com/danog/psalm.git synced 2024-11-26 20:34:47 +01:00
psalm/tests/IncludeTest.php
Philip Hofstetter d315822bfa make skipping of checks after invalid includes configurable
as suggested in the PR it's best to make the setting configurable.

In order not to break existing installations, we default to keeping the
old behaviour, but in a later version of psalm, we might change the
default.
2020-02-27 18:49:23 -05:00

825 lines
30 KiB
PHP

<?php
namespace Psalm\Tests;
use const DIRECTORY_SEPARATOR;
use function getcwd;
use function preg_quote;
use Psalm\Internal\Analyzer\FileAnalyzer;
use function strpos;
class IncludeTest extends TestCase
{
/**
* @dataProvider providerTestValidIncludes
*
* @param array<int, string> $files_to_check
* @param array<string, string> $files
* @param bool $hoist_constants
* @param array<string, string> $error_levels
*
* @return void
*/
public function testValidInclude(
array $files,
array $files_to_check,
$hoist_constants = false,
array $error_levels = []
) {
$codebase = $this->project_analyzer->getCodebase();
foreach ($files as $file_path => $contents) {
$this->addFile($file_path, $contents);
$codebase->scanner->addFilesToShallowScan([$file_path => $file_path]);
}
foreach ($files_to_check as $file_path) {
$codebase->addFilesToAnalyze([$file_path => $file_path]);
}
$config = $codebase->config;
$config->skip_checks_on_unresolvable_includes = true;
foreach ($error_levels as $error_level) {
$config->setCustomErrorLevel($error_level, \Psalm\Config::REPORT_SUPPRESS);
}
$codebase->scanFiles();
$config->hoist_constants = $hoist_constants;
foreach ($files_to_check as $file_path) {
$file_analyzer = new FileAnalyzer($this->project_analyzer, $file_path, $config->shortenFileName($file_path));
$file_analyzer->analyze();
}
}
/**
* @dataProvider providerTestInvalidIncludes
*
* @param array<int, string> $files_to_check
* @param array<string, string> $files
* @param string $error_message
* @param bool $hoist_constants
*
* @return void
*/
public function testInvalidInclude(
array $files,
array $files_to_check,
$error_message
) {
if (strpos($this->getTestName(), 'SKIPPED-') !== false) {
$this->markTestSkipped();
}
$codebase = $this->project_analyzer->getCodebase();
foreach ($files as $file_path => $contents) {
$this->addFile($file_path, $contents);
$codebase->scanner->addFilesToShallowScan([$file_path => $file_path]);
}
foreach ($files_to_check as $file_path) {
$codebase->addFilesToAnalyze([$file_path => $file_path]);
}
$config = $codebase->config;
$config->skip_checks_on_unresolvable_includes = false;
$this->expectException(\Psalm\Exception\CodeException::class);
$this->expectExceptionMessageRegExp('/\b' . preg_quote($error_message, '/') . '\b/');
$codebase->scanFiles();
foreach ($files_to_check as $file_path) {
$file_analyzer = new FileAnalyzer($this->project_analyzer, $file_path, $config->shortenFileName($file_path));
$file_analyzer->analyze();
}
}
/**
* @return array<string,array{files:array<string,string>,files_to_check:array<int,string>}>
*/
public function providerTestValidIncludes()
{
return [
'basicRequire' => [
'files' => [
getcwd() . DIRECTORY_SEPARATOR . 'file2.php' => '<?php
require("file1.php");
class B {
public function foo(): void {
(new A)->fooFoo();
}
}',
getcwd() . DIRECTORY_SEPARATOR . 'file1.php' => '<?php
class A{
public function fooFoo(): void {
}
}',
],
'files_to_check' => [
getcwd() . DIRECTORY_SEPARATOR . 'file2.php',
],
],
'requireSingleStringType' => [
'files' => [
getcwd() . DIRECTORY_SEPARATOR . 'file2.php' => '<?php
$a = "file1.php";
require($a);
class B {
public function foo(): void {
(new A)->fooFoo();
}
}',
getcwd() . DIRECTORY_SEPARATOR . 'file1.php' => '<?php
class A{
public function fooFoo(): void {
}
}',
],
'files_to_check' => [
getcwd() . DIRECTORY_SEPARATOR . 'file2.php',
],
],
'nestedRequire' => [
'files' => [
getcwd() . DIRECTORY_SEPARATOR . 'file1.php' => '<?php
class A{
public function fooFoo(): void {
}
}',
getcwd() . DIRECTORY_SEPARATOR . 'file2.php' => '<?php
require("file1.php");
class B extends A{
}',
getcwd() . DIRECTORY_SEPARATOR . 'file3.php' => '<?php
require("file2.php");
class C extends B {
public function doFoo(): void {
$this->fooFoo();
}
}',
],
'files_to_check' => [
getcwd() . DIRECTORY_SEPARATOR . 'file3.php',
],
],
'requireNamespace' => [
'files' => [
getcwd() . DIRECTORY_SEPARATOR . 'file1.php' => '<?php
namespace Foo;
class A{
}',
getcwd() . DIRECTORY_SEPARATOR . 'file2.php' => '<?php
require("file1.php");
class B {
public function foo(): void {
(new Foo\A);
}
}',
],
'files_to_check' => [
getcwd() . DIRECTORY_SEPARATOR . 'file2.php',
],
],
'requireFunction' => [
'files' => [
getcwd() . DIRECTORY_SEPARATOR . 'file1.php' => '<?php
function fooFoo(): void {
}',
getcwd() . DIRECTORY_SEPARATOR . 'file2.php' => '<?php
require("file1.php");
fooFoo();',
],
'files_to_check' => [
getcwd() . DIRECTORY_SEPARATOR . 'file2.php',
],
],
'namespacedRequireFunction' => [
'files' => [
getcwd() . DIRECTORY_SEPARATOR . 'file1.php' => '<?php
function fooFoo(): void {
}',
getcwd() . DIRECTORY_SEPARATOR . 'file2.php' => '<?php
namespace Foo;
require("file1.php");
\fooFoo();',
],
'files_to_check' => [
getcwd() . DIRECTORY_SEPARATOR . 'file2.php',
],
],
'requireConstant' => [
'files' => [
getcwd() . DIRECTORY_SEPARATOR . 'file1.php' => '<?php
const FOO = 5;
define("BAR", "bat");',
getcwd() . DIRECTORY_SEPARATOR . 'file2.php' => '<?php
require("file1.php");
echo FOO;
echo BAR;',
],
'files_to_check' => [
getcwd() . DIRECTORY_SEPARATOR . 'file2.php',
],
],
'requireNamespacedWithUse' => [
'files' => [
getcwd() . DIRECTORY_SEPARATOR . 'file1.php' => '<?php
namespace Foo;
class A{
}',
getcwd() . DIRECTORY_SEPARATOR . 'file2.php' => '<?php
require("file1.php");
use Foo\A;
class B {
public function foo(): void {
(new A);
}
}',
],
'files_to_check' => [
getcwd() . DIRECTORY_SEPARATOR . 'file2.php',
],
],
'noInfiniteRequireLoop' => [
'files' => [
getcwd() . DIRECTORY_SEPARATOR . 'file1.php' => '<?php
require_once("file2.php");
require_once("file3.php");
class B extends A {
public function doFoo(): void {
$this->fooFoo();
}
}
class C {}
new D();',
getcwd() . DIRECTORY_SEPARATOR . 'file2.php' => '<?php
require_once("file3.php");
class A{
public function fooFoo(): void { }
}
new C();',
getcwd() . DIRECTORY_SEPARATOR . 'file3.php' => '<?php
require_once("file1.php");
class D{ }
new C();',
],
'files_to_check' => [
getcwd() . DIRECTORY_SEPARATOR . 'file1.php',
getcwd() . DIRECTORY_SEPARATOR . 'file2.php',
getcwd() . DIRECTORY_SEPARATOR . 'file3.php',
],
],
'analyzeAllClasses' => [
'files' => [
getcwd() . DIRECTORY_SEPARATOR . 'file1.php' => '<?php
require_once("file2.php");
class B extends A {
public function doFoo(): void {
$this->fooFoo();
}
}
class C {
public function barBar(): void { }
}',
getcwd() . DIRECTORY_SEPARATOR . 'file2.php' => '<?php
require_once("file1.php");
class A{
public function fooFoo(): void { }
}
class D extends C {
public function doBar(): void {
$this->barBar();
}
}',
],
'files_to_check' => [
getcwd() . DIRECTORY_SEPARATOR . 'file1.php',
getcwd() . DIRECTORY_SEPARATOR . 'file2.php',
],
],
'loopWithInterdependencies' => [
'files' => [
getcwd() . DIRECTORY_SEPARATOR . 'file1.php' => '<?php
require_once("file2.php");
class A {}
class D extends C {}
new B();',
getcwd() . DIRECTORY_SEPARATOR . 'file2.php' => '<?php
require_once("file1.php");
class C {}
class B extends A {}
new D();',
],
'files_to_check' => [
getcwd() . DIRECTORY_SEPARATOR . 'file1.php',
getcwd() . DIRECTORY_SEPARATOR . 'file2.php',
],
],
'variadicArgs' => [
'files' => [
getcwd() . DIRECTORY_SEPARATOR . 'file1.php' => '<?php
require_once("file2.php");
variadicArgs(5, 2, "hello");',
getcwd() . DIRECTORY_SEPARATOR . 'file2.php' => '<?php
function variadicArgs() : void {
$args = func_get_args();
}',
],
'files_to_check' => [
getcwd() . DIRECTORY_SEPARATOR . 'file1.php',
],
],
'globalIncludedVar' => [
'files' => [
getcwd() . DIRECTORY_SEPARATOR . 'file1.php' => '<?php
$a = 5;
require_once("file2.php");',
getcwd() . DIRECTORY_SEPARATOR . 'file2.php' => '<?php
require_once("file3.php");',
getcwd() . DIRECTORY_SEPARATOR . 'file3.php' => '<?php
function getGlobal() : void {
global $a;
echo $a;
}',
],
'files_to_check' => [
getcwd() . DIRECTORY_SEPARATOR . 'file1.php',
],
],
'returnNamespacedFunctionCallType' => [
'files' => [
getcwd() . DIRECTORY_SEPARATOR . 'file1.php' => '<?php
namespace Foo;
class A{
function doThing() : void {}
}',
getcwd() . DIRECTORY_SEPARATOR . 'file2.php' => '<?php
namespace Bar;
require("file1.php");
use Foo\A;
/** @return A */
function getThing() {
return new A;
}',
getcwd() . DIRECTORY_SEPARATOR . 'file3.php' => '<?php
namespace Bat;
require("file2.php");
class C {
function boop() : void {
\Bar\getThing()->doThing();
}
}',
],
'files_to_check' => [
getcwd() . DIRECTORY_SEPARATOR . 'file3.php',
],
],
'functionUsedElsewhere' => [
'files' => [
getcwd() . DIRECTORY_SEPARATOR . 'file1.php' => '<?php
require_once("file2.php");
require_once("file3.php");
function foo() : void {}',
getcwd() . DIRECTORY_SEPARATOR . 'file2.php' => '<?php
foo();
array_filter([1, 2, 3, 4], "bar");',
getcwd() . DIRECTORY_SEPARATOR . 'file3.php' => '<?php
function bar(int $i) : bool { return (bool) rand(0, 1); }',
],
'files_to_check' => [
getcwd() . DIRECTORY_SEPARATOR . 'file1.php',
],
],
'closureInIncludedFile' => [
'files' => [
getcwd() . DIRECTORY_SEPARATOR . 'file1.php' => '<?php
require_once("file2.php");',
getcwd() . DIRECTORY_SEPARATOR . 'file2.php' => '<?php
return function(): string { return "asd"; };',
],
'files_to_check' => [
getcwd() . DIRECTORY_SEPARATOR . 'file1.php',
],
],
'hoistConstants' => [
'files' => [
getcwd() . DIRECTORY_SEPARATOR . 'file1.php' => '<?php
require_once("file2.php");',
getcwd() . DIRECTORY_SEPARATOR . 'file2.php' => '<?php
function bat() : void {
echo FOO . BAR;
}
define("FOO", 5);
const BAR = "BAR";',
],
'files_to_check' => [
getcwd() . DIRECTORY_SEPARATOR . 'file1.php',
getcwd() . DIRECTORY_SEPARATOR . 'file2.php',
],
'hoist_constants' => true,
],
'duplicateClasses' => [
'files' => [
getcwd() . DIRECTORY_SEPARATOR . 'file1.php' => '<?php
class A {
/** @var string|null */
protected $a;
public function aa() : void {}
public function bb() : void { $this->aa(); }
}',
getcwd() . DIRECTORY_SEPARATOR . 'file2.php' => '<?php
class A {
/** @var string|null */
protected $b;
public function dd() : void {}
public function zz() : void { $this->dd(); }
}',
],
'files_to_check' => [
getcwd() . DIRECTORY_SEPARATOR . 'file1.php',
getcwd() . DIRECTORY_SEPARATOR . 'file2.php',
],
'hoist_constants' => false,
'error_levels' => ['DuplicateClass'],
],
'duplicateClassesProperty' => [
'files' => [
getcwd() . DIRECTORY_SEPARATOR . 'file1.php' => '<?php
class A {
protected $a;
}',
getcwd() . DIRECTORY_SEPARATOR . 'file2.php' => '<?php
class A {
protected $b;
}',
],
'files_to_check' => [
getcwd() . DIRECTORY_SEPARATOR . 'file1.php',
getcwd() . DIRECTORY_SEPARATOR . 'file2.php',
],
'hoist_constants' => false,
'error_levels' => ['DuplicateClass', 'MissingPropertyType'],
],
'functionsDefined' => [
'files' => [
getcwd() . DIRECTORY_SEPARATOR . 'index.php' => '<?php
include "func.php";
include "Base.php";
include "Child.php";',
getcwd() . DIRECTORY_SEPARATOR . 'func.php' => '<?php
namespace ns;
function func(): void {}
define("ns\\cons", 0);
cons;',
getcwd() . DIRECTORY_SEPARATOR . 'Base.php' => '<?php
namespace ns;
func();
cons;
class Base {
public function __construct() {}
}',
getcwd() . DIRECTORY_SEPARATOR . 'Child.php' => '<?php
namespace ns;
func();
cons;
class Child extends Base {
/**
* @var int
*/
public $x;
public function __construct() {
parent::__construct();
$this->x = 5;
}
}',
],
'files_to_check' => [
getcwd() . DIRECTORY_SEPARATOR . 'index.php',
],
],
'suppressMissingFile' => [
'files' => [
getcwd() . DIRECTORY_SEPARATOR . 'file1.php' => '<?php
function getEndpoints() : void {
$listFile = "tests/fixtures/stubs/custom_functions.php";
if (!file_exists($listFile)) {
throw new RuntimeException("Endpoint list not found");
}
include $listFile;
}',
],
'files_to_check' => [
getcwd() . DIRECTORY_SEPARATOR . 'file1.php',
],
],
'nestedParentFile' => [
'files' => [
getcwd() . DIRECTORY_SEPARATOR . 'a' . DIRECTORY_SEPARATOR . 'b' . DIRECTORY_SEPARATOR . 'c' . DIRECTORY_SEPARATOR . 'd' . DIRECTORY_SEPARATOR . 'script.php' => '<?php
require_once __DIR__ . "/../../../../e/begin.php";',
getcwd() . DIRECTORY_SEPARATOR . 'e' . DIRECTORY_SEPARATOR . 'begin.php' => '<?php
echo "hello";',
],
'files_to_check' => [
getcwd() . DIRECTORY_SEPARATOR . 'a' . DIRECTORY_SEPARATOR . 'b' . DIRECTORY_SEPARATOR . 'c' . DIRECTORY_SEPARATOR . 'd' . DIRECTORY_SEPARATOR . 'script.php',
],
],
'undefinedMethodAfterInvalidRequire' => [
'files' => [
getcwd() . DIRECTORY_SEPARATOR . 'file2.php' => '<?php
/** @psalm-suppress MissingFile */
require("doesnotexist.php");
require("file1.php");
foo();
bar();
',
getcwd() . DIRECTORY_SEPARATOR . 'file1.php' => '<?php
function bar(): void {}
',
],
'files_to_check' => [
getcwd() . DIRECTORY_SEPARATOR . 'file2.php',
],
],
];
}
/**
* @return array<string,array{files:array<string,string>,files_to_check:array<int,string>,error_message:string}>
*/
public function providerTestInvalidIncludes()
{
return [
'undefinedMethodInRequire' => [
'files' => [
getcwd() . DIRECTORY_SEPARATOR . 'file2.php' => '<?php
require("file1.php");
class B {
public function foo(): void {
(new A)->fooFo();
}
}',
getcwd() . DIRECTORY_SEPARATOR . 'file1.php' => '<?php
class A{
public function fooFoo(): void {
}
}',
],
'files_to_check' => [
getcwd() . DIRECTORY_SEPARATOR . 'file2.php',
],
'error_message' => 'UndefinedMethod',
],
'requireFunctionWithStrictTypes' => [
'files' => [
getcwd() . DIRECTORY_SEPARATOR . 'file1.php' => '<?php
function fooFoo(int $bar): void {
}',
getcwd() . DIRECTORY_SEPARATOR . 'file2.php' => '<?php declare(strict_types=1);
require("file1.php");
fooFoo("hello");',
],
'files_to_check' => [
getcwd() . DIRECTORY_SEPARATOR . 'file2.php',
],
'error_message' => 'InvalidArgument',
],
'requireFunctionWithStrictTypesInClass' => [
'files' => [
getcwd() . DIRECTORY_SEPARATOR . 'file1.php' => '<?php
function fooFoo(int $bar): void {
}',
getcwd() . DIRECTORY_SEPARATOR . 'file2.php' => '<?php declare(strict_types=1);
require("file1.php");
class A {
public function foo() {
fooFoo("hello");
}
}',
],
'files_to_check' => [
getcwd() . DIRECTORY_SEPARATOR . 'file2.php',
],
'error_message' => 'InvalidArgument',
],
'requireFunctionWithWeakTypes' => [
'files' => [
getcwd() . DIRECTORY_SEPARATOR . 'file1.php' => '<?php
function fooFoo(int $bar): void {
}',
getcwd() . DIRECTORY_SEPARATOR . 'file2.php' => '<?php
require("file1.php");
fooFoo("hello");',
],
'files_to_check' => [
getcwd() . DIRECTORY_SEPARATOR . 'file2.php',
],
'error_message' => 'InvalidScalarArgument',
],
'requireFunctionWithStrictTypesButDocblockType' => [
'files' => [
getcwd() . DIRECTORY_SEPARATOR . 'file1.php' => '<?php
/** @param int $bar */
function fooFoo($bar): void {
}',
getcwd() . DIRECTORY_SEPARATOR . 'file2.php' => '<?php declare(strict_types=1);
require("file1.php");
fooFoo("hello");',
],
'files_to_check' => [
getcwd() . DIRECTORY_SEPARATOR . 'file2.php',
],
'error_message' => 'InvalidScalarArgument',
],
'namespacedRequireFunction' => [
'files' => [
getcwd() . DIRECTORY_SEPARATOR . 'file1.php' => '<?php
function fooFoo(): void {
}',
getcwd() . DIRECTORY_SEPARATOR . 'file2.php' => '<?php
namespace Foo;
require("file1.php");
\Foo\fooFoo();',
],
'files_to_check' => [
getcwd() . DIRECTORY_SEPARATOR . 'file2.php',
],
'error_message' => 'UndefinedFunction',
],
'globalIncludedIncorrectVar' => [
'files' => [
getcwd() . DIRECTORY_SEPARATOR . 'file1.php' => '<?php
$a = 5;
require_once("file2.php");',
getcwd() . DIRECTORY_SEPARATOR . 'file2.php' => '<?php
require_once("file3.php");',
getcwd() . DIRECTORY_SEPARATOR . 'file3.php' => '<?php
function getGlobal() : void {
global $b;
echo $a;
}',
],
'files_to_check' => [
getcwd() . DIRECTORY_SEPARATOR . 'file1.php',
],
'error_message' => 'UndefinedVariable',
],
'invalidTraitFunctionReturnInUncheckedFile' => [
'files' => [
getcwd() . DIRECTORY_SEPARATOR . 'file2.php' => '<?php
require("file1.php");
class B {
use A;
}',
getcwd() . DIRECTORY_SEPARATOR . 'file1.php' => '<?php
trait A{
public function fooFoo(): string {
return 5;
}
}',
],
'files_to_check' => [
getcwd() . DIRECTORY_SEPARATOR . 'file2.php',
],
'error_message' => 'InvalidReturnType',
],
'invalidDoubleNestedTraitFunctionReturnInUncheckedFile' => [
'files' => [
getcwd() . DIRECTORY_SEPARATOR . 'file3.php' => '<?php
namespace Foo;
require("file2.php");
use Bar\B;
class C {
use B;
}',
getcwd() . DIRECTORY_SEPARATOR . 'file2.php' => '<?php
namespace Bar;
require("file1.php");
use Bat\A;
trait B {
use A;
}',
getcwd() . DIRECTORY_SEPARATOR . 'file1.php' => '<?php
namespace Bat;
trait A{
public function fooFoo(): string {
return 5;
}
}',
],
'files_to_check' => [
getcwd() . DIRECTORY_SEPARATOR . 'file3.php',
],
'error_message' => 'InvalidReturnType',
],
'SKIPPED-noHoistConstants' => [
'files' => [
getcwd() . DIRECTORY_SEPARATOR . 'file1.php' => '<?php
require_once("file2.php");',
getcwd() . DIRECTORY_SEPARATOR . 'file2.php' => '<?php
function bat() : void {
echo FOO . BAR;
}
define("FOO", 5);
const BAR = "BAR";',
],
'files_to_check' => [
getcwd() . DIRECTORY_SEPARATOR . 'file1.php',
],
'error_message' => 'UndefinedConstant',
],
'undefinedMethodAfterInvalidRequire' => [
'files' => [
getcwd() . DIRECTORY_SEPARATOR . 'file2.php' => '<?php
/** @psalm-suppress MissingFile */
require("doesnotexist.php");
require("file1.php");
foo();
bar();
',
getcwd() . DIRECTORY_SEPARATOR . 'file1.php' => '<?php
function bar(): void {}
',
],
'files_to_check' => [
getcwd() . DIRECTORY_SEPARATOR . 'file2.php',
],
'error_message' => 'UndefinedFunction',
],
];
}
}