mirror of
https://github.com/danog/psalm.git
synced 2025-01-22 05:41:20 +01:00
Fix #731 - report issues in files required by the source
This commit is contained in:
parent
9227e61097
commit
68dbe509a8
@ -398,69 +398,16 @@ class ClassChecker extends ClassLikeChecker
|
||||
$constructor_checker = $method_checker;
|
||||
}
|
||||
} elseif ($stmt instanceof PhpParser\Node\Stmt\TraitUse) {
|
||||
$previous_context_include_location = $class_context->include_location;
|
||||
foreach ($stmt->traits as $trait) {
|
||||
$class_context->include_location = new CodeLocation($this, $trait, null, true);
|
||||
|
||||
$fq_trait_name = self::getFQCLNFromNameObject(
|
||||
$trait,
|
||||
$this->source->getAliases()
|
||||
);
|
||||
|
||||
if (!$codebase->classlikes->hasFullyQualifiedTraitName($fq_trait_name)) {
|
||||
if (IssueBuffer::accepts(
|
||||
new UndefinedTrait(
|
||||
'Trait ' . $fq_trait_name . ' does not exist',
|
||||
new CodeLocation($this, $trait)
|
||||
),
|
||||
array_merge($storage->suppressed_issues, $this->getSuppressedIssues())
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if (!$codebase->traitHasCorrectCase($fq_trait_name)) {
|
||||
if (IssueBuffer::accepts(
|
||||
new UndefinedTrait(
|
||||
'Trait ' . $fq_trait_name . ' has wrong casing',
|
||||
new CodeLocation($this, $trait)
|
||||
),
|
||||
array_merge($storage->suppressed_issues, $this->getSuppressedIssues())
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$trait_file_checker = $project_checker->getFileCheckerForClassLike($fq_trait_name);
|
||||
$trait_node = $codebase->classlikes->getTraitNode($fq_trait_name);
|
||||
$trait_aliases = $codebase->classlikes->getTraitAliases($fq_trait_name);
|
||||
$trait_checker = new TraitChecker(
|
||||
$trait_node,
|
||||
$trait_file_checker,
|
||||
$fq_trait_name,
|
||||
$trait_aliases
|
||||
);
|
||||
|
||||
foreach ($trait_node->stmts as $trait_stmt) {
|
||||
if ($trait_stmt instanceof PhpParser\Node\Stmt\ClassMethod) {
|
||||
$trait_method_checker = $this->analyzeClassMethod(
|
||||
$trait_stmt,
|
||||
$storage,
|
||||
$trait_checker,
|
||||
$class_context,
|
||||
$global_context
|
||||
);
|
||||
|
||||
if ($trait_stmt->name->name === '__construct') {
|
||||
$constructor_checker = $trait_method_checker;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($this->analyzeTraitUse(
|
||||
$stmt,
|
||||
$project_checker,
|
||||
$storage,
|
||||
$class_context,
|
||||
$global_context,
|
||||
$constructor_checker
|
||||
) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$class_context->include_location = $previous_context_include_location;
|
||||
} elseif ($stmt instanceof PhpParser\Node\Stmt\Property) {
|
||||
foreach ($stmt->props as $prop) {
|
||||
if ($prop->default) {
|
||||
@ -693,6 +640,96 @@ class ClassChecker extends ClassLikeChecker
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return false|null
|
||||
*/
|
||||
private function analyzeTraitUse(
|
||||
PhpParser\Node\Stmt\TraitUse $stmt,
|
||||
ProjectChecker $project_checker,
|
||||
ClassLikeStorage $storage,
|
||||
Context $class_context,
|
||||
Context $global_context = null,
|
||||
MethodChecker &$constructor_checker = null
|
||||
) {
|
||||
$codebase = $project_checker->codebase;
|
||||
|
||||
$previous_context_include_location = $class_context->include_location;
|
||||
|
||||
foreach ($stmt->traits as $trait_name) {
|
||||
$class_context->include_location = new CodeLocation($this, $trait_name, null, true);
|
||||
|
||||
$fq_trait_name = self::getFQCLNFromNameObject(
|
||||
$trait_name,
|
||||
$this->source->getAliases()
|
||||
);
|
||||
|
||||
if (!$codebase->classlikes->hasFullyQualifiedTraitName($fq_trait_name)) {
|
||||
if (IssueBuffer::accepts(
|
||||
new UndefinedTrait(
|
||||
'Trait ' . $fq_trait_name . ' does not exist',
|
||||
new CodeLocation($this, $trait_name)
|
||||
),
|
||||
array_merge($storage->suppressed_issues, $this->getSuppressedIssues())
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if (!$codebase->traitHasCorrectCase($fq_trait_name)) {
|
||||
if (IssueBuffer::accepts(
|
||||
new UndefinedTrait(
|
||||
'Trait ' . $fq_trait_name . ' has wrong casing',
|
||||
new CodeLocation($this, $trait_name)
|
||||
),
|
||||
array_merge($storage->suppressed_issues, $this->getSuppressedIssues())
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$trait_file_checker = $project_checker->getFileCheckerForClassLike($fq_trait_name);
|
||||
$trait_node = $codebase->classlikes->getTraitNode($fq_trait_name);
|
||||
$trait_aliases = $codebase->classlikes->getTraitAliases($fq_trait_name);
|
||||
$trait_checker = new TraitChecker(
|
||||
$trait_node,
|
||||
$trait_file_checker,
|
||||
$fq_trait_name,
|
||||
$trait_aliases
|
||||
);
|
||||
|
||||
foreach ($trait_node->stmts as $trait_stmt) {
|
||||
if ($trait_stmt instanceof PhpParser\Node\Stmt\ClassMethod) {
|
||||
$trait_method_checker = $this->analyzeClassMethod(
|
||||
$trait_stmt,
|
||||
$storage,
|
||||
$trait_checker,
|
||||
$class_context,
|
||||
$global_context
|
||||
);
|
||||
|
||||
if ($trait_stmt->name->name === '__construct') {
|
||||
$constructor_checker = $trait_method_checker;
|
||||
}
|
||||
} elseif ($trait_stmt instanceof PhpParser\Node\Stmt\TraitUse) {
|
||||
if ($this->analyzeTraitUse(
|
||||
$trait_stmt,
|
||||
$project_checker,
|
||||
$storage,
|
||||
$class_context,
|
||||
$global_context,
|
||||
$constructor_checker
|
||||
) === false) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$class_context->include_location = $previous_context_include_location;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param PhpParser\Node\Stmt\Property $stmt
|
||||
*
|
||||
|
@ -35,7 +35,7 @@ class FileChecker extends SourceChecker implements StatementsSource
|
||||
/**
|
||||
* @var array<string, bool>
|
||||
*/
|
||||
protected $included_file_paths = [];
|
||||
protected $required_file_paths = [];
|
||||
|
||||
/**
|
||||
* @var array<int, string>
|
||||
@ -351,7 +351,7 @@ class FileChecker extends SourceChecker implements StatementsSource
|
||||
*/
|
||||
public function addCheckedFilePath($file_path, $file_name)
|
||||
{
|
||||
$this->included_file_paths[$file_path] = true;
|
||||
$this->required_file_paths[$file_path] = true;
|
||||
$this->checked_file_names[] = $file_name;
|
||||
$this->checked_file_paths[] = $file_path;
|
||||
}
|
||||
@ -390,7 +390,7 @@ class FileChecker extends SourceChecker implements StatementsSource
|
||||
*/
|
||||
public function hasAlreadyIncludedFilePath($file_path)
|
||||
{
|
||||
return isset($this->included_file_paths[$file_path]);
|
||||
return isset($this->required_file_paths[$file_path]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -385,9 +385,9 @@ class Populator
|
||||
|
||||
$dependent_file_paths[$file_path_lc] = true;
|
||||
|
||||
$all_included_file_paths = $storage->included_file_paths;
|
||||
$all_required_file_paths = $storage->required_file_paths;
|
||||
|
||||
foreach ($storage->included_file_paths as $included_file_path => $_) {
|
||||
foreach ($storage->required_file_paths as $included_file_path => $_) {
|
||||
try {
|
||||
$included_file_storage = $this->file_storage_provider->get($included_file_path);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
@ -396,10 +396,10 @@ class Populator
|
||||
|
||||
$this->populateFileStorage($included_file_storage, $dependent_file_paths);
|
||||
|
||||
$all_included_file_paths = $all_included_file_paths + $included_file_storage->included_file_paths;
|
||||
$all_required_file_paths = $all_required_file_paths + $included_file_storage->required_file_paths;
|
||||
}
|
||||
|
||||
foreach ($all_included_file_paths as $included_file_path => $_) {
|
||||
foreach ($all_required_file_paths as $included_file_path => $_) {
|
||||
try {
|
||||
$included_file_storage = $this->file_storage_provider->get($included_file_path);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
@ -417,7 +417,37 @@ class Populator
|
||||
);
|
||||
}
|
||||
|
||||
$storage->included_file_paths = $all_included_file_paths;
|
||||
$storage->required_file_paths = $all_required_file_paths;
|
||||
|
||||
foreach ($all_required_file_paths as $required_file_path) {
|
||||
try {
|
||||
$required_file_storage = $this->file_storage_provider->get($required_file_path);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$required_file_storage->required_by_file_paths += [$file_path_lc => $storage->file_path];
|
||||
}
|
||||
|
||||
foreach ($storage->required_classes as $required_classlike) {
|
||||
try {
|
||||
$classlike_storage = $this->classlike_storage_provider->get($required_classlike);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$classlike_storage->location) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
$required_file_storage = $this->file_storage_provider->get($classlike_storage->location->file_path);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$required_file_storage->required_by_file_paths += [$file_path_lc => $storage->file_path];
|
||||
}
|
||||
|
||||
$storage->populated = true;
|
||||
}
|
||||
|
@ -316,8 +316,8 @@ class Scanner
|
||||
$this->file_storage_provider->cache->writeToCache($file_storage, $file_contents);
|
||||
}
|
||||
} else {
|
||||
foreach ($file_storage->included_file_paths as $include_file_path) {
|
||||
$this->addFileToShallowScan($include_file_path);
|
||||
foreach ($file_storage->required_file_paths as $required_file_path) {
|
||||
$this->addFileToShallowScan($required_file_path);
|
||||
}
|
||||
|
||||
foreach ($file_storage->classlikes_in_file as $fq_classlike_name) {
|
||||
|
@ -791,7 +791,25 @@ class Config
|
||||
if ($this->hide_external_errors) {
|
||||
$codebase = ProjectChecker::getInstance()->codebase;
|
||||
|
||||
if (!$codebase->analyzer->canReportIssues($file_path)) {
|
||||
$dependent_files = [strtolower($file_path) => $file_path];
|
||||
|
||||
try {
|
||||
$file_storage = $codebase->file_storage_provider->get($file_path);
|
||||
$dependent_files += $file_storage->required_by_file_paths;
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
$any_file_path_matched = false;
|
||||
|
||||
foreach ($dependent_files as $file_path) {
|
||||
if ($codebase->analyzer->canReportIssues($file_path)) {
|
||||
$any_file_path_matched = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$any_file_path_matched) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -48,7 +48,10 @@ class FileStorage
|
||||
public $declaring_constants = [];
|
||||
|
||||
/** @var array<string, string> */
|
||||
public $included_file_paths = [];
|
||||
public $required_file_paths = [];
|
||||
|
||||
/** @var array<string, string> */
|
||||
public $required_by_file_paths = [];
|
||||
|
||||
/** @var bool */
|
||||
public $populated = false;
|
||||
|
@ -1632,7 +1632,7 @@ class DependencyFinderVisitor extends PhpParser\NodeVisitorAbstract implements P
|
||||
$this->codebase->scanner->addFileToShallowScan($path_to_file);
|
||||
}
|
||||
|
||||
$this->file_storage->included_file_paths[strtolower($path_to_file)] = $path_to_file;
|
||||
$this->file_storage->required_file_paths[strtolower($path_to_file)] = $path_to_file;
|
||||
|
||||
return;
|
||||
}
|
||||
|
@ -440,6 +440,52 @@ class IncludeTest extends TestCase
|
||||
'error_message' => 'UndefinedGlobalVariable',
|
||||
'hide_external_errors' => false
|
||||
],
|
||||
'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
|
||||
require("file2.php");
|
||||
|
||||
class C {
|
||||
use B;
|
||||
}',
|
||||
getcwd() . DIRECTORY_SEPARATOR . 'file2.php' => '<?php
|
||||
require("file1.php");
|
||||
|
||||
trait B {
|
||||
use A;
|
||||
}',
|
||||
getcwd() . DIRECTORY_SEPARATOR . 'file1.php' => '<?php
|
||||
trait A{
|
||||
public function fooFoo(): string {
|
||||
return 5;
|
||||
}
|
||||
}',
|
||||
],
|
||||
'files_to_check' => [
|
||||
getcwd() . DIRECTORY_SEPARATOR . 'file3.php',
|
||||
],
|
||||
'error_message' => 'InvalidReturnType',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -460,7 +460,7 @@ class PropertyTypeTest extends TestCase
|
||||
}
|
||||
|
||||
class A {
|
||||
use T;
|
||||
use T;
|
||||
}',
|
||||
],
|
||||
'abstractClassWithNoConstructor' => [
|
||||
|
@ -695,6 +695,23 @@ class TraitTest extends TestCase
|
||||
}',
|
||||
'error_message' => 'MissingPropertyType',
|
||||
],
|
||||
'nestedTraitWithBadReturnType' => [
|
||||
'<?php
|
||||
trait A {
|
||||
public function foo() : string {
|
||||
return 5;
|
||||
}
|
||||
}
|
||||
|
||||
trait B {
|
||||
use A;
|
||||
}
|
||||
|
||||
class C {
|
||||
use B;
|
||||
}',
|
||||
'error_message' => 'InvalidReturnType',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user