1
0
mirror of https://github.com/danog/psalm.git synced 2024-11-26 20:34:47 +01:00

Read more from config and fix switch snafu

This commit is contained in:
Matthew Brown 2016-06-10 14:47:44 -04:00
parent 4edd11cd44
commit 46005ddd29
8 changed files with 193 additions and 114 deletions

View File

@ -0,0 +1,7 @@
<?php
namespace CodeInspector;
class CodeException extends \Exception {
}

View File

@ -11,49 +11,66 @@ class Config
{
protected static $_config;
public $stopOnError = true;
public $useDocblockReturnType = false;
public $stop_on_error = true;
public $use_docblock_return_type = false;
protected $errorHandlers;
protected $inspect_files;
protected $inspectFiles;
protected $base_dir;
protected $fileExtensions = ['php'];
protected $file_extensions = ['php'];
protected $issue_handlers = [];
protected $mock_classes = [];
private function __construct()
{
self::$_config = $this;
}
public static function loadFromXML($file_contents)
public static function loadFromXML($file_name)
{
$config = new self();
$file_contents = file_get_contents($file_name);
$config->base_dir = dirname($file_name) . '/';
$config_xml = new SimpleXMLElement($file_contents);
if (isset($config_xml['stopOnError'])) {
$config->stopOnError = (bool) $config_xml['stopOnError'];
$config->stop_on_error = (bool) $config_xml['stopOnError'];
}
if (isset($config_xml['useDocblockReturnType'])) {
$config->stopOnError = (bool) $config_xml['useDocblockReturnType'];
$config->use_docblock_return_type = (bool) $config_xml['useDocblockReturnType'];
}
if (isset($config_xml->inspectFiles)) {
$config->inspectFiles = new FileFilter($config_xml->inspectFiles);
$config->inspect_files = FileFilter::loadFromXML($config_xml->inspectFiles, true);
}
if (isset($config_xml->fileExtensions)) {
$config->fileExtensions = [];
if ($config_xml->fileExtensions->extension instanceof SimpleXMLElement) {
$config->fileExtensions[] = preg_replace('/^.?/', '', $config_xml->fileExtensions->extension);
$config->file_extensions = [];
foreach ($config_xml->fileExtensions->extension as $extension) {
$config->file_extensions[] = preg_replace('/^\.?/', '', $extension['name']);
}
else {
foreach ($config_xml->fileExtensions->extension as $extension) {
$config->fileExtensions[] = preg_replace('/^.?/', '', $extension);
}
if (isset($config_xml->mockClasses) && isset($config_xml->mockClasses->class)) {
foreach ($config_xml->mockClasses->class as $mock_class) {
$config->mock_classes[] = $mock_class['name'];
}
}
if (isset($config_xml->issueHandler)) {
foreach ($config_xml->issueHandler->children() as $key => $issue_handler) {
if (isset($issue_handler->excludeFiles)) {
$config->issue_handlers[$key] = FileFilter::loadFromXML($issue_handler->excludeFiles, false);
}
}
$config->inspectFiles = new FileFilter($config_xml->inspectFiles);
}
}
@ -71,7 +88,14 @@ class Config
public function excludeIssueInFile($issue_type, $file_name)
{
$issue_type = array_pop(explode('\\', $issue_type));
$file_name = preg_replace('/^' . preg_quote($this->base_dir, '/') . '/', '', $file_name);
if (!isset($this->issue_handlers[$issue_type])) {
return false;
}
return !$this->issue_handlers[$issue_type]->allows($file_name);
}
public function doesInheritVariables($file_name)
@ -81,16 +105,16 @@ class Config
public function getFilesToCheck()
{
$files = $this->inspectFiles->getIncludeFiles();
$files = $this->inspect_files->getIncludeFiles();
foreach ($this->inspectFiles->getIncludeFolders() as $folder) {
foreach ($this->inspect_files->getIncludeDirs() as $dir) {
/** @var RecursiveDirectoryIterator */
$iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($folder));
$iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($this->base_dir . '/' . $dir));
$iterator->rewind();
while ($iterator->valid()) {
if (!$iterator->isDot()) {
if (in_array($iterator->getExtension(), $this->extensions)) {
if (in_array($iterator->getExtension(), $this->file_extensions)) {
$files[] = $iterator->getRealPath();
}
}
@ -101,4 +125,9 @@ class Config
return $files;
}
public function getMockClasses()
{
return $this->mock_classes;
}
}

View File

@ -39,47 +39,27 @@ class FileFilter
$filter->inclusive = true;
if ($e->directory) {
if ($e->directory instanceof \SimpleXMLElement) {
$filter->include_dirs[] = self::slashify($e->directory['name']);
}
else {
foreach ($e->directory as $directory) {
$filter->include_dirs[] = self::slashify($directory['name']);
}
foreach ($e->directory as $directory) {
$filter->include_dirs[] = self::slashify($directory['name']);
}
}
if ($e->file) {
if ($e->file instanceof \SimpleXMLElement) {
$filter->include_files[] = $e->file['name'];
}
else {
foreach ($e->file as $file) {
$filter->include_files[] = $file['name'];
}
foreach ($e->file as $file) {
$filter->include_files[] = $file['name'];
}
}
}
else {
if ($e->directory) {
if ($e->directory instanceof \SimpleXMLElement) {
$filter->exclude_dirs[] = self::slashify($e->directory['name']);
}
else {
foreach ($e->directory as $directory) {
$filter->exclude_dirs[] = self::slashify($directory['name']);
}
foreach ($e->directory as $directory) {
$filter->exclude_dirs[] = self::slashify($directory['name']);
}
}
if ($e->file) {
if ($e->file instanceof \SimpleXMLElement) {
$filter->exclude_files[] = $e->file['name'];
}
else {
foreach ($e->file as $file) {
$filter->exclude_files[] = $file['name'];
}
foreach ($e->file as $file) {
$filter->exclude_files[] = $file['name'];
}
}
}
@ -96,7 +76,7 @@ class FileFilter
{
if ($this->inclusive) {
foreach ($this->include_dirs as $include_dir) {
if (strpos($file_name, $include_dir) !== false) {
if (strpos($file_name, $include_dir) === 0) {
return true;
}
}
@ -110,7 +90,7 @@ class FileFilter
// exclusive
foreach ($this->exclude_dirs as $exclude_dir) {
if (strpos($file_name, $exclude_dir) !== false) {
if (strpos($file_name, $exclude_dir) === 0) {
return false;
}
@ -121,4 +101,24 @@ class FileFilter
return true;
}
public function getIncludeDirs()
{
return $this->include_dirs;
}
public function getExcludeDirs()
{
return $this->exclude_dirs;
}
public function getIncludeFiles()
{
return $this->include_files;
}
public function getExcludeFiles()
{
return $this->exclude_files;
}
}

View File

@ -8,14 +8,14 @@ class ExceptionHandler
{
$config = Config::getInstance();
if ($config->stopOnError) {
die($e->getMessage());
}
if ($config->excludeIssueInFile(get_class($e), $e->getFileName())) {
return false;
}
if ($config->stop_on_error) {
throw new CodeException($e->getMessage());
}
}
}

View File

@ -2,6 +2,6 @@
namespace CodeInspector\Issue;
class PossiblyUndefinedVariableError extends CodeError
class PossiblyUndefinedVariableNotice extends CodeIssue
{
}

View File

@ -13,26 +13,29 @@ class ScopeChecker
* @param bool $check_continue - also looks for a continue
* @return bool
*/
public static function doesLeaveBlock(array $stmts, $check_continue = true)
public static function doesLeaveBlock(array $stmts, $check_continue = true, $check_break = true)
{
for ($i = count($stmts) - 1; $i >= 0; $i--) {
$stmt = $stmts[$i];
if ($stmt instanceof PhpParser\Node\Stmt\Return_ ||
$stmt instanceof PhpParser\Node\Stmt\Throw_ ||
($check_continue && ($stmt instanceof PhpParser\Node\Stmt\Continue_ || $stmt instanceof PhpParser\Node\Stmt\Break_))) {
($check_continue && $stmt instanceof PhpParser\Node\Stmt\Continue_) ||
($check_break && $stmt instanceof PhpParser\Node\Stmt\Break_)) {
return true;
}
if ($stmt instanceof PhpParser\Node\Stmt\If_) {
if ($stmt->else && self::doesLeaveBlock($stmt->stmts, $check_continue) && self::doesLeaveBlock($stmt->else->stmts, $check_continue)) {
if ($stmt->else && self::doesLeaveBlock($stmt->stmts, $check_continue, $check_break) &&
self::doesLeaveBlock($stmt->else->stmts, $check_continue, $check_break)) {
if (empty($stmt->elseifs)) {
return true;
}
foreach ($stmt->elseifs as $elseif) {
if (!self::doesLeaveBlock($elseif->stmts, $check_continue)) {
if (!self::doesLeaveBlock($elseif->stmts, $check_continue, $check_break)) {
return false;
}
}

View File

@ -132,7 +132,7 @@ class StatementsChecker
$this->_checkThrow($stmt, $vars_in_scope, $vars_possibly_in_scope);
} elseif ($stmt instanceof PhpParser\Node\Stmt\Switch_) {
$this->_checkSwitch($stmt, $vars_in_scope, $vars_possibly_in_scope);
$this->_checkSwitch($stmt, $vars_in_scope, $vars_possibly_in_scope, $for_vars_possibly_in_scope);
} elseif ($stmt instanceof PhpParser\Node\Stmt\Break_) {
// do nothing
@ -252,7 +252,7 @@ class StatementsChecker
$post_type_assertions = [];
if (count($stmt->stmts)) {
$has_leaving_statments = ScopeChecker::doesLeaveBlock($stmt->stmts, true);
$has_leaving_statments = ScopeChecker::doesLeaveBlock($stmt->stmts, true, true);
if (!$has_leaving_statments) {
$new_vars = array_diff_key($if_vars, $vars_in_scope);
@ -278,7 +278,7 @@ class StatementsChecker
$post_type_assertions = $negated_types;
}
$has_ending_statments = ScopeChecker::doesLeaveBlock($stmt->stmts, false);
$has_ending_statments = ScopeChecker::doesLeaveBlock($stmt->stmts, false, false);
if (!$has_ending_statments) {
$vars = array_diff_key($if_vars_possibly_in_scope, $vars_possibly_in_scope);
@ -327,7 +327,7 @@ class StatementsChecker
}
if (count($elseif->stmts)) {
$has_leaving_statements = ScopeChecker::doesLeaveBlock($elseif->stmts, true);
$has_leaving_statements = ScopeChecker::doesLeaveBlock($elseif->stmts, true, true);
if (!$has_leaving_statements) {
$elseif_redefined_vars = [];
@ -384,7 +384,7 @@ class StatementsChecker
}
// has a return/throw at end
$has_ending_statments = ScopeChecker::doesLeaveBlock($elseif->stmts, false);
$has_ending_statments = ScopeChecker::doesLeaveBlock($elseif->stmts, false, false);
if (!$has_ending_statments) {
$vars = array_diff_key($elseif_vars_possibly_in_scope, $vars_possibly_in_scope);
@ -421,7 +421,7 @@ class StatementsChecker
}
if (count($stmt->else->stmts)) {
$has_leaving_statements = ScopeChecker::doesLeaveBlock($stmt->else->stmts, true);
$has_leaving_statements = ScopeChecker::doesLeaveBlock($stmt->else->stmts, true, true);
// if it doesn't end in a return
if (!$has_leaving_statements) {
@ -470,7 +470,7 @@ class StatementsChecker
}
// has a return/throw at end
$has_ending_statments = ScopeChecker::doesLeaveBlock($stmt->else->stmts, false);
$has_ending_statments = ScopeChecker::doesLeaveBlock($stmt->else->stmts, false, false);
if (!$has_ending_statments) {
$vars = array_diff_key($else_vars_possibly_in_scope, $vars_possibly_in_scope);
@ -502,7 +502,7 @@ class StatementsChecker
* let's get the type assertions from the condition if it's a terminator
* so that we can negate them going forward
*/
if (ScopeChecker::doesLeaveBlock($stmt->stmts, false) && $negated_if_types) {
if (ScopeChecker::doesLeaveBlock($stmt->stmts, false, false) && $negated_if_types) {
$vars_in_scope_reconciled = TypeChecker::reconcileTypes($negated_if_types, $vars_in_scope, true, $this->_file_name, $stmt->getLine());
if ($vars_in_scope_reconciled === false) {
@ -620,7 +620,7 @@ class StatementsChecker
}
/**
* @return void
* @return false|null
*/
protected function _checkExpression(PhpParser\Node\Expr $stmt, array &$vars_in_scope, array &$vars_possibly_in_scope = [])
{
@ -921,7 +921,7 @@ class StatementsChecker
}
/**
* @return void
* @return false|null
*/
protected function _checkVariable(PhpParser\Node\Expr\Variable $stmt, array &$vars_in_scope, array &$vars_possibly_in_scope, $method_id = null, $argument_offset = -1)
{
@ -1159,7 +1159,7 @@ class StatementsChecker
}
foreach ($stmt->cond as $condition) {
if ($this->_checkCondition($init, $for_vars, $vars_possibly_in_scope) === false) {
if ($this->_checkCondition($condition, $for_vars, $vars_possibly_in_scope) === false) {
return false;
}
}
@ -2019,7 +2019,7 @@ class StatementsChecker
}
/**
* @return void
* @return null|false
*/
protected function _checkStaticPropertyFetch(PhpParser\Node\Expr\StaticPropertyFetch $stmt, array &$vars_in_scope, array &$vars_possibly_in_scope)
{
@ -2144,7 +2144,7 @@ class StatementsChecker
return $this->_checkExpression($stmt->expr, $vars_in_scope, $vars_possibly_in_scope);
}
protected function _checkSwitch(PhpParser\Node\Stmt\Switch_ $stmt, array &$vars_in_scope, array &$vars_possibly_in_scope)
protected function _checkSwitch(PhpParser\Node\Stmt\Switch_ $stmt, array &$vars_in_scope, array &$vars_possibly_in_scope, array &$for_vars_possibly_in_scope)
{
$type_candidate_var = null;
@ -2196,44 +2196,57 @@ class StatementsChecker
$last_stmt = $case->stmts[count($case->stmts) - 1];
if (!($last_stmt instanceof PhpParser\Node\Stmt\Return_)) {
$case_redefined_vars = [];
// has a return/throw at end
$has_ending_statments = ScopeChecker::doesLeaveBlock($case->stmts, false, false);
foreach ($old_case_vars as $case_var => $type) {
if ($case_vars_in_scope[$case_var] !== $type) {
$case_redefined_vars[$case_var] = $case_vars_in_scope[$case_var];
}
}
if (!$has_ending_statments) {
$vars = array_diff_key($case_vars_possibly_in_scope, $vars_possibly_in_scope);
if ($redefined_vars === null) {
$redefined_vars = $case_redefined_vars;
$has_leaving_statements = ScopeChecker::doesLeaveBlock($case->stmts, true, false);
// if we're leaving this block, add vars to outer for loop scope
if ($has_leaving_statements) {
$for_vars_possibly_in_scope = array_merge($vars, $for_vars_possibly_in_scope);
}
else {
foreach ($redefined_vars as $redefined_var => $type) {
if (!isset($case_redefined_vars[$redefined_var])) {
unset($redefined_vars[$redefined_var]);
}
}
}
$case_redefined_vars = [];
if ($new_vars_in_scope === null) {
$new_vars_in_scope = array_diff_key($case_vars_in_scope, $vars_in_scope);
$new_vars_possibly_in_scope = array_diff_key($case_vars_possibly_in_scope, $vars_possibly_in_scope);
}
else {
foreach ($new_vars_in_scope as $new_var => $type) {
if (!isset($case_vars_in_scope[$new_var])) {
unset($new_vars_in_scope[$new_var]);
foreach ($old_case_vars as $case_var => $type) {
if ($case_vars_in_scope[$case_var] !== $type) {
$case_redefined_vars[$case_var] = $case_vars_in_scope[$case_var];
}
}
$new_vars_possibly_in_scope = array_merge(
array_diff_key(
$case_vars_possibly_in_scope,
$vars_possibly_in_scope
),
$new_vars_possibly_in_scope
);
if ($redefined_vars === null) {
$redefined_vars = $case_redefined_vars;
}
else {
foreach ($redefined_vars as $redefined_var => $type) {
if (!isset($case_redefined_vars[$redefined_var])) {
unset($redefined_vars[$redefined_var]);
}
}
}
if ($new_vars_in_scope === null) {
$new_vars_in_scope = array_diff_key($case_vars_in_scope, $vars_in_scope);
$new_vars_possibly_in_scope = array_diff_key($case_vars_possibly_in_scope, $vars_possibly_in_scope);
}
else {
foreach ($new_vars_in_scope as $new_var => $type) {
if (!isset($case_vars_in_scope[$new_var])) {
unset($new_vars_in_scope[$new_var]);
}
}
$new_vars_possibly_in_scope = array_merge(
array_diff_key(
$case_vars_possibly_in_scope,
$vars_possibly_in_scope
),
$new_vars_possibly_in_scope
);
}
}
}
}
@ -2243,7 +2256,8 @@ class StatementsChecker
}
// only update vars if there is a default
if ($case->cond === null && !($last_stmt instanceof PhpParser\Node\Stmt\Return_)) {
// if that default has a throw/return/continue, that should be handled above
if ($case->cond === null) {
if ($new_vars_in_scope) {
$vars_in_scope = array_merge($vars_in_scope, $new_vars_in_scope);
}
@ -2436,7 +2450,7 @@ class StatementsChecker
}
/**
* @return void
* @return false|null
*/
public function _checkFunctionExists($method_id, $stmt)
{
@ -2789,7 +2803,7 @@ class StatementsChecker
public static function isMock($absolute_class)
{
return in_array($absolute_class, self::$_mock_interfaces);
return in_array($absolute_class, Config::getInstance()->getMockClasses());
}
/**

View File

@ -16,7 +16,7 @@ class TypeTest extends PHPUnit_Framework_TestCase
}
/**
* @expectedException CodeInspector\Exception\CodeException
* @expectedException CodeInspector\CodeException
*/
public function testNullableMethodCall()
{
@ -123,7 +123,7 @@ class TypeTest extends PHPUnit_Framework_TestCase
}
/**
* @expectedException CodeInspector\Exception\CodeException
* @expectedException CodeInspector\CodeException
*/
public function testNullableMethodCallWithThis()
{
@ -201,7 +201,7 @@ class TypeTest extends PHPUnit_Framework_TestCase
}
/**
* @expectedException CodeInspector\Exception\CodeException
* @expectedException CodeInspector\CodeException
*/
public function testNullableMethodWithWrongIfGuard()
{
@ -272,7 +272,7 @@ class TypeTest extends PHPUnit_Framework_TestCase
}
/**
* @expectedException CodeInspector\Exception\CodeException
* @expectedException CodeInspector\CodeException
*/
public function testNullableMethodWithWrongBooleanIfGuard()
{
@ -397,7 +397,7 @@ class TypeTest extends PHPUnit_Framework_TestCase
}
/**
* @expectedException CodeInspector\Exception\CodeException
* @expectedException CodeInspector\CodeException
*/
public function testNullableMethodWithWrongIfGuardBefore()
{
@ -450,7 +450,7 @@ class TypeTest extends PHPUnit_Framework_TestCase
}
/**
* @expectedException CodeInspector\Exception\CodeException
* @expectedException CodeInspector\CodeException
*/
public function testNullableMethodWithWrongBooleanIfGuardBefore()
{
@ -503,7 +503,7 @@ class TypeTest extends PHPUnit_Framework_TestCase
}
/**
* @expectedException CodeInspector\Exception\CodeException
* @expectedException CodeInspector\CodeException
*/
public function testNullableMethodWithGuardedNestedIncompleteRedefinition()
{
@ -668,7 +668,7 @@ class TypeTest extends PHPUnit_Framework_TestCase
}
/**
* @expectedException CodeInspector\Exception\CodeException
* @expectedException CodeInspector\CodeException
*/
public function testNullableMethodWithGuardedNestedRedefinitionWithUselessElseReturn()
{
@ -887,7 +887,7 @@ class TypeTest extends PHPUnit_Framework_TestCase
}
/**
* @expectedException CodeInspector\Exception\CodeException
* @expectedException CodeInspector\CodeException
*/
public function testVariableReassignmentInIfWithOutsideCall()
{
@ -1004,4 +1004,30 @@ class TypeTest extends PHPUnit_Framework_TestCase
$this->assertSame('mixed', $return_stmt->returnType);
}
public function testSwitchVariableWithContinue()
{
$stmts = self::$_parser->parse('<?php
class B {
public function bar() {
foreach ([\'a\', \'b\', \'c\'] as $letter) {
switch ($letter) {
case \'a\':
$foo = 1;
break;
case \'b\':
$foo = 2;
break;
default:
continue;
}
$moo = $foo;
}
}
}');
$file_checker = new \CodeInspector\FileChecker('somefile.php', $stmts);
$file_checker->check();
}
}